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