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    for (MetricSnapshot s : metricSnapshots) {
119      MetricSnapshot snapshot = escapeMetricSnapshot(s, scheme);
120      if (!snapshot.getDataPoints().isEmpty()) {
121        if (snapshot instanceof CounterSnapshot) {
122          writeCounter(writer, (CounterSnapshot) snapshot, scheme);
123        } else if (snapshot instanceof GaugeSnapshot) {
124          writeGauge(writer, (GaugeSnapshot) snapshot, scheme);
125        } else if (snapshot instanceof HistogramSnapshot) {
126          writeHistogram(writer, (HistogramSnapshot) snapshot, scheme);
127        } else if (snapshot instanceof SummarySnapshot) {
128          writeSummary(writer, (SummarySnapshot) snapshot, scheme);
129        } else if (snapshot instanceof InfoSnapshot) {
130          writeInfo(writer, (InfoSnapshot) snapshot, scheme);
131        } else if (snapshot instanceof StateSetSnapshot) {
132          writeStateSet(writer, (StateSetSnapshot) snapshot, scheme);
133        } else if (snapshot instanceof UnknownSnapshot) {
134          writeUnknown(writer, (UnknownSnapshot) snapshot, scheme);
135        }
136      }
137    }
138    if (writeCreatedTimestamps) {
139      for (MetricSnapshot s : metricSnapshots) {
140        MetricSnapshot snapshot = escapeMetricSnapshot(s, scheme);
141        if (!snapshot.getDataPoints().isEmpty()) {
142          if (snapshot instanceof CounterSnapshot) {
143            writeCreated(writer, snapshot, scheme);
144          } else if (snapshot instanceof HistogramSnapshot) {
145            writeCreated(writer, snapshot, scheme);
146          } else if (snapshot instanceof SummarySnapshot) {
147            writeCreated(writer, snapshot, scheme);
148          }
149        }
150      }
151    }
152    writer.flush();
153  }
154
155  public void writeCreated(Writer writer, MetricSnapshot snapshot, EscapingScheme scheme)
156      throws IOException {
157    boolean metadataWritten = false;
158    MetricMetadata metadata = snapshot.getMetadata();
159    for (DataPointSnapshot data : snapshot.getDataPoints()) {
160      if (data.hasCreatedTimestamp()) {
161        if (!metadataWritten) {
162          writeMetadata(writer, "_created", "gauge", metadata, scheme);
163          metadataWritten = true;
164        }
165        writeNameAndLabels(
166            writer, getMetadataName(metadata, scheme), "_created", data.getLabels(), scheme);
167        writePrometheusTimestamp(writer, data.getCreatedTimestampMillis(), timestampsInMs);
168        writeScrapeTimestampAndNewline(writer, data);
169      }
170    }
171  }
172
173  private void writeCounter(Writer writer, CounterSnapshot snapshot, EscapingScheme scheme)
174      throws IOException {
175    if (!snapshot.getDataPoints().isEmpty()) {
176      MetricMetadata metadata = snapshot.getMetadata();
177      writeMetadata(writer, "_total", "counter", metadata, scheme);
178      for (CounterSnapshot.CounterDataPointSnapshot data : snapshot.getDataPoints()) {
179        writeNameAndLabels(
180            writer, getMetadataName(metadata, scheme), "_total", data.getLabels(), scheme);
181        writeDouble(writer, data.getValue());
182        writeScrapeTimestampAndNewline(writer, data);
183      }
184    }
185  }
186
187  private void writeGauge(Writer writer, GaugeSnapshot snapshot, EscapingScheme scheme)
188      throws IOException {
189    MetricMetadata metadata = snapshot.getMetadata();
190    writeMetadata(writer, "", "gauge", metadata, scheme);
191    for (GaugeSnapshot.GaugeDataPointSnapshot data : snapshot.getDataPoints()) {
192      writeNameAndLabels(writer, getMetadataName(metadata, scheme), null, data.getLabels(), scheme);
193      writeDouble(writer, data.getValue());
194      writeScrapeTimestampAndNewline(writer, data);
195    }
196  }
197
198  private void writeHistogram(Writer writer, HistogramSnapshot snapshot, EscapingScheme scheme)
199      throws IOException {
200    MetricMetadata metadata = snapshot.getMetadata();
201    writeMetadata(writer, "", "histogram", metadata, scheme);
202    for (HistogramSnapshot.HistogramDataPointSnapshot data : snapshot.getDataPoints()) {
203      ClassicHistogramBuckets buckets = getClassicBuckets(data);
204      long cumulativeCount = 0;
205      for (int i = 0; i < buckets.size(); i++) {
206        cumulativeCount += buckets.getCount(i);
207        writeNameAndLabels(
208            writer,
209            getMetadataName(metadata, scheme),
210            "_bucket",
211            data.getLabels(),
212            scheme,
213            "le",
214            buckets.getUpperBound(i));
215        writeLong(writer, cumulativeCount);
216        writeScrapeTimestampAndNewline(writer, data);
217      }
218      if (!snapshot.isGaugeHistogram()) {
219        if (data.hasCount()) {
220          writeNameAndLabels(
221              writer, getMetadataName(metadata, scheme), "_count", data.getLabels(), scheme);
222          writeLong(writer, data.getCount());
223          writeScrapeTimestampAndNewline(writer, data);
224        }
225        if (data.hasSum()) {
226          writeNameAndLabels(
227              writer, getMetadataName(metadata, scheme), "_sum", data.getLabels(), scheme);
228          writeDouble(writer, data.getSum());
229          writeScrapeTimestampAndNewline(writer, data);
230        }
231      }
232    }
233    if (snapshot.isGaugeHistogram()) {
234      writeGaugeCountSum(writer, snapshot, metadata, scheme);
235    }
236  }
237
238  private ClassicHistogramBuckets getClassicBuckets(
239      HistogramSnapshot.HistogramDataPointSnapshot data) {
240    if (data.getClassicBuckets().isEmpty()) {
241      return ClassicHistogramBuckets.of(
242          new double[] {Double.POSITIVE_INFINITY}, new long[] {data.getCount()});
243    } else {
244      return data.getClassicBuckets();
245    }
246  }
247
248  private void writeGaugeCountSum(
249      Writer writer, HistogramSnapshot snapshot, MetricMetadata metadata, EscapingScheme scheme)
250      throws IOException {
251    // Prometheus text format does not support gaugehistogram's _gcount and _gsum.
252    // So we append _gcount and _gsum as gauge metrics.
253    boolean metadataWritten = false;
254    for (HistogramSnapshot.HistogramDataPointSnapshot data : snapshot.getDataPoints()) {
255      if (data.hasCount()) {
256        if (!metadataWritten) {
257          writeMetadata(writer, "_gcount", "gauge", metadata, scheme);
258          metadataWritten = true;
259        }
260        writeNameAndLabels(
261            writer, getMetadataName(metadata, scheme), "_gcount", data.getLabels(), scheme);
262        writeLong(writer, data.getCount());
263        writeScrapeTimestampAndNewline(writer, data);
264      }
265    }
266    metadataWritten = false;
267    for (HistogramSnapshot.HistogramDataPointSnapshot data : snapshot.getDataPoints()) {
268      if (data.hasSum()) {
269        if (!metadataWritten) {
270          writeMetadata(writer, "_gsum", "gauge", metadata, scheme);
271          metadataWritten = true;
272        }
273        writeNameAndLabels(
274            writer, getMetadataName(metadata, scheme), "_gsum", data.getLabels(), scheme);
275        writeDouble(writer, data.getSum());
276        writeScrapeTimestampAndNewline(writer, data);
277      }
278    }
279  }
280
281  private void writeSummary(Writer writer, SummarySnapshot snapshot, EscapingScheme scheme)
282      throws IOException {
283    boolean metadataWritten = false;
284    MetricMetadata metadata = snapshot.getMetadata();
285    for (SummarySnapshot.SummaryDataPointSnapshot data : snapshot.getDataPoints()) {
286      if (data.getQuantiles().size() == 0 && !data.hasCount() && !data.hasSum()) {
287        continue;
288      }
289      if (!metadataWritten) {
290        writeMetadata(writer, "", "summary", metadata, scheme);
291        metadataWritten = true;
292      }
293      for (Quantile quantile : data.getQuantiles()) {
294        writeNameAndLabels(
295            writer,
296            getMetadataName(metadata, scheme),
297            null,
298            data.getLabels(),
299            scheme,
300            "quantile",
301            quantile.getQuantile());
302        writeDouble(writer, quantile.getValue());
303        writeScrapeTimestampAndNewline(writer, data);
304      }
305      if (data.hasCount()) {
306        writeNameAndLabels(
307            writer, getMetadataName(metadata, scheme), "_count", data.getLabels(), scheme);
308        writeLong(writer, data.getCount());
309        writeScrapeTimestampAndNewline(writer, data);
310      }
311      if (data.hasSum()) {
312        writeNameAndLabels(
313            writer, getMetadataName(metadata, scheme), "_sum", data.getLabels(), scheme);
314        writeDouble(writer, data.getSum());
315        writeScrapeTimestampAndNewline(writer, data);
316      }
317    }
318  }
319
320  private void writeInfo(Writer writer, InfoSnapshot snapshot, EscapingScheme scheme)
321      throws IOException {
322    MetricMetadata metadata = snapshot.getMetadata();
323    writeMetadata(writer, "_info", "gauge", metadata, scheme);
324    for (InfoSnapshot.InfoDataPointSnapshot data : snapshot.getDataPoints()) {
325      writeNameAndLabels(
326          writer, getMetadataName(metadata, scheme), "_info", data.getLabels(), scheme);
327      writer.write("1");
328      writeScrapeTimestampAndNewline(writer, data);
329    }
330  }
331
332  private void writeStateSet(Writer writer, StateSetSnapshot snapshot, EscapingScheme scheme)
333      throws IOException {
334    MetricMetadata metadata = snapshot.getMetadata();
335    writeMetadata(writer, "", "gauge", metadata, scheme);
336    for (StateSetSnapshot.StateSetDataPointSnapshot data : snapshot.getDataPoints()) {
337      for (int i = 0; i < data.size(); i++) {
338        writer.write(getMetadataName(metadata, scheme));
339        writer.write('{');
340        for (int j = 0; j < data.getLabels().size(); j++) {
341          if (j > 0) {
342            writer.write(",");
343          }
344          writer.write(getSnapshotLabelName(data.getLabels(), j, scheme));
345          writer.write("=\"");
346          writeEscapedString(writer, data.getLabels().getValue(j));
347          writer.write("\"");
348        }
349        if (!data.getLabels().isEmpty()) {
350          writer.write(",");
351        }
352        writer.write(getMetadataName(metadata, scheme));
353        writer.write("=\"");
354        writeEscapedString(writer, data.getName(i));
355        writer.write("\"} ");
356        if (data.isTrue(i)) {
357          writer.write("1");
358        } else {
359          writer.write("0");
360        }
361        writeScrapeTimestampAndNewline(writer, data);
362      }
363    }
364  }
365
366  private void writeUnknown(Writer writer, UnknownSnapshot snapshot, EscapingScheme scheme)
367      throws IOException {
368    MetricMetadata metadata = snapshot.getMetadata();
369    writeMetadata(writer, "", "untyped", metadata, scheme);
370    for (UnknownSnapshot.UnknownDataPointSnapshot data : snapshot.getDataPoints()) {
371      writeNameAndLabels(writer, getMetadataName(metadata, scheme), null, data.getLabels(), scheme);
372      writeDouble(writer, data.getValue());
373      writeScrapeTimestampAndNewline(writer, data);
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 scheme,
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.isValidLegacyLabelName(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, labels, additionalLabelName, additionalLabelValue, metricInsideBraces, scheme);
407    } else if (metricInsideBraces) {
408      writer.write('}');
409    }
410    writer.write(' ');
411  }
412
413  private void writeMetadata(
414      Writer writer,
415      @Nullable String suffix,
416      String typeString,
417      MetricMetadata metadata,
418      EscapingScheme scheme)
419      throws IOException {
420    String name = getMetadataName(metadata, scheme) + (suffix != null ? suffix : "");
421    if (metadata.getHelp() != null && !metadata.getHelp().isEmpty()) {
422      writer.write("# HELP ");
423      writeName(writer, name, NameType.Metric);
424      writer.write(' ');
425      writeEscapedHelp(writer, metadata.getHelp());
426      writer.write('\n');
427    }
428    writer.write("# TYPE ");
429    writeName(writer, name, NameType.Metric);
430    writer.write(' ');
431    writer.write(typeString);
432    writer.write('\n');
433  }
434
435  private void writeEscapedHelp(Writer writer, String s) throws IOException {
436    for (int i = 0; i < s.length(); i++) {
437      char c = s.charAt(i);
438      switch (c) {
439        case '\\':
440          writer.append("\\\\");
441          break;
442        case '\n':
443          writer.append("\\n");
444          break;
445        default:
446          writer.append(c);
447      }
448    }
449  }
450
451  private void writeScrapeTimestampAndNewline(Writer writer, DataPointSnapshot data)
452      throws IOException {
453    if (data.hasScrapeTimestamp()) {
454      writer.write(' ');
455      writePrometheusTimestamp(writer, data.getScrapeTimestampMillis(), timestampsInMs);
456    }
457    writer.write('\n');
458  }
459}