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