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.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.DistributionDataPointSnapshot;
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.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 java.util.List;
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 — no {@code _total} or unit suffix appending. The {@code _info} suffix is
045 * 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 */
049public class OpenMetrics2TextFormatWriter implements ExpositionFormatWriter {
050
051  public static class Builder {
052    private OpenMetrics2Properties openMetrics2Properties =
053        OpenMetrics2Properties.builder().build();
054    boolean createdTimestampsEnabled;
055    boolean exemplarsOnAllMetricTypesEnabled;
056
057    private Builder() {}
058
059    /**
060     * @param openMetrics2Properties OpenMetrics 2.0 feature flags
061     */
062    public Builder setOpenMetrics2Properties(OpenMetrics2Properties openMetrics2Properties) {
063      this.openMetrics2Properties = openMetrics2Properties;
064      return this;
065    }
066
067    /**
068     * @param createdTimestampsEnabled whether to include the _created timestamp in the output
069     */
070    public Builder setCreatedTimestampsEnabled(boolean createdTimestampsEnabled) {
071      this.createdTimestampsEnabled = createdTimestampsEnabled;
072      return this;
073    }
074
075    /**
076     * @param exemplarsOnAllMetricTypesEnabled whether to include exemplars in the output for all
077     *     metric types
078     */
079    public Builder setExemplarsOnAllMetricTypesEnabled(boolean exemplarsOnAllMetricTypesEnabled) {
080      this.exemplarsOnAllMetricTypesEnabled = exemplarsOnAllMetricTypesEnabled;
081      return this;
082    }
083
084    public OpenMetrics2TextFormatWriter build() {
085      return new OpenMetrics2TextFormatWriter(
086          openMetrics2Properties, createdTimestampsEnabled, exemplarsOnAllMetricTypesEnabled);
087    }
088  }
089
090  public static final String CONTENT_TYPE =
091      "application/openmetrics-text; version=2.0.0; charset=utf-8";
092  private final OpenMetrics2Properties openMetrics2Properties;
093  private final boolean createdTimestampsEnabled;
094  private final boolean exemplarsOnAllMetricTypesEnabled;
095
096  /**
097   * @param openMetrics2Properties OpenMetrics 2.0 feature flags
098   * @param createdTimestampsEnabled whether to include the _created timestamp in the output - This
099   *     will produce an invalid OpenMetrics output, but is kept for backwards compatibility.
100   * @param exemplarsOnAllMetricTypesEnabled whether to include exemplars on all metric types
101   */
102  public OpenMetrics2TextFormatWriter(
103      OpenMetrics2Properties openMetrics2Properties,
104      boolean createdTimestampsEnabled,
105      boolean exemplarsOnAllMetricTypesEnabled) {
106    this.openMetrics2Properties = openMetrics2Properties;
107    this.createdTimestampsEnabled = createdTimestampsEnabled;
108    this.exemplarsOnAllMetricTypesEnabled = exemplarsOnAllMetricTypesEnabled;
109  }
110
111  public static Builder builder() {
112    return new Builder();
113  }
114
115  public static OpenMetrics2TextFormatWriter create() {
116    return builder().build();
117  }
118
119  @Override
120  public boolean accepts(@Nullable String acceptHeader) {
121    if (acceptHeader == null) {
122      return false;
123    }
124    return acceptHeader.contains("application/openmetrics-text");
125  }
126
127  @Override
128  public String getContentType() {
129    // When contentNegotiation=false (default), masquerade as OM1 for compatibility
130    // When contentNegotiation=true, use proper OM2 version
131    if (openMetrics2Properties.getContentNegotiation()) {
132      return CONTENT_TYPE;
133    } else {
134      return OpenMetricsTextFormatWriter.CONTENT_TYPE;
135    }
136  }
137
138  public OpenMetrics2Properties getOpenMetrics2Properties() {
139    return openMetrics2Properties;
140  }
141
142  @Override
143  public void write(OutputStream out, MetricSnapshots metricSnapshots, EscapingScheme scheme)
144      throws IOException {
145    Writer writer = new BufferedWriter(new OutputStreamWriter(out, StandardCharsets.UTF_8));
146    MetricSnapshots merged = TextFormatUtil.mergeDuplicates(metricSnapshots);
147    for (MetricSnapshot s : merged) {
148      MetricSnapshot snapshot = SnapshotEscaper.escapeMetricSnapshot(s, scheme);
149      if (!snapshot.getDataPoints().isEmpty()) {
150        if (snapshot instanceof CounterSnapshot) {
151          writeCounter(writer, (CounterSnapshot) snapshot, scheme);
152        } else if (snapshot instanceof GaugeSnapshot) {
153          writeGauge(writer, (GaugeSnapshot) snapshot, scheme);
154        } else if (snapshot instanceof HistogramSnapshot) {
155          writeHistogram(writer, (HistogramSnapshot) snapshot, scheme);
156        } else if (snapshot instanceof SummarySnapshot) {
157          writeSummary(writer, (SummarySnapshot) snapshot, scheme);
158        } else if (snapshot instanceof InfoSnapshot) {
159          writeInfo(writer, (InfoSnapshot) snapshot, scheme);
160        } else if (snapshot instanceof StateSetSnapshot) {
161          writeStateSet(writer, (StateSetSnapshot) snapshot, scheme);
162        } else if (snapshot instanceof UnknownSnapshot) {
163          writeUnknown(writer, (UnknownSnapshot) snapshot, scheme);
164        }
165      }
166    }
167    writer.write("# EOF\n");
168    writer.flush();
169  }
170
171  private void writeCounter(Writer writer, CounterSnapshot snapshot, EscapingScheme scheme)
172      throws IOException {
173    MetricMetadata metadata = snapshot.getMetadata();
174    // OM2: use the name as provided by the user, no _total appending
175    String counterName = getExpositionBaseMetadataName(metadata, scheme);
176    writeMetadataWithName(writer, counterName, "counter", metadata);
177    for (CounterSnapshot.CounterDataPointSnapshot data : snapshot.getDataPoints()) {
178      writeNameAndLabels(writer, counterName, null, data.getLabels(), scheme);
179      writeDouble(writer, data.getValue());
180      writeScrapeTimestampAndExemplar(writer, data, data.getExemplar(), scheme);
181      writeCreated(writer, counterName, data, scheme);
182    }
183  }
184
185  private void writeGauge(Writer writer, GaugeSnapshot snapshot, EscapingScheme scheme)
186      throws IOException {
187    MetricMetadata metadata = snapshot.getMetadata();
188    String name = getExpositionBaseMetadataName(metadata, scheme);
189    writeMetadataWithName(writer, name, "gauge", metadata);
190    for (GaugeSnapshot.GaugeDataPointSnapshot data : snapshot.getDataPoints()) {
191      writeNameAndLabels(writer, name, null, data.getLabels(), scheme);
192      writeDouble(writer, data.getValue());
193      if (exemplarsOnAllMetricTypesEnabled) {
194        writeScrapeTimestampAndExemplar(writer, data, data.getExemplar(), scheme);
195      } else {
196        writeScrapeTimestampAndExemplar(writer, data, null, scheme);
197      }
198    }
199  }
200
201  private void writeHistogram(Writer writer, HistogramSnapshot snapshot, EscapingScheme scheme)
202      throws IOException {
203    MetricMetadata metadata = snapshot.getMetadata();
204    String name = getExpositionBaseMetadataName(metadata, scheme);
205    if (snapshot.isGaugeHistogram()) {
206      writeMetadataWithName(writer, name, "gaugehistogram", metadata);
207      writeClassicHistogramBuckets(
208          writer, name, "_gcount", "_gsum", snapshot.getDataPoints(), scheme);
209    } else {
210      writeMetadataWithName(writer, name, "histogram", metadata);
211      writeClassicHistogramBuckets(
212          writer, name, "_count", "_sum", snapshot.getDataPoints(), scheme);
213    }
214  }
215
216  private void writeClassicHistogramBuckets(
217      Writer writer,
218      String name,
219      String countSuffix,
220      String sumSuffix,
221      List<HistogramSnapshot.HistogramDataPointSnapshot> dataList,
222      EscapingScheme scheme)
223      throws IOException {
224    for (HistogramSnapshot.HistogramDataPointSnapshot data : dataList) {
225      ClassicHistogramBuckets buckets = getClassicBuckets(data);
226      Exemplars exemplars = data.getExemplars();
227      long cumulativeCount = 0;
228      for (int i = 0; i < buckets.size(); i++) {
229        cumulativeCount += buckets.getCount(i);
230        writeNameAndLabels(
231            writer, name, "_bucket", data.getLabels(), scheme, "le", buckets.getUpperBound(i));
232        writeLong(writer, cumulativeCount);
233        Exemplar exemplar;
234        if (i == 0) {
235          exemplar = exemplars.get(Double.NEGATIVE_INFINITY, buckets.getUpperBound(i));
236        } else {
237          exemplar = exemplars.get(buckets.getUpperBound(i - 1), buckets.getUpperBound(i));
238        }
239        writeScrapeTimestampAndExemplar(writer, data, exemplar, scheme);
240      }
241      // In OpenMetrics format, histogram _count and _sum are either both present or both absent.
242      if (data.hasCount() && data.hasSum()) {
243        writeCountAndSum(writer, name, data, countSuffix, sumSuffix, exemplars, scheme);
244      }
245      writeCreated(writer, name, data, scheme);
246    }
247  }
248
249  private ClassicHistogramBuckets getClassicBuckets(
250      HistogramSnapshot.HistogramDataPointSnapshot data) {
251    if (data.getClassicBuckets().isEmpty()) {
252      return ClassicHistogramBuckets.of(
253          new double[] {Double.POSITIVE_INFINITY}, new long[] {data.getCount()});
254    } else {
255      return data.getClassicBuckets();
256    }
257  }
258
259  private void writeSummary(Writer writer, SummarySnapshot snapshot, EscapingScheme scheme)
260      throws IOException {
261    boolean metadataWritten = false;
262    MetricMetadata metadata = snapshot.getMetadata();
263    String name = getExpositionBaseMetadataName(metadata, scheme);
264    for (SummarySnapshot.SummaryDataPointSnapshot data : snapshot.getDataPoints()) {
265      if (data.getQuantiles().size() == 0 && !data.hasCount() && !data.hasSum()) {
266        continue;
267      }
268      if (!metadataWritten) {
269        writeMetadataWithName(writer, name, "summary", metadata);
270        metadataWritten = true;
271      }
272      Exemplars exemplars = data.getExemplars();
273      // Exemplars for summaries are new, and there's no best practice yet which Exemplars to choose
274      // for which
275      // time series. We select exemplars[0] for _count, exemplars[1] for _sum, and exemplars[2...]
276      // for the
277      // quantiles, all indexes modulo exemplars.length.
278      int exemplarIndex = 1;
279      for (Quantile quantile : data.getQuantiles()) {
280        writeNameAndLabels(
281            writer, name, null, data.getLabels(), scheme, "quantile", quantile.getQuantile());
282        writeDouble(writer, quantile.getValue());
283        if (exemplars.size() > 0 && exemplarsOnAllMetricTypesEnabled) {
284          exemplarIndex = (exemplarIndex + 1) % exemplars.size();
285          writeScrapeTimestampAndExemplar(writer, data, exemplars.get(exemplarIndex), scheme);
286        } else {
287          writeScrapeTimestampAndExemplar(writer, data, null, scheme);
288        }
289      }
290      // Unlike histograms, summaries can have only a count or only a sum according to OpenMetrics.
291      writeCountAndSum(writer, name, data, "_count", "_sum", exemplars, scheme);
292      writeCreated(writer, name, data, scheme);
293    }
294  }
295
296  private void writeInfo(Writer writer, InfoSnapshot snapshot, EscapingScheme scheme)
297      throws IOException {
298    MetricMetadata metadata = snapshot.getMetadata();
299    // OM2 spec: Info MetricFamily name MUST end in _info.
300    // In OM2, TYPE/HELP use the same name as the data lines.
301    String infoName = ensureSuffix(getExpositionBaseMetadataName(metadata, scheme), "_info");
302    writeMetadataWithName(writer, infoName, "info", metadata);
303    for (InfoSnapshot.InfoDataPointSnapshot data : snapshot.getDataPoints()) {
304      writeNameAndLabels(writer, infoName, null, data.getLabels(), scheme);
305      writer.write("1");
306      writeScrapeTimestampAndExemplar(writer, data, null, scheme);
307    }
308  }
309
310  private void writeStateSet(Writer writer, StateSetSnapshot snapshot, EscapingScheme scheme)
311      throws IOException {
312    MetricMetadata metadata = snapshot.getMetadata();
313    String name = getExpositionBaseMetadataName(metadata, scheme);
314    writeMetadataWithName(writer, name, "stateset", metadata);
315    for (StateSetSnapshot.StateSetDataPointSnapshot data : snapshot.getDataPoints()) {
316      for (int i = 0; i < data.size(); i++) {
317        writer.write(name);
318        writer.write('{');
319        Labels labels = data.getLabels();
320        for (int j = 0; j < labels.size(); j++) {
321          if (j > 0) {
322            writer.write(",");
323          }
324          writer.write(getSnapshotLabelName(labels, j, scheme));
325          writer.write("=\"");
326          writeEscapedString(writer, labels.getValue(j));
327          writer.write("\"");
328        }
329        if (!labels.isEmpty()) {
330          writer.write(",");
331        }
332        writer.write(name);
333        writer.write("=\"");
334        writeEscapedString(writer, data.getName(i));
335        writer.write("\"} ");
336        if (data.isTrue(i)) {
337          writer.write("1");
338        } else {
339          writer.write("0");
340        }
341        writeScrapeTimestampAndExemplar(writer, data, null, scheme);
342      }
343    }
344  }
345
346  private void writeUnknown(Writer writer, UnknownSnapshot snapshot, EscapingScheme scheme)
347      throws IOException {
348    MetricMetadata metadata = snapshot.getMetadata();
349    String name = getExpositionBaseMetadataName(metadata, scheme);
350    writeMetadataWithName(writer, name, "unknown", metadata);
351    for (UnknownSnapshot.UnknownDataPointSnapshot data : snapshot.getDataPoints()) {
352      writeNameAndLabels(writer, name, null, data.getLabels(), scheme);
353      writeDouble(writer, data.getValue());
354      if (exemplarsOnAllMetricTypesEnabled) {
355        writeScrapeTimestampAndExemplar(writer, data, data.getExemplar(), scheme);
356      } else {
357        writeScrapeTimestampAndExemplar(writer, data, null, scheme);
358      }
359    }
360  }
361
362  private void writeCountAndSum(
363      Writer writer,
364      String name,
365      DistributionDataPointSnapshot data,
366      String countSuffix,
367      String sumSuffix,
368      Exemplars exemplars,
369      EscapingScheme scheme)
370      throws IOException {
371    if (data.hasCount()) {
372      writeNameAndLabels(writer, name, countSuffix, data.getLabels(), scheme);
373      writeLong(writer, data.getCount());
374      if (exemplarsOnAllMetricTypesEnabled) {
375        writeScrapeTimestampAndExemplar(writer, data, exemplars.getLatest(), scheme);
376      } else {
377        writeScrapeTimestampAndExemplar(writer, data, null, scheme);
378      }
379    }
380    if (data.hasSum()) {
381      writeNameAndLabels(writer, name, sumSuffix, data.getLabels(), scheme);
382      writeDouble(writer, data.getSum());
383      writeScrapeTimestampAndExemplar(writer, data, null, scheme);
384    }
385  }
386
387  private void writeCreated(
388      Writer writer, String name, DataPointSnapshot data, EscapingScheme scheme)
389      throws IOException {
390    if (createdTimestampsEnabled && data.hasCreatedTimestamp()) {
391      writeNameAndLabels(writer, name, "_created", data.getLabels(), scheme);
392      writeOpenMetricsTimestamp(writer, data.getCreatedTimestampMillis());
393      if (data.hasScrapeTimestamp()) {
394        writer.write(' ');
395        writeOpenMetricsTimestamp(writer, data.getScrapeTimestampMillis());
396      }
397      writer.write('\n');
398    }
399  }
400
401  private void writeNameAndLabels(
402      Writer writer,
403      String name,
404      @Nullable String suffix,
405      Labels labels,
406      EscapingScheme escapingScheme)
407      throws IOException {
408    writeNameAndLabels(writer, name, suffix, labels, escapingScheme, null, 0.0);
409  }
410
411  private void writeNameAndLabels(
412      Writer writer,
413      String name,
414      @Nullable String suffix,
415      Labels labels,
416      EscapingScheme escapingScheme,
417      @Nullable String additionalLabelName,
418      double additionalLabelValue)
419      throws IOException {
420    boolean metricInsideBraces = false;
421    // If the name does not pass the legacy validity check, we must put the
422    // metric name inside the braces.
423    if (!PrometheusNaming.isValidLegacyMetricName(name)) {
424      metricInsideBraces = true;
425      writer.write('{');
426    }
427    writeName(writer, name + (suffix != null ? suffix : ""), NameType.Metric);
428    if (!labels.isEmpty() || additionalLabelName != null) {
429      writeLabels(
430          writer,
431          labels,
432          additionalLabelName,
433          additionalLabelValue,
434          metricInsideBraces,
435          escapingScheme);
436    } else if (metricInsideBraces) {
437      writer.write('}');
438    }
439    writer.write(' ');
440  }
441
442  private void writeScrapeTimestampAndExemplar(
443      Writer writer, DataPointSnapshot data, @Nullable Exemplar exemplar, EscapingScheme scheme)
444      throws IOException {
445    if (data.hasScrapeTimestamp()) {
446      writer.write(' ');
447      writeOpenMetricsTimestamp(writer, data.getScrapeTimestampMillis());
448    }
449    if (exemplar != null) {
450      writer.write(" # ");
451      writeLabels(writer, exemplar.getLabels(), null, 0, false, scheme);
452      writer.write(' ');
453      writeDouble(writer, exemplar.getValue());
454      if (exemplar.hasTimestamp()) {
455        writer.write(' ');
456        writeOpenMetricsTimestamp(writer, exemplar.getTimestampMillis());
457      }
458    }
459    writer.write('\n');
460  }
461
462  private void writeMetadataWithName(
463      Writer writer, String name, String typeName, MetricMetadata metadata) throws IOException {
464    writer.write("# TYPE ");
465    writeName(writer, name, NameType.Metric);
466    writer.write(' ');
467    writer.write(typeName);
468    writer.write('\n');
469    if (metadata.getUnit() != null) {
470      writer.write("# UNIT ");
471      writeName(writer, name, NameType.Metric);
472      writer.write(' ');
473      writeEscapedString(writer, metadata.getUnit().toString());
474      writer.write('\n');
475    }
476    if (metadata.getHelp() != null && !metadata.getHelp().isEmpty()) {
477      writer.write("# HELP ");
478      writeName(writer, name, NameType.Metric);
479      writer.write(' ');
480      writeEscapedString(writer, metadata.getHelp());
481      writer.write('\n');
482    }
483  }
484
485  private static String ensureSuffix(String name, String suffix) {
486    if (name.endsWith(suffix)) {
487      return name;
488    }
489    return name + suffix;
490  }
491}