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