001package io.prometheus.metrics.expositionformats;
002
003import io.prometheus.metrics.model.snapshots.ClassicHistogramBuckets;
004import io.prometheus.metrics.model.snapshots.CounterSnapshot;
005import io.prometheus.metrics.model.snapshots.DataPointSnapshot;
006import io.prometheus.metrics.model.snapshots.DistributionDataPointSnapshot;
007import io.prometheus.metrics.model.snapshots.Exemplar;
008import io.prometheus.metrics.model.snapshots.Exemplars;
009import io.prometheus.metrics.model.snapshots.GaugeSnapshot;
010import io.prometheus.metrics.model.snapshots.HistogramSnapshot;
011import io.prometheus.metrics.model.snapshots.InfoSnapshot;
012import io.prometheus.metrics.model.snapshots.Labels;
013import io.prometheus.metrics.model.snapshots.MetricMetadata;
014import io.prometheus.metrics.model.snapshots.MetricSnapshot;
015import io.prometheus.metrics.model.snapshots.MetricSnapshots;
016import io.prometheus.metrics.model.snapshots.Quantile;
017import io.prometheus.metrics.model.snapshots.StateSetSnapshot;
018import io.prometheus.metrics.model.snapshots.SummarySnapshot;
019import io.prometheus.metrics.model.snapshots.UnknownSnapshot;
020
021import java.io.IOException;
022import java.io.OutputStream;
023import java.io.OutputStreamWriter;
024import java.nio.charset.StandardCharsets;
025import java.util.List;
026
027import static io.prometheus.metrics.expositionformats.TextFormatUtil.writeDouble;
028import static io.prometheus.metrics.expositionformats.TextFormatUtil.writeEscapedLabelValue;
029import static io.prometheus.metrics.expositionformats.TextFormatUtil.writeLabels;
030import static io.prometheus.metrics.expositionformats.TextFormatUtil.writeLong;
031import static io.prometheus.metrics.expositionformats.TextFormatUtil.writeTimestamp;
032
033/**
034 * Write the OpenMetrics text format as defined on <a href="https://openmetrics.io/">https://openmetrics.io</a>.
035 */
036public class OpenMetricsTextFormatWriter implements ExpositionFormatWriter {
037
038    public static final String CONTENT_TYPE = "application/openmetrics-text; version=1.0.0; charset=utf-8";
039    private final boolean createdTimestampsEnabled;
040    private final boolean exemplarsOnAllMetricTypesEnabled;
041
042    /**
043     * @param createdTimestampsEnabled defines if {@code _created} timestamps should be included in the output or not.
044     */
045    public OpenMetricsTextFormatWriter(boolean createdTimestampsEnabled, boolean exemplarsOnAllMetricTypesEnabled) {
046        this.createdTimestampsEnabled = createdTimestampsEnabled;
047        this.exemplarsOnAllMetricTypesEnabled = exemplarsOnAllMetricTypesEnabled;
048    }
049
050    @Override
051    public boolean accepts(String acceptHeader) {
052        if (acceptHeader == null) {
053            return false;
054        }
055        return acceptHeader.contains("application/openmetrics-text");
056    }
057
058    @Override
059    public String getContentType() {
060        return CONTENT_TYPE;
061    }
062
063    public void write(OutputStream out, MetricSnapshots metricSnapshots) throws IOException {
064        OutputStreamWriter writer = new OutputStreamWriter(out, StandardCharsets.UTF_8);
065        for (MetricSnapshot snapshot : metricSnapshots) {
066            if (snapshot.getDataPoints().size() > 0) {
067                if (snapshot instanceof CounterSnapshot) {
068                    writeCounter(writer, (CounterSnapshot) snapshot);
069                } else if (snapshot instanceof GaugeSnapshot) {
070                    writeGauge(writer, (GaugeSnapshot) snapshot);
071                } else if (snapshot instanceof HistogramSnapshot) {
072                    writeHistogram(writer, (HistogramSnapshot) snapshot);
073                } else if (snapshot instanceof SummarySnapshot) {
074                    writeSummary(writer, (SummarySnapshot) snapshot);
075                } else if (snapshot instanceof InfoSnapshot) {
076                    writeInfo(writer, (InfoSnapshot) snapshot);
077                } else if (snapshot instanceof StateSetSnapshot) {
078                    writeStateSet(writer, (StateSetSnapshot) snapshot);
079                } else if (snapshot instanceof UnknownSnapshot) {
080                    writeUnknown(writer, (UnknownSnapshot) snapshot);
081                }
082            }
083        }
084        writer.write("# EOF\n");
085        writer.flush();
086    }
087
088    private void writeCounter(OutputStreamWriter writer, CounterSnapshot snapshot) throws IOException {
089        MetricMetadata metadata = snapshot.getMetadata();
090        writeMetadata(writer, "counter", metadata);
091        for (CounterSnapshot.CounterDataPointSnapshot data : snapshot.getDataPoints()) {
092            writeNameAndLabels(writer, metadata.getPrometheusName(), "_total", data.getLabels());
093            writeDouble(writer, data.getValue());
094            writeScrapeTimestampAndExemplar(writer, data, data.getExemplar());
095            writeCreated(writer, metadata, data);
096        }
097    }
098
099    private void writeGauge(OutputStreamWriter writer, GaugeSnapshot snapshot) throws IOException {
100        MetricMetadata metadata = snapshot.getMetadata();
101        writeMetadata(writer, "gauge", metadata);
102        for (GaugeSnapshot.GaugeDataPointSnapshot data : snapshot.getDataPoints()) {
103            writeNameAndLabels(writer, metadata.getPrometheusName(), null, data.getLabels());
104            writeDouble(writer, data.getValue());
105            if (exemplarsOnAllMetricTypesEnabled) {
106                writeScrapeTimestampAndExemplar(writer, data, data.getExemplar());
107            } else {
108                writeScrapeTimestampAndExemplar(writer, data, null);
109            }
110        }
111    }
112
113    private void writeHistogram(OutputStreamWriter writer, HistogramSnapshot snapshot) throws IOException {
114        MetricMetadata metadata = snapshot.getMetadata();
115        if (snapshot.isGaugeHistogram()) {
116            writeMetadata(writer, "gaugehistogram", metadata);
117            writeClassicHistogramBuckets(writer, metadata, "_gcount", "_gsum", snapshot.getDataPoints());
118        } else {
119            writeMetadata(writer, "histogram", metadata);
120            writeClassicHistogramBuckets(writer, metadata, "_count", "_sum", snapshot.getDataPoints());
121        }
122    }
123
124    private void writeClassicHistogramBuckets(OutputStreamWriter writer, MetricMetadata metadata, String countSuffix, String sumSuffix, List<HistogramSnapshot.HistogramDataPointSnapshot> dataList) throws IOException {
125        for (HistogramSnapshot.HistogramDataPointSnapshot data : dataList) {
126            ClassicHistogramBuckets buckets = getClassicBuckets(data);
127            Exemplars exemplars = data.getExemplars();
128            long cumulativeCount = 0;
129            for (int i = 0; i < buckets.size(); i++) {
130                cumulativeCount += buckets.getCount(i);
131                writeNameAndLabels(writer, metadata.getPrometheusName(), "_bucket", data.getLabels(), "le", buckets.getUpperBound(i));
132                writeLong(writer, cumulativeCount);
133                Exemplar exemplar;
134                if (i == 0) {
135                    exemplar = exemplars.get(Double.NEGATIVE_INFINITY, buckets.getUpperBound(i));
136                } else {
137                    exemplar = exemplars.get(buckets.getUpperBound(i - 1), buckets.getUpperBound(i));
138                }
139                writeScrapeTimestampAndExemplar(writer, data, exemplar);
140            }
141            if (data.hasCount() && data.hasSum()) {
142                // In OpenMetrics format, histogram _count and _sum are either both present or both absent.
143                // While Prometheus allows Exemplars for histogram's _count and _sum now, we don't
144                // use Exemplars here to be backwards compatible with previous behavior.
145                writeCountAndSum(writer, metadata, data, countSuffix, sumSuffix, Exemplars.EMPTY);
146            }
147            writeCreated(writer, metadata, data);
148        }
149    }
150
151    private ClassicHistogramBuckets getClassicBuckets(HistogramSnapshot.HistogramDataPointSnapshot data) {
152        if (data.getClassicBuckets().isEmpty()) {
153            return ClassicHistogramBuckets.of(
154                    new double[]{Double.POSITIVE_INFINITY},
155                    new long[]{data.getCount()}
156            );
157        } else {
158            return data.getClassicBuckets();
159        }
160    }
161
162    private void writeSummary(OutputStreamWriter writer, SummarySnapshot snapshot) throws IOException {
163        boolean metadataWritten = false;
164        MetricMetadata metadata = snapshot.getMetadata();
165        for (SummarySnapshot.SummaryDataPointSnapshot data : snapshot.getDataPoints()) {
166            if (data.getQuantiles().size() == 0 && !data.hasCount() && !data.hasSum()) {
167                continue;
168            }
169            if (!metadataWritten) {
170                writeMetadata(writer, "summary", metadata);
171                metadataWritten = true;
172            }
173            Exemplars exemplars = data.getExemplars();
174            // Exemplars for summaries are new, and there's no best practice yet which Exemplars to choose for which
175            // time series. We select exemplars[0] for _count, exemplars[1] for _sum, and exemplars[2...] for the
176            // quantiles, all indexes modulo exemplars.length.
177            int exemplarIndex = 1;
178            for (Quantile quantile : data.getQuantiles()) {
179                writeNameAndLabels(writer, metadata.getPrometheusName(), null, data.getLabels(), "quantile", quantile.getQuantile());
180                writeDouble(writer, quantile.getValue());
181                if (exemplars.size() > 0 && exemplarsOnAllMetricTypesEnabled) {
182                    exemplarIndex = (exemplarIndex + 1) % exemplars.size();
183                    writeScrapeTimestampAndExemplar(writer, data, exemplars.get(exemplarIndex));
184                } else {
185                    writeScrapeTimestampAndExemplar(writer, data, null);
186                }
187            }
188            // Unlike histograms, summaries can have only a count or only a sum according to OpenMetrics.
189            writeCountAndSum(writer, metadata, data, "_count", "_sum", exemplars);
190            writeCreated(writer, metadata, data);
191        }
192    }
193
194    private void writeInfo(OutputStreamWriter writer, InfoSnapshot snapshot) throws IOException {
195        MetricMetadata metadata = snapshot.getMetadata();
196        writeMetadata(writer, "info", metadata);
197        for (InfoSnapshot.InfoDataPointSnapshot data : snapshot.getDataPoints()) {
198            writeNameAndLabels(writer, metadata.getPrometheusName(), "_info", data.getLabels());
199            writer.write("1");
200            writeScrapeTimestampAndExemplar(writer, data, null);
201        }
202    }
203
204    private void writeStateSet(OutputStreamWriter writer, StateSetSnapshot snapshot) throws IOException {
205        MetricMetadata metadata = snapshot.getMetadata();
206        writeMetadata(writer, "stateset", metadata);
207        for (StateSetSnapshot.StateSetDataPointSnapshot data : snapshot.getDataPoints()) {
208            for (int i = 0; i < data.size(); i++) {
209                writer.write(metadata.getPrometheusName());
210                writer.write('{');
211                for (int j = 0; j < data.getLabels().size(); j++) {
212                    if (j > 0) {
213                        writer.write(",");
214                    }
215                    writer.write(data.getLabels().getPrometheusName(j));
216                    writer.write("=\"");
217                    writeEscapedLabelValue(writer, data.getLabels().getValue(j));
218                    writer.write("\"");
219                }
220                if (!data.getLabels().isEmpty()) {
221                    writer.write(",");
222                }
223                writer.write(metadata.getPrometheusName());
224                writer.write("=\"");
225                writeEscapedLabelValue(writer, data.getName(i));
226                writer.write("\"} ");
227                if (data.isTrue(i)) {
228                    writer.write("1");
229                } else {
230                    writer.write("0");
231                }
232                writeScrapeTimestampAndExemplar(writer, data, null);
233            }
234        }
235    }
236
237    private void writeUnknown(OutputStreamWriter writer, UnknownSnapshot snapshot) throws IOException {
238        MetricMetadata metadata = snapshot.getMetadata();
239        writeMetadata(writer, "unknown", metadata);
240        for (UnknownSnapshot.UnknownDataPointSnapshot data : snapshot.getDataPoints()) {
241            writeNameAndLabels(writer, metadata.getPrometheusName(), null, data.getLabels());
242            writeDouble(writer, data.getValue());
243            if (exemplarsOnAllMetricTypesEnabled) {
244            writeScrapeTimestampAndExemplar(writer, data, data.getExemplar());
245            } else {
246                writeScrapeTimestampAndExemplar(writer, data, null);
247            }
248        }
249    }
250
251    private void writeCountAndSum(OutputStreamWriter writer, MetricMetadata metadata, DistributionDataPointSnapshot data, String countSuffix, String sumSuffix, Exemplars exemplars) throws IOException {
252        int exemplarIndex = 0;
253        if (data.hasCount()) {
254            writeNameAndLabels(writer, metadata.getPrometheusName(), countSuffix, data.getLabels());
255            writeLong(writer, data.getCount());
256            if (exemplars.size() > 0) {
257                writeScrapeTimestampAndExemplar(writer, data, exemplars.get(exemplarIndex));
258                exemplarIndex = exemplarIndex + 1 % exemplars.size();
259            } else {
260                writeScrapeTimestampAndExemplar(writer, data, null);
261            }
262        }
263        if (data.hasSum()) {
264            writeNameAndLabels(writer, metadata.getPrometheusName(), sumSuffix, data.getLabels());
265            writeDouble(writer, data.getSum());
266            if (exemplars.size() > 0) {
267                writeScrapeTimestampAndExemplar(writer, data, exemplars.get(exemplarIndex));
268            } else {
269                writeScrapeTimestampAndExemplar(writer, data, null);
270            }
271        }
272    }
273
274    private void writeCreated(OutputStreamWriter writer, MetricMetadata metadata, DataPointSnapshot data) throws IOException {
275        if (createdTimestampsEnabled && data.hasCreatedTimestamp()) {
276            writeNameAndLabels(writer, metadata.getPrometheusName(), "_created", data.getLabels());
277            writeTimestamp(writer, data.getCreatedTimestampMillis());
278            if (data.hasScrapeTimestamp()) {
279                writer.write(' ');
280                writeTimestamp(writer, data.getScrapeTimestampMillis());
281            }
282            writer.write('\n');
283        }
284    }
285
286    private void writeNameAndLabels(OutputStreamWriter writer, String name, String suffix, Labels labels) throws IOException {
287        writeNameAndLabels(writer, name, suffix, labels, null, 0.0);
288    }
289
290    private void writeNameAndLabels(OutputStreamWriter writer, String name, String suffix, Labels labels,
291                                    String additionalLabelName, double additionalLabelValue) throws IOException {
292        writer.write(name);
293        if (suffix != null) {
294            writer.write(suffix);
295        }
296        if (!labels.isEmpty() || additionalLabelName != null) {
297            writeLabels(writer, labels, additionalLabelName, additionalLabelValue);
298        }
299        writer.write(' ');
300    }
301
302    private void writeScrapeTimestampAndExemplar(OutputStreamWriter writer, DataPointSnapshot data, Exemplar exemplar) throws IOException {
303        if (data.hasScrapeTimestamp()) {
304            writer.write(' ');
305            writeTimestamp(writer, data.getScrapeTimestampMillis());
306        }
307        if (exemplar != null) {
308            writer.write(" # ");
309            writeLabels(writer, exemplar.getLabels(), null, 0);
310            writer.write(' ');
311            writeDouble(writer, exemplar.getValue());
312            if (exemplar.hasTimestamp()) {
313                writer.write(' ');
314                writeTimestamp(writer, exemplar.getTimestampMillis());
315            }
316        }
317        writer.write('\n');
318    }
319
320    private void writeMetadata(OutputStreamWriter writer, String typeName, MetricMetadata metadata) throws IOException {
321        writer.write("# TYPE ");
322        writer.write(metadata.getPrometheusName());
323        writer.write(' ');
324        writer.write(typeName);
325        writer.write('\n');
326        if (metadata.getUnit() != null) {
327            writer.write("# UNIT ");
328            writer.write(metadata.getPrometheusName());
329            writer.write(' ');
330            writeEscapedLabelValue(writer, metadata.getUnit().toString());
331            writer.write('\n');
332        }
333        if (metadata.getHelp() != null && !metadata.getHelp().isEmpty()) {
334            writer.write("# HELP ");
335            writer.write(metadata.getPrometheusName());
336            writer.write(' ');
337            writeEscapedLabelValue(writer, metadata.getHelp());
338            writer.write('\n');
339        }
340    }
341}