001package io.prometheus.metrics.core.metrics; 002 003import io.prometheus.metrics.config.MetricsProperties; 004import io.prometheus.metrics.config.PrometheusProperties; 005import io.prometheus.metrics.core.exemplars.ExemplarSampler; 006import io.prometheus.metrics.core.exemplars.ExemplarSamplerConfig; 007import io.prometheus.metrics.model.snapshots.Exemplars; 008import io.prometheus.metrics.model.snapshots.Labels; 009import io.prometheus.metrics.model.snapshots.Quantile; 010import io.prometheus.metrics.model.snapshots.Quantiles; 011import io.prometheus.metrics.model.snapshots.SummarySnapshot; 012import io.prometheus.metrics.core.datapoints.DistributionDataPoint; 013 014import java.util.ArrayList; 015import java.util.Collections; 016import java.util.List; 017import java.util.concurrent.TimeUnit; 018import java.util.concurrent.atomic.DoubleAdder; 019import java.util.concurrent.atomic.LongAdder; 020 021/** 022 * Summary metric. Example: 023 * <pre>{@code 024 * Summary summary = Summary.builder() 025 * .name("http_request_duration_seconds_hi") 026 * .help("HTTP request service time in seconds") 027 * .unit(SECONDS) 028 * .labelNames("method", "path", "status_code") 029 * .quantile(0.5, 0.01) 030 * .quantile(0.95, 0.001) 031 * .quantile(0.99, 0.001) 032 * .register(); 033 * 034 * long start = System.nanoTime(); 035 * // process a request, duration will be observed 036 * summary.labelValues("GET", "/", "200").observe(Unit.nanosToSeconds(System.nanoTime() - start)); 037 * }</pre> 038 * See {@link Summary.Builder} for configuration options. 039 */ 040public class Summary extends StatefulMetric<DistributionDataPoint, Summary.DataPoint> implements DistributionDataPoint { 041 042 private final List<CKMSQuantiles.Quantile> quantiles; // May be empty, but cannot be null. 043 private final long maxAgeSeconds; 044 private final int ageBuckets; 045 private final boolean exemplarsEnabled; 046 private final ExemplarSamplerConfig exemplarSamplerConfig; 047 048 private Summary(Builder builder, PrometheusProperties prometheusProperties) { 049 super(builder); 050 MetricsProperties[] properties = getMetricProperties(builder, prometheusProperties); 051 this.exemplarsEnabled = getConfigProperty(properties, MetricsProperties::getExemplarsEnabled); 052 this.quantiles = Collections.unmodifiableList(makeQuantiles(properties)); 053 this.maxAgeSeconds = getConfigProperty(properties, MetricsProperties::getSummaryMaxAgeSeconds); 054 this.ageBuckets = getConfigProperty(properties, MetricsProperties::getSummaryNumberOfAgeBuckets); 055 this.exemplarSamplerConfig = new ExemplarSamplerConfig(prometheusProperties.getExemplarProperties(), 4); 056 } 057 058 private List<CKMSQuantiles.Quantile> makeQuantiles(MetricsProperties[] properties) { 059 List<CKMSQuantiles.Quantile> result = new ArrayList<>(); 060 List<Double> quantiles = getConfigProperty(properties, MetricsProperties::getSummaryQuantiles); 061 List<Double> quantileErrors = getConfigProperty(properties, MetricsProperties::getSummaryQuantileErrors); 062 if (quantiles != null) { 063 for (int i = 0; i < quantiles.size(); i++) { 064 if (quantileErrors.size() > 0) { 065 result.add(new CKMSQuantiles.Quantile(quantiles.get(i), quantileErrors.get(i))); 066 } else { 067 result.add(new CKMSQuantiles.Quantile(quantiles.get(i), Builder.defaultError(quantiles.get(i)))); 068 } 069 } 070 } 071 return result; 072 } 073 074 @Override 075 protected boolean isExemplarsEnabled() { 076 return exemplarsEnabled; 077 } 078 079 /** 080 * {@inheritDoc} 081 */ 082 @Override 083 public void observe(double amount) { 084 getNoLabels().observe(amount); 085 } 086 087 /** 088 * {@inheritDoc} 089 */ 090 @Override 091 public void observeWithExemplar(double amount, Labels labels) { 092 getNoLabels().observeWithExemplar(amount, labels); 093 } 094 095 /** 096 * {@inheritDoc} 097 */ 098 @Override 099 public SummarySnapshot collect() { 100 return (SummarySnapshot) super.collect(); 101 } 102 103 @Override 104 protected SummarySnapshot collect(List<Labels> labels, List<DataPoint> metricData) { 105 List<SummarySnapshot.SummaryDataPointSnapshot> data = new ArrayList<>(labels.size()); 106 for (int i = 0; i < labels.size(); i++) { 107 data.add(metricData.get(i).collect(labels.get(i))); 108 } 109 return new SummarySnapshot(getMetadata(), data); 110 } 111 112 @Override 113 protected DataPoint newDataPoint() { 114 return new DataPoint(); 115 } 116 117 118 public class DataPoint implements DistributionDataPoint { 119 120 private final LongAdder count = new LongAdder(); 121 private final DoubleAdder sum = new DoubleAdder(); 122 private final SlidingWindow<CKMSQuantiles> quantileValues; 123 private final Buffer buffer = new Buffer(); 124 private final ExemplarSampler exemplarSampler; 125 126 private final long createdTimeMillis = System.currentTimeMillis(); 127 128 private DataPoint() { 129 if (quantiles.size() > 0) { 130 CKMSQuantiles.Quantile[] quantilesArray = quantiles.toArray(new CKMSQuantiles.Quantile[0]); 131 quantileValues = new SlidingWindow<>(CKMSQuantiles.class, () -> new CKMSQuantiles(quantilesArray), CKMSQuantiles::insert, maxAgeSeconds, ageBuckets); 132 } else { 133 quantileValues = null; 134 } 135 if (exemplarsEnabled) { 136 exemplarSampler = new ExemplarSampler(exemplarSamplerConfig); 137 } else { 138 exemplarSampler = null; 139 } 140 } 141 142 /** 143 * {@inheritDoc} 144 */ 145 @Override 146 public void observe(double value) { 147 if (Double.isNaN(value)) { 148 return; 149 } 150 if (!buffer.append(value)) { 151 doObserve(value); 152 } 153 if (isExemplarsEnabled()) { 154 exemplarSampler.observe(value); 155 } 156 } 157 158 /** 159 * {@inheritDoc} 160 */ 161 @Override 162 public void observeWithExemplar(double value, Labels labels) { 163 if (Double.isNaN(value)) { 164 return; 165 } 166 if (!buffer.append(value)) { 167 doObserve(value); 168 } 169 if (isExemplarsEnabled()) { 170 exemplarSampler.observeWithExemplar(value, labels); 171 } 172 } 173 174 private void doObserve(double amount) { 175 sum.add(amount); 176 if (quantileValues != null) { 177 quantileValues.observe(amount); 178 } 179 // count must be incremented last, because in collect() the count 180 // indicates the number of completed observations. 181 count.increment(); 182 } 183 184 private SummarySnapshot.SummaryDataPointSnapshot collect(Labels labels) { 185 return buffer.run( 186 expectedCount -> count.sum() == expectedCount, 187 // TODO Exemplars (are hard-coded as empty in the line below) 188 () -> new SummarySnapshot.SummaryDataPointSnapshot(count.sum(), sum.sum(), makeQuantiles(), labels, Exemplars.EMPTY, createdTimeMillis), 189 this::doObserve 190 ); 191 } 192 193 private List<CKMSQuantiles.Quantile> getQuantiles() { 194 return quantiles; 195 } 196 197 private Quantiles makeQuantiles() { 198 Quantile[] quantiles = new Quantile[getQuantiles().size()]; 199 for (int i = 0; i < getQuantiles().size(); i++) { 200 CKMSQuantiles.Quantile quantile = getQuantiles().get(i); 201 quantiles[i] = new Quantile(quantile.quantile, quantileValues.current().get(quantile.quantile)); 202 } 203 return Quantiles.of(quantiles); 204 } 205 } 206 207 public static Summary.Builder builder() { 208 return new Builder(PrometheusProperties.get()); 209 } 210 211 public static Summary.Builder builder(PrometheusProperties config) { 212 return new Builder(config); 213 } 214 215 public static class Builder extends StatefulMetric.Builder<Summary.Builder, Summary> { 216 217 /** 218 * 5 minutes. See {@link #maxAgeSeconds(long)}. 219 */ 220 public static final long DEFAULT_MAX_AGE_SECONDS = TimeUnit.MINUTES.toSeconds(5); 221 222 /** 223 * 5. See {@link #numberOfAgeBuckets(int)} 224 */ 225 public static final int DEFAULT_NUMBER_OF_AGE_BUCKETS = 5; 226 private final List<CKMSQuantiles.Quantile> quantiles = new ArrayList<>(); 227 private Long maxAgeSeconds; 228 private Integer ageBuckets; 229 230 private Builder(PrometheusProperties properties) { 231 super(Collections.singletonList("quantile"), properties); 232 } 233 234 private static double defaultError(double quantile) { 235 if (quantile <= 0.01 || quantile >= 0.99) { 236 return 0.001; 237 } else if (quantile <= 0.02 || quantile >= 0.98) { 238 return 0.005; 239 } else { 240 return 0.01; 241 } 242 } 243 244 /** 245 * Add a quantile. See {@link #quantile(double, double)}. 246 * <p> 247 * Default errors are: 248 * <ul> 249 * <li>error = 0.001 if quantile <= 0.01 or quantile >= 0.99</li> 250 * <li>error = 0.005 if quantile <= 0.02 or quantile >= 0.98</li> 251 * <li>error = 0.01 else. 252 * </ul> 253 */ 254 public Builder quantile(double quantile) { 255 return quantile(quantile, defaultError(quantile)); 256 } 257 258 /** 259 * Add a quantile. Call multiple times to add multiple quantiles. 260 * <p> 261 * Example: The following will track the 0.95 quantile: 262 * <pre>{@code 263 * .quantile(0.95, 0.001) 264 * }</pre> 265 * The second argument is the acceptable error margin, i.e. with the code above the quantile 266 * will not be exactly the 0.95 quantile but something between 0.949 and 0.951. 267 * <p> 268 * There are two special cases: 269 * <ul> 270 * <li>{@code .quantile(0.0, 0.0)} gives you the minimum observed value</li> 271 * <li>{@code .quantile(1.0, 0.0)} gives you the maximum observed value</li> 272 * </ul> 273 */ 274 public Builder quantile(double quantile, double error) { 275 if (quantile < 0.0 || quantile > 1.0) { 276 throw new IllegalArgumentException("Quantile " + quantile + " invalid: Expected number between 0.0 and 1.0."); 277 } 278 if (error < 0.0 || error > 1.0) { 279 throw new IllegalArgumentException("Error " + error + " invalid: Expected number between 0.0 and 1.0."); 280 } 281 quantiles.add(new CKMSQuantiles.Quantile(quantile, error)); 282 return this; 283 } 284 285 /** 286 * The quantiles are relative to a moving time window. 287 * {@code maxAgeSeconds} is the size of that time window. 288 * Default is {@link #DEFAULT_MAX_AGE_SECONDS}. 289 */ 290 public Builder maxAgeSeconds(long maxAgeSeconds) { 291 if (maxAgeSeconds <= 0) { 292 throw new IllegalArgumentException("maxAgeSeconds cannot be " + maxAgeSeconds); 293 } 294 this.maxAgeSeconds = maxAgeSeconds; 295 return this; 296 } 297 298 /** 299 * The quantiles are relative to a moving time window. 300 * The {@code numberOfAgeBuckets} defines how smoothly the time window moves forward. 301 * For example, if the time window is 5 minutes and has 5 age buckets, 302 * then it is moving forward every minute by one minute. 303 * Default is {@link #DEFAULT_NUMBER_OF_AGE_BUCKETS}. 304 */ 305 public Builder numberOfAgeBuckets(int ageBuckets) { 306 if (ageBuckets <= 0) { 307 throw new IllegalArgumentException("ageBuckets cannot be " + ageBuckets); 308 } 309 this.ageBuckets = ageBuckets; 310 return this; 311 } 312 313 @Override 314 protected MetricsProperties toProperties() { 315 double[] quantiles = null; 316 double[] quantileErrors = null; 317 if (!this.quantiles.isEmpty()) { 318 quantiles = new double[this.quantiles.size()]; 319 quantileErrors = new double[this.quantiles.size()]; 320 for (int i = 0; i < this.quantiles.size(); i++) { 321 quantiles[i] = this.quantiles.get(i).quantile; 322 quantileErrors[i] = this.quantiles.get(i).epsilon; 323 } 324 } 325 return MetricsProperties.builder() 326 .exemplarsEnabled(exemplarsEnabled) 327 .summaryQuantiles(quantiles) 328 .summaryQuantileErrors(quantileErrors) 329 .summaryNumberOfAgeBuckets(ageBuckets) 330 .summaryMaxAgeSeconds(maxAgeSeconds) 331 .build(); 332 } 333 334 /** 335 * Default properties for summary metrics. 336 */ 337 @Override 338 public MetricsProperties getDefaultProperties() { 339 return MetricsProperties.builder() 340 .exemplarsEnabled(true) 341 .summaryQuantiles() 342 .summaryNumberOfAgeBuckets(DEFAULT_NUMBER_OF_AGE_BUCKETS) 343 .summaryMaxAgeSeconds(DEFAULT_MAX_AGE_SECONDS) 344 .build(); 345 } 346 347 @Override 348 public Summary build() { 349 return new Summary(this, properties); 350 } 351 352 @Override 353 protected Builder self() { 354 return this; 355 } 356 } 357}