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