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