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 */ 282 public void observe(double amt) { 283 count.add(1); 284 sum.add(amt); 285 if (quantileValues != null) { 286 quantileValues.insert(amt); 287 } 288 } 289 /** 290 * Start a timer to track a duration. 291 * <p> 292 * Call {@link Timer#observeDuration} at the end of what you want to measure the duration of. 293 */ 294 public Timer startTimer() { 295 return new Timer(this, SimpleTimer.defaultTimeProvider.nanoTime()); 296 } 297 /** 298 * Get the value of the Summary. 299 * <p> 300 * <em>Warning:</em> The definition of {@link Value} is subject to change. 301 */ 302 public Value get() { 303 return new Value(count.sum(), sum.sum(), quantiles, quantileValues, created); 304 } 305 } 306 307 // Convenience methods. 308 /** 309 * Observe the given amount on the summary with no labels. 310 */ 311 public void observe(double amt) { 312 noLabelsChild.observe(amt); 313 } 314 /** 315 * Start a timer to track a duration on the summary with no labels. 316 * <p> 317 * Call {@link Timer#observeDuration} at the end of what you want to measure the duration of. 318 */ 319 public Timer startTimer() { 320 return noLabelsChild.startTimer(); 321 } 322 323 /** 324 * Executes runnable code (e.g. a Java 8 Lambda) and observes a duration of how long it took to run. 325 * 326 * @param timeable Code that is being timed 327 * @return Measured duration in seconds for timeable to complete. 328 */ 329 public double time(Runnable timeable){ 330 return noLabelsChild.time(timeable); 331 } 332 333 /** 334 * Executes callable code (e.g. a Java 8 Lambda) and observes a duration of how long it took to run. 335 * 336 * @param timeable Code that is being timed 337 * @return Result returned by callable. 338 */ 339 public <E> E time(Callable<E> timeable){ 340 return noLabelsChild.time(timeable); 341 } 342 343 /** 344 * Get the value of the Summary. 345 * <p> 346 * <em>Warning:</em> The definition of {@link Child.Value} is subject to change. 347 */ 348 public Child.Value get() { 349 return noLabelsChild.get(); 350 } 351 352 @Override 353 public List<MetricFamilySamples> collect() { 354 List<MetricFamilySamples.Sample> samples = new ArrayList<MetricFamilySamples.Sample>(); 355 for(Map.Entry<List<String>, Child> c: children.entrySet()) { 356 Child.Value v = c.getValue().get(); 357 List<String> labelNamesWithQuantile = new ArrayList<String>(labelNames); 358 labelNamesWithQuantile.add("quantile"); 359 for(Map.Entry<Double, Double> q : v.quantiles.entrySet()) { 360 List<String> labelValuesWithQuantile = new ArrayList<String>(c.getKey()); 361 labelValuesWithQuantile.add(doubleToGoString(q.getKey())); 362 samples.add(new MetricFamilySamples.Sample(fullname, labelNamesWithQuantile, labelValuesWithQuantile, q.getValue())); 363 } 364 samples.add(new MetricFamilySamples.Sample(fullname + "_count", labelNames, c.getKey(), v.count)); 365 samples.add(new MetricFamilySamples.Sample(fullname + "_sum", labelNames, c.getKey(), v.sum)); 366 samples.add(new MetricFamilySamples.Sample(fullname + "_created", labelNames, c.getKey(), v.created / 1000.0)); 367 } 368 369 return familySamplesList(Type.SUMMARY, samples); 370 } 371 372 @Override 373 public List<MetricFamilySamples> describe() { 374 return Collections.<MetricFamilySamples>singletonList(new SummaryMetricFamily(fullname, help, labelNames)); 375 } 376 377}