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