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