001package io.prometheus.metrics.model.snapshots; 002 003import io.prometheus.metrics.annotations.StableApi; 004import io.prometheus.metrics.config.EscapingScheme; 005import java.util.ArrayList; 006import java.util.Collection; 007import java.util.List; 008 009/** Immutable snapshot of a Histogram. */ 010@StableApi 011public final class HistogramSnapshot extends MetricSnapshot { 012 013 private final boolean gaugeHistogram; 014 public static final int CLASSIC_HISTOGRAM = Integer.MIN_VALUE; 015 016 /** 017 * To create a new {@link HistogramSnapshot}, you can either call the constructor directly or use 018 * the builder with {@link HistogramSnapshot#builder()}. 019 * 020 * @param metadata see {@link MetricMetadata} for naming conventions. 021 * @param data the constructor will create a sorted copy of the collection. 022 */ 023 public HistogramSnapshot(MetricMetadata metadata, Collection<HistogramDataPointSnapshot> data) { 024 this(false, metadata, data); 025 } 026 027 /** 028 * Use this with the first parameter {@code true} to create a snapshot of a Gauge Histogram. The 029 * data model for Gauge Histograms is the same as for regular histograms, except that bucket 030 * values are semantically gauges and not counters. See <a 031 * href="https://openmetrics.io">openmetrics.io</a> for more info on Gauge Histograms. 032 */ 033 public HistogramSnapshot( 034 boolean isGaugeHistogram, 035 MetricMetadata metadata, 036 Collection<HistogramDataPointSnapshot> data) { 037 this(isGaugeHistogram, metadata, data, false); 038 } 039 040 private HistogramSnapshot( 041 boolean isGaugeHistogram, 042 MetricMetadata metadata, 043 Collection<HistogramDataPointSnapshot> data, 044 boolean internal) { 045 super(metadata, data, internal); 046 this.gaugeHistogram = isGaugeHistogram; 047 } 048 049 public boolean isGaugeHistogram() { 050 return gaugeHistogram; 051 } 052 053 @SuppressWarnings("unchecked") 054 @Override 055 public List<HistogramDataPointSnapshot> getDataPoints() { 056 return (List<HistogramDataPointSnapshot>) dataPoints; 057 } 058 059 @SuppressWarnings("unchecked") 060 @Override 061 MetricSnapshot escape( 062 EscapingScheme escapingScheme, List<? extends DataPointSnapshot> dataPointSnapshots) { 063 return new HistogramSnapshot( 064 gaugeHistogram, 065 getMetadata().escape(escapingScheme), 066 (List<HistogramSnapshot.HistogramDataPointSnapshot>) dataPointSnapshots, 067 true); 068 } 069 070 public static final class HistogramDataPointSnapshot extends DistributionDataPointSnapshot { 071 072 // There are two types of histograms: Classic histograms and native histograms. 073 // Classic histograms have a fixed set of buckets. 074 // Native histograms have "infinitely many" buckets with exponentially growing boundaries. 075 // The OpenTelemetry terminology for native histogram is "exponential histogram". 076 // --- 077 // A histogram can be a classic histogram (indicated by nativeSchema == CLASSIC_HISTOGRAM), 078 // or a native histogram (indicated by classicBuckets == ClassicHistogramBuckets.EMPTY), 079 // or both. 080 // --- 081 // A histogram that is both classic and native is great for migrating from classic histograms 082 // to native histograms: Old Prometheus servers can still scrape the classic histogram, while 083 // new Prometheus servers can scrape the native histogram. 084 085 private final ClassicHistogramBuckets 086 classicBuckets; // May be ClassicHistogramBuckets.EMPTY for native histograms. 087 private final int 088 nativeSchema; // Number in [-4, 8]. May be CLASSIC_HISTOGRAM for classic histograms. 089 private final long nativeZeroCount; // only used if nativeSchema != CLASSIC_HISTOGRAM 090 private final double nativeZeroThreshold; // only used if nativeSchema != CLASSIC_HISTOGRAM 091 private final NativeHistogramBuckets 092 nativeBucketsForPositiveValues; // only used if nativeSchema != CLASSIC_HISTOGRAM 093 private final NativeHistogramBuckets 094 nativeBucketsForNegativeValues; // only used if nativeSchema != CLASSIC_HISTOGRAM 095 096 /** 097 * Constructor for classic histograms (as opposed to native histograms). 098 * 099 * <p>To create a new {@link HistogramDataPointSnapshot}, you can either call the constructor 100 * directly or use the Builder with {@link HistogramSnapshot#builder()}. 101 * 102 * @param classicBuckets required. Must not be empty. Must at least contain the +Inf bucket. 103 * @param sum sum of all observed values. Optional, pass {@link Double#NaN} if not available. 104 * @param labels must not be null. Use {@link Labels#EMPTY} if there are no labels. 105 * @param exemplars must not be null. Use {@link Exemplars#EMPTY} if there are no Exemplars. 106 * @param createdTimestampMillis timestamp (as in {@link System#currentTimeMillis()}) when the 107 * time series (this specific set of labels) was created (or reset to zero). It's optional. 108 * Use {@code 0L} if there is no created timestamp. 109 */ 110 public HistogramDataPointSnapshot( 111 ClassicHistogramBuckets classicBuckets, 112 double sum, 113 Labels labels, 114 Exemplars exemplars, 115 long createdTimestampMillis) { 116 this( 117 classicBuckets, 118 CLASSIC_HISTOGRAM, 119 0, 120 0, 121 NativeHistogramBuckets.EMPTY, 122 NativeHistogramBuckets.EMPTY, 123 sum, 124 labels, 125 exemplars, 126 createdTimestampMillis, 127 0L); 128 } 129 130 /** 131 * Constructor for native histograms (as opposed to classic histograms). 132 * 133 * <p>To create a new {@link HistogramDataPointSnapshot}, you can either call the constructor 134 * directly or use the Builder with {@link HistogramSnapshot#builder()}. 135 * 136 * @param nativeSchema number in [-4, 8]. See <a 137 * href="https://github.com/prometheus/client_model/blob/7f720d22828060526c55ac83bceff08f43d4cdbc/io/prometheus/client/metrics.proto#L76-L80">Prometheus 138 * client_model metrics.proto</a>. 139 * @param nativeZeroCount number of observed zero values (zero is special because there is no 140 * histogram bucket for zero values). 141 * @param nativeZeroThreshold observations in [-zeroThreshold, +zeroThreshold] are treated as 142 * zero. This is to avoid creating a large number of buckets if observations fluctuate 143 * around zero. 144 * @param nativeBucketsForPositiveValues must not be {@code null}. Use {@link 145 * NativeHistogramBuckets#EMPTY} if empty. 146 * @param nativeBucketsForNegativeValues must not be {@code null}. Use {@link 147 * NativeHistogramBuckets#EMPTY} if empty. 148 * @param sum sum of all observed values. Optional, use {@link Double#NaN} if not available. 149 * @param labels must not be null. Use {@link Labels#EMPTY} if there are no labels. 150 * @param exemplars must not be null. Use {@link Exemplars#EMPTY} if there are no Exemplars. 151 * @param createdTimestampMillis timestamp (as in {@link System#currentTimeMillis()}) when the 152 * time series (this specific set of labels) was created (or reset to zero). It's optional. 153 * Use {@code 0L} if there is no created timestamp. 154 */ 155 public HistogramDataPointSnapshot( 156 int nativeSchema, 157 long nativeZeroCount, 158 double nativeZeroThreshold, 159 NativeHistogramBuckets nativeBucketsForPositiveValues, 160 NativeHistogramBuckets nativeBucketsForNegativeValues, 161 double sum, 162 Labels labels, 163 Exemplars exemplars, 164 long createdTimestampMillis) { 165 this( 166 ClassicHistogramBuckets.EMPTY, 167 nativeSchema, 168 nativeZeroCount, 169 nativeZeroThreshold, 170 nativeBucketsForPositiveValues, 171 nativeBucketsForNegativeValues, 172 sum, 173 labels, 174 exemplars, 175 createdTimestampMillis, 176 0L); 177 } 178 179 /** 180 * Constructor for a histogram with both, classic and native data. 181 * 182 * <p>To create a new {@link HistogramDataPointSnapshot}, you can either call the constructor 183 * directly or use the Builder with {@link HistogramSnapshot#builder()}. 184 * 185 * @param classicBuckets required. Must not be empty. Must at least contain the +Inf bucket. 186 * @param nativeSchema number in [-4, 8]. See <a 187 * href="https://github.com/prometheus/client_model/blob/7f720d22828060526c55ac83bceff08f43d4cdbc/io/prometheus/client/metrics.proto#L76-L80">Prometheus 188 * client_model metrics.proto</a>. 189 * @param nativeZeroCount number of observed zero values (zero is special because there is no 190 * histogram bucket for zero values). 191 * @param nativeZeroThreshold observations in [-zeroThreshold, +zeroThreshold] are treated as 192 * zero. This is to avoid creating a large number of buckets if observations fluctuate 193 * around zero. 194 * @param nativeBucketsForPositiveValues must not be {@code null}. Use {@link 195 * NativeHistogramBuckets#EMPTY} if empty. 196 * @param nativeBucketsForNegativeValues must not be {@code null}. Use {@link 197 * NativeHistogramBuckets#EMPTY} if empty. 198 * @param sum sum of all observed values. Optional, use {@link Double#NaN} if not available. 199 * @param labels must not be null. Use {@link Labels#EMPTY} if there are no labels. 200 * @param exemplars must not be null. Use {@link Exemplars#EMPTY} if there are no Exemplars. 201 * @param createdTimestampMillis timestamp (as in {@link System#currentTimeMillis()}) when the 202 * time series (this specific set of labels) was created (or reset to zero). It's optional. 203 * Use {@code 0L} if there is no created timestamp. 204 */ 205 public HistogramDataPointSnapshot( 206 ClassicHistogramBuckets classicBuckets, 207 int nativeSchema, 208 long nativeZeroCount, 209 double nativeZeroThreshold, 210 NativeHistogramBuckets nativeBucketsForPositiveValues, 211 NativeHistogramBuckets nativeBucketsForNegativeValues, 212 double sum, 213 Labels labels, 214 Exemplars exemplars, 215 long createdTimestampMillis) { 216 this( 217 classicBuckets, 218 nativeSchema, 219 nativeZeroCount, 220 nativeZeroThreshold, 221 nativeBucketsForPositiveValues, 222 nativeBucketsForNegativeValues, 223 sum, 224 labels, 225 exemplars, 226 createdTimestampMillis, 227 0L); 228 } 229 230 /** 231 * Constructor with an additional scrape timestamp. This is only useful in rare cases as the 232 * scrape timestamp is usually set by the Prometheus server during scraping. Exceptions include 233 * mirroring metrics with given timestamps from other metric sources. 234 */ 235 public HistogramDataPointSnapshot( 236 ClassicHistogramBuckets classicBuckets, 237 int nativeSchema, 238 long nativeZeroCount, 239 double nativeZeroThreshold, 240 NativeHistogramBuckets nativeBucketsForPositiveValues, 241 NativeHistogramBuckets nativeBucketsForNegativeValues, 242 double sum, 243 Labels labels, 244 Exemplars exemplars, 245 long createdTimestampMillis, 246 long scrapeTimestampMillis) { 247 this( 248 classicBuckets, 249 nativeSchema, 250 sum, 251 labels, 252 exemplars, 253 createdTimestampMillis, 254 scrapeTimestampMillis, 255 calculateCount( 256 classicBuckets, 257 nativeSchema, 258 nativeZeroCount, 259 nativeBucketsForPositiveValues, 260 nativeBucketsForNegativeValues), 261 nativeSchema == CLASSIC_HISTOGRAM 262 ? NativeHistogramBuckets.EMPTY 263 : nativeBucketsForPositiveValues, 264 nativeSchema == CLASSIC_HISTOGRAM 265 ? NativeHistogramBuckets.EMPTY 266 : nativeBucketsForNegativeValues, 267 nativeSchema == CLASSIC_HISTOGRAM ? 0 : nativeZeroCount, 268 nativeSchema == CLASSIC_HISTOGRAM ? 0 : nativeZeroThreshold, 269 false); 270 validate(); 271 } 272 273 private HistogramDataPointSnapshot( 274 ClassicHistogramBuckets classicBuckets, 275 int nativeSchema, 276 double sum, 277 Labels labels, 278 Exemplars exemplars, 279 long createdTimestampMillis, 280 long scrapeTimestampMillis, 281 long count, 282 NativeHistogramBuckets nativeBucketsForPositiveValues, 283 NativeHistogramBuckets nativeBucketsForNegativeValues, 284 long nativeZeroCount, 285 double nativeZeroThreshold, 286 boolean internal) { 287 super(count, sum, exemplars, labels, createdTimestampMillis, scrapeTimestampMillis, internal); 288 this.classicBuckets = classicBuckets; 289 this.nativeSchema = nativeSchema; 290 this.nativeZeroCount = nativeZeroCount; 291 this.nativeZeroThreshold = nativeZeroThreshold; 292 this.nativeBucketsForPositiveValues = nativeBucketsForPositiveValues; 293 this.nativeBucketsForNegativeValues = nativeBucketsForNegativeValues; 294 } 295 296 private static long calculateCount( 297 ClassicHistogramBuckets classicBuckets, 298 int nativeSchema, 299 long nativeZeroCount, 300 NativeHistogramBuckets nativeBucketsForPositiveValues, 301 NativeHistogramBuckets nativeBucketsForNegativeValues) { 302 if (classicBuckets.isEmpty()) { 303 // This is a native histogram 304 return calculateNativeCount( 305 nativeZeroCount, nativeBucketsForPositiveValues, nativeBucketsForNegativeValues); 306 } else if (nativeSchema == CLASSIC_HISTOGRAM) { 307 // This is a classic histogram 308 return calculateClassicCount(classicBuckets); 309 } else { 310 // This is both, a native and a classic histogram. Count should be the same for both. 311 long classicCount = calculateClassicCount(classicBuckets); 312 long nativeCount = 313 calculateNativeCount( 314 nativeZeroCount, nativeBucketsForPositiveValues, nativeBucketsForNegativeValues); 315 if (classicCount != nativeCount) { 316 throw new IllegalArgumentException( 317 "Inconsistent observation count: If a histogram has both classic and native " 318 + "data the observation count must be the same. Classic count is " 319 + classicCount 320 + " but native count is " 321 + nativeCount 322 + "."); 323 } 324 return classicCount; 325 } 326 } 327 328 private static long calculateClassicCount(ClassicHistogramBuckets classicBuckets) { 329 long count = 0; 330 for (int i = 0; i < classicBuckets.size(); i++) { 331 count += classicBuckets.getCount(i); 332 } 333 return count; 334 } 335 336 private static long calculateNativeCount( 337 long nativeZeroCount, 338 NativeHistogramBuckets nativeBucketsForPositiveValues, 339 NativeHistogramBuckets nativeBucketsForNegativeValues) { 340 long count = nativeZeroCount; 341 for (int i = 0; i < nativeBucketsForNegativeValues.size(); i++) { 342 count += nativeBucketsForNegativeValues.getCount(i); 343 } 344 for (int i = 0; i < nativeBucketsForPositiveValues.size(); i++) { 345 count += nativeBucketsForPositiveValues.getCount(i); 346 } 347 return count; 348 } 349 350 public boolean hasClassicHistogramData() { 351 return !classicBuckets.isEmpty(); 352 } 353 354 public boolean hasNativeHistogramData() { 355 return nativeSchema != CLASSIC_HISTOGRAM; 356 } 357 358 /** Will return garbage if {@link #hasClassicHistogramData()} is {@code false}. */ 359 public ClassicHistogramBuckets getClassicBuckets() { 360 return classicBuckets; 361 } 362 363 /** 364 * The schema defines the scale of the native histogram, i.g. the granularity of the buckets. 365 * Current supported values are -4 <= schema <= 8. See {@link NativeHistogramBuckets} for 366 * more info. This will return garbage if {@link #hasNativeHistogramData()} is {@code false}. 367 */ 368 public int getNativeSchema() { 369 return nativeSchema; 370 } 371 372 /** 373 * Number of observed zero values. Will return garbage if {@link #hasNativeHistogramData()} is 374 * {@code false}. 375 */ 376 public long getNativeZeroCount() { 377 return nativeZeroCount; 378 } 379 380 /** 381 * All observations in [-nativeZeroThreshold; +nativeZeroThreshold] are treated as zero. This is 382 * useful to avoid creation of a large number of buckets if observations fluctuate around zero. 383 * Will return garbage if {@link #hasNativeHistogramData()} is {@code false}. 384 */ 385 public double getNativeZeroThreshold() { 386 return nativeZeroThreshold; 387 } 388 389 /** Will return garbage if {@link #hasNativeHistogramData()} is {@code false}. */ 390 public NativeHistogramBuckets getNativeBucketsForPositiveValues() { 391 return nativeBucketsForPositiveValues; 392 } 393 394 /** Will return garbage if {@link #hasNativeHistogramData()} is {@code false}. */ 395 public NativeHistogramBuckets getNativeBucketsForNegativeValues() { 396 return nativeBucketsForNegativeValues; 397 } 398 399 private void validate() { 400 for (Label label : getLabels()) { 401 if (label.getName().equals("le")) { 402 throw new IllegalArgumentException("le is a reserved label name for histograms"); 403 } 404 } 405 if (nativeSchema == CLASSIC_HISTOGRAM && classicBuckets.isEmpty()) { 406 throw new IllegalArgumentException( 407 "Histogram buckets cannot be empty, must at least have the +Inf bucket."); 408 } 409 if (nativeSchema != CLASSIC_HISTOGRAM) { 410 if (nativeSchema < -4 || nativeSchema > 8) { 411 throw new IllegalArgumentException( 412 nativeSchema + ": illegal schema. Expecting number in [-4, 8]."); 413 } 414 if (nativeZeroCount < 0) { 415 throw new IllegalArgumentException( 416 nativeZeroCount + ": nativeZeroCount cannot be negative"); 417 } 418 if (Double.isNaN(nativeZeroThreshold) || nativeZeroThreshold < 0) { 419 throw new IllegalArgumentException( 420 nativeZeroThreshold + ": illegal nativeZeroThreshold. Must be >= 0."); 421 } 422 } 423 } 424 425 @Override 426 DataPointSnapshot escape(EscapingScheme escapingScheme) { 427 return new HistogramSnapshot.HistogramDataPointSnapshot( 428 classicBuckets, 429 nativeSchema, 430 getSum(), 431 SnapshotEscaper.escapeLabels(getLabels(), escapingScheme), 432 SnapshotEscaper.escapeExemplars(getExemplars(), escapingScheme), 433 getCreatedTimestampMillis(), 434 getScrapeTimestampMillis(), 435 getCount(), 436 nativeBucketsForPositiveValues, 437 nativeBucketsForNegativeValues, 438 nativeZeroCount, 439 nativeZeroThreshold, 440 true); 441 } 442 443 public static Builder builder() { 444 return new Builder(); 445 } 446 447 public static class Builder extends DistributionDataPointSnapshot.Builder<Builder> { 448 449 private ClassicHistogramBuckets classicHistogramBuckets = ClassicHistogramBuckets.EMPTY; 450 private int nativeSchema = CLASSIC_HISTOGRAM; 451 private long nativeZeroCount = 0; 452 private double nativeZeroThreshold = 0; 453 private NativeHistogramBuckets nativeBucketsForPositiveValues = NativeHistogramBuckets.EMPTY; 454 private NativeHistogramBuckets nativeBucketsForNegativeValues = NativeHistogramBuckets.EMPTY; 455 456 private Builder() {} 457 458 @Override 459 protected Builder self() { 460 return this; 461 } 462 463 public Builder classicHistogramBuckets(ClassicHistogramBuckets classicBuckets) { 464 this.classicHistogramBuckets = classicBuckets; 465 return this; 466 } 467 468 public Builder nativeSchema(int nativeSchema) { 469 this.nativeSchema = nativeSchema; 470 return this; 471 } 472 473 public Builder nativeZeroCount(long zeroCount) { 474 this.nativeZeroCount = zeroCount; 475 return this; 476 } 477 478 public Builder nativeZeroThreshold(double zeroThreshold) { 479 this.nativeZeroThreshold = zeroThreshold; 480 return this; 481 } 482 483 public Builder nativeBucketsForPositiveValues( 484 NativeHistogramBuckets bucketsForPositiveValues) { 485 this.nativeBucketsForPositiveValues = bucketsForPositiveValues; 486 return this; 487 } 488 489 public Builder nativeBucketsForNegativeValues( 490 NativeHistogramBuckets bucketsForNegativeValues) { 491 this.nativeBucketsForNegativeValues = bucketsForNegativeValues; 492 return this; 493 } 494 495 public HistogramDataPointSnapshot build() { 496 if (nativeSchema == CLASSIC_HISTOGRAM && classicHistogramBuckets.isEmpty()) { 497 throw new IllegalArgumentException( 498 "One of nativeSchema and classicHistogramBuckets is required."); 499 } 500 return new HistogramDataPointSnapshot( 501 classicHistogramBuckets, 502 nativeSchema, 503 nativeZeroCount, 504 nativeZeroThreshold, 505 nativeBucketsForPositiveValues, 506 nativeBucketsForNegativeValues, 507 sum, 508 labels, 509 exemplars, 510 createdTimestampMillis, 511 scrapeTimestampMillis); 512 } 513 } 514 } 515 516 public static Builder builder() { 517 return new Builder(); 518 } 519 520 public static class Builder extends MetricSnapshot.Builder<Builder> { 521 522 private final List<HistogramDataPointSnapshot> dataPoints = new ArrayList<>(); 523 private boolean isGaugeHistogram = false; 524 525 private Builder() {} 526 527 /** Add a data point. Call multiple times to add multiple data points. */ 528 public Builder dataPoint(HistogramDataPointSnapshot dataPoint) { 529 dataPoints.add(dataPoint); 530 return this; 531 } 532 533 /** 534 * {@code true} indicates that this histogram is a gauge histogram. The data model for gauge 535 * histograms is the same as for regular histograms, except that bucket values are semantically 536 * gauges and not counters. See <a href="https://openmetrics.io">openmetrics.io</a> for more 537 * info on gauge histograms. 538 */ 539 public Builder gaugeHistogram(boolean isGaugeHistogram) { 540 this.isGaugeHistogram = isGaugeHistogram; 541 return this; 542 } 543 544 @Override 545 public HistogramSnapshot build() { 546 return new HistogramSnapshot(isGaugeHistogram, buildMetadata(), dataPoints); 547 } 548 549 @Override 550 protected Builder self() { 551 return this; 552 } 553 } 554}