001package io.prometheus.client;
002
003import java.io.Closeable;
004import java.io.IOException;
005import java.util.ArrayList;
006import java.util.List;
007import java.util.Map;
008
009/**
010 * Histogram metric, to track distributions of events.
011 * <p>
012 * Example of uses for Histograms include:
013 * <ul>
014 *  <li>Response latency</li>
015 *  <li>Request size</li>
016 * </ul>
017 * <p>
018 * <em>Note:</em> Each bucket is one timeseries. Many buckets and/or many dimensions with labels
019 * can produce large amount of time series, that may cause performance problems.
020 *
021 * <p>
022 * The default buckets are intended to cover a typical web/rpc request from milliseconds to seconds.
023 * <p>
024 * Example Histograms:
025 * <pre>
026 * {@code
027 *   class YourClass {
028 *     static final Histogram requestLatency = Histogram.build()
029 *         .name("requests_latency_seconds").help("Request latency in seconds.").register();
030 *
031 *     void processRequest(Request req) {
032 *        Histogram.Timer requestTimer = requestLatency.startTimer();
033 *        try {
034 *          // Your code here.
035 *        } finally {
036 *          requestTimer.observeDuration();
037 *        }
038 *     }
039 *
040 *     // Or if using Java 8 lambdas.
041 *     void processRequestLambda(Request req) {
042 *        requestLatency.time(() -> {
043 *          // Your code here.
044 *        });
045 *     }
046 *   }
047 * }
048 * </pre>
049 * <p>
050 * You can choose your own buckets:
051 * <pre>
052 * {@code
053 *     static final Histogram requestLatency = Histogram.build()
054 *         .buckets(.01, .02, .03, .04)
055 *         .name("requests_latency_seconds").help("Request latency in seconds.").register();
056 * }
057 * </pre>
058 * {@link Histogram.Builder#linearBuckets(double, double, int) linearBuckets} and
059 * {@link Histogram.Builder#exponentialBuckets(double, double, int) exponentialBuckets}
060 * offer easy ways to set common bucket patterns.
061 */
062public class Histogram extends SimpleCollector<Histogram.Child> implements Collector.Describable {
063  private final double[] buckets;
064
065  Histogram(Builder b) {
066    super(b);
067    buckets = b.buckets;
068    initializeNoLabelsChild();
069  }
070
071  public static class Builder extends SimpleCollector.Builder<Builder, Histogram> {
072    private double[] buckets = new double[]{.005, .01, .025, .05, .075, .1, .25, .5, .75, 1, 2.5, 5, 7.5, 10};
073
074    @Override
075    public Histogram create() {
076      for (int i = 0; i < buckets.length - 1; i++) {
077        if (buckets[i] >= buckets[i + 1]) {
078          throw new IllegalStateException("Histogram buckets must be in increasing order: "
079              + buckets[i] + " >= " + buckets[i + 1]);
080        }
081      }
082      if (buckets.length == 0) {
083          throw new IllegalStateException("Histogram must have at least one bucket.");
084      }
085      for (String label: labelNames) {
086        if (label.equals("le")) {
087            throw new IllegalStateException("Histogram cannot have a label named 'le'.");
088        }
089      }
090
091      // Append infinity bucket if it's not already there.
092      if (buckets[buckets.length - 1] != Double.POSITIVE_INFINITY) {
093        double[] tmp = new double[buckets.length + 1];
094        System.arraycopy(buckets, 0, tmp, 0, buckets.length);
095        tmp[buckets.length] = Double.POSITIVE_INFINITY;
096        buckets = tmp;
097      }
098      dontInitializeNoLabelsChild = true;
099      return new Histogram(this);
100    }
101
102    /**
103      * Set the upper bounds of buckets for the histogram.
104      */
105    public Builder buckets(double... buckets) {
106      this.buckets = buckets;
107      return this;
108    }
109
110    /**
111      * Set the upper bounds of buckets for the histogram with a linear sequence.
112      */
113    public Builder linearBuckets(double start, double width, int count) {
114      buckets = new double[count];
115      for (int i = 0; i < count; i++){
116        buckets[i] = start + i*width;
117      }
118      return this;
119    }
120    /**
121      * Set the upper bounds of buckets for the histogram with an exponential sequence.
122      */
123    public Builder exponentialBuckets(double start, double factor, int count) {
124      buckets = new double[count];
125      for (int i = 0; i < count; i++) {
126        buckets[i] = start * Math.pow(factor, i);
127      }
128      return this;
129    }
130
131  }
132
133  /**
134   *  Return a Builder to allow configuration of a new Histogram. Ensures required fields are provided.
135   *
136   *  @param name The name of the metric
137   *  @param help The help string of the metric
138   */
139  public static Builder build(String name, String help) {
140    return new Builder().name(name).help(help);
141  }
142
143  /**
144   *  Return a Builder to allow configuration of a new Histogram.
145   */
146  public static Builder build() {
147    return new Builder();
148  }
149
150  @Override
151  protected Child newChild() {
152    return new Child(buckets);
153  }
154
155  /**
156   * Represents an event being timed.
157   */
158  public static class Timer implements Closeable {
159    private final Child child;
160    private final long start;
161    private Timer(Child child, long start) {
162      this.child = child;
163      this.start = start;
164    }
165    /**
166     * Observe the amount of time in seconds since {@link Child#startTimer} was called.
167     * @return Measured duration in seconds since {@link Child#startTimer} was called.
168     */
169    public double observeDuration() {
170        double elapsed = SimpleTimer.elapsedSecondsFromNanos(start, SimpleTimer.defaultTimeProvider.nanoTime());
171        child.observe(elapsed);
172        return elapsed;
173    }
174
175    /**
176     * Equivalent to calling {@link #observeDuration()}.
177     * @throws IOException
178     */
179    @Override
180    public void close() throws IOException {
181      observeDuration();
182    }
183  }
184
185  /**
186   * The value of a single Histogram.
187   * <p>
188   * <em>Warning:</em> References to a Child become invalid after using
189   * {@link SimpleCollector#remove} or {@link SimpleCollector#clear}.
190   */
191  public static class Child {
192
193    /**
194     * Executes runnable code (i.e. a Java 8 Lambda) and observes a duration of how long it took to run.
195     *
196     * @param timeable Code that is being timed
197     * @return Measured duration in seconds for timeable to complete.
198     */
199    public double time(Runnable timeable) {
200      Timer timer = startTimer();
201
202      double elapsed;
203      try {
204        timeable.run();
205      } finally {
206        elapsed = timer.observeDuration();
207      }
208      return elapsed;
209    }
210
211    public static class Value {
212      public final double sum;
213      public final double[] buckets;
214
215      public Value(double sum, double[] buckets) {
216        this.sum = sum;
217        this.buckets = buckets;
218      }
219    }
220
221    private Child(double[] buckets) {
222      upperBounds = buckets;
223      cumulativeCounts = new DoubleAdder[buckets.length];
224      for (int i = 0; i < buckets.length; ++i) {
225        cumulativeCounts[i] = new DoubleAdder();
226      }
227    }
228    private final double[] upperBounds;
229    private final DoubleAdder[] cumulativeCounts;
230    private final DoubleAdder sum = new DoubleAdder();
231
232
233    /**
234     * Observe the given amount.
235     */
236    public void observe(double amt) {
237      for (int i = 0; i < upperBounds.length; ++i) {
238        // The last bucket is +Inf, so we always increment.
239        if (amt <= upperBounds[i]) {
240          cumulativeCounts[i].add(1);
241          break;
242        }
243      }
244      sum.add(amt);
245    }
246    /**
247     * Start a timer to track a duration.
248     * <p>
249     * Call {@link Timer#observeDuration} at the end of what you want to measure the duration of.
250     */
251    public Timer startTimer() {
252      return new Timer(this, SimpleTimer.defaultTimeProvider.nanoTime());
253    }
254    /**
255     * Get the value of the Histogram.
256     * <p>
257     * <em>Warning:</em> The definition of {@link Value} is subject to change.
258     */
259    public Value get() {
260      double[] buckets = new double[cumulativeCounts.length];
261      double acc = 0;
262      for (int i = 0; i < cumulativeCounts.length; ++i) {
263        acc += cumulativeCounts[i].sum();
264        buckets[i] = acc;
265      }
266      return new Value(sum.sum(), buckets);
267    }
268  }
269
270  // Convenience methods.
271  /**
272   * Observe the given amount on the histogram with no labels.
273   */
274  public void observe(double amt) {
275    noLabelsChild.observe(amt);
276  }
277  /**
278   * Start a timer to track a duration on the histogram with no labels.
279   * <p>
280   * Call {@link Timer#observeDuration} at the end of what you want to measure the duration of.
281   */
282  public Timer startTimer() {
283    return noLabelsChild.startTimer();
284  }
285
286  /**
287   * Executes runnable code (i.e. a Java 8 Lambda) and observes a duration of how long it took to run.
288   *
289   * @param timeable Code that is being timed
290   * @return Measured duration in seconds for timeable to complete.
291   */
292  public double time(Runnable timeable){
293    return noLabelsChild.time(timeable);
294  }
295
296  @Override
297  public List<MetricFamilySamples> collect() {
298    List<MetricFamilySamples.Sample> samples = new ArrayList<MetricFamilySamples.Sample>();
299    for(Map.Entry<List<String>, Child> c: children.entrySet()) {
300      Child.Value v = c.getValue().get();
301      List<String> labelNamesWithLe = new ArrayList<String>(labelNames);
302      labelNamesWithLe.add("le");
303      for (int i = 0; i < v.buckets.length; ++i) {
304        List<String> labelValuesWithLe = new ArrayList<String>(c.getKey());
305        labelValuesWithLe.add(doubleToGoString(buckets[i]));
306        samples.add(new MetricFamilySamples.Sample(fullname + "_bucket", labelNamesWithLe, labelValuesWithLe, v.buckets[i]));
307      }
308      samples.add(new MetricFamilySamples.Sample(fullname + "_count", labelNames, c.getKey(), v.buckets[buckets.length-1]));
309      samples.add(new MetricFamilySamples.Sample(fullname + "_sum", labelNames, c.getKey(), v.sum));
310    }
311
312    MetricFamilySamples mfs = new MetricFamilySamples(fullname, Type.HISTOGRAM, help, samples);
313    List<MetricFamilySamples> mfsList = new ArrayList<MetricFamilySamples>();
314    mfsList.add(mfs);
315    return mfsList;
316  }
317
318  @Override
319  public List<MetricFamilySamples> describe() {
320    List<MetricFamilySamples> mfsList = new ArrayList<MetricFamilySamples>();
321    mfsList.add(new MetricFamilySamples(fullname, Type.HISTOGRAM, help, new ArrayList<MetricFamilySamples.Sample>()));
322    return mfsList;
323  }
324
325  double[] getBuckets() {
326    return buckets;
327  }
328
329
330}