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}