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