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