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