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    for (MetricSnapshot s : metricSnapshots) {
117      MetricSnapshot snapshot = SnapshotEscaper.escapeMetricSnapshot(s, scheme);
118      if (!snapshot.getDataPoints().isEmpty()) {
119        if (snapshot instanceof CounterSnapshot) {
120          writeCounter(writer, (CounterSnapshot) snapshot, scheme);
121        } else if (snapshot instanceof GaugeSnapshot) {
122          writeGauge(writer, (GaugeSnapshot) snapshot, scheme);
123        } else if (snapshot instanceof HistogramSnapshot) {
124          writeHistogram(writer, (HistogramSnapshot) snapshot, scheme);
125        } else if (snapshot instanceof SummarySnapshot) {
126          writeSummary(writer, (SummarySnapshot) snapshot, scheme);
127        } else if (snapshot instanceof InfoSnapshot) {
128          writeInfo(writer, (InfoSnapshot) snapshot, scheme);
129        } else if (snapshot instanceof StateSetSnapshot) {
130          writeStateSet(writer, (StateSetSnapshot) snapshot, scheme);
131        } else if (snapshot instanceof UnknownSnapshot) {
132          writeUnknown(writer, (UnknownSnapshot) snapshot, scheme);
133        }
134      }
135    }
136    writer.write("# EOF\n");
137    writer.flush();
138  }
139
140  private void writeCounter(Writer writer, CounterSnapshot snapshot, EscapingScheme scheme)
141      throws IOException {
142    MetricMetadata metadata = snapshot.getMetadata();
143    writeMetadata(writer, "counter", metadata, scheme);
144    for (CounterSnapshot.CounterDataPointSnapshot data : snapshot.getDataPoints()) {
145      writeNameAndLabels(
146          writer, getMetadataName(metadata, scheme), "_total", data.getLabels(), scheme);
147      writeDouble(writer, data.getValue());
148      writeScrapeTimestampAndExemplar(writer, data, data.getExemplar(), scheme);
149      writeCreated(writer, metadata, data, scheme);
150    }
151  }
152
153  private void writeGauge(Writer writer, GaugeSnapshot snapshot, EscapingScheme scheme)
154      throws IOException {
155    MetricMetadata metadata = snapshot.getMetadata();
156    writeMetadata(writer, "gauge", metadata, scheme);
157    for (GaugeSnapshot.GaugeDataPointSnapshot data : snapshot.getDataPoints()) {
158      writeNameAndLabels(writer, getMetadataName(metadata, scheme), null, data.getLabels(), scheme);
159      writeDouble(writer, data.getValue());
160      if (exemplarsOnAllMetricTypesEnabled) {
161        writeScrapeTimestampAndExemplar(writer, data, data.getExemplar(), scheme);
162      } else {
163        writeScrapeTimestampAndExemplar(writer, data, null, scheme);
164      }
165    }
166  }
167
168  private void writeHistogram(Writer writer, HistogramSnapshot snapshot, EscapingScheme scheme)
169      throws IOException {
170    MetricMetadata metadata = snapshot.getMetadata();
171    if (snapshot.isGaugeHistogram()) {
172      writeMetadata(writer, "gaugehistogram", metadata, scheme);
173      writeClassicHistogramBuckets(
174          writer, metadata, "_gcount", "_gsum", snapshot.getDataPoints(), scheme);
175    } else {
176      writeMetadata(writer, "histogram", metadata, scheme);
177      writeClassicHistogramBuckets(
178          writer, metadata, "_count", "_sum", snapshot.getDataPoints(), scheme);
179    }
180  }
181
182  private void writeClassicHistogramBuckets(
183      Writer writer,
184      MetricMetadata metadata,
185      String countSuffix,
186      String sumSuffix,
187      List<HistogramSnapshot.HistogramDataPointSnapshot> dataList,
188      EscapingScheme scheme)
189      throws IOException {
190    for (HistogramSnapshot.HistogramDataPointSnapshot data : dataList) {
191      ClassicHistogramBuckets buckets = getClassicBuckets(data);
192      Exemplars exemplars = data.getExemplars();
193      long cumulativeCount = 0;
194      for (int i = 0; i < buckets.size(); i++) {
195        cumulativeCount += buckets.getCount(i);
196        writeNameAndLabels(
197            writer,
198            getMetadataName(metadata, scheme),
199            "_bucket",
200            data.getLabels(),
201            scheme,
202            "le",
203            buckets.getUpperBound(i));
204        writeLong(writer, cumulativeCount);
205        Exemplar exemplar;
206        if (i == 0) {
207          exemplar = exemplars.get(Double.NEGATIVE_INFINITY, buckets.getUpperBound(i));
208        } else {
209          exemplar = exemplars.get(buckets.getUpperBound(i - 1), buckets.getUpperBound(i));
210        }
211        writeScrapeTimestampAndExemplar(writer, data, exemplar, scheme);
212      }
213      // In OpenMetrics format, histogram _count and _sum are either both present or both absent.
214      if (data.hasCount() && data.hasSum()) {
215        writeCountAndSum(writer, metadata, data, countSuffix, sumSuffix, exemplars, scheme);
216      }
217      writeCreated(writer, metadata, data, scheme);
218    }
219  }
220
221  private ClassicHistogramBuckets getClassicBuckets(
222      HistogramSnapshot.HistogramDataPointSnapshot data) {
223    if (data.getClassicBuckets().isEmpty()) {
224      return ClassicHistogramBuckets.of(
225          new double[] {Double.POSITIVE_INFINITY}, new long[] {data.getCount()});
226    } else {
227      return data.getClassicBuckets();
228    }
229  }
230
231  private void writeSummary(Writer writer, SummarySnapshot snapshot, EscapingScheme scheme)
232      throws IOException {
233    boolean metadataWritten = false;
234    MetricMetadata metadata = snapshot.getMetadata();
235    for (SummarySnapshot.SummaryDataPointSnapshot data : snapshot.getDataPoints()) {
236      if (data.getQuantiles().size() == 0 && !data.hasCount() && !data.hasSum()) {
237        continue;
238      }
239      if (!metadataWritten) {
240        writeMetadata(writer, "summary", metadata, scheme);
241        metadataWritten = true;
242      }
243      Exemplars exemplars = data.getExemplars();
244      // Exemplars for summaries are new, and there's no best practice yet which Exemplars to choose
245      // for which
246      // time series. We select exemplars[0] for _count, exemplars[1] for _sum, and exemplars[2...]
247      // for the
248      // quantiles, all indexes modulo exemplars.length.
249      int exemplarIndex = 1;
250      for (Quantile quantile : data.getQuantiles()) {
251        writeNameAndLabels(
252            writer,
253            getMetadataName(metadata, scheme),
254            null,
255            data.getLabels(),
256            scheme,
257            "quantile",
258            quantile.getQuantile());
259        writeDouble(writer, quantile.getValue());
260        if (exemplars.size() > 0 && exemplarsOnAllMetricTypesEnabled) {
261          exemplarIndex = (exemplarIndex + 1) % exemplars.size();
262          writeScrapeTimestampAndExemplar(writer, data, exemplars.get(exemplarIndex), scheme);
263        } else {
264          writeScrapeTimestampAndExemplar(writer, data, null, scheme);
265        }
266      }
267      // Unlike histograms, summaries can have only a count or only a sum according to OpenMetrics.
268      writeCountAndSum(writer, metadata, data, "_count", "_sum", exemplars, scheme);
269      writeCreated(writer, metadata, data, scheme);
270    }
271  }
272
273  private void writeInfo(Writer writer, InfoSnapshot snapshot, EscapingScheme scheme)
274      throws IOException {
275    MetricMetadata metadata = snapshot.getMetadata();
276    writeMetadata(writer, "info", metadata, scheme);
277    for (InfoSnapshot.InfoDataPointSnapshot data : snapshot.getDataPoints()) {
278      writeNameAndLabels(
279          writer, getMetadataName(metadata, scheme), "_info", data.getLabels(), scheme);
280      writer.write("1");
281      writeScrapeTimestampAndExemplar(writer, data, null, scheme);
282    }
283  }
284
285  private void writeStateSet(Writer writer, StateSetSnapshot snapshot, EscapingScheme scheme)
286      throws IOException {
287    MetricMetadata metadata = snapshot.getMetadata();
288    writeMetadata(writer, "stateset", metadata, scheme);
289    for (StateSetSnapshot.StateSetDataPointSnapshot data : snapshot.getDataPoints()) {
290      for (int i = 0; i < data.size(); i++) {
291        writer.write(getMetadataName(metadata, scheme));
292        writer.write('{');
293        Labels labels = data.getLabels();
294        for (int j = 0; j < labels.size(); j++) {
295          if (j > 0) {
296            writer.write(",");
297          }
298          writer.write(getSnapshotLabelName(labels, j, scheme));
299          writer.write("=\"");
300          writeEscapedString(writer, labels.getValue(j));
301          writer.write("\"");
302        }
303        if (!labels.isEmpty()) {
304          writer.write(",");
305        }
306        writer.write(getMetadataName(metadata, scheme));
307        writer.write("=\"");
308        writeEscapedString(writer, data.getName(i));
309        writer.write("\"} ");
310        if (data.isTrue(i)) {
311          writer.write("1");
312        } else {
313          writer.write("0");
314        }
315        writeScrapeTimestampAndExemplar(writer, data, null, scheme);
316      }
317    }
318  }
319
320  private void writeUnknown(Writer writer, UnknownSnapshot snapshot, EscapingScheme scheme)
321      throws IOException {
322    MetricMetadata metadata = snapshot.getMetadata();
323    writeMetadata(writer, "unknown", metadata, scheme);
324    for (UnknownSnapshot.UnknownDataPointSnapshot data : snapshot.getDataPoints()) {
325      writeNameAndLabels(writer, getMetadataName(metadata, scheme), null, data.getLabels(), scheme);
326      writeDouble(writer, data.getValue());
327      if (exemplarsOnAllMetricTypesEnabled) {
328        writeScrapeTimestampAndExemplar(writer, data, data.getExemplar(), scheme);
329      } else {
330        writeScrapeTimestampAndExemplar(writer, data, null, scheme);
331      }
332    }
333  }
334
335  private void writeCountAndSum(
336      Writer writer,
337      MetricMetadata metadata,
338      DistributionDataPointSnapshot data,
339      String countSuffix,
340      String sumSuffix,
341      Exemplars exemplars,
342      EscapingScheme scheme)
343      throws IOException {
344    if (data.hasCount()) {
345      writeNameAndLabels(
346          writer, getMetadataName(metadata, scheme), countSuffix, data.getLabels(), scheme);
347      writeLong(writer, data.getCount());
348      if (exemplarsOnAllMetricTypesEnabled) {
349        writeScrapeTimestampAndExemplar(writer, data, exemplars.getLatest(), scheme);
350      } else {
351        writeScrapeTimestampAndExemplar(writer, data, null, scheme);
352      }
353    }
354    if (data.hasSum()) {
355      writeNameAndLabels(
356          writer, getMetadataName(metadata, scheme), sumSuffix, data.getLabels(), scheme);
357      writeDouble(writer, data.getSum());
358      writeScrapeTimestampAndExemplar(writer, data, null, scheme);
359    }
360  }
361
362  private void writeCreated(
363      Writer writer, MetricMetadata metadata, DataPointSnapshot data, EscapingScheme scheme)
364      throws IOException {
365    if (createdTimestampsEnabled && data.hasCreatedTimestamp()) {
366      writeNameAndLabels(
367          writer, getMetadataName(metadata, scheme), "_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, name + (suffix != null ? suffix : ""), 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  private 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      writer.write(" # ");
427      writeLabels(writer, exemplar.getLabels(), null, 0, false, scheme);
428      writer.write(' ');
429      writeDouble(writer, exemplar.getValue());
430      if (exemplar.hasTimestamp()) {
431        writer.write(' ');
432        writeOpenMetricsTimestamp(writer, exemplar.getTimestampMillis());
433      }
434    }
435    writer.write('\n');
436  }
437
438  private void writeMetadata(
439      Writer writer, String typeName, MetricMetadata metadata, EscapingScheme scheme)
440      throws IOException {
441    writer.write("# TYPE ");
442    writeName(writer, getMetadataName(metadata, scheme), NameType.Metric);
443    writer.write(' ');
444    writer.write(typeName);
445    writer.write('\n');
446    if (metadata.getUnit() != null) {
447      writer.write("# UNIT ");
448      writeName(writer, getMetadataName(metadata, scheme), NameType.Metric);
449      writer.write(' ');
450      writeEscapedString(writer, metadata.getUnit().toString());
451      writer.write('\n');
452    }
453    if (metadata.getHelp() != null && !metadata.getHelp().isEmpty()) {
454      writer.write("# HELP ");
455      writeName(writer, getMetadataName(metadata, scheme), NameType.Metric);
456      writer.write(' ');
457      writeEscapedString(writer, metadata.getHelp());
458      writer.write('\n');
459    }
460  }
461}