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