001package io.prometheus.client; 002 003import io.prometheus.client.CKMSQuantiles.Quantile; 004 005import java.io.Closeable; 006import java.io.IOException; 007import java.util.ArrayList; 008import java.util.Collections; 009import java.util.List; 010import java.util.Map; 011import java.util.SortedMap; 012import java.util.TreeMap; 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 * @throws IOException 186 */ 187 @Override 188 public void close() throws IOException { 189 observeDuration(); 190 } 191 } 192 193 /** 194 * The value of a single Summary. 195 * <p> 196 * <em>Warning:</em> References to a Child become invalid after using 197 * {@link SimpleCollector#remove} or {@link SimpleCollector#clear}. 198 */ 199 public static class Child { 200 201 /** 202 * Executes runnable code (i.e. a Java 8 Lambda) and observes a duration of how long it took to run. 203 * 204 * @param timeable Code that is being timed 205 * @return Measured duration in seconds for timeable to complete. 206 */ 207 public double time(Runnable timeable) { 208 Timer timer = startTimer(); 209 210 double elapsed; 211 try { 212 timeable.run(); 213 } finally { 214 elapsed = timer.observeDuration(); 215 } 216 return elapsed; 217 } 218 219 public static class Value { 220 public final double count; 221 public final double sum; 222 public final SortedMap<Double, Double> quantiles; 223 224 private Value(double count, double sum, List<Quantile> quantiles, TimeWindowQuantiles quantileValues) { 225 this.count = count; 226 this.sum = sum; 227 this.quantiles = Collections.unmodifiableSortedMap(snapshot(quantiles, quantileValues)); 228 } 229 230 private SortedMap<Double, Double> snapshot(List<Quantile> quantiles, TimeWindowQuantiles quantileValues) { 231 SortedMap<Double, Double> result = new TreeMap<Double, Double>(); 232 for (Quantile q : quantiles) { 233 result.put(q.quantile, quantileValues.get(q.quantile)); 234 } 235 return result; 236 } 237 } 238 239 // Having these separate leaves us open to races, 240 // however Prometheus as whole has other races 241 // that mean adding atomicity here wouldn't be useful. 242 // This should be reevaluated in the future. 243 private final DoubleAdder count = new DoubleAdder(); 244 private final DoubleAdder sum = new DoubleAdder(); 245 private final List<Quantile> quantiles; 246 private final TimeWindowQuantiles quantileValues; 247 248 private Child(List<Quantile> quantiles, long maxAgeSeconds, int ageBuckets) { 249 this.quantiles = quantiles; 250 if (quantiles.size() > 0) { 251 quantileValues = new TimeWindowQuantiles(quantiles.toArray(new Quantile[]{}), maxAgeSeconds, ageBuckets); 252 } else { 253 quantileValues = null; 254 } 255 } 256 257 /** 258 * Observe the given amount. 259 */ 260 public void observe(double amt) { 261 count.add(1); 262 sum.add(amt); 263 if (quantileValues != null) { 264 quantileValues.insert(amt); 265 } 266 } 267 /** 268 * Start a timer to track a duration. 269 * <p> 270 * Call {@link Timer#observeDuration} at the end of what you want to measure the duration of. 271 */ 272 public Timer startTimer() { 273 return new Timer(this, SimpleTimer.defaultTimeProvider.nanoTime()); 274 } 275 /** 276 * Get the value of the Summary. 277 * <p> 278 * <em>Warning:</em> The definition of {@link Value} is subject to change. 279 */ 280 public Value get() { 281 return new Value(count.sum(), sum.sum(), quantiles, quantileValues); 282 } 283 } 284 285 // Convenience methods. 286 /** 287 * Observe the given amount on the summary with no labels. 288 */ 289 public void observe(double amt) { 290 noLabelsChild.observe(amt); 291 } 292 /** 293 * Start a timer to track a duration on the summary with no labels. 294 * <p> 295 * Call {@link Timer#observeDuration} at the end of what you want to measure the duration of. 296 */ 297 public Timer startTimer() { 298 return noLabelsChild.startTimer(); 299 } 300 301 /** 302 * Executes runnable code (i.e. a Java 8 Lambda) and observes a duration of how long it took to run. 303 * 304 * @param timeable Code that is being timed 305 * @return Measured duration in seconds for timeable to complete. 306 */ 307 public double time(Runnable timeable){ 308 return noLabelsChild.time(timeable); 309 } 310 311 /** 312 * Get the value of the Summary. 313 * <p> 314 * <em>Warning:</em> The definition of {@link Child.Value} is subject to change. 315 */ 316 public Child.Value get() { 317 return noLabelsChild.get(); 318 } 319 320 @Override 321 public List<MetricFamilySamples> collect() { 322 List<MetricFamilySamples.Sample> samples = new ArrayList<MetricFamilySamples.Sample>(); 323 for(Map.Entry<List<String>, Child> c: children.entrySet()) { 324 Child.Value v = c.getValue().get(); 325 List<String> labelNamesWithQuantile = new ArrayList<String>(labelNames); 326 labelNamesWithQuantile.add("quantile"); 327 for(Map.Entry<Double, Double> q : v.quantiles.entrySet()) { 328 List<String> labelValuesWithQuantile = new ArrayList<String>(c.getKey()); 329 labelValuesWithQuantile.add(doubleToGoString(q.getKey())); 330 samples.add(new MetricFamilySamples.Sample(fullname, labelNamesWithQuantile, labelValuesWithQuantile, q.getValue())); 331 } 332 samples.add(new MetricFamilySamples.Sample(fullname + "_count", labelNames, c.getKey(), v.count)); 333 samples.add(new MetricFamilySamples.Sample(fullname + "_sum", labelNames, c.getKey(), v.sum)); 334 } 335 336 return familySamplesList(Type.SUMMARY, samples); 337 } 338 339 @Override 340 public List<MetricFamilySamples> describe() { 341 return Collections.<MetricFamilySamples>singletonList(new SummaryMetricFamily(fullname, help, labelNames)); 342 } 343 344}