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.getMetadataName;
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. This is currently a skeleton implementation that produces
044 * identical output to OpenMetrics 1.0, with infrastructure for future OM2 features. This is
045 * 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    writeMetadata(writer, "counter", metadata, scheme);
175    for (CounterSnapshot.CounterDataPointSnapshot data : snapshot.getDataPoints()) {
176      writeNameAndLabels(
177          writer, getMetadataName(metadata, scheme), "_total", data.getLabels(), scheme);
178      writeDouble(writer, data.getValue());
179      writeScrapeTimestampAndExemplar(writer, data, data.getExemplar(), scheme);
180      writeCreated(writer, metadata, data, scheme);
181    }
182  }
183
184  private void writeGauge(Writer writer, GaugeSnapshot snapshot, EscapingScheme scheme)
185      throws IOException {
186    MetricMetadata metadata = snapshot.getMetadata();
187    writeMetadata(writer, "gauge", metadata, scheme);
188    for (GaugeSnapshot.GaugeDataPointSnapshot data : snapshot.getDataPoints()) {
189      writeNameAndLabels(writer, getMetadataName(metadata, scheme), null, data.getLabels(), scheme);
190      writeDouble(writer, data.getValue());
191      if (exemplarsOnAllMetricTypesEnabled) {
192        writeScrapeTimestampAndExemplar(writer, data, data.getExemplar(), scheme);
193      } else {
194        writeScrapeTimestampAndExemplar(writer, data, null, scheme);
195      }
196    }
197  }
198
199  private void writeHistogram(Writer writer, HistogramSnapshot snapshot, EscapingScheme scheme)
200      throws IOException {
201    MetricMetadata metadata = snapshot.getMetadata();
202    if (snapshot.isGaugeHistogram()) {
203      writeMetadata(writer, "gaugehistogram", metadata, scheme);
204      writeClassicHistogramBuckets(
205          writer, metadata, "_gcount", "_gsum", snapshot.getDataPoints(), scheme);
206    } else {
207      writeMetadata(writer, "histogram", metadata, scheme);
208      writeClassicHistogramBuckets(
209          writer, metadata, "_count", "_sum", snapshot.getDataPoints(), scheme);
210    }
211  }
212
213  private void writeClassicHistogramBuckets(
214      Writer writer,
215      MetricMetadata metadata,
216      String countSuffix,
217      String sumSuffix,
218      List<HistogramSnapshot.HistogramDataPointSnapshot> dataList,
219      EscapingScheme scheme)
220      throws IOException {
221    for (HistogramSnapshot.HistogramDataPointSnapshot data : dataList) {
222      ClassicHistogramBuckets buckets = getClassicBuckets(data);
223      Exemplars exemplars = data.getExemplars();
224      long cumulativeCount = 0;
225      for (int i = 0; i < buckets.size(); i++) {
226        cumulativeCount += buckets.getCount(i);
227        writeNameAndLabels(
228            writer,
229            getMetadataName(metadata, scheme),
230            "_bucket",
231            data.getLabels(),
232            scheme,
233            "le",
234            buckets.getUpperBound(i));
235        writeLong(writer, cumulativeCount);
236        Exemplar exemplar;
237        if (i == 0) {
238          exemplar = exemplars.get(Double.NEGATIVE_INFINITY, buckets.getUpperBound(i));
239        } else {
240          exemplar = exemplars.get(buckets.getUpperBound(i - 1), buckets.getUpperBound(i));
241        }
242        writeScrapeTimestampAndExemplar(writer, data, exemplar, scheme);
243      }
244      // In OpenMetrics format, histogram _count and _sum are either both present or both absent.
245      if (data.hasCount() && data.hasSum()) {
246        writeCountAndSum(writer, metadata, data, countSuffix, sumSuffix, exemplars, scheme);
247      }
248      writeCreated(writer, metadata, data, scheme);
249    }
250  }
251
252  private ClassicHistogramBuckets getClassicBuckets(
253      HistogramSnapshot.HistogramDataPointSnapshot data) {
254    if (data.getClassicBuckets().isEmpty()) {
255      return ClassicHistogramBuckets.of(
256          new double[] {Double.POSITIVE_INFINITY}, new long[] {data.getCount()});
257    } else {
258      return data.getClassicBuckets();
259    }
260  }
261
262  private void writeSummary(Writer writer, SummarySnapshot snapshot, EscapingScheme scheme)
263      throws IOException {
264    boolean metadataWritten = false;
265    MetricMetadata metadata = snapshot.getMetadata();
266    for (SummarySnapshot.SummaryDataPointSnapshot data : snapshot.getDataPoints()) {
267      if (data.getQuantiles().size() == 0 && !data.hasCount() && !data.hasSum()) {
268        continue;
269      }
270      if (!metadataWritten) {
271        writeMetadata(writer, "summary", metadata, scheme);
272        metadataWritten = true;
273      }
274      Exemplars exemplars = data.getExemplars();
275      // Exemplars for summaries are new, and there's no best practice yet which Exemplars to choose
276      // for which
277      // time series. We select exemplars[0] for _count, exemplars[1] for _sum, and exemplars[2...]
278      // for the
279      // quantiles, all indexes modulo exemplars.length.
280      int exemplarIndex = 1;
281      for (Quantile quantile : data.getQuantiles()) {
282        writeNameAndLabels(
283            writer,
284            getMetadataName(metadata, scheme),
285            null,
286            data.getLabels(),
287            scheme,
288            "quantile",
289            quantile.getQuantile());
290        writeDouble(writer, quantile.getValue());
291        if (exemplars.size() > 0 && exemplarsOnAllMetricTypesEnabled) {
292          exemplarIndex = (exemplarIndex + 1) % exemplars.size();
293          writeScrapeTimestampAndExemplar(writer, data, exemplars.get(exemplarIndex), scheme);
294        } else {
295          writeScrapeTimestampAndExemplar(writer, data, null, scheme);
296        }
297      }
298      // Unlike histograms, summaries can have only a count or only a sum according to OpenMetrics.
299      writeCountAndSum(writer, metadata, data, "_count", "_sum", exemplars, scheme);
300      writeCreated(writer, metadata, data, scheme);
301    }
302  }
303
304  private void writeInfo(Writer writer, InfoSnapshot snapshot, EscapingScheme scheme)
305      throws IOException {
306    MetricMetadata metadata = snapshot.getMetadata();
307    writeMetadata(writer, "info", metadata, scheme);
308    for (InfoSnapshot.InfoDataPointSnapshot data : snapshot.getDataPoints()) {
309      writeNameAndLabels(
310          writer, getMetadataName(metadata, scheme), "_info", data.getLabels(), scheme);
311      writer.write("1");
312      writeScrapeTimestampAndExemplar(writer, data, null, scheme);
313    }
314  }
315
316  private void writeStateSet(Writer writer, StateSetSnapshot snapshot, EscapingScheme scheme)
317      throws IOException {
318    MetricMetadata metadata = snapshot.getMetadata();
319    writeMetadata(writer, "stateset", metadata, scheme);
320    for (StateSetSnapshot.StateSetDataPointSnapshot data : snapshot.getDataPoints()) {
321      for (int i = 0; i < data.size(); i++) {
322        writer.write(getMetadataName(metadata, scheme));
323        writer.write('{');
324        Labels labels = data.getLabels();
325        for (int j = 0; j < labels.size(); j++) {
326          if (j > 0) {
327            writer.write(",");
328          }
329          writer.write(getSnapshotLabelName(labels, j, scheme));
330          writer.write("=\"");
331          writeEscapedString(writer, labels.getValue(j));
332          writer.write("\"");
333        }
334        if (!labels.isEmpty()) {
335          writer.write(",");
336        }
337        writer.write(getMetadataName(metadata, scheme));
338        writer.write("=\"");
339        writeEscapedString(writer, data.getName(i));
340        writer.write("\"} ");
341        if (data.isTrue(i)) {
342          writer.write("1");
343        } else {
344          writer.write("0");
345        }
346        writeScrapeTimestampAndExemplar(writer, data, null, scheme);
347      }
348    }
349  }
350
351  private void writeUnknown(Writer writer, UnknownSnapshot snapshot, EscapingScheme scheme)
352      throws IOException {
353    MetricMetadata metadata = snapshot.getMetadata();
354    writeMetadata(writer, "unknown", metadata, scheme);
355    for (UnknownSnapshot.UnknownDataPointSnapshot data : snapshot.getDataPoints()) {
356      writeNameAndLabels(writer, getMetadataName(metadata, scheme), null, data.getLabels(), scheme);
357      writeDouble(writer, data.getValue());
358      if (exemplarsOnAllMetricTypesEnabled) {
359        writeScrapeTimestampAndExemplar(writer, data, data.getExemplar(), scheme);
360      } else {
361        writeScrapeTimestampAndExemplar(writer, data, null, scheme);
362      }
363    }
364  }
365
366  private void writeCountAndSum(
367      Writer writer,
368      MetricMetadata metadata,
369      DistributionDataPointSnapshot data,
370      String countSuffix,
371      String sumSuffix,
372      Exemplars exemplars,
373      EscapingScheme scheme)
374      throws IOException {
375    if (data.hasCount()) {
376      writeNameAndLabels(
377          writer, getMetadataName(metadata, scheme), countSuffix, data.getLabels(), scheme);
378      writeLong(writer, data.getCount());
379      if (exemplarsOnAllMetricTypesEnabled) {
380        writeScrapeTimestampAndExemplar(writer, data, exemplars.getLatest(), scheme);
381      } else {
382        writeScrapeTimestampAndExemplar(writer, data, null, scheme);
383      }
384    }
385    if (data.hasSum()) {
386      writeNameAndLabels(
387          writer, getMetadataName(metadata, scheme), sumSuffix, data.getLabels(), scheme);
388      writeDouble(writer, data.getSum());
389      writeScrapeTimestampAndExemplar(writer, data, null, scheme);
390    }
391  }
392
393  private void writeCreated(
394      Writer writer, MetricMetadata metadata, DataPointSnapshot data, EscapingScheme scheme)
395      throws IOException {
396    if (createdTimestampsEnabled && data.hasCreatedTimestamp()) {
397      writeNameAndLabels(
398          writer, getMetadataName(metadata, scheme), "_created", data.getLabels(), scheme);
399      writeOpenMetricsTimestamp(writer, data.getCreatedTimestampMillis());
400      if (data.hasScrapeTimestamp()) {
401        writer.write(' ');
402        writeOpenMetricsTimestamp(writer, data.getScrapeTimestampMillis());
403      }
404      writer.write('\n');
405    }
406  }
407
408  private void writeNameAndLabels(
409      Writer writer,
410      String name,
411      @Nullable String suffix,
412      Labels labels,
413      EscapingScheme escapingScheme)
414      throws IOException {
415    writeNameAndLabels(writer, name, suffix, labels, escapingScheme, null, 0.0);
416  }
417
418  private void writeNameAndLabels(
419      Writer writer,
420      String name,
421      @Nullable String suffix,
422      Labels labels,
423      EscapingScheme escapingScheme,
424      @Nullable String additionalLabelName,
425      double additionalLabelValue)
426      throws IOException {
427    boolean metricInsideBraces = false;
428    // If the name does not pass the legacy validity check, we must put the
429    // metric name inside the braces.
430    if (!PrometheusNaming.isValidLegacyMetricName(name)) {
431      metricInsideBraces = true;
432      writer.write('{');
433    }
434    writeName(writer, name + (suffix != null ? suffix : ""), NameType.Metric);
435    if (!labels.isEmpty() || additionalLabelName != null) {
436      writeLabels(
437          writer,
438          labels,
439          additionalLabelName,
440          additionalLabelValue,
441          metricInsideBraces,
442          escapingScheme);
443    } else if (metricInsideBraces) {
444      writer.write('}');
445    }
446    writer.write(' ');
447  }
448
449  private void writeScrapeTimestampAndExemplar(
450      Writer writer, DataPointSnapshot data, @Nullable Exemplar exemplar, EscapingScheme scheme)
451      throws IOException {
452    if (data.hasScrapeTimestamp()) {
453      writer.write(' ');
454      writeOpenMetricsTimestamp(writer, data.getScrapeTimestampMillis());
455    }
456    if (exemplar != null) {
457      writer.write(" # ");
458      writeLabels(writer, exemplar.getLabels(), null, 0, false, scheme);
459      writer.write(' ');
460      writeDouble(writer, exemplar.getValue());
461      if (exemplar.hasTimestamp()) {
462        writer.write(' ');
463        writeOpenMetricsTimestamp(writer, exemplar.getTimestampMillis());
464      }
465    }
466    writer.write('\n');
467  }
468
469  private void writeMetadata(
470      Writer writer, String typeName, MetricMetadata metadata, EscapingScheme scheme)
471      throws IOException {
472    writer.write("# TYPE ");
473    writeName(writer, getMetadataName(metadata, scheme), NameType.Metric);
474    writer.write(' ');
475    writer.write(typeName);
476    writer.write('\n');
477    if (metadata.getUnit() != null) {
478      writer.write("# UNIT ");
479      writeName(writer, getMetadataName(metadata, scheme), NameType.Metric);
480      writer.write(' ');
481      writeEscapedString(writer, metadata.getUnit().toString());
482      writer.write('\n');
483    }
484    if (metadata.getHelp() != null && !metadata.getHelp().isEmpty()) {
485      writer.write("# HELP ");
486      writeName(writer, getMetadataName(metadata, scheme), NameType.Metric);
487      writer.write(' ');
488      writeEscapedString(writer, metadata.getHelp());
489      writer.write('\n');
490    }
491  }
492}