001package io.prometheus.metrics.core.metrics;
002
003import io.prometheus.metrics.annotations.StableApi;
004import io.prometheus.metrics.config.ExemplarsProperties;
005import io.prometheus.metrics.config.MetricsProperties;
006import io.prometheus.metrics.config.PrometheusProperties;
007import io.prometheus.metrics.core.datapoints.DistributionDataPoint;
008import io.prometheus.metrics.core.exemplars.ExemplarSampler;
009import io.prometheus.metrics.core.exemplars.ExemplarSamplerConfig;
010import io.prometheus.metrics.core.util.Scheduler;
011import io.prometheus.metrics.model.registry.MetricType;
012import io.prometheus.metrics.model.snapshots.ClassicHistogramBuckets;
013import io.prometheus.metrics.model.snapshots.Exemplars;
014import io.prometheus.metrics.model.snapshots.HistogramSnapshot;
015import io.prometheus.metrics.model.snapshots.Labels;
016import io.prometheus.metrics.model.snapshots.NativeHistogramBuckets;
017import java.math.BigDecimal;
018import java.util.ArrayList;
019import java.util.Collections;
020import java.util.List;
021import java.util.Map;
022import java.util.SortedSet;
023import java.util.TreeSet;
024import java.util.concurrent.ConcurrentHashMap;
025import java.util.concurrent.TimeUnit;
026import java.util.concurrent.atomic.AtomicBoolean;
027import java.util.concurrent.atomic.DoubleAdder;
028import java.util.concurrent.atomic.LongAdder;
029import java.util.function.Supplier;
030import javax.annotation.Nullable;
031
032/**
033 * Histogram metric. Example usage:
034 *
035 * <pre>{@code
036 * Histogram histogram = Histogram.builder()
037 *         .name("http_request_duration_seconds")
038 *         .help("HTTP request service time in seconds")
039 *         .unit(SECONDS)
040 *         .labelNames("method", "path", "status_code")
041 *         .register();
042 *
043 * long start = System.nanoTime();
044 * // do something
045 * histogram.labelValues("GET", "/", "200").observe(Unit.nanosToSeconds(System.nanoTime() - start));
046 * }</pre>
047 *
048 * Prometheus supports two internal representations of histograms:
049 *
050 * <ol>
051 *   <li><i>Classic Histograms</i> have a fixed number of buckets with fixed bucket boundaries.
052 *   <li><i>Native Histograms</i> have an infinite number of buckets with a dynamic resolution.
053 *       Prometheus native histograms are the same as OpenTelemetry's exponential histograms.
054 * </ol>
055 *
056 * By default, a histogram maintains both representations, i.e. the example above will maintain a
057 * classic histogram representation with Prometheus' default bucket boundaries as well as native
058 * histogram representation. Which representation is used depends on the exposition format, i.e.
059 * which content type the Prometheus server accepts when scraping. Exposition format "Text" exposes
060 * the classic histogram, exposition format "Protobuf" exposes both representations. This is great
061 * for migrating from classic histograms to native histograms.
062 *
063 * <p>If you want the classic representation only, use {@link Histogram.Builder#classicOnly}. If you
064 * want the native representation only, use {@link Histogram.Builder#nativeOnly}.
065 */
066@StableApi
067public class Histogram extends StatefulMetric<DistributionDataPoint, Histogram.DataPoint>
068    implements DistributionDataPoint {
069
070  // nativeSchema == CLASSIC_HISTOGRAM indicates that this is a classic histogram only.
071  private static final int CLASSIC_HISTOGRAM = Integer.MIN_VALUE;
072
073  // NATIVE_BOUNDS is used to look up the native bucket index depending on the current schema.
074  private static final double[][] NATIVE_BOUNDS;
075
076  @Nullable private final ExemplarSamplerConfig exemplarSamplerConfig;
077  @Nullable private final Supplier<Labels> exemplarLabelsSupplier;
078
079  // Upper bounds for the classic histogram buckets. Contains at least +Inf.
080  // An empty array indicates that this is a native histogram only.
081  private final double[] classicUpperBounds;
082
083  // The schema defines the resolution of the native histogram.
084  // Schema is Prometheus terminology, in OpenTelemetry it's named "scale".
085  // The formula for the bucket boundaries at position "index" is:
086  //
087  // base := base = (2^(2^-scale))
088  // lowerBound := base^(index-1)
089  // upperBound := base^(index)
090  //
091  // Note that this is off-by-one compared to OpenTelemetry.
092  //
093  // Example: With schema 0 the bucket boundaries are ... 1/16, 1/8, 1/4, 1/2, 1, 2, 4, 8, 16, ...
094  // Each increment in schema doubles the number of buckets.
095  //
096  // The initialNativeSchema is the schema we start with. The histogram will automatically scale
097  // down
098  // if the number of native histogram buckets exceeds nativeMaxBuckets.
099  private final int nativeInitialSchema; // integer in [-4, 8]
100
101  // Native histogram buckets get smaller and smaller the closer they get to zero.
102  // To avoid wasting a lot of buckets for observations fluctuating around zero, we consider all
103  // values in [-zeroThreshold, +zeroThreshold] to be equal to zero.
104  //
105  // The zeroThreshold is initialized with minZeroThreshold, and will grow up to maxZeroThreshold if
106  // the number of native histogram buckets exceeds nativeMaxBuckets.
107  private final double nativeMinZeroThreshold;
108  private final double nativeMaxZeroThreshold;
109
110  // When the number of native histogram buckets becomes larger than nativeMaxBuckets,
111  // an attempt is made to reduce the number of buckets:
112  // (1) Reset if the last reset is longer than the reset duration ago
113  // (2) Increase the zero bucket width if it's smaller than nativeMaxZeroThreshold
114  // (3) Decrease the nativeSchema, i.e. merge pairs of neighboring buckets into one
115  private final int nativeMaxBuckets;
116
117  // If the number of native histogram buckets exceeds nativeMaxBuckets,
118  // the histogram may reset (all values set to zero) after nativeResetDurationSeconds is expired.
119  private final long nativeResetDurationSeconds; // 0 indicates no reset
120
121  private Histogram(Histogram.Builder builder, PrometheusProperties prometheusProperties) {
122    super(builder);
123    MetricsProperties[] properties = getMetricProperties(builder, prometheusProperties);
124    nativeInitialSchema =
125        getConfigProperty(
126            properties,
127            props -> {
128              if (Boolean.TRUE.equals(props.getHistogramClassicOnly())) {
129                return CLASSIC_HISTOGRAM;
130              } else {
131                return props.getHistogramNativeInitialSchema();
132              }
133            });
134    classicUpperBounds =
135        getConfigProperty(
136            properties,
137            props -> {
138              if (Boolean.TRUE.equals(props.getHistogramNativeOnly())) {
139                return new double[] {};
140              } else if (props.getHistogramClassicUpperBounds() != null) {
141                SortedSet<Double> upperBounds =
142                    new TreeSet<>(props.getHistogramClassicUpperBounds());
143                upperBounds.add(Double.POSITIVE_INFINITY);
144                double[] result = new double[upperBounds.size()];
145                int i = 0;
146                for (double upperBound : upperBounds) {
147                  result[i++] = upperBound;
148                }
149                return result;
150              } else {
151                return null;
152              }
153            });
154    double max =
155        getConfigProperty(properties, MetricsProperties::getHistogramNativeMaxZeroThreshold);
156    double min =
157        getConfigProperty(properties, MetricsProperties::getHistogramNativeMinZeroThreshold);
158    nativeMaxZeroThreshold =
159        max == Builder.DEFAULT_NATIVE_MAX_ZERO_THRESHOLD && min > max ? min : max;
160    nativeMinZeroThreshold = Math.min(min, nativeMaxZeroThreshold);
161    nativeMaxBuckets =
162        getConfigProperty(properties, MetricsProperties::getHistogramNativeMaxNumberOfBuckets);
163    nativeResetDurationSeconds =
164        getConfigProperty(properties, MetricsProperties::getHistogramNativeResetDurationSeconds);
165    boolean exemplarsEnabled =
166        getConfigProperty(properties, MetricsProperties::getExemplarsEnabled);
167    ExemplarsProperties exemplarsProperties = prometheusProperties.getExemplarProperties();
168    if (exemplarsEnabled) {
169      exemplarSamplerConfig =
170          classicUpperBounds.length == 0
171              ? new ExemplarSamplerConfig(exemplarsProperties, 4)
172              : new ExemplarSamplerConfig(exemplarsProperties, classicUpperBounds);
173    } else {
174      exemplarSamplerConfig = null;
175    }
176    exemplarLabelsSupplier = builder.exemplarLabelsSupplier;
177  }
178
179  @Override
180  public double getSum() {
181    return getNoLabels().getSum();
182  }
183
184  @Override
185  public long getCount() {
186    return getNoLabels().getCount();
187  }
188
189  @Override
190  public void observe(double amount) {
191    getNoLabels().observe(amount);
192  }
193
194  @Override
195  public void observeWithExemplar(double amount, Labels labels) {
196    getNoLabels().observeWithExemplar(amount, labels);
197  }
198
199  public class DataPoint implements DistributionDataPoint {
200    private final LongAdder[] classicBuckets;
201    private final ConcurrentHashMap<Integer, LongAdder> nativeBucketsForPositiveValues =
202        new ConcurrentHashMap<>();
203    private final ConcurrentHashMap<Integer, LongAdder> nativeBucketsForNegativeValues =
204        new ConcurrentHashMap<>();
205    private final LongAdder nativeZeroCount = new LongAdder();
206    private final LongAdder count = new LongAdder();
207    private final DoubleAdder sum = new DoubleAdder();
208    private volatile int nativeSchema =
209        nativeInitialSchema; // integer in [-4, 8] or CLASSIC_HISTOGRAM
210    private volatile double nativeZeroThreshold = Histogram.this.nativeMinZeroThreshold;
211    private volatile long createdTimeMillis = System.currentTimeMillis();
212    private final Buffer buffer = new Buffer();
213    private volatile boolean resetDurationExpired = false;
214    @Nullable private final ExemplarSampler exemplarSampler;
215
216    private DataPoint() {
217      if (exemplarSamplerConfig != null) {
218        exemplarSampler = new ExemplarSampler(exemplarSamplerConfig, null, exemplarLabelsSupplier);
219      } else {
220        exemplarSampler = null;
221      }
222      classicBuckets = new LongAdder[classicUpperBounds.length];
223      for (int i = 0; i < classicUpperBounds.length; i++) {
224        classicBuckets[i] = new LongAdder();
225      }
226      maybeScheduleNextReset();
227    }
228
229    @Override
230    public double getSum() {
231      return sum.sum();
232    }
233
234    @Override
235    public long getCount() {
236      return count.sum();
237    }
238
239    @Override
240    public void observe(double value) {
241      if (Double.isNaN(value)) {
242        // See https://github.com/prometheus/client_golang/issues/1275 on ignoring NaN observations.
243        return;
244      }
245      if (!buffer.append(value)) {
246        doObserve(value, false);
247      }
248      if (exemplarSampler != null) {
249        exemplarSampler.observe(value);
250      }
251    }
252
253    @Override
254    public void observeWithExemplar(double value, Labels labels) {
255      if (Double.isNaN(value)) {
256        // See https://github.com/prometheus/client_golang/issues/1275 on ignoring NaN observations.
257        return;
258      }
259      if (!buffer.append(value)) {
260        doObserve(value, false);
261      }
262      if (exemplarSampler != null) {
263        exemplarSampler.observeWithExemplar(value, labels);
264      }
265    }
266
267    private void doObserve(double value, boolean fromBuffer) {
268      // classicUpperBounds is an empty array if this is a native histogram only.
269      for (int i = 0; i < classicUpperBounds.length; ++i) {
270        // The last bucket is +Inf, so we always increment.
271        if (value <= classicUpperBounds[i]) {
272          classicBuckets[i].add(1);
273          break;
274        }
275      }
276      boolean nativeBucketCreated = false;
277      if (Histogram.this.nativeInitialSchema != CLASSIC_HISTOGRAM) {
278        if (value > nativeZeroThreshold) {
279          nativeBucketCreated = addToNativeBucket(value, nativeBucketsForPositiveValues);
280        } else if (value < -nativeZeroThreshold) {
281          nativeBucketCreated = addToNativeBucket(-value, nativeBucketsForNegativeValues);
282        } else {
283          nativeZeroCount.add(1);
284        }
285      }
286      sum.add(value);
287      count
288          .increment(); // must be the last step, because count is used to signal that the operation
289      // is complete.
290      if (!fromBuffer) {
291        // maybeResetOrScaleDown will switch to the buffer,
292        // which won't work if we are currently still processing observations from the buffer.
293        // The reason is that before switching to the buffer we wait for all pending observations to
294        // be counted.
295        // If we do this while still applying observations from the buffer, the pending observations
296        // from
297        // the buffer will never be counted, and the buffer.run() method will wait forever.
298        maybeResetOrScaleDown(value, nativeBucketCreated);
299      }
300    }
301
302    private HistogramSnapshot.HistogramDataPointSnapshot collect(Labels labels) {
303      Exemplars exemplars = exemplarSampler != null ? exemplarSampler.collect() : Exemplars.EMPTY;
304      return buffer.run(
305          expectedCount -> count.sum() == expectedCount,
306          () -> {
307            if (classicUpperBounds.length == 0) {
308              // native only
309              return new HistogramSnapshot.HistogramDataPointSnapshot(
310                  nativeSchema,
311                  nativeZeroCount.sum(),
312                  nativeZeroThreshold,
313                  toBucketList(nativeBucketsForPositiveValues),
314                  toBucketList(nativeBucketsForNegativeValues),
315                  sum.sum(),
316                  labels,
317                  exemplars,
318                  createdTimeMillis);
319            } else if (Histogram.this.nativeInitialSchema == CLASSIC_HISTOGRAM) {
320              // classic only
321              return new HistogramSnapshot.HistogramDataPointSnapshot(
322                  ClassicHistogramBuckets.of(classicUpperBounds, classicBuckets),
323                  sum.sum(),
324                  labels,
325                  exemplars,
326                  createdTimeMillis);
327            } else {
328              // hybrid: classic and native
329              return new HistogramSnapshot.HistogramDataPointSnapshot(
330                  ClassicHistogramBuckets.of(classicUpperBounds, classicBuckets),
331                  nativeSchema,
332                  nativeZeroCount.sum(),
333                  nativeZeroThreshold,
334                  toBucketList(nativeBucketsForPositiveValues),
335                  toBucketList(nativeBucketsForNegativeValues),
336                  sum.sum(),
337                  labels,
338                  exemplars,
339                  createdTimeMillis);
340            }
341          },
342          v -> doObserve(v, true));
343    }
344
345    private boolean addToNativeBucket(double value, ConcurrentHashMap<Integer, LongAdder> buckets) {
346      boolean newBucketCreated = false;
347      int bucketIndex;
348      if (Double.isInfinite(value)) {
349        bucketIndex = findBucketIndex(Double.MAX_VALUE) + 1;
350      } else {
351        bucketIndex = findBucketIndex(value);
352      }
353      LongAdder bucketCount = buckets.get(bucketIndex);
354      if (bucketCount == null) {
355        LongAdder newBucketCount = new LongAdder();
356        LongAdder existingBucketCount = buckets.putIfAbsent(bucketIndex, newBucketCount);
357        if (existingBucketCount == null) {
358          newBucketCreated = true;
359          bucketCount = newBucketCount;
360        } else {
361          bucketCount = existingBucketCount;
362        }
363      }
364      bucketCount.increment();
365      return newBucketCreated;
366    }
367
368    private int findBucketIndex(double value) {
369      // Preconditions:
370      // Double.isNan(value) is false;
371      // Double.isInfinite(value) is false;
372      // value > 0
373      // ---
374      // The following is a naive implementation of C's frexp() function.
375      // Performance can be improved by using the internal Bit representation of floating point
376      // numbers.
377      // More info on the Bit representation of floating point numbers:
378      // https://stackoverflow.com/questions/8341395/what-is-a-subnormal-floating-point-number
379      // Result: value == frac * 2^exp where frac in [0.5, 1).
380      double frac = value;
381      int exp = 0;
382      while (frac < 0.5) {
383        frac *= 2.0;
384        exp--;
385      }
386      while (frac >= 1.0) {
387        frac /= 2.0;
388        exp++;
389      }
390      // end of frexp()
391
392      if (nativeSchema >= 1) {
393        return findIndex(NATIVE_BOUNDS[nativeSchema - 1], frac)
394            + (exp - 1) * NATIVE_BOUNDS[nativeSchema - 1].length;
395      } else {
396        int bucketIndex = exp;
397        if (frac == 0.5) {
398          bucketIndex--;
399        }
400        int offset = (1 << -nativeSchema) - 1;
401        bucketIndex = (bucketIndex + offset) >> -nativeSchema;
402        return bucketIndex;
403      }
404    }
405
406    private int findIndex(double[] bounds, double frac) {
407      // The following is the equivalent of golang's sort.SearchFloat64s(bounds, frac)
408      // See https://pkg.go.dev/sort#SearchFloat64s
409      int first = 0;
410      int last = bounds.length - 1;
411      while (first <= last) {
412        int mid = (first + last) / 2;
413        if (bounds[mid] == frac) {
414          return mid;
415        } else if (bounds[mid] < frac) {
416          first = mid + 1;
417        } else {
418          last = mid - 1;
419        }
420      }
421      return last + 1;
422    }
423
424    /**
425     * Makes sure that the number of native buckets does not exceed nativeMaxBuckets.
426     *
427     * <ul>
428     *   <li>If the histogram has already been scaled down (nativeSchema < initialSchema) reset
429     *       after resetIntervalExpired to get back to the original schema.
430     *   <li>If a new bucket was created and we now exceed nativeMaxBuckets run maybeScaleDown() to
431     *       scale down
432     * </ul>
433     */
434    private void maybeResetOrScaleDown(double value, boolean nativeBucketCreated) {
435      AtomicBoolean wasReset = new AtomicBoolean(false);
436      if (resetDurationExpired && nativeSchema < nativeInitialSchema) {
437        // If nativeSchema < initialNativeSchema the histogram has been scaled down.
438        // So if resetDurationExpired we will reset it to restore the original native schema.
439        buffer.run(
440            expectedCount -> count.sum() == expectedCount,
441            () -> {
442              if (maybeReset()) {
443                wasReset.set(true);
444              }
445              return null;
446            },
447            v -> doObserve(v, true));
448      } else if (nativeBucketCreated) {
449        // If a new bucket was created we need to check if nativeMaxBuckets is exceeded
450        // and scale down if so.
451        maybeScaleDown(wasReset);
452      }
453      if (wasReset.get()) {
454        // We just discarded the newly observed value. Observe it again.
455        if (!buffer.append(value)) {
456          doObserve(value, true);
457        }
458      }
459    }
460
461    private void maybeScaleDown(AtomicBoolean wasReset) {
462      if (nativeMaxBuckets == 0 || nativeSchema == -4) {
463        return;
464      }
465      int numberOfBuckets =
466          nativeBucketsForPositiveValues.size() + nativeBucketsForNegativeValues.size();
467      if (numberOfBuckets <= nativeMaxBuckets) {
468        return;
469      }
470      buffer.run(
471          expectedCount -> count.sum() == expectedCount,
472          () -> {
473            // Now we are in the synchronized block while new observations go into the buffer.
474            // Check again if we need to limit the bucket size, because another thread might
475            // have limited it in the meantime.
476            int numBuckets =
477                nativeBucketsForPositiveValues.size() + nativeBucketsForNegativeValues.size();
478            if (numBuckets <= nativeMaxBuckets || nativeSchema == -4) {
479              return null;
480            }
481            if (maybeReset()) {
482              wasReset.set(true);
483              return null;
484            }
485            if (maybeWidenZeroBucket()) {
486              return null;
487            }
488            doubleBucketWidth();
489            return null;
490          },
491          v -> doObserve(v, true));
492    }
493
494    // maybeReset is called in the synchronized block while new observations go into the buffer.
495    private boolean maybeReset() {
496      if (!resetDurationExpired) {
497        return false;
498      }
499      resetDurationExpired = false;
500      buffer.reset();
501      nativeBucketsForPositiveValues.clear();
502      nativeBucketsForNegativeValues.clear();
503      nativeZeroCount.reset();
504      count.reset();
505      sum.reset();
506      for (LongAdder classicBucket : classicBuckets) {
507        classicBucket.reset();
508      }
509      nativeZeroThreshold = nativeMinZeroThreshold;
510      nativeSchema = Histogram.this.nativeInitialSchema;
511      createdTimeMillis = System.currentTimeMillis();
512      if (exemplarSampler != null) {
513        exemplarSampler.reset();
514      }
515      maybeScheduleNextReset();
516      return true;
517    }
518
519    // maybeWidenZeroBucket is called in the synchronized block while new observations go into the
520    // buffer.
521    private boolean maybeWidenZeroBucket() {
522      if (nativeZeroThreshold >= nativeMaxZeroThreshold) {
523        return false;
524      }
525      int smallestIndex = findSmallestIndex(nativeBucketsForPositiveValues);
526      int smallestNegativeIndex = findSmallestIndex(nativeBucketsForNegativeValues);
527      if (smallestNegativeIndex < smallestIndex) {
528        smallestIndex = smallestNegativeIndex;
529      }
530      if (smallestIndex == Integer.MAX_VALUE) {
531        return false;
532      }
533      double newZeroThreshold = nativeBucketIndexToUpperBound(nativeSchema, smallestIndex);
534      if (newZeroThreshold > nativeMaxZeroThreshold) {
535        return false;
536      }
537      mergeWithZeroBucket(smallestIndex, nativeBucketsForPositiveValues);
538      mergeWithZeroBucket(smallestIndex, nativeBucketsForNegativeValues);
539      nativeZeroThreshold = newZeroThreshold;
540      return true;
541    }
542
543    private void mergeWithZeroBucket(int index, Map<Integer, LongAdder> buckets) {
544      LongAdder count = buckets.remove(index);
545      if (count != null) {
546        nativeZeroCount.add(count.sum());
547      }
548    }
549
550    private double nativeBucketIndexToUpperBound(int schema, int index) {
551      double result = calcUpperBound(schema, index);
552      if (Double.isInfinite(result)) {
553        // The last bucket boundary should always be MAX_VALUE, so that the +Inf bucket counts only
554        // actual +Inf observations.
555        // However, MAX_VALUE is not a natural bucket boundary, so we introduce MAX_VALUE
556        // as an artificial boundary before +Inf.
557        double previousBucketBoundary = calcUpperBound(schema, index - 1);
558        if (Double.isFinite(previousBucketBoundary) && previousBucketBoundary < Double.MAX_VALUE) {
559          return Double.MAX_VALUE;
560        }
561      }
562      return result;
563    }
564
565    private double calcUpperBound(int schema, int index) {
566      // The actual formula is:
567      // ---
568      // base := 2^(2^-schema);
569      // upperBound := base^index;
570      // ---
571      // The following implementation reduces the numerical error for index > 0.
572      // It's not very efficient. We should refactor and use an algorithm as in client_golang's
573      // getLe()
574      double factor = 1.0;
575      while (index > 0) {
576        if (index % 2 == 0) {
577          index /= 2;
578          schema -= 1;
579        } else {
580          index -= 1;
581          factor *= Math.pow(2, Math.pow(2, -schema));
582        }
583      }
584      return factor * Math.pow(2, index * Math.pow(2, -schema));
585    }
586
587    private int findSmallestIndex(Map<Integer, LongAdder> nativeBuckets) {
588      int result = Integer.MAX_VALUE;
589      for (int key : nativeBuckets.keySet()) {
590        if (key < result) {
591          result = key;
592        }
593      }
594      return result;
595    }
596
597    // doubleBucketWidth is called in the synchronized block while new observations go into the
598    // buffer.
599    @SuppressWarnings("NonAtomicVolatileUpdate")
600    private void doubleBucketWidth() {
601      doubleBucketWidth(nativeBucketsForPositiveValues);
602      doubleBucketWidth(nativeBucketsForNegativeValues);
603      nativeSchema--;
604    }
605
606    private void doubleBucketWidth(Map<Integer, LongAdder> buckets) {
607      int[] keys = new int[buckets.size()];
608      long[] values = new long[keys.length];
609      int i = 0;
610      for (Map.Entry<Integer, LongAdder> entry : buckets.entrySet()) {
611        keys[i] = entry.getKey();
612        values[i] = entry.getValue().sum();
613        i++;
614      }
615      buckets.clear();
616      for (i = 0; i < keys.length; i++) {
617        int index = (keys[i] > 0 ? keys[i] + 1 : keys[i]) / 2;
618        LongAdder count = buckets.computeIfAbsent(index, k -> new LongAdder());
619        count.add(values[i]);
620      }
621    }
622
623    private NativeHistogramBuckets toBucketList(ConcurrentHashMap<Integer, LongAdder> map) {
624      int[] bucketIndexes = new int[map.size()];
625      long[] counts = new long[map.size()];
626      int i = 0;
627      for (Map.Entry<Integer, LongAdder> entry : map.entrySet()) {
628        bucketIndexes[i] = entry.getKey();
629        counts[i] = entry.getValue().sum();
630        i++;
631      }
632      return NativeHistogramBuckets.of(bucketIndexes, counts);
633    }
634
635    @SuppressWarnings("FutureReturnValueIgnored")
636    private void maybeScheduleNextReset() {
637      if (nativeResetDurationSeconds > 0) {
638        Scheduler.schedule(
639            () -> resetDurationExpired = true, nativeResetDurationSeconds, TimeUnit.SECONDS);
640      }
641    }
642  }
643
644  @Override
645  public HistogramSnapshot collect() {
646    return (HistogramSnapshot) super.collect();
647  }
648
649  @Override
650  protected HistogramSnapshot collect(List<Labels> labels, List<DataPoint> metricData) {
651    List<HistogramSnapshot.HistogramDataPointSnapshot> data = new ArrayList<>(labels.size());
652    for (int i = 0; i < labels.size(); i++) {
653      data.add(metricData.get(i).collect(labels.get(i)));
654    }
655    return new HistogramSnapshot(metadata, data);
656  }
657
658  /**
659   * @deprecated Use {@link #getMetricFamilyDescriptor()} instead.
660   */
661  @Override
662  @Deprecated
663  @SuppressWarnings("InlineMeSuggester")
664  public MetricType getMetricType() {
665    return MetricType.HISTOGRAM;
666  }
667
668  @Override
669  protected DataPoint newDataPoint() {
670    return new DataPoint();
671  }
672
673  static {
674    // See bounds in client_golang's histogram implementation.
675    NATIVE_BOUNDS = new double[8][];
676    for (int schema = 1; schema <= 8; schema++) {
677      NATIVE_BOUNDS[schema - 1] = new double[1 << schema];
678      NATIVE_BOUNDS[schema - 1][0] = 0.5;
679      // https://github.com/open-telemetry/opentelemetry-proto/blob/main/opentelemetry/proto/metrics/v1/metrics.proto#L501
680      double base = Math.pow(2, Math.pow(2, -schema));
681      for (int i = 1; i < NATIVE_BOUNDS[schema - 1].length; i++) {
682        if (i % 2 == 0 && schema > 1) {
683          // Use previously calculated value for increased precision, see comment in client_golang's
684          // implementation.
685          NATIVE_BOUNDS[schema - 1][i] = NATIVE_BOUNDS[schema - 2][i / 2];
686        } else {
687          NATIVE_BOUNDS[schema - 1][i] = NATIVE_BOUNDS[schema - 1][i - 1] * base;
688        }
689      }
690    }
691  }
692
693  public static Builder builder() {
694    return new Builder(PrometheusProperties.get());
695  }
696
697  public static Builder builder(PrometheusProperties config) {
698    return new Builder(config);
699  }
700
701  public static class Builder extends StatefulMetric.Builder<Histogram.Builder, Histogram> {
702
703    @SuppressWarnings("MutablePublicArray")
704    public static final double[] DEFAULT_CLASSIC_UPPER_BOUNDS =
705        new double[] {.005, .01, .025, .05, .1, .25, .5, 1, 2.5, 5, 10};
706
707    private static final double DEFAULT_NATIVE_MIN_ZERO_THRESHOLD = Math.pow(2.0, -128);
708    private static final double DEFAULT_NATIVE_MAX_ZERO_THRESHOLD = Math.pow(2.0, -128);
709    private static final int DEFAULT_NATIVE_INITIAL_SCHEMA = 5;
710    private static final int DEFAULT_NATIVE_MAX_NUMBER_OF_BUCKETS = 160;
711    private static final long DEFAULT_NATIVE_RESET_DURATION_SECONDS = 0; // 0 means no reset
712
713    @Nullable private Boolean nativeOnly;
714    @Nullable private Boolean classicOnly;
715    @Nullable private double[] classicUpperBounds;
716    @Nullable private Integer nativeInitialSchema;
717    @Nullable private Double nativeMaxZeroThreshold;
718    @Nullable private Double nativeMinZeroThreshold;
719    @Nullable private Integer nativeMaxNumberOfBuckets;
720    @Nullable private Long nativeResetDurationSeconds;
721
722    @Override
723    public Histogram build() {
724      return new Histogram(this, properties);
725    }
726
727    @Override
728    protected MetricsProperties toProperties() {
729      MetricsProperties.Builder builder = MetricsProperties.builder();
730      if (classicUpperBounds != null) {
731        builder.histogramClassicUpperBounds(classicUpperBounds);
732      }
733      return builder
734          .exemplarsEnabled(exemplarsEnabled)
735          .histogramNativeOnly(nativeOnly)
736          .histogramClassicOnly(classicOnly)
737          .histogramNativeInitialSchema(nativeInitialSchema)
738          .histogramNativeMinZeroThreshold(nativeMinZeroThreshold)
739          .histogramNativeMaxZeroThreshold(nativeMaxZeroThreshold)
740          .histogramNativeMaxNumberOfBuckets(nativeMaxNumberOfBuckets)
741          .histogramNativeResetDurationSeconds(nativeResetDurationSeconds)
742          .build();
743    }
744
745    /** Default properties for histogram metrics. */
746    @Override
747    public MetricsProperties getDefaultProperties() {
748      return MetricsProperties.builder()
749          .exemplarsEnabled(true)
750          .histogramNativeOnly(false)
751          .histogramClassicOnly(false)
752          .histogramClassicUpperBounds(DEFAULT_CLASSIC_UPPER_BOUNDS)
753          .histogramNativeInitialSchema(DEFAULT_NATIVE_INITIAL_SCHEMA)
754          .histogramNativeMinZeroThreshold(DEFAULT_NATIVE_MIN_ZERO_THRESHOLD)
755          .histogramNativeMaxZeroThreshold(DEFAULT_NATIVE_MAX_ZERO_THRESHOLD)
756          .histogramNativeMaxNumberOfBuckets(DEFAULT_NATIVE_MAX_NUMBER_OF_BUCKETS)
757          .histogramNativeResetDurationSeconds(DEFAULT_NATIVE_RESET_DURATION_SECONDS)
758          .build();
759    }
760
761    private Builder(PrometheusProperties config) {
762      super(Collections.singletonList("le"), config);
763    }
764
765    /**
766     * Use the native histogram representation only, i.e. don't maintain classic histogram buckets.
767     * See {@link Histogram} for more info.
768     */
769    public Builder nativeOnly() {
770      if (Boolean.TRUE.equals(classicOnly)) {
771        throw new IllegalArgumentException("Cannot call nativeOnly() after calling classicOnly().");
772      }
773      nativeOnly = true;
774      return this;
775    }
776
777    /**
778     * Use the classic histogram representation only, i.e. don't maintain native histogram buckets.
779     * See {@link Histogram} for more info.
780     */
781    public Builder classicOnly() {
782      if (Boolean.TRUE.equals(nativeOnly)) {
783        throw new IllegalArgumentException("Cannot call classicOnly() after calling nativeOnly().");
784      }
785      classicOnly = true;
786      return this;
787    }
788
789    /**
790     * Set the upper bounds for the classic histogram buckets. Default is {@link
791     * Builder#DEFAULT_CLASSIC_UPPER_BOUNDS}. If the +Inf bucket is missing it will be added. If
792     * upperBounds contains duplicates the duplicates will be removed.
793     */
794    public Builder classicUpperBounds(double... upperBounds) {
795      this.classicUpperBounds = upperBounds;
796      for (double bound : upperBounds) {
797        if (Double.isNaN(bound)) {
798          throw new IllegalArgumentException("Cannot use NaN as upper bound for a histogram");
799        }
800      }
801      return this;
802    }
803
804    /**
805     * Create classic histogram buckets with linear bucket boundaries.
806     *
807     * <p>Example: {@code classicLinearUpperBounds(1.0, 0.5, 10)} creates bucket boundaries {@code
808     * [[1.0, 1.5, 2.0, 2.5, 3.0, 3.5, 4.0, 4.5, 5.0, 5.5]}.
809     *
810     * @param start is the first bucket boundary
811     * @param width is the width of each bucket
812     * @param count is the total number of buckets, including start
813     */
814    public Builder classicLinearUpperBounds(double start, double width, int count) {
815      this.classicUpperBounds = new double[count];
816      // Use BigDecimal to avoid weird bucket boundaries like 0.7000000000000001.
817      BigDecimal s = new BigDecimal(Double.toString(start));
818      BigDecimal w = new BigDecimal(Double.toString(width));
819      for (int i = 0; i < count; i++) {
820        classicUpperBounds[i] = s.add(w.multiply(new BigDecimal(i))).doubleValue();
821      }
822      return this;
823    }
824
825    /**
826     * Create classic histogram buckets with exponential boundaries.
827     *
828     * <p>Example: {@code classicExponentialUpperBounds(1.0, 2.0, 10)} creates bucket boundaries
829     * {@code [1.0, 2.0, 4.0, 8.0, 16.0, 32.0, 64.0, 128.0, 256.0, 512.0]}
830     *
831     * @param start is the first bucket boundary
832     * @param factor growth factor
833     * @param count total number of buckets, including start
834     */
835    public Builder classicExponentialUpperBounds(double start, double factor, int count) {
836      classicUpperBounds = new double[count];
837      for (int i = 0; i < count; i++) {
838        classicUpperBounds[i] = start * Math.pow(factor, i);
839      }
840      return this;
841    }
842
843    /**
844     * The schema is a number in [-4, 8] defining the resolution of the native histogram. Default is
845     * {@link Builder#DEFAULT_NATIVE_INITIAL_SCHEMA}.
846     *
847     * <p>The higher the schema, the finer the resolution. Schema is Prometheus terminology. In
848     * OpenTelemetry it's called "scale".
849     *
850     * <p>Note that the schema for a histogram may be automatically decreased at runtime if the
851     * number of native histogram buckets exceeds {@link #nativeMaxNumberOfBuckets(int)}.
852     *
853     * <p>The following table shows:
854     *
855     * <ul>
856     *   <li>factor: The growth factor for bucket boundaries, i.e. next bucket boundary = growth
857     *       factor * previous bucket boundary.
858     *   <li>max quantile error: The maximum error for quantiles calculated using the Prometheus
859     *       histogram_quantile() function, relative to the observed value, assuming harmonic mean.
860     * </ul>
861     *
862     * <table border="1">
863     *     <caption>max quantile errors for different growth factors</caption>
864     *     <tr>
865     *         <td>schema</td><td>factor</td><td>max quantile error</td>
866     *     </tr>
867     *     <tr>
868     *         <td>-4</td><td>65.536</td><td>99%</td>
869     *     </tr>
870     *     <tr>
871     *         <td>-3</td><td>256</td><td>99%</td>
872     *     </tr>
873     *     <tr>
874     *         <td>-2</td><td>16</td><td>88%</td>
875     *     </tr>
876     *     <tr>
877     *         <td>-1</td><td>4</td><td>60%</td>
878     *     </tr>
879     *     <tr>
880     *         <td>0</td><td>2</td><td>33%</td>
881     *     </tr>
882     *     <tr>
883     *         <td>1</td><td>1.4142...</td><td>17%</td>
884     *     </tr>
885     *     <tr>
886     *         <td>2</td><td>1.1892...</td><td>9%</td>
887     *     </tr>
888     *     <tr>
889     *         <td>3</td><td>1.1090...</td><td>4%</td>
890     *     </tr>
891     *     <tr>
892     *         <td>4</td><td>1.0442...</td><td>2%</td>
893     *     </tr>
894     *     <tr>
895     *         <td>5</td><td>1.0218...</td><td>1%</td>
896     *     </tr>
897     *     <tr>
898     *         <td>6</td><td>1.0108...</td><td>0.5%</td>
899     *     </tr>
900     *     <tr>
901     *         <td>7</td><td>1.0054...</td><td>0.3%</td>
902     *     </tr>
903     *     <tr>
904     *         <td>8</td><td>1.0027...</td><td>0.1%</td>
905     *     </tr>
906     * </table>
907     */
908    public Builder nativeInitialSchema(int nativeSchema) {
909      if (nativeSchema < -4 || nativeSchema > 8) {
910        throw new IllegalArgumentException(
911            "Unsupported native histogram schema "
912                + nativeSchema
913                + ": expecting -4 <= schema <= 8.");
914      }
915      this.nativeInitialSchema = nativeSchema;
916      return this;
917    }
918
919    /**
920     * Native histogram buckets get smaller and smaller the closer they get to zero. To avoid
921     * wasting a lot of buckets for observations fluctuating around zero, we consider all values in
922     * [-zeroThreshold, +zeroThreshold] to be equal to zero.
923     *
924     * <p>The zeroThreshold is initialized with minZeroThreshold, and will grow up to
925     * maxZeroThreshold if the number of native histogram buckets exceeds nativeMaxBuckets.
926     *
927     * <p>Default is {@link Builder#DEFAULT_NATIVE_MAX_NUMBER_OF_BUCKETS}.
928     */
929    public Builder nativeMaxZeroThreshold(double nativeMaxZeroThreshold) {
930      if (nativeMaxZeroThreshold < 0) {
931        throw new IllegalArgumentException(
932            "Illegal native max zero threshold " + nativeMaxZeroThreshold + ": must be >= 0");
933      }
934      this.nativeMaxZeroThreshold = nativeMaxZeroThreshold;
935      return this;
936    }
937
938    /**
939     * Native histogram buckets get smaller and smaller the closer they get to zero. To avoid
940     * wasting a lot of buckets for observations fluctuating around zero, we consider all values in
941     * [-zeroThreshold, +zeroThreshold] to be equal to zero.
942     *
943     * <p>The zeroThreshold is initialized with minZeroThreshold, and will grow up to
944     * maxZeroThreshold if the number of native histogram buckets exceeds nativeMaxBuckets.
945     *
946     * <p>Default is {@link Builder#DEFAULT_NATIVE_MIN_ZERO_THRESHOLD}.
947     */
948    public Builder nativeMinZeroThreshold(double nativeMinZeroThreshold) {
949      if (nativeMinZeroThreshold < 0) {
950        throw new IllegalArgumentException(
951            "Illegal native min zero threshold " + nativeMinZeroThreshold + ": must be >= 0");
952      }
953      this.nativeMinZeroThreshold = nativeMinZeroThreshold;
954      return this;
955    }
956
957    /**
958     * Limit the number of native buckets.
959     *
960     * <p>If the number of native buckets exceeds the maximum, the {@link #nativeInitialSchema(int)}
961     * is decreased, i.e. the resolution of the histogram is decreased to reduce the number of
962     * buckets.
963     *
964     * <p>Default is {@link Builder#DEFAULT_NATIVE_MAX_NUMBER_OF_BUCKETS}.
965     */
966    public Builder nativeMaxNumberOfBuckets(int nativeMaxBuckets) {
967      this.nativeMaxNumberOfBuckets = nativeMaxBuckets;
968      return this;
969    }
970
971    /**
972     * If the histogram needed to be scaled down because {@link #nativeMaxNumberOfBuckets(int)} was
973     * exceeded, reset the histogram after a certain time interval to go back to the original {@link
974     * #nativeInitialSchema(int)}.
975     *
976     * <p>Reset means all values are set to zero. A good value might be 24h or 7d.
977     *
978     * <p>Default is no reset.
979     */
980    public Builder nativeResetDuration(long duration, TimeUnit unit) {
981      if (duration <= 0) {
982        throw new IllegalArgumentException(duration + ": value > 0 expected");
983      }
984      long seconds = unit.toSeconds(duration);
985      if (seconds == 0) {
986        throw new IllegalArgumentException(
987            duration
988                + " "
989                + unit
990                + ": duration must be at least 1 second. Sub-second durations are not supported.");
991      }
992      nativeResetDurationSeconds = seconds;
993      return this;
994    }
995
996    @Override
997    protected Builder self() {
998      return this;
999    }
1000  }
1001}