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