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 >= 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 >= 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}