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