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