001package io.prometheus.client;
002
003import io.prometheus.client.CKMSQuantiles.Quantile;
004
005import java.io.Closeable;
006import java.io.IOException;
007import java.util.ArrayList;
008import java.util.Collections;
009import java.util.List;
010import java.util.Map;
011import java.util.SortedMap;
012import java.util.TreeMap;
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     * @throws IOException
186     */
187    @Override
188    public void close() throws IOException {
189      observeDuration();
190    }
191  }
192
193  /**
194   * The value of a single Summary.
195   * <p>
196   * <em>Warning:</em> References to a Child become invalid after using
197   * {@link SimpleCollector#remove} or {@link SimpleCollector#clear}.
198   */
199  public static class Child {
200
201    /**
202     * Executes runnable code (i.e. a Java 8 Lambda) and observes a duration of how long it took to run.
203     *
204     * @param timeable Code that is being timed
205     * @return Measured duration in seconds for timeable to complete.
206     */
207    public double time(Runnable timeable) {
208      Timer timer = startTimer();
209
210      double elapsed;
211      try {
212        timeable.run();
213      } finally {
214        elapsed = timer.observeDuration();
215      }
216      return elapsed;
217    }
218
219    public static class Value {
220      public final double count;
221      public final double sum;
222      public final SortedMap<Double, Double> quantiles;
223
224      private Value(double count, double sum, List<Quantile> quantiles, TimeWindowQuantiles quantileValues) {
225        this.count = count;
226        this.sum = sum;
227        this.quantiles = Collections.unmodifiableSortedMap(snapshot(quantiles, quantileValues));
228      }
229
230      private SortedMap<Double, Double> snapshot(List<Quantile> quantiles, TimeWindowQuantiles quantileValues) {
231        SortedMap<Double, Double> result = new TreeMap<Double, Double>();
232        for (Quantile q : quantiles) {
233          result.put(q.quantile, quantileValues.get(q.quantile));
234        }
235        return result;
236      }
237    }
238
239    // Having these separate leaves us open to races,
240    // however Prometheus as whole has other races
241    // that mean adding atomicity here wouldn't be useful.
242    // This should be reevaluated in the future.
243    private final DoubleAdder count = new DoubleAdder();
244    private final DoubleAdder sum = new DoubleAdder();
245    private final List<Quantile> quantiles;
246    private final TimeWindowQuantiles quantileValues;
247
248    private Child(List<Quantile> quantiles, long maxAgeSeconds, int ageBuckets) {
249      this.quantiles = quantiles;
250      if (quantiles.size() > 0) {
251        quantileValues = new TimeWindowQuantiles(quantiles.toArray(new Quantile[]{}), maxAgeSeconds, ageBuckets);
252      } else {
253        quantileValues = null;
254      }
255    }
256
257    /**
258     * Observe the given amount.
259     */
260    public void observe(double amt) {
261      count.add(1);
262      sum.add(amt);
263      if (quantileValues != null) {
264        quantileValues.insert(amt);
265      }
266    }
267    /**
268     * Start a timer to track a duration.
269     * <p>
270     * Call {@link Timer#observeDuration} at the end of what you want to measure the duration of.
271     */
272    public Timer startTimer() {
273      return new Timer(this, SimpleTimer.defaultTimeProvider.nanoTime());
274    }
275    /**
276     * Get the value of the Summary.
277     * <p>
278     * <em>Warning:</em> The definition of {@link Value} is subject to change.
279     */
280    public Value get() {
281      return new Value(count.sum(), sum.sum(), quantiles, quantileValues);
282    }
283  }
284
285  // Convenience methods.
286  /**
287   * Observe the given amount on the summary with no labels.
288   */
289  public void observe(double amt) {
290    noLabelsChild.observe(amt);
291  }
292  /**
293   * Start a timer to track a duration on the summary with no labels.
294   * <p>
295   * Call {@link Timer#observeDuration} at the end of what you want to measure the duration of.
296   */
297  public Timer startTimer() {
298    return noLabelsChild.startTimer();
299  }
300
301  /**
302   * Executes runnable code (i.e. a Java 8 Lambda) and observes a duration of how long it took to run.
303   *
304   * @param timeable Code that is being timed
305   * @return Measured duration in seconds for timeable to complete.
306   */
307  public double time(Runnable timeable){
308    return noLabelsChild.time(timeable);
309  }
310  
311  /**
312   * Get the value of the Summary.
313   * <p>
314   * <em>Warning:</em> The definition of {@link Child.Value} is subject to change.
315   */
316  public Child.Value get() {
317    return noLabelsChild.get();
318  }
319
320  @Override
321  public List<MetricFamilySamples> collect() {
322    List<MetricFamilySamples.Sample> samples = new ArrayList<MetricFamilySamples.Sample>();
323    for(Map.Entry<List<String>, Child> c: children.entrySet()) {
324      Child.Value v = c.getValue().get();
325      List<String> labelNamesWithQuantile = new ArrayList<String>(labelNames);
326      labelNamesWithQuantile.add("quantile");
327      for(Map.Entry<Double, Double> q : v.quantiles.entrySet()) {
328        List<String> labelValuesWithQuantile = new ArrayList<String>(c.getKey());
329        labelValuesWithQuantile.add(doubleToGoString(q.getKey()));
330        samples.add(new MetricFamilySamples.Sample(fullname, labelNamesWithQuantile, labelValuesWithQuantile, q.getValue()));
331      }
332      samples.add(new MetricFamilySamples.Sample(fullname + "_count", labelNames, c.getKey(), v.count));
333      samples.add(new MetricFamilySamples.Sample(fullname + "_sum", labelNames, c.getKey(), v.sum));
334    }
335
336    return familySamplesList(Type.SUMMARY, samples);
337  }
338
339  @Override
340  public List<MetricFamilySamples> describe() {
341    return Collections.<MetricFamilySamples>singletonList(new SummaryMetricFamily(fullname, help, labelNames));
342  }
343
344}