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