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 (Exception e) { 230 throw new RuntimeException(e); 231 } finally { 232 timer.observeDuration(); 233 } 234 } 235 236 public static class Value { 237 public final double count; 238 public final double sum; 239 public final SortedMap<Double, Double> quantiles; 240 241 private Value(double count, double sum, List<Quantile> quantiles, TimeWindowQuantiles quantileValues) { 242 this.count = count; 243 this.sum = sum; 244 this.quantiles = Collections.unmodifiableSortedMap(snapshot(quantiles, quantileValues)); 245 } 246 247 private SortedMap<Double, Double> snapshot(List<Quantile> quantiles, TimeWindowQuantiles quantileValues) { 248 SortedMap<Double, Double> result = new TreeMap<Double, Double>(); 249 for (Quantile q : quantiles) { 250 result.put(q.quantile, quantileValues.get(q.quantile)); 251 } 252 return result; 253 } 254 } 255 256 // Having these separate leaves us open to races, 257 // however Prometheus as whole has other races 258 // that mean adding atomicity here wouldn't be useful. 259 // This should be reevaluated in the future. 260 private final DoubleAdder count = new DoubleAdder(); 261 private final DoubleAdder sum = new DoubleAdder(); 262 private final List<Quantile> quantiles; 263 private final TimeWindowQuantiles quantileValues; 264 265 private Child(List<Quantile> quantiles, long maxAgeSeconds, int ageBuckets) { 266 this.quantiles = quantiles; 267 if (quantiles.size() > 0) { 268 quantileValues = new TimeWindowQuantiles(quantiles.toArray(new Quantile[]{}), maxAgeSeconds, ageBuckets); 269 } else { 270 quantileValues = null; 271 } 272 } 273 274 /** 275 * Observe the given amount. 276 */ 277 public void observe(double amt) { 278 count.add(1); 279 sum.add(amt); 280 if (quantileValues != null) { 281 quantileValues.insert(amt); 282 } 283 } 284 /** 285 * Start a timer to track a duration. 286 * <p> 287 * Call {@link Timer#observeDuration} at the end of what you want to measure the duration of. 288 */ 289 public Timer startTimer() { 290 return new Timer(this, SimpleTimer.defaultTimeProvider.nanoTime()); 291 } 292 /** 293 * Get the value of the Summary. 294 * <p> 295 * <em>Warning:</em> The definition of {@link Value} is subject to change. 296 */ 297 public Value get() { 298 return new Value(count.sum(), sum.sum(), quantiles, quantileValues); 299 } 300 } 301 302 // Convenience methods. 303 /** 304 * Observe the given amount on the summary with no labels. 305 */ 306 public void observe(double amt) { 307 noLabelsChild.observe(amt); 308 } 309 /** 310 * Start a timer to track a duration on the summary with no labels. 311 * <p> 312 * Call {@link Timer#observeDuration} at the end of what you want to measure the duration of. 313 */ 314 public Timer startTimer() { 315 return noLabelsChild.startTimer(); 316 } 317 318 /** 319 * Executes runnable code (e.g. a Java 8 Lambda) and observes a duration of how long it took to run. 320 * 321 * @param timeable Code that is being timed 322 * @return Measured duration in seconds for timeable to complete. 323 */ 324 public double time(Runnable timeable){ 325 return noLabelsChild.time(timeable); 326 } 327 328 /** 329 * Executes callable code (e.g. a Java 8 Lambda) and observes a duration of how long it took to run. 330 * 331 * @param timeable Code that is being timed 332 * @return Result returned by callable. 333 */ 334 public <E> E time(Callable<E> timeable){ 335 return noLabelsChild.time(timeable); 336 } 337 338 /** 339 * Get the value of the Summary. 340 * <p> 341 * <em>Warning:</em> The definition of {@link Child.Value} is subject to change. 342 */ 343 public Child.Value get() { 344 return noLabelsChild.get(); 345 } 346 347 @Override 348 public List<MetricFamilySamples> collect() { 349 List<MetricFamilySamples.Sample> samples = new ArrayList<MetricFamilySamples.Sample>(); 350 for(Map.Entry<List<String>, Child> c: children.entrySet()) { 351 Child.Value v = c.getValue().get(); 352 List<String> labelNamesWithQuantile = new ArrayList<String>(labelNames); 353 labelNamesWithQuantile.add("quantile"); 354 for(Map.Entry<Double, Double> q : v.quantiles.entrySet()) { 355 List<String> labelValuesWithQuantile = new ArrayList<String>(c.getKey()); 356 labelValuesWithQuantile.add(doubleToGoString(q.getKey())); 357 samples.add(new MetricFamilySamples.Sample(fullname, labelNamesWithQuantile, labelValuesWithQuantile, q.getValue())); 358 } 359 samples.add(new MetricFamilySamples.Sample(fullname + "_count", labelNames, c.getKey(), v.count)); 360 samples.add(new MetricFamilySamples.Sample(fullname + "_sum", labelNames, c.getKey(), v.sum)); 361 } 362 363 return familySamplesList(Type.SUMMARY, samples); 364 } 365 366 @Override 367 public List<MetricFamilySamples> describe() { 368 return Collections.<MetricFamilySamples>singletonList(new SummaryMetricFamily(fullname, help, labelNames)); 369 } 370 371}