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