001package io.prometheus.metrics.simpleclient.bridge;
002
003import io.prometheus.client.Collector;
004import io.prometheus.client.CollectorRegistry;
005import io.prometheus.metrics.config.PrometheusProperties;
006import io.prometheus.metrics.model.registry.MultiCollector;
007import io.prometheus.metrics.model.registry.PrometheusRegistry;
008import io.prometheus.metrics.model.snapshots.ClassicHistogramBuckets;
009import io.prometheus.metrics.model.snapshots.CounterSnapshot;
010import io.prometheus.metrics.model.snapshots.Exemplar;
011import io.prometheus.metrics.model.snapshots.Exemplars;
012import io.prometheus.metrics.model.snapshots.GaugeSnapshot;
013import io.prometheus.metrics.model.snapshots.HistogramSnapshot;
014import io.prometheus.metrics.model.snapshots.InfoSnapshot;
015import io.prometheus.metrics.model.snapshots.Labels;
016import io.prometheus.metrics.model.snapshots.MetricSnapshot;
017import io.prometheus.metrics.model.snapshots.MetricSnapshots;
018import io.prometheus.metrics.model.snapshots.Quantile;
019import io.prometheus.metrics.model.snapshots.Quantiles;
020import io.prometheus.metrics.model.snapshots.StateSetSnapshot;
021import io.prometheus.metrics.model.snapshots.SummarySnapshot;
022import io.prometheus.metrics.model.snapshots.Unit;
023import io.prometheus.metrics.model.snapshots.UnknownSnapshot;
024
025import java.util.ArrayList;
026import java.util.Collections;
027import java.util.Enumeration;
028import java.util.HashMap;
029import java.util.List;
030import java.util.Map;
031import java.util.function.Predicate;
032
033/**
034 * Bridge from {@code simpleclient} (version 0.16.0 and older) to the new {@code prometheus-metrics} (version 1.0.0 and newer).
035 * <p>
036 * Usage: The following line will register all metrics from a {@code simpleclient} {@link CollectorRegistry#defaultRegistry}
037 * to a {@code prometheus-metrics} {@link PrometheusRegistry#defaultRegistry}:
038 * <pre>{@code
039 * SimpleclientCollector.builder().register();
040 * }</pre>
041 * <p>
042 * If you have custom registries (not the default registries), use the following snippet:
043 * <pre>{@code
044 * CollectorRegistry simpleclientRegistry = ...;
045 * PrometheusRegistry prometheusRegistry = ...;
046 * SimpleclientCollector.builder()
047 *     .collectorRegistry(simpleclientRegistry)
048 *     .register(prometheusRegistry);
049 * }</pre>
050 */
051public class SimpleclientCollector implements MultiCollector {
052
053    private final CollectorRegistry simpleclientRegistry;
054
055    private SimpleclientCollector(CollectorRegistry simpleclientRegistry) {
056        this.simpleclientRegistry = simpleclientRegistry;
057    }
058
059    @Override
060    public MetricSnapshots collect() {
061        return convert(simpleclientRegistry.metricFamilySamples());
062    }
063
064    @Override
065    public MetricSnapshots collect(Predicate<String> includedNames) {
066        return MultiCollector.super.collect(includedNames);
067    }
068
069    @Override
070    public List<String> getPrometheusNames() {
071        return MultiCollector.super.getPrometheusNames();
072    }
073
074    private MetricSnapshots convert(Enumeration<Collector.MetricFamilySamples> samples) {
075        MetricSnapshots.Builder result = MetricSnapshots.builder();
076        while (samples.hasMoreElements()) {
077            Collector.MetricFamilySamples sample = samples.nextElement();
078            switch (sample.type) {
079                case COUNTER:
080                    result.metricSnapshot(convertCounter(sample));
081                    break;
082                case GAUGE:
083                    result.metricSnapshot(convertGauge(sample));
084                    break;
085                case HISTOGRAM:
086                    result.metricSnapshot(convertHistogram(sample, false));
087                    break;
088                case GAUGE_HISTOGRAM:
089                    result.metricSnapshot(convertHistogram(sample, true));
090                    break;
091                case SUMMARY:
092                    result.metricSnapshot(convertSummary(sample));
093                    break;
094                case INFO:
095                    result.metricSnapshot(convertInfo(sample));
096                    break;
097                case STATE_SET:
098                    result.metricSnapshot(convertStateSet(sample));
099                    break;
100                case UNKNOWN:
101                    result.metricSnapshot(convertUnknown(sample));
102                    break;
103                default:
104                    throw new IllegalStateException(sample.type + ": Unexpected metric type");
105            }
106        }
107        return result.build();
108    }
109
110    private MetricSnapshot convertCounter(Collector.MetricFamilySamples samples) {
111        CounterSnapshot.Builder counter = CounterSnapshot.builder()
112                .name(stripSuffix(samples.name, "_total"))
113                .help(samples.help)
114                .unit(convertUnit(samples));
115        Map<Labels, CounterSnapshot.CounterDataPointSnapshot.Builder> dataPoints = new HashMap<>();
116        for (Collector.MetricFamilySamples.Sample sample : samples.samples) {
117            Labels labels = Labels.of(sample.labelNames, sample.labelValues);
118            CounterSnapshot.CounterDataPointSnapshot.Builder dataPoint = dataPoints.computeIfAbsent(labels, l -> CounterSnapshot.CounterDataPointSnapshot.builder().labels(labels));
119            if (sample.name.endsWith("_created")) {
120                dataPoint.createdTimestampMillis((long) Unit.secondsToMillis(sample.value));
121            } else {
122                dataPoint.value(sample.value).exemplar(convertExemplar(sample.exemplar));
123                if (sample.timestampMs != null) {
124                    dataPoint.scrapeTimestampMillis(sample.timestampMs);
125                }
126            }
127        }
128        for (CounterSnapshot.CounterDataPointSnapshot.Builder dataPoint : dataPoints.values()) {
129            counter.dataPoint(dataPoint.build());
130        }
131        return counter.build();
132    }
133
134    private MetricSnapshot convertGauge(Collector.MetricFamilySamples samples) {
135        GaugeSnapshot.Builder gauge = GaugeSnapshot.builder()
136                .name(samples.name)
137                .help(samples.help)
138                .unit(convertUnit(samples));
139        for (Collector.MetricFamilySamples.Sample sample : samples.samples) {
140            GaugeSnapshot.GaugeDataPointSnapshot.Builder dataPoint = GaugeSnapshot.GaugeDataPointSnapshot.builder()
141                    .value(sample.value)
142                    .labels(Labels.of(sample.labelNames, sample.labelValues))
143                    .exemplar(convertExemplar(sample.exemplar));
144            if (sample.timestampMs != null) {
145                dataPoint.scrapeTimestampMillis(sample.timestampMs);
146            }
147            gauge.dataPoint(dataPoint.build());
148        }
149        return gauge.build();
150    }
151
152    private MetricSnapshot convertHistogram(Collector.MetricFamilySamples samples, boolean isGaugeHistogram) {
153        HistogramSnapshot.Builder histogram = HistogramSnapshot.builder()
154                .name(samples.name)
155                .help(samples.help)
156                .unit(convertUnit(samples))
157                .gaugeHistogram(isGaugeHistogram);
158        Map<Labels, HistogramSnapshot.HistogramDataPointSnapshot.Builder> dataPoints = new HashMap<>();
159        Map<Labels, Map<Double, Long>> cumulativeBuckets = new HashMap<>();
160        Map<Labels, Exemplars.Builder> exemplars = new HashMap<>();
161        for (Collector.MetricFamilySamples.Sample sample : samples.samples) {
162            Labels labels = labelsWithout(sample, "le");
163            dataPoints.computeIfAbsent(labels, l -> HistogramSnapshot.HistogramDataPointSnapshot.builder()
164                    .labels(labels));
165            cumulativeBuckets.computeIfAbsent(labels, l -> new HashMap<>());
166            exemplars.computeIfAbsent(labels, l -> Exemplars.builder());
167            if (sample.name.endsWith("_sum")) {
168                dataPoints.get(labels).sum(sample.value);
169            }
170            if (sample.name.endsWith("_bucket")) {
171                addBucket(cumulativeBuckets.get(labels), sample);
172            }
173            if (sample.name.endsWith("_created")) {
174                dataPoints.get(labels).createdTimestampMillis((long) Unit.secondsToMillis(sample.value));
175            }
176            if (sample.exemplar != null) {
177                exemplars.get(labels).exemplar(convertExemplar(sample.exemplar));
178            }
179            if (sample.timestampMs != null) {
180                dataPoints.get(labels).scrapeTimestampMillis(sample.timestampMs);
181            }
182        }
183        for (Labels labels : dataPoints.keySet()) {
184            histogram.dataPoint(dataPoints.get(labels)
185                    .classicHistogramBuckets(makeBuckets(cumulativeBuckets.get(labels)))
186                    .exemplars(exemplars.get(labels).build())
187                    .build());
188        }
189        return histogram.build();
190    }
191
192    private MetricSnapshot convertSummary(Collector.MetricFamilySamples samples) {
193        SummarySnapshot.Builder summary = SummarySnapshot.builder()
194                .name(samples.name)
195                .help(samples.help)
196                .unit(convertUnit(samples));
197        Map<Labels, SummarySnapshot.SummaryDataPointSnapshot.Builder> dataPoints = new HashMap<>();
198        Map<Labels, Quantiles.Builder> quantiles = new HashMap<>();
199        Map<Labels, Exemplars.Builder> exemplars = new HashMap<>();
200        for (Collector.MetricFamilySamples.Sample sample : samples.samples) {
201            Labels labels = labelsWithout(sample, "quantile");
202            dataPoints.computeIfAbsent(labels, l -> SummarySnapshot.SummaryDataPointSnapshot.builder()
203                    .labels(labels));
204            quantiles.computeIfAbsent(labels, l -> Quantiles.builder());
205            exemplars.computeIfAbsent(labels, l -> Exemplars.builder());
206            if (sample.name.endsWith("_sum")) {
207                dataPoints.get(labels).sum(sample.value);
208            } else if (sample.name.endsWith("_count")) {
209                dataPoints.get(labels).count((long) sample.value);
210            } else if (sample.name.endsWith("_created")) {
211                dataPoints.get(labels).createdTimestampMillis((long) Unit.secondsToMillis(sample.value));
212            } else {
213                for (int i=0; i<sample.labelNames.size(); i++) {
214                    if (sample.labelNames.get(i).equals("quantile")) {
215                        quantiles.get(labels).quantile(new Quantile(Double.parseDouble(sample.labelValues.get(i)), sample.value));
216                        break;
217                    }
218                }
219            }
220            if (sample.exemplar != null) {
221                exemplars.get(labels).exemplar(convertExemplar(sample.exemplar));
222            }
223            if (sample.timestampMs != null) {
224                dataPoints.get(labels).scrapeTimestampMillis(sample.timestampMs);
225            }
226        }
227        for (Labels labels : dataPoints.keySet()) {
228            summary.dataPoint(dataPoints.get(labels)
229                    .quantiles(quantiles.get(labels).build())
230                    .exemplars(exemplars.get(labels).build())
231                    .build());
232        }
233        return summary.build();
234    }
235
236    private MetricSnapshot convertStateSet(Collector.MetricFamilySamples samples) {
237        StateSetSnapshot.Builder stateSet = StateSetSnapshot.builder()
238                .name(samples.name)
239                .help(samples.help);
240        Map<Labels, StateSetSnapshot.StateSetDataPointSnapshot.Builder> dataPoints = new HashMap<>();
241        for (Collector.MetricFamilySamples.Sample sample : samples.samples) {
242            Labels labels = labelsWithout(sample, sample.name);
243            dataPoints.computeIfAbsent(labels, l -> StateSetSnapshot.StateSetDataPointSnapshot.builder().labels(labels));
244            String stateName = null;
245            for (int i=0; i<sample.labelNames.size(); i++) {
246                if (sample.labelNames.get(i).equals(sample.name)) {
247                    stateName = sample.labelValues.get(i);
248                    break;
249                }
250            }
251            if (stateName == null) {
252                throw new IllegalStateException("Invalid StateSet metric: No state name found.");
253            }
254            dataPoints.get(labels).state(stateName, sample.value == 1.0);
255            if (sample.timestampMs != null) {
256                dataPoints.get(labels).scrapeTimestampMillis(sample.timestampMs);
257            }
258        }
259        for (StateSetSnapshot.StateSetDataPointSnapshot.Builder dataPoint : dataPoints.values()) {
260            stateSet.dataPoint(dataPoint.build());
261        }
262        return stateSet.build();
263    }
264
265    private MetricSnapshot convertUnknown(Collector.MetricFamilySamples samples) {
266        UnknownSnapshot.Builder unknown = UnknownSnapshot.builder()
267                .name(samples.name)
268                .help(samples.help)
269                .unit(convertUnit(samples));
270        for (Collector.MetricFamilySamples.Sample sample : samples.samples) {
271            UnknownSnapshot.UnknownDataPointSnapshot.Builder dataPoint = UnknownSnapshot.UnknownDataPointSnapshot.builder()
272                    .value(sample.value)
273                    .labels(Labels.of(sample.labelNames, sample.labelValues))
274                    .exemplar(convertExemplar(sample.exemplar));
275            if (sample.timestampMs != null) {
276                dataPoint.scrapeTimestampMillis(sample.timestampMs);
277            }
278            unknown.dataPoint(dataPoint.build());
279        }
280        return unknown.build();
281    }
282
283    private String stripSuffix(String name, String suffix) {
284        if (name.endsWith(suffix)) {
285            return name.substring(0, name.length() - suffix.length());
286        } else {
287            return name;
288        }
289    }
290
291    private Unit convertUnit(Collector.MetricFamilySamples samples) {
292        if (samples.unit != null && !samples.unit.isEmpty()) {
293            return new Unit(samples.unit);
294        } else {
295            return null;
296        }
297    }
298
299    private ClassicHistogramBuckets makeBuckets(Map<Double, Long> cumulativeBuckets) {
300        List<Double> upperBounds = new ArrayList<>(cumulativeBuckets.size());
301        Collections.sort(upperBounds);
302        upperBounds.addAll(cumulativeBuckets.keySet());
303        ClassicHistogramBuckets.Builder result = ClassicHistogramBuckets.builder();
304        long previousCount = 0L;
305        for (Double upperBound : upperBounds) {
306            long cumulativeCount = cumulativeBuckets.get(upperBound);
307            result.bucket(upperBound, cumulativeCount - previousCount);
308            previousCount = cumulativeCount;
309        }
310        return result.build();
311    }
312
313    private void addBucket(Map<Double, Long> buckets, Collector.MetricFamilySamples.Sample sample) {
314        for (int i = 0; i < sample.labelNames.size(); i++) {
315            if (sample.labelNames.get(i).equals("le")) {
316                double upperBound;
317                switch (sample.labelValues.get(i)) {
318                    case "+Inf":
319                        upperBound = Double.POSITIVE_INFINITY;
320                        break;
321                    case "-Inf": // Doesn't make sense as count would always be zero. Catch this anyway.
322                        upperBound = Double.NEGATIVE_INFINITY;
323                        break;
324                    default:
325                        upperBound = Double.parseDouble(sample.labelValues.get(i));
326                }
327                buckets.put(upperBound, (long) sample.value);
328                return;
329            }
330        }
331        throw new IllegalStateException(sample.name + " does not have a le label.");
332    }
333
334
335    private Labels labelsWithout(Collector.MetricFamilySamples.Sample sample, String excludedLabelName) {
336        Labels.Builder labels = Labels.builder();
337        for (int i = 0; i < sample.labelNames.size(); i++) {
338            if (!sample.labelNames.get(i).equals(excludedLabelName)) {
339                labels.label(sample.labelNames.get(i), sample.labelValues.get(i));
340            }
341        }
342        return labels.build();
343    }
344
345    private MetricSnapshot convertInfo(Collector.MetricFamilySamples samples) {
346        InfoSnapshot.Builder info = InfoSnapshot.builder()
347                .name(stripSuffix(samples.name, "_info"))
348                .help(samples.help);
349        for (Collector.MetricFamilySamples.Sample sample : samples.samples) {
350            info.dataPoint(InfoSnapshot.InfoDataPointSnapshot.builder()
351                    .labels(Labels.of(sample.labelNames, sample.labelValues))
352                    .build());
353        }
354        return info.build();
355    }
356
357    private Exemplar convertExemplar(io.prometheus.client.exemplars.Exemplar exemplar) {
358        if (exemplar == null) {
359            return null;
360        }
361        Exemplar.Builder result = Exemplar.builder().value(exemplar.getValue());
362        if (exemplar.getTimestampMs() != null) {
363            result.timestampMillis(exemplar.getTimestampMs());
364        }
365        Labels.Builder labels = Labels.builder();
366        for (int i = 0; i < exemplar.getNumberOfLabels(); i++) {
367            labels.label(exemplar.getLabelName(i), exemplar.getLabelValue(i));
368        }
369        return result.labels(labels.build()).build();
370    }
371
372    /**
373     * Currently there are no configuration options for the SimpleclientCollector.
374     * However, we want to follow the pattern to pass the config everywhere so that
375     * we can introduce config options later without the need for API changes.
376     */
377    public static Builder builder(PrometheusProperties config) {
378        return new Builder(config);
379    }
380
381    public static Builder builder() {
382        return builder(PrometheusProperties.get());
383    }
384
385    public static class Builder {
386
387        private final PrometheusProperties config;
388        private CollectorRegistry collectorRegistry;
389
390        private Builder(PrometheusProperties config) {
391            this.config = config;
392        }
393
394        public Builder collectorRegistry(CollectorRegistry registry) {
395            this.collectorRegistry = registry;
396            return this;
397        }
398
399        public SimpleclientCollector build() {
400            return collectorRegistry != null ? new SimpleclientCollector(collectorRegistry) : new SimpleclientCollector(CollectorRegistry.defaultRegistry);
401        }
402
403        public SimpleclientCollector register() {
404            return register(PrometheusRegistry.defaultRegistry);
405        }
406
407        public SimpleclientCollector register(PrometheusRegistry registry) {
408            SimpleclientCollector result = build();
409            registry.register(result);
410            return result;
411        }
412    }
413}