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