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