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