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 &lt;= 0.01 or quantile &gt;= 0.99</li>
250         *     <li>error = 0.005 if quantile &lt;= 0.02 or quantile &gt;= 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}