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