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