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