001package io.prometheus.client; 002 003import java.io.Closeable; 004import java.util.ArrayList; 005import java.util.Collections; 006import java.util.List; 007import java.util.Map; 008import java.util.concurrent.Callable; 009 010/** 011 * Histogram metric, to track distributions of events. 012 * <p> 013 * Example of uses for Histograms include: 014 * <ul> 015 * <li>Response latency</li> 016 * <li>Request size</li> 017 * </ul> 018 * <p> 019 * <em>Note:</em> Each bucket is one timeseries. Many buckets and/or many dimensions with labels 020 * can produce large amount of time series, that may cause performance problems. 021 * 022 * <p> 023 * The default buckets are intended to cover a typical web/rpc request from milliseconds to seconds. 024 * <p> 025 * Example Histograms: 026 * <pre> 027 * {@code 028 * class YourClass { 029 * static final Histogram requestLatency = Histogram.build() 030 * .name("requests_latency_seconds").help("Request latency in seconds.").register(); 031 * 032 * void processRequest(Request req) { 033 * Histogram.Timer requestTimer = requestLatency.startTimer(); 034 * try { 035 * // Your code here. 036 * } finally { 037 * requestTimer.observeDuration(); 038 * } 039 * } 040 * 041 * // Or if using Java 8 lambdas. 042 * void processRequestLambda(Request req) { 043 * requestLatency.time(() -> { 044 * // Your code here. 045 * }); 046 * } 047 * } 048 * } 049 * </pre> 050 * <p> 051 * You can choose your own buckets: 052 * <pre> 053 * {@code 054 * static final Histogram requestLatency = Histogram.build() 055 * .buckets(.01, .02, .03, .04) 056 * .name("requests_latency_seconds").help("Request latency in seconds.").register(); 057 * } 058 * </pre> 059 * {@link Histogram.Builder#linearBuckets(double, double, int) linearBuckets} and 060 * {@link Histogram.Builder#exponentialBuckets(double, double, int) exponentialBuckets} 061 * offer easy ways to set common bucket patterns. 062 */ 063public class Histogram extends SimpleCollector<Histogram.Child> implements Collector.Describable { 064 private final double[] buckets; 065 066 Histogram(Builder b) { 067 super(b); 068 buckets = b.buckets; 069 initializeNoLabelsChild(); 070 } 071 072 public static class Builder extends SimpleCollector.Builder<Builder, Histogram> { 073 private double[] buckets = new double[]{.005, .01, .025, .05, .075, .1, .25, .5, .75, 1, 2.5, 5, 7.5, 10}; 074 075 @Override 076 public Histogram create() { 077 for (int i = 0; i < buckets.length - 1; i++) { 078 if (buckets[i] >= buckets[i + 1]) { 079 throw new IllegalStateException("Histogram buckets must be in increasing order: " 080 + buckets[i] + " >= " + buckets[i + 1]); 081 } 082 } 083 if (buckets.length == 0) { 084 throw new IllegalStateException("Histogram must have at least one bucket."); 085 } 086 for (String label: labelNames) { 087 if (label.equals("le")) { 088 throw new IllegalStateException("Histogram cannot have a label named 'le'."); 089 } 090 } 091 092 // Append infinity bucket if it's not already there. 093 if (buckets[buckets.length - 1] != Double.POSITIVE_INFINITY) { 094 double[] tmp = new double[buckets.length + 1]; 095 System.arraycopy(buckets, 0, tmp, 0, buckets.length); 096 tmp[buckets.length] = Double.POSITIVE_INFINITY; 097 buckets = tmp; 098 } 099 dontInitializeNoLabelsChild = true; 100 return new Histogram(this); 101 } 102 103 /** 104 * Set the upper bounds of buckets for the histogram. 105 */ 106 public Builder buckets(double... buckets) { 107 this.buckets = buckets; 108 return this; 109 } 110 111 /** 112 * Set the upper bounds of buckets for the histogram with a linear sequence. 113 */ 114 public Builder linearBuckets(double start, double width, int count) { 115 buckets = new double[count]; 116 for (int i = 0; i < count; i++){ 117 buckets[i] = start + i*width; 118 } 119 return this; 120 } 121 /** 122 * Set the upper bounds of buckets for the histogram with an exponential sequence. 123 */ 124 public Builder exponentialBuckets(double start, double factor, int count) { 125 buckets = new double[count]; 126 for (int i = 0; i < count; i++) { 127 buckets[i] = start * Math.pow(factor, i); 128 } 129 return this; 130 } 131 132 } 133 134 /** 135 * Return a Builder to allow configuration of a new Histogram. Ensures required fields are provided. 136 * 137 * @param name The name of the metric 138 * @param help The help string of the metric 139 */ 140 public static Builder build(String name, String help) { 141 return new Builder().name(name).help(help); 142 } 143 144 /** 145 * Return a Builder to allow configuration of a new Histogram. 146 */ 147 public static Builder build() { 148 return new Builder(); 149 } 150 151 @Override 152 protected Child newChild() { 153 return new Child(buckets); 154 } 155 156 /** 157 * Represents an event being timed. 158 */ 159 public static class Timer implements Closeable { 160 private final Child child; 161 private final long start; 162 private Timer(Child child, long start) { 163 this.child = child; 164 this.start = start; 165 } 166 /** 167 * Observe the amount of time in seconds since {@link Child#startTimer} was called. 168 * @return Measured duration in seconds since {@link Child#startTimer} was called. 169 */ 170 public double observeDuration() { 171 double elapsed = SimpleTimer.elapsedSecondsFromNanos(start, SimpleTimer.defaultTimeProvider.nanoTime()); 172 child.observe(elapsed); 173 return elapsed; 174 } 175 176 /** 177 * Equivalent to calling {@link #observeDuration()}. 178 */ 179 @Override 180 public void close() { 181 observeDuration(); 182 } 183 } 184 185 /** 186 * The value of a single Histogram. 187 * <p> 188 * <em>Warning:</em> References to a Child become invalid after using 189 * {@link SimpleCollector#remove} or {@link SimpleCollector#clear}. 190 */ 191 public static class Child { 192 193 /** 194 * Executes runnable code (e.g. a Java 8 Lambda) and observes a duration of how long it took to run. 195 * 196 * @param timeable Code that is being timed 197 * @return Measured duration in seconds for timeable to complete. 198 */ 199 public double time(Runnable timeable) { 200 Timer timer = startTimer(); 201 202 double elapsed; 203 try { 204 timeable.run(); 205 } finally { 206 elapsed = timer.observeDuration(); 207 } 208 return elapsed; 209 } 210 211 /** 212 * Executes callable code (e.g. a Java 8 Lambda) and observes a duration of how long it took to run. 213 * 214 * @param timeable Code that is being timed 215 * @return Result returned by callable. 216 */ 217 public <E> E time(Callable<E> timeable) { 218 Timer timer = startTimer(); 219 220 try { 221 return timeable.call(); 222 } catch (RuntimeException e) { 223 throw e; 224 } catch (Exception e) { 225 throw new RuntimeException(e); 226 } finally { 227 timer.observeDuration(); 228 } 229 } 230 231 public static class Value { 232 public final double sum; 233 public final double[] buckets; 234 235 public Value(double sum, double[] buckets) { 236 this.sum = sum; 237 this.buckets = buckets; 238 } 239 } 240 241 private Child(double[] buckets) { 242 upperBounds = buckets; 243 cumulativeCounts = new DoubleAdder[buckets.length]; 244 for (int i = 0; i < buckets.length; ++i) { 245 cumulativeCounts[i] = new DoubleAdder(); 246 } 247 } 248 private final double[] upperBounds; 249 private final DoubleAdder[] cumulativeCounts; 250 private final DoubleAdder sum = new DoubleAdder(); 251 252 253 /** 254 * Observe the given amount. 255 */ 256 public void observe(double amt) { 257 for (int i = 0; i < upperBounds.length; ++i) { 258 // The last bucket is +Inf, so we always increment. 259 if (amt <= upperBounds[i]) { 260 cumulativeCounts[i].add(1); 261 break; 262 } 263 } 264 sum.add(amt); 265 } 266 /** 267 * Start a timer to track a duration. 268 * <p> 269 * Call {@link Timer#observeDuration} at the end of what you want to measure the duration of. 270 */ 271 public Timer startTimer() { 272 return new Timer(this, SimpleTimer.defaultTimeProvider.nanoTime()); 273 } 274 /** 275 * Get the value of the Histogram. 276 * <p> 277 * <em>Warning:</em> The definition of {@link Value} is subject to change. 278 */ 279 public Value get() { 280 double[] buckets = new double[cumulativeCounts.length]; 281 double acc = 0; 282 for (int i = 0; i < cumulativeCounts.length; ++i) { 283 acc += cumulativeCounts[i].sum(); 284 buckets[i] = acc; 285 } 286 return new Value(sum.sum(), buckets); 287 } 288 } 289 290 // Convenience methods. 291 /** 292 * Observe the given amount on the histogram with no labels. 293 */ 294 public void observe(double amt) { 295 noLabelsChild.observe(amt); 296 } 297 /** 298 * Start a timer to track a duration on the histogram with no labels. 299 * <p> 300 * Call {@link Timer#observeDuration} at the end of what you want to measure the duration of. 301 */ 302 public Timer startTimer() { 303 return noLabelsChild.startTimer(); 304 } 305 306 /** 307 * Executes runnable code (e.g. a Java 8 Lambda) and observes a duration of how long it took to run. 308 * 309 * @param timeable Code that is being timed 310 * @return Measured duration in seconds for timeable to complete. 311 */ 312 public double time(Runnable timeable){ 313 return noLabelsChild.time(timeable); 314 } 315 316 /** 317 * Executes callable code (e.g. a Java 8 Lambda) and observes a duration of how long it took to run. 318 * 319 * @param timeable Code that is being timed 320 * @return Result returned by callable. 321 */ 322 public <E> E time(Callable<E> timeable){ 323 return noLabelsChild.time(timeable); 324 } 325 326 @Override 327 public List<MetricFamilySamples> collect() { 328 List<MetricFamilySamples.Sample> samples = new ArrayList<MetricFamilySamples.Sample>(); 329 for(Map.Entry<List<String>, Child> c: children.entrySet()) { 330 Child.Value v = c.getValue().get(); 331 List<String> labelNamesWithLe = new ArrayList<String>(labelNames); 332 labelNamesWithLe.add("le"); 333 for (int i = 0; i < v.buckets.length; ++i) { 334 List<String> labelValuesWithLe = new ArrayList<String>(c.getKey()); 335 labelValuesWithLe.add(doubleToGoString(buckets[i])); 336 samples.add(new MetricFamilySamples.Sample(fullname + "_bucket", labelNamesWithLe, labelValuesWithLe, v.buckets[i])); 337 } 338 samples.add(new MetricFamilySamples.Sample(fullname + "_count", labelNames, c.getKey(), v.buckets[buckets.length-1])); 339 samples.add(new MetricFamilySamples.Sample(fullname + "_sum", labelNames, c.getKey(), v.sum)); 340 } 341 342 return familySamplesList(Type.HISTOGRAM, samples); 343 } 344 345 @Override 346 public List<MetricFamilySamples> describe() { 347 return Collections.singletonList( 348 new MetricFamilySamples(fullname, Type.HISTOGRAM, help, Collections.<MetricFamilySamples.Sample>emptyList())); 349 } 350 351 double[] getBuckets() { 352 return buckets; 353 } 354 355 356}