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