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