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