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 &lt;= schema &lt;= 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}