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