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