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