001package io.prometheus.metrics.expositionformats;
002
003import static io.prometheus.metrics.expositionformats.TextFormatUtil.writeDouble;
004import static io.prometheus.metrics.expositionformats.TextFormatUtil.writeEscapedString;
005import static io.prometheus.metrics.expositionformats.TextFormatUtil.writeLabels;
006import static io.prometheus.metrics.expositionformats.TextFormatUtil.writeLong;
007import static io.prometheus.metrics.expositionformats.TextFormatUtil.writeName;
008import static io.prometheus.metrics.expositionformats.TextFormatUtil.writeOpenMetricsTimestamp;
009import static io.prometheus.metrics.model.snapshots.SnapshotEscaper.getExpositionBaseMetadataName;
010import static io.prometheus.metrics.model.snapshots.SnapshotEscaper.getOriginalMetadataName;
011import static io.prometheus.metrics.model.snapshots.SnapshotEscaper.getSnapshotLabelName;
012
013import io.prometheus.metrics.annotations.StableApi;
014import io.prometheus.metrics.config.EscapingScheme;
015import io.prometheus.metrics.config.OpenMetrics2Properties;
016import io.prometheus.metrics.model.snapshots.ClassicHistogramBuckets;
017import io.prometheus.metrics.model.snapshots.CounterSnapshot;
018import io.prometheus.metrics.model.snapshots.DataPointSnapshot;
019import io.prometheus.metrics.model.snapshots.Exemplar;
020import io.prometheus.metrics.model.snapshots.Exemplars;
021import io.prometheus.metrics.model.snapshots.GaugeSnapshot;
022import io.prometheus.metrics.model.snapshots.HistogramSnapshot;
023import io.prometheus.metrics.model.snapshots.InfoSnapshot;
024import io.prometheus.metrics.model.snapshots.Labels;
025import io.prometheus.metrics.model.snapshots.MetricMetadata;
026import io.prometheus.metrics.model.snapshots.MetricSnapshot;
027import io.prometheus.metrics.model.snapshots.MetricSnapshots;
028import io.prometheus.metrics.model.snapshots.NativeHistogramBuckets;
029import io.prometheus.metrics.model.snapshots.PrometheusNaming;
030import io.prometheus.metrics.model.snapshots.Quantile;
031import io.prometheus.metrics.model.snapshots.SnapshotEscaper;
032import io.prometheus.metrics.model.snapshots.StateSetSnapshot;
033import io.prometheus.metrics.model.snapshots.SummarySnapshot;
034import io.prometheus.metrics.model.snapshots.UnknownSnapshot;
035import java.io.BufferedWriter;
036import java.io.IOException;
037import java.io.OutputStream;
038import java.io.OutputStreamWriter;
039import java.io.Writer;
040import java.nio.charset.StandardCharsets;
041import javax.annotation.Nullable;
042
043/**
044 * Write the OpenMetrics 2.0 text format. By default, this writer preserves metric names as provided
045 * by the user. Unit and type suffixes can be appended for OM1 compatibility. The {@code _info}
046 * suffix is always enforced per the OM2 spec (MUST). This is experimental and subject to change as
047 * the <a
048 * href="https://github.com/prometheus/docs/blob/main/docs/specs/om/open_metrics_spec_2_0.md">OpenMetrics
049 * 2.0 specification</a> evolves.
050 */
051@StableApi
052public class OpenMetrics2TextFormatWriter implements ExpositionFormatWriter {
053
054  public static class Builder {
055    private OpenMetrics2Properties openMetrics2Properties =
056        OpenMetrics2Properties.builder().build();
057    boolean createdTimestampsEnabled;
058    boolean exemplarsOnAllMetricTypesEnabled;
059
060    private Builder() {}
061
062    /**
063     * @param openMetrics2Properties OpenMetrics 2.0 feature flags
064     */
065    public Builder setOpenMetrics2Properties(OpenMetrics2Properties openMetrics2Properties) {
066      this.openMetrics2Properties = openMetrics2Properties;
067      return this;
068    }
069
070    /**
071     * @param createdTimestampsEnabled whether delegated OM1 output includes _created metrics
072     */
073    public Builder setCreatedTimestampsEnabled(boolean createdTimestampsEnabled) {
074      this.createdTimestampsEnabled = createdTimestampsEnabled;
075      return this;
076    }
077
078    /**
079     * @param exemplarsOnAllMetricTypesEnabled whether to include exemplars in the output for all
080     *     metric types
081     */
082    public Builder setExemplarsOnAllMetricTypesEnabled(boolean exemplarsOnAllMetricTypesEnabled) {
083      this.exemplarsOnAllMetricTypesEnabled = exemplarsOnAllMetricTypesEnabled;
084      return this;
085    }
086
087    public OpenMetrics2TextFormatWriter build() {
088      return new OpenMetrics2TextFormatWriter(
089          openMetrics2Properties, createdTimestampsEnabled, exemplarsOnAllMetricTypesEnabled);
090    }
091  }
092
093  public static final String CONTENT_TYPE =
094      "application/openmetrics-text; version=2.0.0; charset=utf-8";
095  private final OpenMetrics2Properties openMetrics2Properties;
096  private final boolean createdTimestampsEnabled;
097  private final boolean exemplarsOnAllMetricTypesEnabled;
098  private final OpenMetricsTextFormatWriter om1Writer;
099
100  /**
101   * @param openMetrics2Properties OpenMetrics 2.0 feature flags
102   * @param createdTimestampsEnabled whether delegated OM1 output includes _created metrics
103   * @param exemplarsOnAllMetricTypesEnabled whether to include exemplars on all metric types
104   */
105  public OpenMetrics2TextFormatWriter(
106      OpenMetrics2Properties openMetrics2Properties,
107      boolean createdTimestampsEnabled,
108      boolean exemplarsOnAllMetricTypesEnabled) {
109    this.openMetrics2Properties = openMetrics2Properties;
110    this.createdTimestampsEnabled = createdTimestampsEnabled;
111    this.exemplarsOnAllMetricTypesEnabled = exemplarsOnAllMetricTypesEnabled;
112    this.om1Writer =
113        new OpenMetricsTextFormatWriter(createdTimestampsEnabled, exemplarsOnAllMetricTypesEnabled);
114  }
115
116  public static Builder builder() {
117    return new Builder();
118  }
119
120  public static OpenMetrics2TextFormatWriter create() {
121    return builder().build();
122  }
123
124  @Override
125  public boolean accepts(@Nullable String acceptHeader) {
126    if (acceptHeader == null) {
127      return false;
128    }
129    return acceptHeader.contains("application/openmetrics-text");
130  }
131
132  @Override
133  public String getContentType() {
134    // When contentNegotiation=false (default), masquerade as OM1 for compatibility.
135    // When contentNegotiation=true, use proper OM2 version.
136    if (openMetrics2Properties.getContentNegotiation()) {
137      return CONTENT_TYPE;
138    } else {
139      return OpenMetricsTextFormatWriter.CONTENT_TYPE;
140    }
141  }
142
143  public OpenMetrics2Properties getOpenMetrics2Properties() {
144    return openMetrics2Properties;
145  }
146
147  @Override
148  public void write(OutputStream out, MetricSnapshots metricSnapshots, EscapingScheme scheme)
149      throws IOException {
150    Writer writer = new BufferedWriter(new OutputStreamWriter(out, StandardCharsets.UTF_8));
151    MetricSnapshots merged = TextFormatUtil.mergeDuplicates(metricSnapshots);
152    for (MetricSnapshot s : merged) {
153      MetricSnapshot snapshot = SnapshotEscaper.escapeMetricSnapshot(s, scheme);
154      if (!snapshot.getDataPoints().isEmpty()) {
155        if (snapshot instanceof CounterSnapshot) {
156          writeCounter(writer, (CounterSnapshot) snapshot, scheme);
157        } else if (snapshot instanceof GaugeSnapshot) {
158          writeGauge(writer, (GaugeSnapshot) snapshot, scheme);
159        } else if (snapshot instanceof HistogramSnapshot) {
160          writeHistogram(writer, (HistogramSnapshot) snapshot, scheme);
161        } else if (snapshot instanceof SummarySnapshot) {
162          writeSummary(writer, (SummarySnapshot) snapshot, scheme);
163        } else if (snapshot instanceof InfoSnapshot) {
164          writeInfo(writer, (InfoSnapshot) snapshot, scheme);
165        } else if (snapshot instanceof StateSetSnapshot) {
166          writeStateSet(writer, (StateSetSnapshot) snapshot, scheme);
167        } else if (snapshot instanceof UnknownSnapshot) {
168          writeUnknown(writer, (UnknownSnapshot) snapshot, scheme);
169        }
170      }
171    }
172    writer.write("# EOF\n");
173    writer.flush();
174  }
175
176  private void writeCounter(Writer writer, CounterSnapshot snapshot, EscapingScheme scheme)
177      throws IOException {
178    MetricMetadata metadata = snapshot.getMetadata();
179    String counterName = getMetricName(metadata, scheme);
180    if (openMetrics2Properties.getSuffixes()) {
181      counterName = ensureSuffix(counterName, "_total");
182    }
183    boolean nameValidLegacy = PrometheusNaming.isValidLegacyMetricName(counterName);
184    writeMetadataWithName(writer, counterName, "counter", metadata);
185    for (CounterSnapshot.CounterDataPointSnapshot data : snapshot.getDataPoints()) {
186      writeNameAndLabels(writer, counterName, null, data.getLabels(), scheme, nameValidLegacy);
187      writeDouble(writer, data.getValue());
188      if (data.hasScrapeTimestamp()) {
189        writer.write(' ');
190        writeOpenMetricsTimestamp(writer, data.getScrapeTimestampMillis());
191      }
192      if (data.hasCreatedTimestamp()) {
193        writer.write(" st@");
194        writeOpenMetricsTimestamp(writer, data.getCreatedTimestampMillis());
195      }
196      writeExemplar(writer, data.getExemplar(), scheme);
197      writer.write('\n');
198    }
199  }
200
201  private void writeGauge(Writer writer, GaugeSnapshot snapshot, EscapingScheme scheme)
202      throws IOException {
203    MetricMetadata metadata = snapshot.getMetadata();
204    String name = getMetricName(metadata, scheme);
205    boolean nameValidLegacy = PrometheusNaming.isValidLegacyMetricName(name);
206    writeMetadataWithName(writer, name, "gauge", metadata);
207    for (GaugeSnapshot.GaugeDataPointSnapshot data : snapshot.getDataPoints()) {
208      writeNameAndLabels(writer, name, null, data.getLabels(), scheme, nameValidLegacy);
209      writeDouble(writer, data.getValue());
210      if (exemplarsOnAllMetricTypesEnabled) {
211        writeScrapeTimestampAndExemplar(writer, data, data.getExemplar(), scheme);
212      } else {
213        writeScrapeTimestampAndExemplar(writer, data, null, scheme);
214      }
215    }
216  }
217
218  private void writeHistogram(Writer writer, HistogramSnapshot snapshot, EscapingScheme scheme)
219      throws IOException {
220    boolean compositeHistogram =
221        openMetrics2Properties.getCompositeValues() || openMetrics2Properties.getNativeHistograms();
222    MetricMetadata metadata = snapshot.getMetadata();
223    String name = getMetricName(metadata, scheme);
224    boolean nameValidLegacy = PrometheusNaming.isValidLegacyMetricName(name);
225    if (!compositeHistogram && !openMetrics2Properties.getExemplarCompliance()) {
226      writeClassicHistogram(writer, name, snapshot, scheme, nameValidLegacy);
227      return;
228    }
229    if (snapshot.isGaugeHistogram()) {
230      writeMetadataWithName(writer, name, "gaugehistogram", metadata);
231      for (HistogramSnapshot.HistogramDataPointSnapshot data : snapshot.getDataPoints()) {
232        if (openMetrics2Properties.getNativeHistograms() && data.hasNativeHistogramData()) {
233          writeNativeHistogramDataPoint(
234              writer, name, "gcount", "gsum", data, scheme, false, nameValidLegacy);
235        } else {
236          writeCompositeHistogramDataPoint(
237              writer, name, "gcount", "gsum", data, scheme, false, nameValidLegacy);
238        }
239      }
240    } else {
241      writeMetadataWithName(writer, name, "histogram", metadata);
242      for (HistogramSnapshot.HistogramDataPointSnapshot data : snapshot.getDataPoints()) {
243        if (openMetrics2Properties.getNativeHistograms() && data.hasNativeHistogramData()) {
244          writeNativeHistogramDataPoint(
245              writer, name, "count", "sum", data, scheme, true, nameValidLegacy);
246        } else {
247          writeCompositeHistogramDataPoint(
248              writer, name, "count", "sum", data, scheme, true, nameValidLegacy);
249        }
250      }
251    }
252  }
253
254  private void writeClassicHistogram(
255      Writer writer,
256      String name,
257      HistogramSnapshot snapshot,
258      EscapingScheme scheme,
259      boolean nameValidLegacy)
260      throws IOException {
261    if (snapshot.isGaugeHistogram()) {
262      writeMetadataWithName(writer, name, "gaugehistogram", snapshot.getMetadata());
263      writeClassicHistogramDataPoints(
264          writer, name, "_gcount", "_gsum", snapshot, scheme, nameValidLegacy);
265    } else {
266      writeMetadataWithName(writer, name, "histogram", snapshot.getMetadata());
267      writeClassicHistogramDataPoints(
268          writer, name, "_count", "_sum", snapshot, scheme, nameValidLegacy);
269    }
270  }
271
272  private void writeClassicHistogramDataPoints(
273      Writer writer,
274      String name,
275      String countSuffix,
276      String sumSuffix,
277      HistogramSnapshot snapshot,
278      EscapingScheme scheme,
279      boolean nameValidLegacy)
280      throws IOException {
281    String bucketName = name + "_bucket";
282    for (HistogramSnapshot.HistogramDataPointSnapshot data : snapshot.getDataPoints()) {
283      ClassicHistogramBuckets buckets = getClassicBuckets(data);
284      Exemplars exemplars = data.getExemplars();
285      long cumulativeCount = 0;
286      for (int i = 0; i < buckets.size(); i++) {
287        cumulativeCount += buckets.getCount(i);
288        writeNameAndLabels(
289            writer,
290            bucketName,
291            null,
292            data.getLabels(),
293            scheme,
294            "le",
295            buckets.getUpperBound(i),
296            nameValidLegacy);
297        writeLong(writer, cumulativeCount);
298        Exemplar exemplar;
299        if (i == 0) {
300          exemplar = exemplars.get(Double.NEGATIVE_INFINITY, buckets.getUpperBound(i));
301        } else {
302          exemplar = exemplars.get(buckets.getUpperBound(i - 1), buckets.getUpperBound(i));
303        }
304        writeScrapeTimestampAndExemplar(writer, data, exemplar, scheme);
305      }
306      if (data.hasCount() && data.hasSum()) {
307        writeClassicCountAndSum(
308            writer, name, data, countSuffix, sumSuffix, exemplars, scheme, nameValidLegacy);
309      }
310      writeClassicCreated(writer, name, data, scheme, nameValidLegacy);
311    }
312  }
313
314  private void writeClassicCountAndSum(
315      Writer writer,
316      String name,
317      HistogramSnapshot.HistogramDataPointSnapshot data,
318      String countSuffix,
319      String sumSuffix,
320      Exemplars exemplars,
321      EscapingScheme scheme,
322      boolean nameValidLegacy)
323      throws IOException {
324    writeNameAndLabels(writer, name, countSuffix, data.getLabels(), scheme, nameValidLegacy);
325    writeLong(writer, data.getCount());
326    if (exemplarsOnAllMetricTypesEnabled) {
327      writeScrapeTimestampAndExemplar(writer, data, exemplars.getLatest(), scheme);
328    } else {
329      writeScrapeTimestampAndExemplar(writer, data, null, scheme);
330    }
331    writeNameAndLabels(writer, name, sumSuffix, data.getLabels(), scheme, nameValidLegacy);
332    writeDouble(writer, data.getSum());
333    writeScrapeTimestampAndExemplar(writer, data, null, scheme);
334  }
335
336  private void writeClassicCreated(
337      Writer writer,
338      String name,
339      HistogramSnapshot.HistogramDataPointSnapshot data,
340      EscapingScheme scheme,
341      boolean nameValidLegacy)
342      throws IOException {
343    if (createdTimestampsEnabled && data.hasCreatedTimestamp()) {
344      writeNameAndLabels(writer, name, "_created", data.getLabels(), scheme, nameValidLegacy);
345      writeOpenMetricsTimestamp(writer, data.getCreatedTimestampMillis());
346      if (data.hasScrapeTimestamp()) {
347        writer.write(' ');
348        writeOpenMetricsTimestamp(writer, data.getScrapeTimestampMillis());
349      }
350      writer.write('\n');
351    }
352  }
353
354  private void writeCompositeHistogramDataPoint(
355      Writer writer,
356      String name,
357      String countKey,
358      String sumKey,
359      HistogramSnapshot.HistogramDataPointSnapshot data,
360      EscapingScheme scheme,
361      boolean includeStartTimestamp,
362      boolean nameValidLegacy)
363      throws IOException {
364    writeNameAndLabels(writer, name, null, data.getLabels(), scheme, nameValidLegacy);
365    writer.write('{');
366    writer.write(countKey);
367    writer.write(':');
368    writeLong(writer, data.getCount());
369    writer.write(',');
370    writer.write(sumKey);
371    writer.write(':');
372    writeDouble(writer, data.getSum());
373    writeClassicBucketsField(writer, data);
374    writer.write('}');
375    if (data.hasScrapeTimestamp()) {
376      writer.write(' ');
377      writeOpenMetricsTimestamp(writer, data.getScrapeTimestampMillis());
378    }
379    if (includeStartTimestamp && data.hasCreatedTimestamp()) {
380      writer.write(" st@");
381      writeOpenMetricsTimestamp(writer, data.getCreatedTimestampMillis());
382    }
383    writeExemplars(writer, data.getExemplars(), scheme);
384    writer.write('\n');
385  }
386
387  private void writeNativeHistogramDataPoint(
388      Writer writer,
389      String name,
390      String countKey,
391      String sumKey,
392      HistogramSnapshot.HistogramDataPointSnapshot data,
393      EscapingScheme scheme,
394      boolean includeStartTimestamp,
395      boolean nameValidLegacy)
396      throws IOException {
397    writeNameAndLabels(writer, name, null, data.getLabels(), scheme, nameValidLegacy);
398    writer.write('{');
399    writer.write(countKey);
400    writer.write(':');
401    writeLong(writer, data.getCount());
402    writer.write(',');
403    writer.write(sumKey);
404    writer.write(':');
405    writeDouble(writer, data.getSum());
406    writer.write(",schema:");
407    writer.write(Integer.toString(data.getNativeSchema()));
408    writer.write(",zero_threshold:");
409    writeDouble(writer, data.getNativeZeroThreshold());
410    writer.write(",zero_count:");
411    writeLong(writer, data.getNativeZeroCount());
412    writeNativeBucketFields(writer, "negative", data.getNativeBucketsForNegativeValues());
413    writeNativeBucketFields(writer, "positive", data.getNativeBucketsForPositiveValues());
414    if (data.hasClassicHistogramData()) {
415      writeClassicBucketsField(writer, data);
416    }
417    writer.write('}');
418    if (data.hasScrapeTimestamp()) {
419      writer.write(' ');
420      writeOpenMetricsTimestamp(writer, data.getScrapeTimestampMillis());
421    }
422    if (includeStartTimestamp && data.hasCreatedTimestamp()) {
423      writer.write(" st@");
424      writeOpenMetricsTimestamp(writer, data.getCreatedTimestampMillis());
425    }
426    writeExemplars(writer, data.getExemplars(), scheme);
427    writer.write('\n');
428  }
429
430  private ClassicHistogramBuckets getClassicBuckets(
431      HistogramSnapshot.HistogramDataPointSnapshot data) {
432    if (data.getClassicBuckets().isEmpty()) {
433      return ClassicHistogramBuckets.of(
434          new double[] {Double.POSITIVE_INFINITY}, new long[] {data.getCount()});
435    } else {
436      return data.getClassicBuckets();
437    }
438  }
439
440  private void writeClassicBucketsField(
441      Writer writer, HistogramSnapshot.HistogramDataPointSnapshot data) throws IOException {
442    writer.write(",bucket:[");
443    ClassicHistogramBuckets buckets = getClassicBuckets(data);
444    long cumulativeCount = 0;
445    for (int i = 0; i < buckets.size(); i++) {
446      if (i > 0) {
447        writer.write(',');
448      }
449      cumulativeCount += buckets.getCount(i);
450      writeDouble(writer, buckets.getUpperBound(i));
451      writer.write(':');
452      writeLong(writer, cumulativeCount);
453    }
454    writer.write(']');
455  }
456
457  private void writeNativeBucketFields(Writer writer, String prefix, NativeHistogramBuckets buckets)
458      throws IOException {
459    if (buckets.size() == 0) {
460      return;
461    }
462    writer.write(',');
463    writer.write(prefix);
464    writer.write("_spans:[");
465    writeNativeBucketSpans(writer, buckets);
466    writer.write("],");
467    writer.write(prefix);
468    writer.write("_buckets:[");
469    for (int i = 0; i < buckets.size(); i++) {
470      if (i > 0) {
471        writer.write(',');
472      }
473      writeLong(writer, buckets.getCount(i));
474    }
475    writer.write(']');
476  }
477
478  private void writeNativeBucketSpans(Writer writer, NativeHistogramBuckets buckets)
479      throws IOException {
480    int spanOffset = buckets.getBucketIndex(0);
481    int spanLength = 1;
482    int previousIndex = buckets.getBucketIndex(0);
483    boolean firstSpan = true;
484    for (int i = 1; i < buckets.size(); i++) {
485      int bucketIndex = buckets.getBucketIndex(i);
486      if (bucketIndex == previousIndex + 1) {
487        spanLength++;
488      } else {
489        firstSpan = writeNativeBucketSpan(writer, spanOffset, spanLength, firstSpan);
490        spanOffset = bucketIndex - previousIndex - 1;
491        spanLength = 1;
492      }
493      previousIndex = bucketIndex;
494    }
495    writeNativeBucketSpan(writer, spanOffset, spanLength, firstSpan);
496  }
497
498  private boolean writeNativeBucketSpan(Writer writer, int offset, int length, boolean firstSpan)
499      throws IOException {
500    if (!firstSpan) {
501      writer.write(',');
502    }
503    writer.write(Integer.toString(offset));
504    writer.write(':');
505    writer.write(Integer.toString(length));
506    return false;
507  }
508
509  private void writeSummary(Writer writer, SummarySnapshot snapshot, EscapingScheme scheme)
510      throws IOException {
511    MetricMetadata metadata = snapshot.getMetadata();
512    String name = getMetricName(metadata, scheme);
513    if (!openMetrics2Properties.getCompositeValues()
514        && !openMetrics2Properties.getExemplarCompliance()) {
515      om1Writer.writeSummary(writer, snapshot, scheme, name);
516      return;
517    }
518    boolean nameValidLegacy = PrometheusNaming.isValidLegacyMetricName(name);
519    boolean metadataWritten = false;
520    for (SummarySnapshot.SummaryDataPointSnapshot data : snapshot.getDataPoints()) {
521      if (data.getQuantiles().size() == 0 && !data.hasCount() && !data.hasSum()) {
522        continue;
523      }
524      if (!metadataWritten) {
525        writeMetadataWithName(writer, name, "summary", metadata);
526        metadataWritten = true;
527      }
528      writeCompositeSummaryDataPoint(writer, name, data, scheme, nameValidLegacy);
529    }
530  }
531
532  private void writeCompositeSummaryDataPoint(
533      Writer writer,
534      String name,
535      SummarySnapshot.SummaryDataPointSnapshot data,
536      EscapingScheme scheme,
537      boolean nameValidLegacy)
538      throws IOException {
539    writeNameAndLabels(writer, name, null, data.getLabels(), scheme, nameValidLegacy);
540    writer.write('{');
541    boolean first = true;
542    if (data.hasCount()) {
543      writer.write("count:");
544      writeLong(writer, data.getCount());
545      first = false;
546    }
547    if (data.hasSum()) {
548      if (!first) {
549        writer.write(',');
550      }
551      writer.write("sum:");
552      writeDouble(writer, data.getSum());
553      first = false;
554    }
555    if (!first) {
556      writer.write(',');
557    }
558    writer.write("quantile:[");
559    for (int i = 0; i < data.getQuantiles().size(); i++) {
560      if (i > 0) {
561        writer.write(',');
562      }
563      Quantile q = data.getQuantiles().get(i);
564      writeDouble(writer, q.getQuantile());
565      writer.write(':');
566      writeDouble(writer, q.getValue());
567    }
568    writer.write(']');
569    writer.write('}');
570    if (data.hasScrapeTimestamp()) {
571      writer.write(' ');
572      writeOpenMetricsTimestamp(writer, data.getScrapeTimestampMillis());
573    }
574    if (data.hasCreatedTimestamp()) {
575      writer.write(" st@");
576      writeOpenMetricsTimestamp(writer, data.getCreatedTimestampMillis());
577    }
578    writeExemplars(writer, data.getExemplars(), scheme);
579    writer.write('\n');
580  }
581
582  private void writeInfo(Writer writer, InfoSnapshot snapshot, EscapingScheme scheme)
583      throws IOException {
584    MetricMetadata metadata = snapshot.getMetadata();
585    // OM2 spec: Info MetricFamily name MUST end in _info.
586    // In OM2, TYPE/HELP use the same name as the data lines.
587    String infoName = ensureSuffix(getMetricName(metadata, scheme), "_info");
588    boolean nameValidLegacy = PrometheusNaming.isValidLegacyMetricName(infoName);
589    writeMetadataWithName(writer, infoName, "info", metadata);
590    for (InfoSnapshot.InfoDataPointSnapshot data : snapshot.getDataPoints()) {
591      writeNameAndLabels(writer, infoName, null, data.getLabels(), scheme, nameValidLegacy);
592      writer.write("1");
593      writeScrapeTimestampAndExemplar(writer, data, null, scheme);
594    }
595  }
596
597  private void writeStateSet(Writer writer, StateSetSnapshot snapshot, EscapingScheme scheme)
598      throws IOException {
599    MetricMetadata metadata = snapshot.getMetadata();
600    String name = getMetricName(metadata, scheme);
601    writeMetadataWithName(writer, name, "stateset", metadata);
602    for (StateSetSnapshot.StateSetDataPointSnapshot data : snapshot.getDataPoints()) {
603      for (int i = 0; i < data.size(); i++) {
604        writer.write(name);
605        writer.write('{');
606        Labels labels = data.getLabels();
607        for (int j = 0; j < labels.size(); j++) {
608          if (j > 0) {
609            writer.write(",");
610          }
611          writer.write(getSnapshotLabelName(labels, j, scheme));
612          writer.write("=\"");
613          writeEscapedString(writer, labels.getValue(j));
614          writer.write("\"");
615        }
616        if (!labels.isEmpty()) {
617          writer.write(",");
618        }
619        writer.write(name);
620        writer.write("=\"");
621        writeEscapedString(writer, data.getName(i));
622        writer.write("\"} ");
623        if (data.isTrue(i)) {
624          writer.write("1");
625        } else {
626          writer.write("0");
627        }
628        writeScrapeTimestampAndExemplar(writer, data, null, scheme);
629      }
630    }
631  }
632
633  private void writeUnknown(Writer writer, UnknownSnapshot snapshot, EscapingScheme scheme)
634      throws IOException {
635    MetricMetadata metadata = snapshot.getMetadata();
636    String name = getMetricName(metadata, scheme);
637    boolean nameValidLegacy = PrometheusNaming.isValidLegacyMetricName(name);
638    writeMetadataWithName(writer, name, "unknown", metadata);
639    for (UnknownSnapshot.UnknownDataPointSnapshot data : snapshot.getDataPoints()) {
640      writeNameAndLabels(writer, name, null, data.getLabels(), scheme, nameValidLegacy);
641      writeDouble(writer, data.getValue());
642      if (exemplarsOnAllMetricTypesEnabled) {
643        writeScrapeTimestampAndExemplar(writer, data, data.getExemplar(), scheme);
644      } else {
645        writeScrapeTimestampAndExemplar(writer, data, null, scheme);
646      }
647    }
648  }
649
650  private void writeNameAndLabels(
651      Writer writer,
652      String name,
653      @Nullable String suffix,
654      Labels labels,
655      EscapingScheme escapingScheme,
656      boolean nameValidLegacy)
657      throws IOException {
658    writeNameAndLabels(writer, name, suffix, labels, escapingScheme, null, 0.0, nameValidLegacy);
659  }
660
661  private void writeNameAndLabels(
662      Writer writer,
663      String name,
664      @Nullable String suffix,
665      Labels labels,
666      EscapingScheme escapingScheme,
667      @Nullable String additionalLabelName,
668      double additionalLabelValue,
669      boolean nameValidLegacy)
670      throws IOException {
671    boolean metricInsideBraces = false;
672    // If the name does not pass the legacy validity check, we must put the metric name inside the
673    // braces. Validity is scanned once per family by the caller (the metric name is constant across
674    // all data points), not re-scanned for every series. A suffix built from legacy characters
675    // (_total, _bucket, ...) does not change legacy validity, so the base-name check applies here.
676    if (!nameValidLegacy) {
677      metricInsideBraces = true;
678      writer.write('{');
679    }
680    writeName(writer, suffix != null ? name + suffix : name, NameType.Metric, nameValidLegacy);
681    if (!labels.isEmpty() || additionalLabelName != null) {
682      writeLabels(
683          writer,
684          labels,
685          additionalLabelName,
686          additionalLabelValue,
687          metricInsideBraces,
688          escapingScheme);
689    } else if (metricInsideBraces) {
690      writer.write('}');
691    }
692    writer.write(' ');
693  }
694
695  private void writeScrapeTimestampAndExemplar(
696      Writer writer, DataPointSnapshot data, @Nullable Exemplar exemplar, EscapingScheme scheme)
697      throws IOException {
698    if (!openMetrics2Properties.getExemplarCompliance()) {
699      om1Writer.writeScrapeTimestampAndExemplar(writer, data, exemplar, scheme);
700      return;
701    }
702    if (data.hasScrapeTimestamp()) {
703      writer.write(' ');
704      writeOpenMetricsTimestamp(writer, data.getScrapeTimestampMillis());
705    }
706    writeExemplar(writer, exemplar, scheme);
707    writer.write('\n');
708  }
709
710  private void writeExemplar(Writer writer, @Nullable Exemplar exemplar, EscapingScheme scheme)
711      throws IOException {
712    if (exemplar == null) {
713      return;
714    }
715    if (!openMetrics2Properties.getExemplarCompliance()) {
716      om1Writer.writeExemplar(writer, exemplar, scheme);
717      return;
718    }
719    // exemplarCompliance=true: exemplars MUST have a timestamp per the OM2 spec.
720    if (exemplar.hasTimestamp()) {
721      om1Writer.writeExemplar(writer, exemplar, scheme);
722    }
723  }
724
725  private void writeExemplars(Writer writer, Exemplars exemplars, EscapingScheme scheme)
726      throws IOException {
727    for (Exemplar exemplar : exemplars) {
728      writeExemplar(writer, exemplar, scheme);
729    }
730  }
731
732  private void writeMetadataWithName(
733      Writer writer, String name, String typeName, MetricMetadata metadata) throws IOException {
734    writer.write("# TYPE ");
735    writeName(writer, name, NameType.Metric);
736    writer.write(' ');
737    writer.write(typeName);
738    writer.write('\n');
739    if (metadata.getUnit() != null) {
740      writer.write("# UNIT ");
741      writeName(writer, name, NameType.Metric);
742      writer.write(' ');
743      writeEscapedString(writer, metadata.getUnit().toString());
744      writer.write('\n');
745    }
746    if (metadata.getHelp() != null && !metadata.getHelp().isEmpty()) {
747      writer.write("# HELP ");
748      writeName(writer, name, NameType.Metric);
749      writer.write(' ');
750      writeEscapedString(writer, metadata.getHelp());
751      writer.write('\n');
752    }
753  }
754
755  private String getMetricName(MetricMetadata metadata, EscapingScheme scheme) {
756    if (openMetrics2Properties.getSuffixes()) {
757      return getExpositionBaseMetadataName(metadata, scheme);
758    }
759    return getOriginalMetadataName(metadata, scheme);
760  }
761
762  private static String ensureSuffix(String name, String suffix) {
763    if (name.endsWith(suffix)) {
764      return name;
765    }
766    return name + suffix;
767  }
768}