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