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}