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