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