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