001package io.prometheus.client;
002
003import io.prometheus.client.CKMSQuantiles.Quantile;
004
005import java.io.Closeable;
006import java.util.ArrayList;
007import java.util.Collections;
008import java.util.List;
009import java.util.Map;
010import java.util.SortedMap;
011import java.util.TreeMap;
012import java.util.concurrent.Callable;
013import java.util.concurrent.TimeUnit;
014
015/**
016 * Summary metric, to track the size of events.
017 * <p>
018 * Example of uses for Summaries include:
019 * <ul>
020 *  <li>Response latency</li>
021 *  <li>Request size</li>
022 * </ul>
023 *
024 * <p>
025 * Example Summaries:
026 * <pre>
027 * {@code
028 *   class YourClass {
029 *     static final Summary receivedBytes = Summary.build()
030 *         .name("requests_size_bytes").help("Request size in bytes.").register();
031 *     static final Summary requestLatency = Summary.build()
032 *         .name("requests_latency_seconds").help("Request latency in seconds.").register();
033 *
034 *     void processRequest(Request req) {
035 *        Summary.Timer requestTimer = requestLatency.startTimer();
036 *        try {
037 *          // Your code here.
038 *        } finally {
039 *          receivedBytes.observe(req.size());
040 *          requestTimer.observeDuration();
041 *        }
042 *     }
043 *
044 *     // Or if using Java 8 and lambdas.
045 *     void processRequestLambda(Request req) {
046 *       receivedBytes.observe(req.size());
047 *       requestLatency.time(() -> {
048 *         // Your code here.
049 *       });
050 *     }
051 * }
052 * }
053 * </pre>
054 * This would allow you to track request rate, average latency and average request size.
055 *
056 * <p>
057 * How to add custom quantiles:
058 * <pre>
059 * {@code
060 *     static final Summary myMetric = Summary.build()
061 *             .quantile(0.5, 0.05)   // Add 50th percentile (= median) with 5% tolerated error
062 *             .quantile(0.9, 0.01)   // Add 90th percentile with 1% tolerated error
063 *             .quantile(0.99, 0.001) // Add 99th percentile with 0.1% tolerated error
064 *             .name("requests_size_bytes")
065 *             .help("Request size in bytes.")
066 *             .register();
067 * }
068 * </pre>
069 *
070 * The quantiles are calculated over a sliding window of time. There are two options to configure this time window:
071 * <ul>
072 *   <li>maxAgeSeconds(long): Set the duration of the time window is, i.e. how long observations are kept before they are discarded.
073 *       Default is 10 minutes.
074 *   <li>ageBuckets(int): Set the number of buckets used to implement the sliding time window. If your time window is 10 minutes, and you have ageBuckets=5,
075 *       buckets will be switched every 2 minutes. The value is a trade-off between resources (memory and cpu for maintaining the bucket)
076 *       and how smooth the time window is moved. Default value is 5.
077 * </ul>
078 *
079 * See https://prometheus.io/docs/practices/histograms/ for more info on quantiles.
080 */
081public class Summary extends SimpleCollector<Summary.Child> implements Counter.Describable {
082
083  final List<Quantile> quantiles; // Can be empty, but can never be null.
084  final long maxAgeSeconds;
085  final int ageBuckets;
086
087  Summary(Builder b) {
088    super(b);
089    quantiles = Collections.unmodifiableList(new ArrayList<Quantile>(b.quantiles));
090    this.maxAgeSeconds = b.maxAgeSeconds;
091    this.ageBuckets = b.ageBuckets;
092    initializeNoLabelsChild();
093  }
094
095  public static class Builder extends SimpleCollector.Builder<Builder, Summary> {
096
097    private final List<Quantile> quantiles = new ArrayList<Quantile>();
098    private long maxAgeSeconds = TimeUnit.MINUTES.toSeconds(10);
099    private int ageBuckets = 5;
100
101    public Builder quantile(double quantile, double error) {
102      if (quantile < 0.0 || quantile > 1.0) {
103        throw new IllegalArgumentException("Quantile " + quantile + " invalid: Expected number between 0.0 and 1.0.");
104      }
105      if (error < 0.0 || error > 1.0) {
106        throw new IllegalArgumentException("Error " + error + " invalid: Expected number between 0.0 and 1.0.");
107      }
108      quantiles.add(new Quantile(quantile, error));
109      return this;
110    }
111
112    public Builder maxAgeSeconds(long maxAgeSeconds) {
113      if (maxAgeSeconds <= 0) {
114        throw new IllegalArgumentException("maxAgeSeconds cannot be " + maxAgeSeconds);
115      }
116      this.maxAgeSeconds = maxAgeSeconds;
117      return this;
118    }
119
120    public Builder ageBuckets(int ageBuckets) {
121      if (ageBuckets <= 0) {
122        throw new IllegalArgumentException("ageBuckets cannot be " + ageBuckets);
123      }
124      this.ageBuckets = ageBuckets;
125      return this;
126    }
127
128    @Override
129    public Summary create() {
130      for (String label : labelNames) {
131        if (label.equals("quantile")) {
132          throw new IllegalStateException("Summary cannot have a label named 'quantile'.");
133        }
134      }
135      dontInitializeNoLabelsChild = true;
136      return new Summary(this);
137    }
138  }
139
140  /**
141   *  Return a Builder to allow configuration of a new Summary. Ensures required fields are provided.
142   *
143   *  @param name The name of the metric
144   *  @param help The help string of the metric
145   */
146  public static Builder build(String name, String help) {
147    return new Builder().name(name).help(help);
148  }
149
150  /**
151   *  Return a Builder to allow configuration of a new Summary.
152   */
153  public static Builder build() {
154    return new Builder();
155  }
156
157  @Override
158  protected Child newChild() {
159    return new Child(quantiles, maxAgeSeconds, ageBuckets);
160  }
161
162
163  /**
164   * Represents an event being timed.
165   */
166  public static class Timer implements Closeable {
167    private final Child child;
168    private final long start;
169    private Timer(Child child, long start) {
170      this.child = child;
171      this.start = start;
172    }
173    /**
174     * Observe the amount of time in seconds since {@link Child#startTimer} was called.
175     * @return Measured duration in seconds since {@link Child#startTimer} was called.
176     */
177    public double observeDuration() {
178      double elapsed = SimpleTimer.elapsedSecondsFromNanos(start, SimpleTimer.defaultTimeProvider.nanoTime());
179      child.observe(elapsed);
180      return elapsed;
181    }
182
183    /**
184     * Equivalent to calling {@link #observeDuration()}.
185     */
186    @Override
187    public void close() {
188      observeDuration();
189    }
190  }
191
192  /**
193   * The value of a single Summary.
194   * <p>
195   * <em>Warning:</em> References to a Child become invalid after using
196   * {@link SimpleCollector#remove} or {@link SimpleCollector#clear}.
197   */
198  public static class Child {
199
200    /**
201     * Executes runnable code (e.g. a Java 8 Lambda) and observes a duration of how long it took to run.
202     *
203     * @param timeable Code that is being timed
204     * @return Measured duration in seconds for timeable to complete.
205     */
206    public double time(Runnable timeable) {
207      Timer timer = startTimer();
208
209      double elapsed;
210      try {
211        timeable.run();
212      } finally {
213        elapsed = timer.observeDuration();
214      }
215      return elapsed;
216    }
217
218    /**
219     * Executes callable code (e.g. a Java 8 Lambda) and observes a duration of how long it took to run.
220     *
221     * @param timeable Code that is being timed
222     * @return Result returned by callable.
223     */
224    public <E> E time(Callable<E> timeable) {
225      Timer timer = startTimer();
226
227      try {
228        return timeable.call();
229      } catch (RuntimeException e) {
230        throw e;
231      } catch (Exception e) {
232        throw new RuntimeException(e);
233      } finally {
234        timer.observeDuration();
235      }
236    }
237
238    public static class Value {
239      public final double count;
240      public final double sum;
241      public final SortedMap<Double, Double> quantiles;
242      public final long created;
243
244      private Value(double count, double sum, List<Quantile> quantiles, TimeWindowQuantiles quantileValues, long created) {
245        this.count = count;
246        this.sum = sum;
247        this.quantiles = Collections.unmodifiableSortedMap(snapshot(quantiles, quantileValues));
248        this.created = created;
249      }
250
251      private SortedMap<Double, Double> snapshot(List<Quantile> quantiles, TimeWindowQuantiles quantileValues) {
252        SortedMap<Double, Double> result = new TreeMap<Double, Double>();
253        for (Quantile q : quantiles) {
254          result.put(q.quantile, quantileValues.get(q.quantile));
255        }
256        return result;
257      }
258    }
259
260    // Having these separate leaves us open to races,
261    // however Prometheus as whole has other races
262    // that mean adding atomicity here wouldn't be useful.
263    // This should be reevaluated in the future.
264    private final DoubleAdder count = new DoubleAdder();
265    private final DoubleAdder sum = new DoubleAdder();
266    private final List<Quantile> quantiles;
267    private final TimeWindowQuantiles quantileValues;
268    private final long created = System.currentTimeMillis();
269
270    private Child(List<Quantile> quantiles, long maxAgeSeconds, int ageBuckets) {
271      this.quantiles = quantiles;
272      if (quantiles.size() > 0) {
273        quantileValues = new TimeWindowQuantiles(quantiles.toArray(new Quantile[]{}), maxAgeSeconds, ageBuckets);
274      } else {
275        quantileValues = null;
276      }
277    }
278
279    /**
280     * Observe the given amount.
281     * @param amt in most cases amt should be &gt;= 0. Negative values are supported, but you should read
282     *            <a href="https://prometheus.io/docs/practices/histograms/#count-and-sum-of-observations">
283     *            https://prometheus.io/docs/practices/histograms/#count-and-sum-of-observations</a> for
284     *            implications and alternatives.
285     */
286    public void observe(double amt) {
287      count.add(1);
288      sum.add(amt);
289      if (quantileValues != null) {
290        quantileValues.insert(amt);
291      }
292    }
293    /**
294     * Start a timer to track a duration.
295     * <p>
296     * Call {@link Timer#observeDuration} at the end of what you want to measure the duration of.
297     */
298    public Timer startTimer() {
299      return new Timer(this, SimpleTimer.defaultTimeProvider.nanoTime());
300    }
301    /**
302     * Get the value of the Summary.
303     * <p>
304     * <em>Warning:</em> The definition of {@link Value} is subject to change.
305     */
306    public Value get() {
307      return new Value(count.sum(), sum.sum(), quantiles, quantileValues, created);
308    }
309  }
310
311  // Convenience methods.
312  /**
313   * Observe the given amount on the summary with no labels.
314   * @param amt in most cases amt should be &gt;= 0. Negative values are supported, but you should read
315   *            <a href="https://prometheus.io/docs/practices/histograms/#count-and-sum-of-observations">
316   *            https://prometheus.io/docs/practices/histograms/#count-and-sum-of-observations</a> for
317   *            implications and alternatives.
318   */
319  public void observe(double amt) {
320    noLabelsChild.observe(amt);
321  }
322  /**
323   * Start a timer to track a duration on the summary with no labels.
324   * <p>
325   * Call {@link Timer#observeDuration} at the end of what you want to measure the duration of.
326   */
327  public Timer startTimer() {
328    return noLabelsChild.startTimer();
329  }
330
331  /**
332   * Executes runnable code (e.g. a Java 8 Lambda) and observes a duration of how long it took to run.
333   *
334   * @param timeable Code that is being timed
335   * @return Measured duration in seconds for timeable to complete.
336   */
337  public double time(Runnable timeable){
338    return noLabelsChild.time(timeable);
339  }
340
341  /**
342   * Executes callable code (e.g. a Java 8 Lambda) and observes a duration of how long it took to run.
343   *
344   * @param timeable Code that is being timed
345   * @return Result returned by callable.
346   */
347  public <E> E time(Callable<E> timeable){
348    return noLabelsChild.time(timeable);
349  }
350
351  /**
352   * Get the value of the Summary.
353   * <p>
354   * <em>Warning:</em> The definition of {@link Child.Value} is subject to change.
355   */
356  public Child.Value get() {
357    return noLabelsChild.get();
358  }
359
360  @Override
361  public List<MetricFamilySamples> collect() {
362    List<MetricFamilySamples.Sample> samples = new ArrayList<MetricFamilySamples.Sample>();
363    for(Map.Entry<List<String>, Child> c: children.entrySet()) {
364      Child.Value v = c.getValue().get();
365      List<String> labelNamesWithQuantile = new ArrayList<String>(labelNames);
366      labelNamesWithQuantile.add("quantile");
367      for(Map.Entry<Double, Double> q : v.quantiles.entrySet()) {
368        List<String> labelValuesWithQuantile = new ArrayList<String>(c.getKey());
369        labelValuesWithQuantile.add(doubleToGoString(q.getKey()));
370        samples.add(new MetricFamilySamples.Sample(fullname, labelNamesWithQuantile, labelValuesWithQuantile, q.getValue()));
371      }
372      samples.add(new MetricFamilySamples.Sample(fullname + "_count", labelNames, c.getKey(), v.count));
373      samples.add(new MetricFamilySamples.Sample(fullname + "_sum", labelNames, c.getKey(), v.sum));
374      samples.add(new MetricFamilySamples.Sample(fullname + "_created", labelNames, c.getKey(), v.created / 1000.0));
375    }
376
377    return familySamplesList(Type.SUMMARY, samples);
378  }
379
380  @Override
381  public List<MetricFamilySamples> describe() {
382    return Collections.<MetricFamilySamples>singletonList(new SummaryMetricFamily(fullname, help, labelNames));
383  }
384
385}