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