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}