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