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