001package io.prometheus.metrics.expositionformats;
002
003import static io.prometheus.metrics.expositionformats.TextFormatUtil.writeDouble;
004import static io.prometheus.metrics.expositionformats.TextFormatUtil.writeEscapedLabelValue;
005import static io.prometheus.metrics.expositionformats.TextFormatUtil.writeLabels;
006import static io.prometheus.metrics.expositionformats.TextFormatUtil.writeLong;
007import static io.prometheus.metrics.expositionformats.TextFormatUtil.writeTimestamp;
008
009import io.prometheus.metrics.model.snapshots.ClassicHistogramBuckets;
010import io.prometheus.metrics.model.snapshots.CounterSnapshot;
011import io.prometheus.metrics.model.snapshots.DataPointSnapshot;
012import io.prometheus.metrics.model.snapshots.GaugeSnapshot;
013import io.prometheus.metrics.model.snapshots.HistogramSnapshot;
014import io.prometheus.metrics.model.snapshots.InfoSnapshot;
015import io.prometheus.metrics.model.snapshots.Labels;
016import io.prometheus.metrics.model.snapshots.MetricMetadata;
017import io.prometheus.metrics.model.snapshots.MetricSnapshot;
018import io.prometheus.metrics.model.snapshots.MetricSnapshots;
019import io.prometheus.metrics.model.snapshots.Quantile;
020import io.prometheus.metrics.model.snapshots.StateSetSnapshot;
021import io.prometheus.metrics.model.snapshots.SummarySnapshot;
022import io.prometheus.metrics.model.snapshots.UnknownSnapshot;
023import java.io.IOException;
024import java.io.OutputStream;
025import java.io.OutputStreamWriter;
026import java.io.Writer;
027import java.nio.charset.StandardCharsets;
028
029/**
030 * Write the Prometheus text format. This is the default if you view a Prometheus endpoint with your
031 * Web browser.
032 */
033public class PrometheusTextFormatWriter implements ExpositionFormatWriter {
034
035  public static final String CONTENT_TYPE = "text/plain; version=0.0.4; charset=utf-8";
036
037  private final boolean writeCreatedTimestamps;
038
039  public PrometheusTextFormatWriter(boolean writeCreatedTimestamps) {
040    this.writeCreatedTimestamps = writeCreatedTimestamps;
041  }
042
043  @Override
044  public boolean accepts(String acceptHeader) {
045    if (acceptHeader == null) {
046      return false;
047    } else {
048      return acceptHeader.contains("text/plain");
049    }
050  }
051
052  @Override
053  public String getContentType() {
054    return CONTENT_TYPE;
055  }
056
057  @Override
058  public void write(OutputStream out, MetricSnapshots metricSnapshots) throws IOException {
059    // See https://prometheus.io/docs/instrumenting/exposition_formats/
060    // "unknown", "gauge", "counter", "stateset", "info", "histogram", "gaugehistogram", and
061    // "summary".
062    OutputStreamWriter writer = new OutputStreamWriter(out, StandardCharsets.UTF_8);
063    for (MetricSnapshot snapshot : metricSnapshots) {
064      if (snapshot.getDataPoints().size() > 0) {
065        if (snapshot instanceof CounterSnapshot) {
066          writeCounter(writer, (CounterSnapshot) snapshot);
067        } else if (snapshot instanceof GaugeSnapshot) {
068          writeGauge(writer, (GaugeSnapshot) snapshot);
069        } else if (snapshot instanceof HistogramSnapshot) {
070          writeHistogram(writer, (HistogramSnapshot) snapshot);
071        } else if (snapshot instanceof SummarySnapshot) {
072          writeSummary(writer, (SummarySnapshot) snapshot);
073        } else if (snapshot instanceof InfoSnapshot) {
074          writeInfo(writer, (InfoSnapshot) snapshot);
075        } else if (snapshot instanceof StateSetSnapshot) {
076          writeStateSet(writer, (StateSetSnapshot) snapshot);
077        } else if (snapshot instanceof UnknownSnapshot) {
078          writeUnknown(writer, (UnknownSnapshot) snapshot);
079        }
080      }
081    }
082    if (writeCreatedTimestamps) {
083      for (MetricSnapshot snapshot : metricSnapshots) {
084        if (snapshot.getDataPoints().size() > 0) {
085          if (snapshot instanceof CounterSnapshot) {
086            writeCreated(writer, snapshot);
087          } else if (snapshot instanceof HistogramSnapshot) {
088            writeCreated(writer, snapshot);
089          } else if (snapshot instanceof SummarySnapshot) {
090            writeCreated(writer, snapshot);
091          }
092        }
093      }
094    }
095    writer.flush();
096  }
097
098  public void writeCreated(OutputStreamWriter writer, MetricSnapshot snapshot) throws IOException {
099    boolean metadataWritten = false;
100    MetricMetadata metadata = snapshot.getMetadata();
101    for (DataPointSnapshot data : snapshot.getDataPoints()) {
102      if (data.hasCreatedTimestamp()) {
103        if (!metadataWritten) {
104          writeMetadata(writer, "_created", "gauge", metadata);
105          metadataWritten = true;
106        }
107        writeNameAndLabels(writer, metadata.getPrometheusName(), "_created", data.getLabels());
108        writeTimestamp(writer, data.getCreatedTimestampMillis());
109        writeScrapeTimestampAndNewline(writer, data);
110      }
111    }
112  }
113
114  private void writeCounter(OutputStreamWriter writer, CounterSnapshot snapshot)
115      throws IOException {
116    if (snapshot.getDataPoints().size() > 0) {
117      MetricMetadata metadata = snapshot.getMetadata();
118      writeMetadata(writer, "_total", "counter", metadata);
119      for (CounterSnapshot.CounterDataPointSnapshot data : snapshot.getDataPoints()) {
120        writeNameAndLabels(writer, metadata.getPrometheusName(), "_total", data.getLabels());
121        writeDouble(writer, data.getValue());
122        writeScrapeTimestampAndNewline(writer, data);
123      }
124    }
125  }
126
127  private void writeGauge(OutputStreamWriter writer, GaugeSnapshot snapshot) throws IOException {
128    MetricMetadata metadata = snapshot.getMetadata();
129    writeMetadata(writer, "", "gauge", metadata);
130    for (GaugeSnapshot.GaugeDataPointSnapshot data : snapshot.getDataPoints()) {
131      writeNameAndLabels(writer, metadata.getPrometheusName(), null, data.getLabels());
132      writeDouble(writer, data.getValue());
133      writeScrapeTimestampAndNewline(writer, data);
134    }
135  }
136
137  private void writeHistogram(OutputStreamWriter writer, HistogramSnapshot snapshot)
138      throws IOException {
139    MetricMetadata metadata = snapshot.getMetadata();
140    writeMetadata(writer, "", "histogram", metadata);
141    for (HistogramSnapshot.HistogramDataPointSnapshot data : snapshot.getDataPoints()) {
142      ClassicHistogramBuckets buckets = getClassicBuckets(data);
143      long cumulativeCount = 0;
144      for (int i = 0; i < buckets.size(); i++) {
145        cumulativeCount += buckets.getCount(i);
146        writeNameAndLabels(
147            writer,
148            metadata.getPrometheusName(),
149            "_bucket",
150            data.getLabels(),
151            "le",
152            buckets.getUpperBound(i));
153        writeLong(writer, cumulativeCount);
154        writeScrapeTimestampAndNewline(writer, data);
155      }
156      if (!snapshot.isGaugeHistogram()) {
157        if (data.hasCount()) {
158          writeNameAndLabels(writer, metadata.getPrometheusName(), "_count", data.getLabels());
159          writeLong(writer, data.getCount());
160          writeScrapeTimestampAndNewline(writer, data);
161        }
162        if (data.hasSum()) {
163          writeNameAndLabels(writer, metadata.getPrometheusName(), "_sum", data.getLabels());
164          writeDouble(writer, data.getSum());
165          writeScrapeTimestampAndNewline(writer, data);
166        }
167      }
168    }
169    if (snapshot.isGaugeHistogram()) {
170      writeGaugeCountSum(writer, snapshot, metadata);
171    }
172  }
173
174  private ClassicHistogramBuckets getClassicBuckets(
175      HistogramSnapshot.HistogramDataPointSnapshot data) {
176    if (data.getClassicBuckets().isEmpty()) {
177      return ClassicHistogramBuckets.of(
178          new double[] {Double.POSITIVE_INFINITY}, new long[] {data.getCount()});
179    } else {
180      return data.getClassicBuckets();
181    }
182  }
183
184  private void writeGaugeCountSum(
185      OutputStreamWriter writer, HistogramSnapshot snapshot, MetricMetadata metadata)
186      throws IOException {
187    // Prometheus text format does not support gaugehistogram's _gcount and _gsum.
188    // So we append _gcount and _gsum as gauge metrics.
189    boolean metadataWritten = false;
190    for (HistogramSnapshot.HistogramDataPointSnapshot data : snapshot.getDataPoints()) {
191      if (data.hasCount()) {
192        if (!metadataWritten) {
193          writeMetadata(writer, "_gcount", "gauge", metadata);
194          metadataWritten = true;
195        }
196        writeNameAndLabels(writer, metadata.getPrometheusName(), "_gcount", data.getLabels());
197        writeLong(writer, data.getCount());
198        writeScrapeTimestampAndNewline(writer, data);
199      }
200    }
201    metadataWritten = false;
202    for (HistogramSnapshot.HistogramDataPointSnapshot data : snapshot.getDataPoints()) {
203      if (data.hasSum()) {
204        if (!metadataWritten) {
205          writeMetadata(writer, "_gsum", "gauge", metadata);
206          metadataWritten = true;
207        }
208        writeNameAndLabels(writer, metadata.getPrometheusName(), "_gsum", data.getLabels());
209        writeDouble(writer, data.getSum());
210        writeScrapeTimestampAndNewline(writer, data);
211      }
212    }
213  }
214
215  private void writeSummary(OutputStreamWriter writer, SummarySnapshot snapshot)
216      throws IOException {
217    boolean metadataWritten = false;
218    MetricMetadata metadata = snapshot.getMetadata();
219    for (SummarySnapshot.SummaryDataPointSnapshot data : snapshot.getDataPoints()) {
220      if (data.getQuantiles().size() == 0 && !data.hasCount() && !data.hasSum()) {
221        continue;
222      }
223      if (!metadataWritten) {
224        writeMetadata(writer, "", "summary", metadata);
225        metadataWritten = true;
226      }
227      for (Quantile quantile : data.getQuantiles()) {
228        writeNameAndLabels(
229            writer,
230            metadata.getPrometheusName(),
231            null,
232            data.getLabels(),
233            "quantile",
234            quantile.getQuantile());
235        writeDouble(writer, quantile.getValue());
236        writeScrapeTimestampAndNewline(writer, data);
237      }
238      if (data.hasCount()) {
239        writeNameAndLabels(writer, metadata.getPrometheusName(), "_count", data.getLabels());
240        writeLong(writer, data.getCount());
241        writeScrapeTimestampAndNewline(writer, data);
242      }
243      if (data.hasSum()) {
244        writeNameAndLabels(writer, metadata.getPrometheusName(), "_sum", data.getLabels());
245        writeDouble(writer, data.getSum());
246        writeScrapeTimestampAndNewline(writer, data);
247      }
248    }
249  }
250
251  private void writeInfo(OutputStreamWriter writer, InfoSnapshot snapshot) throws IOException {
252    MetricMetadata metadata = snapshot.getMetadata();
253    writeMetadata(writer, "_info", "gauge", metadata);
254    for (InfoSnapshot.InfoDataPointSnapshot data : snapshot.getDataPoints()) {
255      writeNameAndLabels(writer, metadata.getPrometheusName(), "_info", data.getLabels());
256      writer.write("1");
257      writeScrapeTimestampAndNewline(writer, data);
258    }
259  }
260
261  private void writeStateSet(OutputStreamWriter writer, StateSetSnapshot snapshot)
262      throws IOException {
263    MetricMetadata metadata = snapshot.getMetadata();
264    writeMetadata(writer, "", "gauge", metadata);
265    for (StateSetSnapshot.StateSetDataPointSnapshot data : snapshot.getDataPoints()) {
266      for (int i = 0; i < data.size(); i++) {
267        writer.write(metadata.getPrometheusName());
268        writer.write('{');
269        for (int j = 0; j < data.getLabels().size(); j++) {
270          if (j > 0) {
271            writer.write(",");
272          }
273          writer.write(data.getLabels().getPrometheusName(j));
274          writer.write("=\"");
275          writeEscapedLabelValue(writer, data.getLabels().getValue(j));
276          writer.write("\"");
277        }
278        if (!data.getLabels().isEmpty()) {
279          writer.write(",");
280        }
281        writer.write(metadata.getPrometheusName());
282        writer.write("=\"");
283        writeEscapedLabelValue(writer, data.getName(i));
284        writer.write("\"} ");
285        if (data.isTrue(i)) {
286          writer.write("1");
287        } else {
288          writer.write("0");
289        }
290        writeScrapeTimestampAndNewline(writer, data);
291      }
292    }
293  }
294
295  private void writeUnknown(OutputStreamWriter writer, UnknownSnapshot snapshot)
296      throws IOException {
297    MetricMetadata metadata = snapshot.getMetadata();
298    writeMetadata(writer, "", "untyped", metadata);
299    for (UnknownSnapshot.UnknownDataPointSnapshot data : snapshot.getDataPoints()) {
300      writeNameAndLabels(writer, metadata.getPrometheusName(), null, data.getLabels());
301      writeDouble(writer, data.getValue());
302      writeScrapeTimestampAndNewline(writer, data);
303    }
304  }
305
306  private void writeNameAndLabels(
307      OutputStreamWriter writer, String name, String suffix, Labels labels) throws IOException {
308    writeNameAndLabels(writer, name, suffix, labels, null, 0.0);
309  }
310
311  private void writeNameAndLabels(
312      OutputStreamWriter writer,
313      String name,
314      String suffix,
315      Labels labels,
316      String additionalLabelName,
317      double additionalLabelValue)
318      throws IOException {
319    writer.write(name);
320    if (suffix != null) {
321      writer.write(suffix);
322    }
323    if (!labels.isEmpty() || additionalLabelName != null) {
324      writeLabels(writer, labels, additionalLabelName, additionalLabelValue);
325    }
326    writer.write(' ');
327  }
328
329  private void writeMetadata(
330      OutputStreamWriter writer, String suffix, String typeString, MetricMetadata metadata)
331      throws IOException {
332    if (metadata.getHelp() != null && !metadata.getHelp().isEmpty()) {
333      writer.write("# HELP ");
334      writer.write(metadata.getPrometheusName());
335      if (suffix != null) {
336        writer.write(suffix);
337      }
338      writer.write(' ');
339      writeEscapedHelp(writer, metadata.getHelp());
340      writer.write('\n');
341    }
342    writer.write("# TYPE ");
343    writer.write(metadata.getPrometheusName());
344    if (suffix != null) {
345      writer.write(suffix);
346    }
347    writer.write(' ');
348    writer.write(typeString);
349    writer.write('\n');
350  }
351
352  private void writeEscapedHelp(Writer writer, String s) throws IOException {
353    for (int i = 0; i < s.length(); i++) {
354      char c = s.charAt(i);
355      switch (c) {
356        case '\\':
357          writer.append("\\\\");
358          break;
359        case '\n':
360          writer.append("\\n");
361          break;
362        default:
363          writer.append(c);
364      }
365    }
366  }
367
368  private void writeScrapeTimestampAndNewline(OutputStreamWriter writer, DataPointSnapshot data)
369      throws IOException {
370    if (data.hasScrapeTimestamp()) {
371      writer.write(' ');
372      writeTimestamp(writer, data.getScrapeTimestampMillis());
373    }
374    writer.write('\n');
375  }
376}