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