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