001package io.prometheus.metrics.expositionformats;
002
003import io.prometheus.metrics.shaded.com_google_protobuf_3_21_7.TextFormat;
004import io.prometheus.metrics.expositionformats.generated.com_google_protobuf_3_21_7.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            // Add a single +Inf bucket for the exemplar.
168            // It is currently not possible to have more than one exemplar in a native histogram,
169            // see https://cloud-native.slack.com/archives/C02KR205UMU/p1688414381799849
170            Exemplar exemplar = data.getExemplars().getLatest();
171            if (exemplar != null) {
172                Metrics.Bucket.Builder bucketBuilder = Metrics.Bucket.newBuilder()
173                        .setCumulativeCount(getNativeCount(data))
174                        .setUpperBound(Double.POSITIVE_INFINITY);
175                bucketBuilder.setExemplar(convert(exemplar));
176                histogramBuilder.addBucket(bucketBuilder);
177            }
178        } else if (data.hasClassicHistogramData()) {
179
180            // Once native histograms support multiple exemplars the above can be changed from "else if" to "if",
181            // so that we always add the complete classic buckets and exemplars.
182
183            ClassicHistogramBuckets buckets = data.getClassicBuckets();
184            double lowerBound = Double.NEGATIVE_INFINITY;
185            long cumulativeCount = 0;
186            for (int i = 0; i < buckets.size(); i++) {
187                cumulativeCount += buckets.getCount(i);
188                double upperBound = buckets.getUpperBound(i);
189                Metrics.Bucket.Builder bucketBuilder = Metrics.Bucket.newBuilder()
190                        .setCumulativeCount(cumulativeCount)
191                        .setUpperBound(upperBound);
192                Exemplar exemplar = data.getExemplars().get(lowerBound, upperBound);
193                if (exemplar != null) {
194                    bucketBuilder.setExemplar(convert(exemplar));
195                }
196                histogramBuilder.addBucket(bucketBuilder);
197                lowerBound = upperBound;
198            }
199        }
200        addLabels(metricBuilder, data.getLabels());
201        setScrapeTimestamp(metricBuilder, data);
202        if (data.hasCount()) {
203            histogramBuilder.setSampleCount(data.getCount());
204        }
205        if (data.hasSum()) {
206            histogramBuilder.setSampleSum(data.getSum());
207        }
208        metricBuilder.setHistogram(histogramBuilder.build());
209        return metricBuilder;
210    }
211
212    private long getNativeCount(HistogramSnapshot.HistogramDataPointSnapshot data) {
213        if (data.hasCount()) {
214            return data.getCount();
215        } else {
216            long count = data.getNativeZeroCount();
217            for (int i = 0; i < data.getNativeBucketsForPositiveValues().size(); i++) {
218                count += data.getNativeBucketsForPositiveValues().getCount(i);
219            }
220            for (int i = 0; i < data.getNativeBucketsForNegativeValues().size(); i++) {
221                count += data.getNativeBucketsForNegativeValues().getCount(i);
222            }
223            return count;
224        }
225    }
226
227    private void addBuckets(Metrics.Histogram.Builder histogramBuilder, NativeHistogramBuckets buckets, int sgn) {
228        if (buckets.size() > 0) {
229            Metrics.BucketSpan.Builder currentSpan = Metrics.BucketSpan.newBuilder();
230            currentSpan.setOffset(buckets.getBucketIndex(0));
231            currentSpan.setLength(0);
232            int previousIndex = currentSpan.getOffset();
233            long previousCount = 0;
234            for (int i = 0; i < buckets.size(); i++) {
235                if (buckets.getBucketIndex(i) > previousIndex + 1) {
236                    // If the gap between bucketIndex and previousIndex is just 1 or 2,
237                    // we don't start a new span but continue the existing span and add 1 or 2 empty buckets.
238                    if (buckets.getBucketIndex(i) <= previousIndex + 3) {
239                        while (buckets.getBucketIndex(i) > previousIndex + 1) {
240                            currentSpan.setLength(currentSpan.getLength() + 1);
241                            previousIndex++;
242                            if (sgn > 0) {
243                                histogramBuilder.addPositiveDelta(-previousCount);
244                            } else {
245                                histogramBuilder.addNegativeDelta(-previousCount);
246                            }
247                            previousCount = 0;
248                        }
249                    } else {
250                        if (sgn > 0) {
251                            histogramBuilder.addPositiveSpan(currentSpan.build());
252                        } else {
253                            histogramBuilder.addNegativeSpan(currentSpan.build());
254                        }
255                        currentSpan = Metrics.BucketSpan.newBuilder();
256                        currentSpan.setOffset(buckets.getBucketIndex(i) - (previousIndex + 1));
257                    }
258                }
259                currentSpan.setLength(currentSpan.getLength() + 1);
260                previousIndex = buckets.getBucketIndex(i);
261                if (sgn > 0) {
262                    histogramBuilder.addPositiveDelta(buckets.getCount(i) - previousCount);
263                } else {
264                    histogramBuilder.addNegativeDelta(buckets.getCount(i) - previousCount);
265                }
266                previousCount = buckets.getCount(i);
267            }
268            if (sgn > 0) {
269                histogramBuilder.addPositiveSpan(currentSpan.build());
270            } else {
271                histogramBuilder.addNegativeSpan(currentSpan.build());
272            }
273        }
274    }
275
276    private Metrics.Metric.Builder convert(SummarySnapshot.SummaryDataPointSnapshot data) {
277        Metrics.Metric.Builder metricBuilder = Metrics.Metric.newBuilder();
278        Metrics.Summary.Builder summaryBuilder = Metrics.Summary.newBuilder();
279        if (data.hasCount()) {
280            summaryBuilder.setSampleCount(data.getCount());
281        }
282        if (data.hasSum()) {
283            summaryBuilder.setSampleSum(data.getSum());
284        }
285        Quantiles quantiles = data.getQuantiles();
286        for (int i = 0; i < quantiles.size(); i++) {
287            summaryBuilder.addQuantile(Metrics.Quantile.newBuilder()
288                    .setQuantile(quantiles.get(i).getQuantile())
289                    .setValue(quantiles.get(i).getValue())
290                    .build());
291        }
292        addLabels(metricBuilder, data.getLabels());
293        metricBuilder.setSummary(summaryBuilder.build());
294        setScrapeTimestamp(metricBuilder, data);
295        return metricBuilder;
296    }
297
298    private Metrics.Metric.Builder convert(InfoSnapshot.InfoDataPointSnapshot data) {
299        Metrics.Metric.Builder metricBuilder = Metrics.Metric.newBuilder();
300        Metrics.Gauge.Builder gaugeBuilder = Metrics.Gauge.newBuilder();
301        gaugeBuilder.setValue(1);
302        addLabels(metricBuilder, data.getLabels());
303        metricBuilder.setGauge(gaugeBuilder);
304        setScrapeTimestamp(metricBuilder, data);
305        return metricBuilder;
306    }
307
308    private Metrics.Metric.Builder convert(StateSetSnapshot.StateSetDataPointSnapshot data, String name, int i) {
309        Metrics.Metric.Builder metricBuilder = Metrics.Metric.newBuilder();
310        Metrics.Gauge.Builder gaugeBuilder = Metrics.Gauge.newBuilder();
311        addLabels(metricBuilder, data.getLabels());
312        metricBuilder.addLabel(Metrics.LabelPair.newBuilder()
313                .setName(name)
314                .setValue(data.getName(i))
315                .build());
316        if (data.isTrue(i)) {
317            gaugeBuilder.setValue(1);
318        } else {
319            gaugeBuilder.setValue(0);
320        }
321        metricBuilder.setGauge(gaugeBuilder);
322        setScrapeTimestamp(metricBuilder, data);
323        return metricBuilder;
324    }
325
326    private Metrics.Metric.Builder convert(UnknownSnapshot.UnknownDataPointSnapshot data) {
327        Metrics.Metric.Builder metricBuilder = Metrics.Metric.newBuilder();
328        Metrics.Untyped.Builder untypedBuilder = Metrics.Untyped.newBuilder();
329        untypedBuilder.setValue(data.getValue());
330        addLabels(metricBuilder, data.getLabels());
331        metricBuilder.setUntyped(untypedBuilder);
332        return metricBuilder;
333    }
334
335    private void addLabels(Metrics.Metric.Builder metricBuilder, Labels labels) {
336        for (int i = 0; i < labels.size(); i++) {
337            metricBuilder.addLabel(Metrics.LabelPair.newBuilder()
338                    .setName(labels.getPrometheusName(i))
339                    .setValue(labels.getValue(i))
340                    .build());
341        }
342    }
343
344    private void addLabels(Metrics.Exemplar.Builder metricBuilder, Labels labels) {
345        for (int i = 0; i < labels.size(); i++) {
346            metricBuilder.addLabel(Metrics.LabelPair.newBuilder()
347                    .setName(labels.getPrometheusName(i))
348                    .setValue(labels.getValue(i))
349                    .build());
350        }
351    }
352
353    private Metrics.Exemplar.Builder convert(Exemplar exemplar) {
354        Metrics.Exemplar.Builder builder = Metrics.Exemplar.newBuilder();
355        builder.setValue(exemplar.getValue());
356        addLabels(builder, exemplar.getLabels());
357        if (exemplar.hasTimestamp()) {
358            builder.setTimestamp(timestampFromMillis(exemplar.getTimestampMillis()));
359        }
360        return builder;
361    }
362
363    private void setScrapeTimestamp(Metrics.Metric.Builder metricBuilder, DataPointSnapshot data) {
364        if (data.hasScrapeTimestamp()) {
365            metricBuilder.setTimestampMs(data.getScrapeTimestampMillis());
366        }
367    }
368}