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            // In OpenMetrics format, histogram _count and _sum are either both present or both absent.
142            if (data.hasCount() && data.hasSum()) {
143                writeCountAndSum(writer, metadata, data, countSuffix, sumSuffix, exemplars);
144            }
145            writeCreated(writer, metadata, data);
146        }
147    }
148
149    private ClassicHistogramBuckets getClassicBuckets(HistogramSnapshot.HistogramDataPointSnapshot data) {
150        if (data.getClassicBuckets().isEmpty()) {
151            return ClassicHistogramBuckets.of(
152                    new double[]{Double.POSITIVE_INFINITY},
153                    new long[]{data.getCount()}
154            );
155        } else {
156            return data.getClassicBuckets();
157        }
158    }
159
160    private void writeSummary(OutputStreamWriter writer, SummarySnapshot snapshot) throws IOException {
161        boolean metadataWritten = false;
162        MetricMetadata metadata = snapshot.getMetadata();
163        for (SummarySnapshot.SummaryDataPointSnapshot data : snapshot.getDataPoints()) {
164            if (data.getQuantiles().size() == 0 && !data.hasCount() && !data.hasSum()) {
165                continue;
166            }
167            if (!metadataWritten) {
168                writeMetadata(writer, "summary", metadata);
169                metadataWritten = true;
170            }
171            Exemplars exemplars = data.getExemplars();
172            // Exemplars for summaries are new, and there's no best practice yet which Exemplars to choose for which
173            // time series. We select exemplars[0] for _count, exemplars[1] for _sum, and exemplars[2...] for the
174            // quantiles, all indexes modulo exemplars.length.
175            int exemplarIndex = 1;
176            for (Quantile quantile : data.getQuantiles()) {
177                writeNameAndLabels(writer, metadata.getPrometheusName(), null, data.getLabels(), "quantile", quantile.getQuantile());
178                writeDouble(writer, quantile.getValue());
179                if (exemplars.size() > 0 && exemplarsOnAllMetricTypesEnabled) {
180                    exemplarIndex = (exemplarIndex + 1) % exemplars.size();
181                    writeScrapeTimestampAndExemplar(writer, data, exemplars.get(exemplarIndex));
182                } else {
183                    writeScrapeTimestampAndExemplar(writer, data, null);
184                }
185            }
186            // Unlike histograms, summaries can have only a count or only a sum according to OpenMetrics.
187            writeCountAndSum(writer, metadata, data, "_count", "_sum", exemplars);
188            writeCreated(writer, metadata, data);
189        }
190    }
191
192    private void writeInfo(OutputStreamWriter writer, InfoSnapshot snapshot) throws IOException {
193        MetricMetadata metadata = snapshot.getMetadata();
194        writeMetadata(writer, "info", metadata);
195        for (InfoSnapshot.InfoDataPointSnapshot data : snapshot.getDataPoints()) {
196            writeNameAndLabels(writer, metadata.getPrometheusName(), "_info", data.getLabels());
197            writer.write("1");
198            writeScrapeTimestampAndExemplar(writer, data, null);
199        }
200    }
201
202    private void writeStateSet(OutputStreamWriter writer, StateSetSnapshot snapshot) throws IOException {
203        MetricMetadata metadata = snapshot.getMetadata();
204        writeMetadata(writer, "stateset", metadata);
205        for (StateSetSnapshot.StateSetDataPointSnapshot data : snapshot.getDataPoints()) {
206            for (int i = 0; i < data.size(); i++) {
207                writer.write(metadata.getPrometheusName());
208                writer.write('{');
209                for (int j = 0; j < data.getLabels().size(); j++) {
210                    if (j > 0) {
211                        writer.write(",");
212                    }
213                    writer.write(data.getLabels().getPrometheusName(j));
214                    writer.write("=\"");
215                    writeEscapedLabelValue(writer, data.getLabels().getValue(j));
216                    writer.write("\"");
217                }
218                if (!data.getLabels().isEmpty()) {
219                    writer.write(",");
220                }
221                writer.write(metadata.getPrometheusName());
222                writer.write("=\"");
223                writeEscapedLabelValue(writer, data.getName(i));
224                writer.write("\"} ");
225                if (data.isTrue(i)) {
226                    writer.write("1");
227                } else {
228                    writer.write("0");
229                }
230                writeScrapeTimestampAndExemplar(writer, data, null);
231            }
232        }
233    }
234
235    private void writeUnknown(OutputStreamWriter writer, UnknownSnapshot snapshot) throws IOException {
236        MetricMetadata metadata = snapshot.getMetadata();
237        writeMetadata(writer, "unknown", metadata);
238        for (UnknownSnapshot.UnknownDataPointSnapshot data : snapshot.getDataPoints()) {
239            writeNameAndLabels(writer, metadata.getPrometheusName(), null, data.getLabels());
240            writeDouble(writer, data.getValue());
241            if (exemplarsOnAllMetricTypesEnabled) {
242                writeScrapeTimestampAndExemplar(writer, data, data.getExemplar());
243            } else {
244                writeScrapeTimestampAndExemplar(writer, data, null);
245            }
246        }
247    }
248
249    private void writeCountAndSum(OutputStreamWriter writer, MetricMetadata metadata, DistributionDataPointSnapshot data, String countSuffix, String sumSuffix, Exemplars exemplars) throws IOException {
250        if (data.hasCount()) {
251            writeNameAndLabels(writer, metadata.getPrometheusName(), countSuffix, data.getLabels());
252            writeLong(writer, data.getCount());
253            if (exemplarsOnAllMetricTypesEnabled) {
254                writeScrapeTimestampAndExemplar(writer, data, exemplars.getLatest());
255            } else {
256                writeScrapeTimestampAndExemplar(writer, data, null);
257            }
258        }
259        if (data.hasSum()) {
260            writeNameAndLabels(writer, metadata.getPrometheusName(), sumSuffix, data.getLabels());
261            writeDouble(writer, data.getSum());
262            writeScrapeTimestampAndExemplar(writer, data, null);
263        }
264    }
265
266    private void writeCreated(OutputStreamWriter writer, MetricMetadata metadata, DataPointSnapshot data) throws IOException {
267        if (createdTimestampsEnabled && data.hasCreatedTimestamp()) {
268            writeNameAndLabels(writer, metadata.getPrometheusName(), "_created", data.getLabels());
269            writeTimestamp(writer, data.getCreatedTimestampMillis());
270            if (data.hasScrapeTimestamp()) {
271                writer.write(' ');
272                writeTimestamp(writer, data.getScrapeTimestampMillis());
273            }
274            writer.write('\n');
275        }
276    }
277
278    private void writeNameAndLabels(OutputStreamWriter writer, String name, String suffix, Labels labels) throws IOException {
279        writeNameAndLabels(writer, name, suffix, labels, null, 0.0);
280    }
281
282    private void writeNameAndLabels(OutputStreamWriter writer, String name, String suffix, Labels labels,
283                                    String additionalLabelName, double additionalLabelValue) throws IOException {
284        writer.write(name);
285        if (suffix != null) {
286            writer.write(suffix);
287        }
288        if (!labels.isEmpty() || additionalLabelName != null) {
289            writeLabels(writer, labels, additionalLabelName, additionalLabelValue);
290        }
291        writer.write(' ');
292    }
293
294    private void writeScrapeTimestampAndExemplar(OutputStreamWriter writer, DataPointSnapshot data, Exemplar exemplar) throws IOException {
295        if (data.hasScrapeTimestamp()) {
296            writer.write(' ');
297            writeTimestamp(writer, data.getScrapeTimestampMillis());
298        }
299        if (exemplar != null) {
300            writer.write(" # ");
301            writeLabels(writer, exemplar.getLabels(), null, 0);
302            writer.write(' ');
303            writeDouble(writer, exemplar.getValue());
304            if (exemplar.hasTimestamp()) {
305                writer.write(' ');
306                writeTimestamp(writer, exemplar.getTimestampMillis());
307            }
308        }
309        writer.write('\n');
310    }
311
312    private void writeMetadata(OutputStreamWriter writer, String typeName, MetricMetadata metadata) throws IOException {
313        writer.write("# TYPE ");
314        writer.write(metadata.getPrometheusName());
315        writer.write(' ');
316        writer.write(typeName);
317        writer.write('\n');
318        if (metadata.getUnit() != null) {
319            writer.write("# UNIT ");
320            writer.write(metadata.getPrometheusName());
321            writer.write(' ');
322            writeEscapedLabelValue(writer, metadata.getUnit().toString());
323            writer.write('\n');
324        }
325        if (metadata.getHelp() != null && !metadata.getHelp().isEmpty()) {
326            writer.write("# HELP ");
327            writer.write(metadata.getPrometheusName());
328            writer.write(' ');
329            writeEscapedLabelValue(writer, metadata.getHelp());
330            writer.write('\n');
331        }
332    }
333}