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    boolean nameValidLegacy = PrometheusNaming.isValidLegacyMetricName(baseName);
172    for (DataPointSnapshot data : snapshot.getDataPoints()) {
173      if (data.hasCreatedTimestamp()) {
174        if (!metadataWritten) {
175          writeMetadataWithFullName(writer, baseName + "_created", "gauge", metadata);
176          metadataWritten = true;
177        }
178        writeNameAndLabels(writer, baseName, "_created", data.getLabels(), scheme, nameValidLegacy);
179        writePrometheusTimestamp(writer, data.getCreatedTimestampMillis(), timestampsInMs);
180        writeScrapeTimestampAndNewline(writer, data);
181      }
182    }
183  }
184
185  private void writeCounter(Writer writer, CounterSnapshot snapshot, EscapingScheme scheme)
186      throws IOException {
187    if (!snapshot.getDataPoints().isEmpty()) {
188      MetricMetadata metadata = snapshot.getMetadata();
189      String counterName = resolveExpositionName(metadata, "_total", scheme);
190      boolean nameValidLegacy = PrometheusNaming.isValidLegacyMetricName(counterName);
191      writeMetadataWithFullName(writer, counterName, "counter", metadata);
192      for (CounterSnapshot.CounterDataPointSnapshot data : snapshot.getDataPoints()) {
193        writeNameAndLabels(writer, counterName, null, data.getLabels(), scheme, nameValidLegacy);
194        writeDouble(writer, data.getValue());
195        writeScrapeTimestampAndNewline(writer, data);
196      }
197    }
198  }
199
200  private void writeGauge(
201      Writer writer, GaugeSnapshot snapshot, String rawOriginalName, EscapingScheme scheme)
202      throws IOException {
203    MetricMetadata metadata = snapshot.getMetadata();
204    String gaugeName = getLegacyGaugeName(metadata, rawOriginalName, scheme);
205    boolean nameValidLegacy = PrometheusNaming.isValidLegacyMetricName(gaugeName);
206    writeMetadataWithFullName(writer, gaugeName, "gauge", metadata);
207    for (GaugeSnapshot.GaugeDataPointSnapshot data : snapshot.getDataPoints()) {
208      writeNameAndLabels(writer, gaugeName, null, data.getLabels(), scheme, nameValidLegacy);
209      writeDouble(writer, data.getValue());
210      writeScrapeTimestampAndNewline(writer, data);
211    }
212  }
213
214  private void writeHistogram(Writer writer, HistogramSnapshot snapshot, EscapingScheme scheme)
215      throws IOException {
216    MetricMetadata metadata = snapshot.getMetadata();
217    writeMetadata(writer, null, "histogram", metadata, scheme);
218    String name = getMetadataName(metadata, scheme);
219    String bucketName = name + "_bucket";
220    String countName = name + "_count";
221    String sumName = name + "_sum";
222    boolean nameValidLegacy = PrometheusNaming.isValidLegacyMetricName(name);
223    for (HistogramSnapshot.HistogramDataPointSnapshot data : snapshot.getDataPoints()) {
224      ClassicHistogramBuckets buckets = getClassicBuckets(data);
225      long cumulativeCount = 0;
226      for (int i = 0; i < buckets.size(); i++) {
227        cumulativeCount += buckets.getCount(i);
228        writeNameAndLabels(
229            writer,
230            bucketName,
231            null,
232            data.getLabels(),
233            scheme,
234            "le",
235            buckets.getUpperBound(i),
236            nameValidLegacy);
237        writeLong(writer, cumulativeCount);
238        writeScrapeTimestampAndNewline(writer, data);
239      }
240      if (!snapshot.isGaugeHistogram()) {
241        if (data.hasCount()) {
242          writeNameAndLabels(writer, countName, null, data.getLabels(), scheme, nameValidLegacy);
243          writeLong(writer, data.getCount());
244          writeScrapeTimestampAndNewline(writer, data);
245        }
246        if (data.hasSum()) {
247          writeNameAndLabels(writer, sumName, null, data.getLabels(), scheme, nameValidLegacy);
248          writeDouble(writer, data.getSum());
249          writeScrapeTimestampAndNewline(writer, data);
250        }
251      }
252    }
253    if (snapshot.isGaugeHistogram()) {
254      writeGaugeCountSum(writer, snapshot, metadata, scheme);
255    }
256  }
257
258  private ClassicHistogramBuckets getClassicBuckets(
259      HistogramSnapshot.HistogramDataPointSnapshot data) {
260    if (data.getClassicBuckets().isEmpty()) {
261      return ClassicHistogramBuckets.of(
262          new double[] {Double.POSITIVE_INFINITY}, new long[] {data.getCount()});
263    } else {
264      return data.getClassicBuckets();
265    }
266  }
267
268  private void writeGaugeCountSum(
269      Writer writer, HistogramSnapshot snapshot, MetricMetadata metadata, EscapingScheme scheme)
270      throws IOException {
271    // Prometheus text format does not support gaugehistogram's _gcount and _gsum.
272    // So we append _gcount and _gsum as gauge metrics.
273    String baseName = getMetadataName(metadata, scheme);
274    String gaugeCountName = baseName + "_gcount";
275    String gaugeSumName = baseName + "_gsum";
276    boolean nameValidLegacy = PrometheusNaming.isValidLegacyMetricName(baseName);
277    boolean metadataWritten = false;
278    for (HistogramSnapshot.HistogramDataPointSnapshot data : snapshot.getDataPoints()) {
279      if (data.hasCount()) {
280        if (!metadataWritten) {
281          writeMetadata(writer, "_gcount", "gauge", metadata, scheme);
282          metadataWritten = true;
283        }
284        writeNameAndLabels(writer, gaugeCountName, null, data.getLabels(), scheme, nameValidLegacy);
285        writeLong(writer, data.getCount());
286        writeScrapeTimestampAndNewline(writer, data);
287      }
288    }
289    metadataWritten = false;
290    for (HistogramSnapshot.HistogramDataPointSnapshot data : snapshot.getDataPoints()) {
291      if (data.hasSum()) {
292        if (!metadataWritten) {
293          writeMetadata(writer, "_gsum", "gauge", metadata, scheme);
294          metadataWritten = true;
295        }
296        writeNameAndLabels(writer, gaugeSumName, null, data.getLabels(), scheme, nameValidLegacy);
297        writeDouble(writer, data.getSum());
298        writeScrapeTimestampAndNewline(writer, data);
299      }
300    }
301  }
302
303  private void writeSummary(Writer writer, SummarySnapshot snapshot, EscapingScheme scheme)
304      throws IOException {
305    boolean metadataWritten = false;
306    MetricMetadata metadata = snapshot.getMetadata();
307    String name = getMetadataName(metadata, scheme);
308    String countName = name + "_count";
309    String sumName = name + "_sum";
310    boolean nameValidLegacy = PrometheusNaming.isValidLegacyMetricName(name);
311    for (SummarySnapshot.SummaryDataPointSnapshot data : snapshot.getDataPoints()) {
312      if (data.getQuantiles().size() == 0 && !data.hasCount() && !data.hasSum()) {
313        continue;
314      }
315      if (!metadataWritten) {
316        writeMetadata(writer, null, "summary", metadata, scheme);
317        metadataWritten = true;
318      }
319      for (Quantile quantile : data.getQuantiles()) {
320        writeNameAndLabels(
321            writer,
322            name,
323            null,
324            data.getLabels(),
325            scheme,
326            "quantile",
327            quantile.getQuantile(),
328            nameValidLegacy);
329        writeDouble(writer, quantile.getValue());
330        writeScrapeTimestampAndNewline(writer, data);
331      }
332      if (data.hasCount()) {
333        writeNameAndLabels(writer, countName, null, data.getLabels(), scheme, nameValidLegacy);
334        writeLong(writer, data.getCount());
335        writeScrapeTimestampAndNewline(writer, data);
336      }
337      if (data.hasSum()) {
338        writeNameAndLabels(writer, sumName, null, data.getLabels(), scheme, nameValidLegacy);
339        writeDouble(writer, data.getSum());
340        writeScrapeTimestampAndNewline(writer, data);
341      }
342    }
343  }
344
345  private void writeInfo(Writer writer, InfoSnapshot snapshot, EscapingScheme scheme)
346      throws IOException {
347    MetricMetadata metadata = snapshot.getMetadata();
348    String infoName = resolveExpositionName(metadata, "_info", scheme);
349    boolean nameValidLegacy = PrometheusNaming.isValidLegacyMetricName(infoName);
350    writeMetadataWithFullName(writer, infoName, "gauge", metadata);
351    for (InfoSnapshot.InfoDataPointSnapshot data : snapshot.getDataPoints()) {
352      writeNameAndLabels(writer, infoName, null, data.getLabels(), scheme, nameValidLegacy);
353      writer.write("1");
354      writeScrapeTimestampAndNewline(writer, data);
355    }
356  }
357
358  private void writeStateSet(Writer writer, StateSetSnapshot snapshot, EscapingScheme scheme)
359      throws IOException {
360    MetricMetadata metadata = snapshot.getMetadata();
361    writeMetadata(writer, null, "gauge", metadata, scheme);
362    String name = getMetadataName(metadata, scheme);
363    for (StateSetSnapshot.StateSetDataPointSnapshot data : snapshot.getDataPoints()) {
364      for (int i = 0; i < data.size(); i++) {
365        writer.write(name);
366        writer.write('{');
367        for (int j = 0; j < data.getLabels().size(); j++) {
368          if (j > 0) {
369            writer.write(",");
370          }
371          writer.write(getSnapshotLabelName(data.getLabels(), j, scheme));
372          writer.write("=\"");
373          writeEscapedString(writer, data.getLabels().getValue(j));
374          writer.write("\"");
375        }
376        if (!data.getLabels().isEmpty()) {
377          writer.write(",");
378        }
379        writer.write(name);
380        writer.write("=\"");
381        writeEscapedString(writer, data.getName(i));
382        writer.write("\"} ");
383        if (data.isTrue(i)) {
384          writer.write("1");
385        } else {
386          writer.write("0");
387        }
388        writeScrapeTimestampAndNewline(writer, data);
389      }
390    }
391  }
392
393  private void writeUnknown(Writer writer, UnknownSnapshot snapshot, EscapingScheme scheme)
394      throws IOException {
395    MetricMetadata metadata = snapshot.getMetadata();
396    writeMetadata(writer, null, "untyped", metadata, scheme);
397    String name = getMetadataName(metadata, scheme);
398    boolean nameValidLegacy = PrometheusNaming.isValidLegacyMetricName(name);
399    for (UnknownSnapshot.UnknownDataPointSnapshot data : snapshot.getDataPoints()) {
400      writeNameAndLabels(writer, name, null, data.getLabels(), scheme, nameValidLegacy);
401      writeDouble(writer, data.getValue());
402      writeScrapeTimestampAndNewline(writer, data);
403    }
404  }
405
406  private void writeNameAndLabels(
407      Writer writer,
408      String name,
409      @Nullable String suffix,
410      Labels labels,
411      EscapingScheme escapingScheme,
412      boolean nameValidLegacy)
413      throws IOException {
414    writeNameAndLabels(writer, name, suffix, labels, escapingScheme, null, 0.0, nameValidLegacy);
415  }
416
417  private void writeNameAndLabels(
418      Writer writer,
419      String name,
420      @Nullable String suffix,
421      Labels labels,
422      EscapingScheme scheme,
423      @Nullable String additionalLabelName,
424      double additionalLabelValue,
425      boolean nameValidLegacy)
426      throws IOException {
427    boolean metricInsideBraces = false;
428    // If the name does not pass the legacy validity check, we must put the
429    // metric name inside the braces. Validity is scanned once per family by the caller (the metric
430    // name is constant across all data points), not re-scanned for every series here. Appending a
431    // suffix built from legacy characters (_total, _bucket, ...) does not change legacy validity,
432    // so the caller's check of the base name applies to the suffixed name too.
433    if (!nameValidLegacy) {
434      metricInsideBraces = true;
435      writer.write('{');
436    }
437    writeName(writer, suffix != null ? name + suffix : name, NameType.Metric, nameValidLegacy);
438    if (!labels.isEmpty() || additionalLabelName != null) {
439      writeLabels(
440          writer, labels, additionalLabelName, additionalLabelValue, metricInsideBraces, scheme);
441    } else if (metricInsideBraces) {
442      writer.write('}');
443    }
444    writer.write(' ');
445  }
446
447  private void writeMetadata(
448      Writer writer,
449      @Nullable String suffix,
450      String typeString,
451      MetricMetadata metadata,
452      EscapingScheme scheme)
453      throws IOException {
454    String baseName = getMetadataName(metadata, scheme);
455    String name = suffix != null ? baseName + suffix : baseName;
456    if (metadata.getHelp() != null && !metadata.getHelp().isEmpty()) {
457      writer.write("# HELP ");
458      writeName(writer, name, NameType.Metric);
459      writer.write(' ');
460      writeEscapedHelp(writer, metadata.getHelp());
461      writer.write('\n');
462    }
463    writer.write("# TYPE ");
464    writeName(writer, name, NameType.Metric);
465    writer.write(' ');
466    writer.write(typeString);
467    writer.write('\n');
468  }
469
470  private void writeMetadataWithFullName(
471      Writer writer, String fullName, String typeString, MetricMetadata metadata)
472      throws IOException {
473    if (metadata.getHelp() != null && !metadata.getHelp().isEmpty()) {
474      writer.write("# HELP ");
475      writeName(writer, fullName, NameType.Metric);
476      writer.write(' ');
477      writeEscapedHelp(writer, metadata.getHelp());
478      writer.write('\n');
479    }
480    writer.write("# TYPE ");
481    writeName(writer, fullName, NameType.Metric);
482    writer.write(' ');
483    writer.write(typeString);
484    writer.write('\n');
485  }
486
487  /**
488   * Returns the full exposition name for a metric. If the original name already ends with the given
489   * suffix (e.g. "_total" for counters), uses the original name directly. Otherwise, appends the
490   * suffix to the base name.
491   */
492  private static String resolveExpositionName(
493      MetricMetadata metadata, String suffix, EscapingScheme scheme) {
494    String expositionBaseName = getExpositionBaseMetadataName(metadata, scheme);
495    if (expositionBaseName.endsWith(suffix)) {
496      return expositionBaseName;
497    }
498    return getMetadataName(metadata, scheme) + suffix;
499  }
500
501  private static String resolveBaseName(String fullName, String suffix) {
502    if (fullName.endsWith(suffix)) {
503      return fullName.substring(0, fullName.length() - suffix.length());
504    }
505    return fullName;
506  }
507
508  private void writeEscapedHelp(Writer writer, String s) throws IOException {
509    for (int i = 0; i < s.length(); i++) {
510      char c = s.charAt(i);
511      switch (c) {
512        case '\\':
513          writer.append("\\\\");
514          break;
515        case '\n':
516          writer.append("\\n");
517          break;
518        default:
519          writer.append(c);
520      }
521    }
522  }
523
524  private void writeScrapeTimestampAndNewline(Writer writer, DataPointSnapshot data)
525      throws IOException {
526    if (data.hasScrapeTimestamp()) {
527      writer.write(' ');
528      writePrometheusTimestamp(writer, data.getScrapeTimestampMillis(), timestampsInMs);
529    }
530    writer.write('\n');
531  }
532}