001package io.prometheus.metrics.expositionformats;
002
003import io.prometheus.metrics.shaded.com_google_protobuf_3_25_3.TextFormat;
004import io.prometheus.metrics.expositionformats.generated.com_google_protobuf_3_25_3.Metrics;
005import io.prometheus.metrics.model.snapshots.ClassicHistogramBuckets;
006import io.prometheus.metrics.model.snapshots.CounterSnapshot;
007import io.prometheus.metrics.model.snapshots.CounterSnapshot.CounterDataPointSnapshot;
008import io.prometheus.metrics.model.snapshots.Exemplar;
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.DataPointSnapshot;
014import io.prometheus.metrics.model.snapshots.MetricMetadata;
015import io.prometheus.metrics.model.snapshots.MetricSnapshot;
016import io.prometheus.metrics.model.snapshots.MetricSnapshots;
017import io.prometheus.metrics.model.snapshots.NativeHistogramBuckets;
018import io.prometheus.metrics.model.snapshots.Quantiles;
019import io.prometheus.metrics.model.snapshots.StateSetSnapshot;
020import io.prometheus.metrics.model.snapshots.SummarySnapshot;
021import io.prometheus.metrics.model.snapshots.UnknownSnapshot;
022
023import java.io.IOException;
024import java.io.OutputStream;
025
026import static io.prometheus.metrics.expositionformats.ProtobufUtil.timestampFromMillis;
027
028/**
029 * Write the Prometheus protobuf format as defined in
030 * <a href="https://github.com/prometheus/client_model/tree/master/io/prometheus/client">github.com/prometheus/client_model</a>.
031 * <p>
032 * As of today, this is the only exposition format that supports native histograms.
033 */
034public class PrometheusProtobufWriter implements ExpositionFormatWriter {
035
036    public static final String CONTENT_TYPE = "application/vnd.google.protobuf; proto=io.prometheus.client.MetricFamily; encoding=delimited";
037
038    @Override
039    public boolean accepts(String acceptHeader) {
040        if (acceptHeader == null) {
041            return false;
042        } else {
043            return acceptHeader.contains("application/vnd.google.protobuf")
044                    && acceptHeader.contains("proto=io.prometheus.client.MetricFamily");
045        }
046    }
047
048    @Override
049    public String getContentType() {
050        return CONTENT_TYPE;
051    }
052
053    public String toDebugString(MetricSnapshots metricSnapshots) {
054        StringBuilder stringBuilder = new StringBuilder();
055        for (MetricSnapshot snapshot : metricSnapshots) {
056            if (snapshot.getDataPoints().size() > 0) {
057                stringBuilder.append(TextFormat.printer().printToString(convert(snapshot)));
058            }
059        }
060        return stringBuilder.toString();
061    }
062
063    @Override
064    public void write(OutputStream out, MetricSnapshots metricSnapshots) throws IOException {
065        for (MetricSnapshot snapshot : metricSnapshots) {
066            if (snapshot.getDataPoints().size() > 0) {
067                convert(snapshot).writeDelimitedTo(out);
068            }
069        }
070    }
071
072    public Metrics.MetricFamily convert(MetricSnapshot snapshot) {
073        Metrics.MetricFamily.Builder builder = Metrics.MetricFamily.newBuilder();
074        if (snapshot instanceof CounterSnapshot) {
075            for (CounterDataPointSnapshot data : ((CounterSnapshot) snapshot).getDataPoints()) {
076                builder.addMetric(convert(data));
077            }
078            setMetadataUnlessEmpty(builder, snapshot.getMetadata(), "_total", Metrics.MetricType.COUNTER);
079        } else if (snapshot instanceof GaugeSnapshot) {
080            for (GaugeSnapshot.GaugeDataPointSnapshot data : ((GaugeSnapshot) snapshot).getDataPoints()) {
081                builder.addMetric(convert(data));
082            }
083            setMetadataUnlessEmpty(builder, snapshot.getMetadata(), null, Metrics.MetricType.GAUGE);
084        } else if (snapshot instanceof HistogramSnapshot) {
085            HistogramSnapshot histogram = (HistogramSnapshot) snapshot;
086            for (HistogramSnapshot.HistogramDataPointSnapshot data : histogram.getDataPoints()) {
087                builder.addMetric(convert(data));
088            }
089            Metrics.MetricType type = histogram.isGaugeHistogram() ? Metrics.MetricType.GAUGE_HISTOGRAM : Metrics.MetricType.HISTOGRAM;
090            setMetadataUnlessEmpty(builder, snapshot.getMetadata(), null, type);
091        } else if (snapshot instanceof SummarySnapshot) {
092            for (SummarySnapshot.SummaryDataPointSnapshot data : ((SummarySnapshot) snapshot).getDataPoints()) {
093                if (data.hasCount() || data.hasSum() || data.getQuantiles().size() > 0) {
094                    builder.addMetric(convert(data));
095                }
096            }
097            setMetadataUnlessEmpty(builder, snapshot.getMetadata(), null, Metrics.MetricType.SUMMARY);
098        } else if (snapshot instanceof InfoSnapshot) {
099            for (InfoSnapshot.InfoDataPointSnapshot data : ((InfoSnapshot) snapshot).getDataPoints()) {
100                builder.addMetric(convert(data));
101            }
102            setMetadataUnlessEmpty(builder, snapshot.getMetadata(), "_info", Metrics.MetricType.GAUGE);
103        } else if (snapshot instanceof StateSetSnapshot) {
104            for (StateSetSnapshot.StateSetDataPointSnapshot data : ((StateSetSnapshot) snapshot).getDataPoints()) {
105                for (int i = 0; i < data.size(); i++) {
106                    builder.addMetric(convert(data, snapshot.getMetadata().getPrometheusName(), i));
107                }
108            }
109            setMetadataUnlessEmpty(builder, snapshot.getMetadata(), null, Metrics.MetricType.GAUGE);
110        } else if (snapshot instanceof UnknownSnapshot) {
111            for (UnknownSnapshot.UnknownDataPointSnapshot data : ((UnknownSnapshot) snapshot).getDataPoints()) {
112                builder.addMetric(convert(data));
113            }
114            setMetadataUnlessEmpty(builder, snapshot.getMetadata(), null, Metrics.MetricType.UNTYPED);
115        }
116        return builder.build();
117    }
118
119    private void setMetadataUnlessEmpty(Metrics.MetricFamily.Builder builder, MetricMetadata metadata, String nameSuffix, Metrics.MetricType type) {
120        if (builder.getMetricCount() == 0) {
121            return;
122        }
123        if (nameSuffix == null) {
124            builder.setName(metadata.getPrometheusName());
125        } else {
126            builder.setName(metadata.getPrometheusName() + nameSuffix);
127        }
128        if (metadata.getHelp() != null) {
129            builder.setHelp(metadata.getHelp());
130        }
131        builder.setType(type);
132    }
133
134    private Metrics.Metric.Builder convert(CounterDataPointSnapshot data) {
135        Metrics.Metric.Builder metricBuilder = Metrics.Metric.newBuilder();
136        Metrics.Counter.Builder counterBuilder = Metrics.Counter.newBuilder();
137        counterBuilder.setValue(data.getValue());
138        if (data.getExemplar() != null) {
139            counterBuilder.setExemplar(convert(data.getExemplar()));
140        }
141        addLabels(metricBuilder, data.getLabels());
142        metricBuilder.setCounter(counterBuilder.build());
143        setScrapeTimestamp(metricBuilder, data);
144        return metricBuilder;
145    }
146
147    private Metrics.Metric.Builder convert(GaugeSnapshot.GaugeDataPointSnapshot data) {
148        Metrics.Metric.Builder metricBuilder = Metrics.Metric.newBuilder();
149        Metrics.Gauge.Builder gaugeBuilder = Metrics.Gauge.newBuilder();
150        gaugeBuilder.setValue(data.getValue());
151        addLabels(metricBuilder, data.getLabels());
152        metricBuilder.setGauge(gaugeBuilder);
153        setScrapeTimestamp(metricBuilder, data);
154        return metricBuilder;
155    }
156
157    private Metrics.Metric.Builder convert(HistogramSnapshot.HistogramDataPointSnapshot data) {
158        Metrics.Metric.Builder metricBuilder = Metrics.Metric.newBuilder();
159        Metrics.Histogram.Builder histogramBuilder = Metrics.Histogram.newBuilder();
160        if (data.hasNativeHistogramData()) {
161            histogramBuilder.setSchema(data.getNativeSchema());
162            histogramBuilder.setZeroCount(data.getNativeZeroCount());
163            histogramBuilder.setZeroThreshold(data.getNativeZeroThreshold());
164            addBuckets(histogramBuilder, data.getNativeBucketsForPositiveValues(), +1);
165            addBuckets(histogramBuilder, data.getNativeBucketsForNegativeValues(), -1);
166
167            if (!data.hasClassicHistogramData()) { // native only
168                // Add a single +Inf bucket for the exemplar.
169                Exemplar exemplar = data.getExemplars().getLatest();
170                if (exemplar != null) {
171                    Metrics.Bucket.Builder bucketBuilder = Metrics.Bucket.newBuilder()
172                            .setCumulativeCount(getNativeCount(data))
173                            .setUpperBound(Double.POSITIVE_INFINITY);
174                    bucketBuilder.setExemplar(convert(exemplar));
175                    histogramBuilder.addBucket(bucketBuilder);
176                }
177            }
178        }
179        if (data.hasClassicHistogramData()) {
180
181            ClassicHistogramBuckets buckets = data.getClassicBuckets();
182            double lowerBound = Double.NEGATIVE_INFINITY;
183            long cumulativeCount = 0;
184            for (int i = 0; i < buckets.size(); i++) {
185                cumulativeCount += buckets.getCount(i);
186                double upperBound = buckets.getUpperBound(i);
187                Metrics.Bucket.Builder bucketBuilder = Metrics.Bucket.newBuilder()
188                        .setCumulativeCount(cumulativeCount)
189                        .setUpperBound(upperBound);
190                Exemplar exemplar = data.getExemplars().get(lowerBound, upperBound);
191                if (exemplar != null) {
192                    bucketBuilder.setExemplar(convert(exemplar));
193                }
194                histogramBuilder.addBucket(bucketBuilder);
195                lowerBound = upperBound;
196            }
197        }
198        addLabels(metricBuilder, data.getLabels());
199        setScrapeTimestamp(metricBuilder, data);
200        if (data.hasCount()) {
201            histogramBuilder.setSampleCount(data.getCount());
202        }
203        if (data.hasSum()) {
204            histogramBuilder.setSampleSum(data.getSum());
205        }
206        metricBuilder.setHistogram(histogramBuilder.build());
207        return metricBuilder;
208    }
209
210    private long getNativeCount(HistogramSnapshot.HistogramDataPointSnapshot data) {
211        if (data.hasCount()) {
212            return data.getCount();
213        } else {
214            long count = data.getNativeZeroCount();
215            for (int i = 0; i < data.getNativeBucketsForPositiveValues().size(); i++) {
216                count += data.getNativeBucketsForPositiveValues().getCount(i);
217            }
218            for (int i = 0; i < data.getNativeBucketsForNegativeValues().size(); i++) {
219                count += data.getNativeBucketsForNegativeValues().getCount(i);
220            }
221            return count;
222        }
223    }
224
225    private void addBuckets(Metrics.Histogram.Builder histogramBuilder, NativeHistogramBuckets buckets, int sgn) {
226        if (buckets.size() > 0) {
227            Metrics.BucketSpan.Builder currentSpan = Metrics.BucketSpan.newBuilder();
228            currentSpan.setOffset(buckets.getBucketIndex(0));
229            currentSpan.setLength(0);
230            int previousIndex = currentSpan.getOffset();
231            long previousCount = 0;
232            for (int i = 0; i < buckets.size(); i++) {
233                if (buckets.getBucketIndex(i) > previousIndex + 1) {
234                    // If the gap between bucketIndex and previousIndex is just 1 or 2,
235                    // we don't start a new span but continue the existing span and add 1 or 2 empty buckets.
236                    if (buckets.getBucketIndex(i) <= previousIndex + 3) {
237                        while (buckets.getBucketIndex(i) > previousIndex + 1) {
238                            currentSpan.setLength(currentSpan.getLength() + 1);
239                            previousIndex++;
240                            if (sgn > 0) {
241                                histogramBuilder.addPositiveDelta(-previousCount);
242                            } else {
243                                histogramBuilder.addNegativeDelta(-previousCount);
244                            }
245                            previousCount = 0;
246                        }
247                    } else {
248                        if (sgn > 0) {
249                            histogramBuilder.addPositiveSpan(currentSpan.build());
250                        } else {
251                            histogramBuilder.addNegativeSpan(currentSpan.build());
252                        }
253                        currentSpan = Metrics.BucketSpan.newBuilder();
254                        currentSpan.setOffset(buckets.getBucketIndex(i) - (previousIndex + 1));
255                    }
256                }
257                currentSpan.setLength(currentSpan.getLength() + 1);
258                previousIndex = buckets.getBucketIndex(i);
259                if (sgn > 0) {
260                    histogramBuilder.addPositiveDelta(buckets.getCount(i) - previousCount);
261                } else {
262                    histogramBuilder.addNegativeDelta(buckets.getCount(i) - previousCount);
263                }
264                previousCount = buckets.getCount(i);
265            }
266            if (sgn > 0) {
267                histogramBuilder.addPositiveSpan(currentSpan.build());
268            } else {
269                histogramBuilder.addNegativeSpan(currentSpan.build());
270            }
271        }
272    }
273
274    private Metrics.Metric.Builder convert(SummarySnapshot.SummaryDataPointSnapshot data) {
275        Metrics.Metric.Builder metricBuilder = Metrics.Metric.newBuilder();
276        Metrics.Summary.Builder summaryBuilder = Metrics.Summary.newBuilder();
277        if (data.hasCount()) {
278            summaryBuilder.setSampleCount(data.getCount());
279        }
280        if (data.hasSum()) {
281            summaryBuilder.setSampleSum(data.getSum());
282        }
283        Quantiles quantiles = data.getQuantiles();
284        for (int i = 0; i < quantiles.size(); i++) {
285            summaryBuilder.addQuantile(Metrics.Quantile.newBuilder()
286                    .setQuantile(quantiles.get(i).getQuantile())
287                    .setValue(quantiles.get(i).getValue())
288                    .build());
289        }
290        addLabels(metricBuilder, data.getLabels());
291        metricBuilder.setSummary(summaryBuilder.build());
292        setScrapeTimestamp(metricBuilder, data);
293        return metricBuilder;
294    }
295
296    private Metrics.Metric.Builder convert(InfoSnapshot.InfoDataPointSnapshot data) {
297        Metrics.Metric.Builder metricBuilder = Metrics.Metric.newBuilder();
298        Metrics.Gauge.Builder gaugeBuilder = Metrics.Gauge.newBuilder();
299        gaugeBuilder.setValue(1);
300        addLabels(metricBuilder, data.getLabels());
301        metricBuilder.setGauge(gaugeBuilder);
302        setScrapeTimestamp(metricBuilder, data);
303        return metricBuilder;
304    }
305
306    private Metrics.Metric.Builder convert(StateSetSnapshot.StateSetDataPointSnapshot data, String name, int i) {
307        Metrics.Metric.Builder metricBuilder = Metrics.Metric.newBuilder();
308        Metrics.Gauge.Builder gaugeBuilder = Metrics.Gauge.newBuilder();
309        addLabels(metricBuilder, data.getLabels());
310        metricBuilder.addLabel(Metrics.LabelPair.newBuilder()
311                .setName(name)
312                .setValue(data.getName(i))
313                .build());
314        if (data.isTrue(i)) {
315            gaugeBuilder.setValue(1);
316        } else {
317            gaugeBuilder.setValue(0);
318        }
319        metricBuilder.setGauge(gaugeBuilder);
320        setScrapeTimestamp(metricBuilder, data);
321        return metricBuilder;
322    }
323
324    private Metrics.Metric.Builder convert(UnknownSnapshot.UnknownDataPointSnapshot data) {
325        Metrics.Metric.Builder metricBuilder = Metrics.Metric.newBuilder();
326        Metrics.Untyped.Builder untypedBuilder = Metrics.Untyped.newBuilder();
327        untypedBuilder.setValue(data.getValue());
328        addLabels(metricBuilder, data.getLabels());
329        metricBuilder.setUntyped(untypedBuilder);
330        return metricBuilder;
331    }
332
333    private void addLabels(Metrics.Metric.Builder metricBuilder, Labels labels) {
334        for (int i = 0; i < labels.size(); i++) {
335            metricBuilder.addLabel(Metrics.LabelPair.newBuilder()
336                    .setName(labels.getPrometheusName(i))
337                    .setValue(labels.getValue(i))
338                    .build());
339        }
340    }
341
342    private void addLabels(Metrics.Exemplar.Builder metricBuilder, Labels labels) {
343        for (int i = 0; i < labels.size(); i++) {
344            metricBuilder.addLabel(Metrics.LabelPair.newBuilder()
345                    .setName(labels.getPrometheusName(i))
346                    .setValue(labels.getValue(i))
347                    .build());
348        }
349    }
350
351    private Metrics.Exemplar.Builder convert(Exemplar exemplar) {
352        Metrics.Exemplar.Builder builder = Metrics.Exemplar.newBuilder();
353        builder.setValue(exemplar.getValue());
354        addLabels(builder, exemplar.getLabels());
355        if (exemplar.hasTimestamp()) {
356            builder.setTimestamp(timestampFromMillis(exemplar.getTimestampMillis()));
357        }
358        return builder;
359    }
360
361    private void setScrapeTimestamp(Metrics.Metric.Builder metricBuilder, DataPointSnapshot data) {
362        if (data.hasScrapeTimestamp()) {
363            metricBuilder.setTimestampMs(data.getScrapeTimestampMillis());
364        }
365    }
366}