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