diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index f384954afe..a87be7796d 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -61,7 +61,7 @@ newrelic-api = "5.14.0" # Kotlin 1.7 sample will fail from OkHttp 4.12.0 due to okio dependency being a Kotlin 1.9 module okhttp = "4.12.0" postgre = "42.7.11" -prometheus = "1.5.1" +prometheus = "1.7.0" prometheusSimpleClient = "0.16.0" reactor = "2022.0.22" rest-assured = "5.5.7" diff --git a/implementations/micrometer-registry-prometheus/src/main/java/io/micrometer/prometheusmetrics/MicrometerCollector.java b/implementations/micrometer-registry-prometheus/src/main/java/io/micrometer/prometheusmetrics/MicrometerCollector.java index 0a04cc9468..dc3762defd 100644 --- a/implementations/micrometer-registry-prometheus/src/main/java/io/micrometer/prometheusmetrics/MicrometerCollector.java +++ b/implementations/micrometer-registry-prometheus/src/main/java/io/micrometer/prometheusmetrics/MicrometerCollector.java @@ -16,13 +16,24 @@ package io.micrometer.prometheusmetrics; import io.micrometer.core.instrument.Meter; +import io.prometheus.metrics.model.registry.MetricType; import io.prometheus.metrics.model.registry.MultiCollector; import io.prometheus.metrics.model.snapshots.DataPointSnapshot; +import io.prometheus.metrics.model.snapshots.MetricFamilyDescriptor; import io.prometheus.metrics.model.snapshots.MetricMetadata; import io.prometheus.metrics.model.snapshots.MetricSnapshot; import io.prometheus.metrics.model.snapshots.MetricSnapshots; - -import java.util.*; +import org.jspecify.annotations.Nullable; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collection; +import java.util.Collections; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; import java.util.concurrent.ConcurrentHashMap; import java.util.function.Function; import java.util.stream.Stream; @@ -40,6 +51,8 @@ class MicrometerCollector implements MultiCollector { private final Map children = new ConcurrentHashMap<>(); + private final Map> registeredFamilies = new ConcurrentHashMap<>(); + private final String conventionName; // the id of the meter used to create this MicrometerCollector @@ -52,12 +65,14 @@ class MicrometerCollector implements MultiCollector { this.originalMeterId = id; } - public void add(Meter.Id id, Child child) { + public void add(Meter.Id id, Child child, RegisteredFamily... registeredFamilies) { children.put(id, child); + this.registeredFamilies.put(id, Arrays.asList(registeredFamilies)); } public void remove(Meter.Id id) { children.remove(id); + registeredFamilies.remove(id); } public boolean isEmpty() { @@ -72,6 +87,42 @@ Meter.Id getOriginalId() { return originalMeterId; } + @Override + public List getPrometheusNames() { + return registrationFamilies().stream().map(RegisteredFamily::getPrometheusName).distinct().collect(toList()); + } + + @Override + public @Nullable MetricType getMetricType(String prometheusName) { + for (RegisteredFamily family : registrationFamilies()) { + if (family.getPrometheusName().equals(prometheusName)) { + return family.getMetricType(); + } + } + return null; + } + + @Override + public @Nullable Set getLabelNames(String prometheusName) { + Set names = new HashSet<>(); + for (RegisteredFamily family : registrationFamilies()) { + if (family.getPrometheusName().equals(prometheusName)) { + names.addAll(family.getLabelNames()); + } + } + return names.isEmpty() ? null : names; + } + + @Override + public @Nullable MetricMetadata getMetadata(String prometheusName) { + for (RegisteredFamily family : registrationFamilies()) { + if (family.getPrometheusName().equals(prometheusName)) { + return family.getMetadata(); + } + } + return null; + } + @Override public MetricSnapshots collect() { Map families = new HashMap<>(); @@ -91,27 +142,54 @@ public MetricSnapshots collect() { return new MetricSnapshots(metricSnapshots); } + private List registrationFamilies() { + return registeredFamilies.values().stream().flatMap(Collection::stream).collect(toList()); + } + interface Child { Stream> samples(String conventionName); } + static final class RegisteredFamily { + + private final MetricFamilyDescriptor descriptor; + + RegisteredFamily(MetricFamilyDescriptor descriptor) { + this.descriptor = descriptor; + } + + String getPrometheusName() { + return descriptor.getPrometheusName(); + } + + MetricType getMetricType() { + return descriptor.getType(); + } + + Set getLabelNames() { + return descriptor.getLabelNames(); + } + + MetricMetadata getMetadata() { + return descriptor.getMetadata(); + } + + } + static class Family { final String conventionName; - final MetricMetadata metadata; - final List dataPointSnapshots = new ArrayList<>(); final Function, MetricSnapshot> metricSnapshotFactory; Family(String conventionName, Function, MetricSnapshot> metricSnapshotFactory, - MetricMetadata metadata, T... dataPointSnapshots) { + T... dataPointSnapshots) { this.conventionName = conventionName; this.metricSnapshotFactory = metricSnapshotFactory; - this.metadata = metadata; Collections.addAll(this.dataPointSnapshots, dataPointSnapshots); } diff --git a/implementations/micrometer-registry-prometheus/src/main/java/io/micrometer/prometheusmetrics/PrometheusMeterRegistry.java b/implementations/micrometer-registry-prometheus/src/main/java/io/micrometer/prometheusmetrics/PrometheusMeterRegistry.java index c2f103baea..8799ce4a6d 100644 --- a/implementations/micrometer-registry-prometheus/src/main/java/io/micrometer/prometheusmetrics/PrometheusMeterRegistry.java +++ b/implementations/micrometer-registry-prometheus/src/main/java/io/micrometer/prometheusmetrics/PrometheusMeterRegistry.java @@ -29,6 +29,7 @@ import io.prometheus.metrics.config.PrometheusProperties; import io.prometheus.metrics.config.PrometheusPropertiesLoader; import io.prometheus.metrics.expositionformats.ExpositionFormats; +import io.prometheus.metrics.model.registry.MetricType; import io.prometheus.metrics.model.registry.PrometheusRegistry; import io.prometheus.metrics.model.snapshots.*; import io.prometheus.metrics.model.snapshots.CounterSnapshot.CounterDataPointSnapshot; @@ -44,6 +45,7 @@ import java.io.OutputStream; import java.nio.charset.StandardCharsets; import java.util.ArrayList; +import java.util.Collection; import java.util.List; import java.util.Set; import java.util.concurrent.ConcurrentHashMap; @@ -217,10 +219,12 @@ public Counter newCounter(Meter.Id id) { applyToCollector(id, (collector) -> { List tagValues = tagValues(id); List tagKeys = tagKeys(id); - collector.add(id, (conventionName) -> Stream.of(new MicrometerCollector.Family<>(conventionName, - family -> new CounterSnapshot(family.metadata, family.dataPointSnapshots), - getMetadata(conventionName, id.getDescription()), new CounterDataPointSnapshot(counter.count(), - Labels.of(tagKeys, tagValues), counter.exemplar(), createdTimestampMillis)))); + String familyName = counterMetricName(conventionName(id), id.getName()); + collector.add(id, (conventionName) -> Stream.of(new MicrometerCollector.Family<>(familyName, + family -> counterSnapshot(family, family.getConventionName(), helpText(id.getDescription())), + new CounterDataPointSnapshot(counter.count(), Labels.of(tagKeys, tagValues), counter.exemplar(), + createdTimestampMillis))), + registeredFamily(MetricType.COUNTER, familyName, tagKeys, id.getDescription())); }); return counter; } @@ -234,6 +238,7 @@ public DistributionSummary newDistributionSummary(Meter.Id id, applyToCollector(id, (collector) -> { List tagValues = tagValues(id); List tagKeys = tagKeys(id); + MetricType primaryType = summary.histogramCounts().length == 0 ? MetricType.SUMMARY : MetricType.HISTOGRAM; collector.add(id, (conventionName) -> { Stream.Builder> families = Stream.builder(); @@ -254,9 +259,10 @@ public DistributionSummary newDistributionSummary(Meter.Id id, Exemplars exemplars = summary.exemplars(); families.add(new MicrometerCollector.Family<>(conventionName, - family -> new SummarySnapshot(family.metadata, family.dataPointSnapshots), - getMetadata(conventionName, id.getDescription()), new SummaryDataPointSnapshot(count, sum, - quantiles, Labels.of(tagKeys, tagValues), exemplars, createdTimestampMillis))); + family -> summarySnapshot(family, family.getConventionName(), + helpText(id.getDescription())), + new SummaryDataPointSnapshot(count, sum, quantiles, Labels.of(tagKeys, tagValues), + exemplars, createdTimestampMillis))); } else { List buckets = new ArrayList<>(); @@ -283,9 +289,8 @@ public DistributionSummary newDistributionSummary(Meter.Id id, Exemplars exemplars = summary.exemplars(); families.add(new MicrometerCollector.Family<>(conventionName, - family -> new io.prometheus.metrics.model.snapshots.HistogramSnapshot(family.metadata, - family.dataPointSnapshots), - getMetadata(conventionName, id.getDescription()), + family -> histogramSnapshot(family, family.getConventionName(), + helpText(id.getDescription()), false), new HistogramDataPointSnapshot(ClassicHistogramBuckets.of(buckets, counts), sum, Labels.of(tagKeys, tagValues), exemplars, createdTimestampMillis))); @@ -300,12 +305,12 @@ public DistributionSummary newDistributionSummary(Meter.Id id, } families.add(new MicrometerCollector.Family<>(conventionName + "_max", - family -> new GaugeSnapshot(family.metadata, family.dataPointSnapshots), - getMetadata(conventionName + "_max", id.getDescription()), + family -> gaugeSnapshot(family, family.getConventionName(), helpText(id.getDescription())), new GaugeDataPointSnapshot(summary.max(), Labels.of(tagKeys, tagValues), null))); return families.build(); - }); + }, registeredFamily(primaryType, conventionName(id), tagKeys, id.getDescription()), + registeredFamily(MetricType.GAUGE, conventionName(id) + "_max", tagKeys, id.getDescription())); }); return summary; @@ -329,18 +334,18 @@ protected io.micrometer.core.instrument.Gauge newGauge(Meter.Id id, @Nullabl List tagValues = tagValues(id); List tagKeys = tagKeys(id); if (id.getName().endsWith(".info")) { - collector.add(id, - (conventionName) -> Stream.of(new MicrometerCollector.Family<>(conventionName, - family -> new InfoSnapshot(family.metadata, family.dataPointSnapshots), - getMetadata(conventionName, id.getDescription()), - new InfoDataPointSnapshot(Labels.of(tagKeys, tagValues))))); + String familyName = infoMetricName(conventionName(id), id.getName()); + collector.add(id, (conventionName) -> Stream.of(new MicrometerCollector.Family<>(familyName, + family -> infoSnapshot(family, family.getConventionName(), helpText(id.getDescription())), + new InfoDataPointSnapshot(Labels.of(tagKeys, tagValues)))), + registeredFamily(MetricType.INFO, familyName, tagKeys, id.getDescription())); } else { - collector.add(id, - (conventionName) -> Stream.of(new MicrometerCollector.Family<>(conventionName, - family -> new GaugeSnapshot(family.metadata, family.dataPointSnapshots), - getMetadata(conventionName, id.getDescription()), - new GaugeDataPointSnapshot(gauge.value(), Labels.of(tagKeys, tagValues), null)))); + String familyName = gaugeMetricName(conventionName(id), id.getName()); + collector.add(id, (conventionName) -> Stream.of(new MicrometerCollector.Family<>(familyName, + family -> gaugeSnapshot(family, family.getConventionName(), helpText(id.getDescription())), + new GaugeDataPointSnapshot(gauge.value(), Labels.of(tagKeys, tagValues), null))), + registeredFamily(MetricType.GAUGE, familyName, tagKeys, id.getDescription())); } }); return gauge; @@ -363,12 +368,11 @@ protected FunctionTimer newFunctionTimer(Meter.Id id, T obj, ToLongFunction< applyToCollector(id, (collector) -> { List tagValues = tagValues(id); List tagKeys = tagKeys(id); - collector.add(id, - (conventionName) -> Stream.of(new MicrometerCollector.Family<>(conventionName, - family -> new SummarySnapshot(family.metadata, family.dataPointSnapshots), - getMetadata(conventionName, id.getDescription()), - new SummaryDataPointSnapshot((long) ft.count(), ft.totalTime(getBaseTimeUnit()), - Quantiles.EMPTY, Labels.of(tagKeys, tagValues), null, createdTimestampMillis)))); + collector.add(id, (conventionName) -> Stream.of(new MicrometerCollector.Family<>(conventionName, + family -> summarySnapshot(family, family.getConventionName(), helpText(id.getDescription())), + new SummaryDataPointSnapshot((long) ft.count(), ft.totalTime(getBaseTimeUnit()), Quantiles.EMPTY, + Labels.of(tagKeys, tagValues), null, createdTimestampMillis))), + registeredFamily(MetricType.SUMMARY, conventionName(id), tagKeys, id.getDescription())); }); return ft; } @@ -380,11 +384,12 @@ protected FunctionCounter newFunctionCounter(Meter.Id id, T obj, ToDoubleFun applyToCollector(id, (collector) -> { List tagValues = tagValues(id); List tagKeys = tagKeys(id); - collector.add(id, - (conventionName) -> Stream.of(new MicrometerCollector.Family<>(conventionName, - family -> new CounterSnapshot(family.metadata, family.dataPointSnapshots), - getMetadata(conventionName, id.getDescription()), new CounterDataPointSnapshot(fc.count(), - Labels.of(tagKeys, tagValues), null, createdTimestampMillis)))); + String familyName = counterMetricName(conventionName(id), id.getName()); + collector.add(id, (conventionName) -> Stream.of(new MicrometerCollector.Family<>(familyName, + family -> counterSnapshot(family, family.getConventionName(), helpText(id.getDescription())), + new CounterDataPointSnapshot(fc.count(), Labels.of(tagKeys, tagValues), null, + createdTimestampMillis))), + registeredFamily(MetricType.COUNTER, familyName, tagKeys, id.getDescription())); }); return fc; } @@ -394,66 +399,92 @@ protected Meter newMeter(Meter.Id id, Meter.Type type, Iterable mea applyToCollector(id, (collector) -> { List tagValues = tagValues(id); List tagKeys = tagKeys(id); + List statKeys = new ArrayList<>(tagKeys); + statKeys.add("statistic"); + List registrationDescriptors = new ArrayList<>(); + String name = conventionName(id); + for (Measurement measurement : measurements) { + registrationDescriptors.add(customMeterDescriptor(id, name, measurement.getStatistic(), statKeys)); + } collector.add(id, (conventionName) -> { Stream.Builder> families = Stream.builder(); - List statKeys = new ArrayList<>(tagKeys); - statKeys.add("statistic"); for (Measurement measurement : measurements) { List statValues = new ArrayList<>(tagValues); statValues.add(measurement.getStatistic().toString()); - switch (measurement.getStatistic()) { - case TOTAL: - case TOTAL_TIME: - families.add(customCounterFamily(id, conventionName, "_sum", - Labels.of(statKeys, statValues), measurement.getValue())); - break; - case COUNT: - families.add(customCounterFamily(id, conventionName, "", Labels.of(statKeys, statValues), - measurement.getValue())); - break; - case MAX: - families.add(customGaugeFamily(id, conventionName, "_max", Labels.of(statKeys, statValues), - measurement.getValue())); - break; - case VALUE: - case UNKNOWN: - families.add(customGaugeFamily(id, conventionName, "_value", - Labels.of(statKeys, statValues), measurement.getValue())); - break; - case ACTIVE_TASKS: - families.add(customGaugeFamily(id, conventionName, "_active_count", - Labels.of(statKeys, statValues), measurement.getValue())); - break; - case DURATION: - families.add(customGaugeFamily(id, conventionName, "_duration_sum", - Labels.of(statKeys, statValues), measurement.getValue())); - break; - } + families.add(customMeterFamily(id, conventionName, measurement.getStatistic(), + Labels.of(statKeys, statValues), measurement.getValue())); } return families.build(); - }); + }, registrationDescriptors.toArray(new MicrometerCollector.RegisteredFamily[0])); }); return new DefaultMeter(id, type, measurements); } - private MicrometerCollector.Family customCounterFamily(Meter.Id id, String conventionName, - String suffix, Labels labels, double value) { + private MicrometerCollector.RegisteredFamily customMeterDescriptor(Meter.Id id, String conventionName, + Statistic statistic, Collection labelNames) { + CustomMeterMetric metric = customMeterMetric(conventionName, statistic); + return registeredFamily(metric.metricType, metric.name, labelNames, id.getDescription()); + } + + private MicrometerCollector.Family customMeterFamily(Meter.Id id, String conventionName, Statistic statistic, + Labels labels, double value) { + CustomMeterMetric metric = customMeterMetric(conventionName, statistic); + if (metric.metricType == MetricType.COUNTER) { + return customCounterFamily(id, metric, labels, value); + } + return customGaugeFamily(id, metric, labels, value); + } + + private CustomMeterMetric customMeterMetric(String conventionName, Statistic statistic) { + switch (statistic) { + case TOTAL: + case TOTAL_TIME: + return new CustomMeterMetric(conventionName + "_sum", MetricType.COUNTER); + case COUNT: + return new CustomMeterMetric(conventionName, MetricType.COUNTER); + case MAX: + return new CustomMeterMetric(conventionName + "_max", MetricType.GAUGE); + case VALUE: + case UNKNOWN: + return new CustomMeterMetric(conventionName + "_value", MetricType.GAUGE); + case ACTIVE_TASKS: + return new CustomMeterMetric(conventionName + "_active_count", MetricType.GAUGE); + case DURATION: + return new CustomMeterMetric(conventionName + "_duration_sum", MetricType.GAUGE); + default: + throw new IllegalArgumentException("Unsupported meter statistic: " + statistic); + } + } + + private MicrometerCollector.Family customCounterFamily(Meter.Id id, + CustomMeterMetric metric, Labels labels, double value) { long createdTimestampMillis = clock.wallTime(); - return new MicrometerCollector.Family<>(conventionName + suffix, - family -> new CounterSnapshot(family.metadata, family.dataPointSnapshots), - getMetadata(conventionName + suffix, id.getDescription()), + return new MicrometerCollector.Family<>(metric.name, + family -> counterSnapshot(family, metric.name, helpText(id.getDescription())), new CounterDataPointSnapshot(value, labels, null, createdTimestampMillis)); } - private MicrometerCollector.Family customGaugeFamily(Meter.Id id, String conventionName, - String suffix, Labels labels, double value) { - return new MicrometerCollector.Family<>(conventionName + suffix, - family -> new GaugeSnapshot(family.metadata, family.dataPointSnapshots), - getMetadata(conventionName + suffix, id.getDescription()), + private MicrometerCollector.Family customGaugeFamily(Meter.Id id, CustomMeterMetric metric, + Labels labels, double value) { + return new MicrometerCollector.Family<>(metric.name, + family -> gaugeSnapshot(family, metric.name, helpText(id.getDescription())), new GaugeDataPointSnapshot(value, labels, null)); } + private static class CustomMeterMetric { + + private final String name; + + private final MetricType metricType; + + private CustomMeterMetric(String name, MetricType metricType) { + this.name = name; + this.metricType = metricType; + } + + } + @Override protected TimeUnit getBaseTimeUnit() { return SECONDS; @@ -471,6 +502,8 @@ private void addDistributionStatisticSamples(Meter.Id id, MicrometerCollector co boolean forLongTaskTimer) { long createdTimestampMillis = clock.wallTime(); List tagKeys = tagKeys(id); + MetricType primaryType = histogramSupport.takeSnapshot().histogramCounts().length == 0 ? MetricType.SUMMARY + : MetricType.HISTOGRAM; collector.add(id, (conventionName) -> { Stream.Builder> families = Stream.builder(); @@ -492,9 +525,9 @@ private void addDistributionStatisticSamples(Meter.Id id, MicrometerCollector co Exemplars exemplars = createExemplarsWithScaledValues(exemplarsSupplier.get()); families.add(new MicrometerCollector.Family<>(conventionName, - family -> new SummarySnapshot(family.metadata, family.dataPointSnapshots), - getMetadata(conventionName, id.getDescription()), new SummaryDataPointSnapshot(count, sum, - quantiles, Labels.of(tagKeys, tagValues), exemplars, createdTimestampMillis))); + family -> summarySnapshot(family, family.getConventionName(), helpText(id.getDescription())), + new SummaryDataPointSnapshot(count, sum, quantiles, Labels.of(tagKeys, tagValues), exemplars, + createdTimestampMillis))); } else { List buckets = new ArrayList<>(); @@ -521,9 +554,8 @@ private void addDistributionStatisticSamples(Meter.Id id, MicrometerCollector co Exemplars exemplars = createExemplarsWithScaledValues(exemplarsSupplier.get()); families.add(new MicrometerCollector.Family<>(conventionName, - family -> new io.prometheus.metrics.model.snapshots.HistogramSnapshot(forLongTaskTimer, - family.metadata, family.dataPointSnapshots), - getMetadata(conventionName, id.getDescription()), + family -> histogramSnapshot(family, family.getConventionName(), helpText(id.getDescription()), + forLongTaskTimer), new HistogramDataPointSnapshot(ClassicHistogramBuckets.of(buckets, counts), sum, Labels.of(tagKeys, tagValues), exemplars, createdTimestampMillis))); @@ -538,12 +570,29 @@ private void addDistributionStatisticSamples(Meter.Id id, MicrometerCollector co } families.add(new MicrometerCollector.Family<>(conventionName + "_max", - family -> new GaugeSnapshot(family.metadata, family.dataPointSnapshots), - getMetadata(conventionName + "_max", id.getDescription()), new GaugeDataPointSnapshot( - histogramSnapshot.max(getBaseTimeUnit()), Labels.of(tagKeys, tagValues), null))); + family -> gaugeSnapshot(family, family.getConventionName(), helpText(id.getDescription())), + new GaugeDataPointSnapshot(histogramSnapshot.max(getBaseTimeUnit()), Labels.of(tagKeys, tagValues), + null))); return families.build(); - }); + }, registeredFamily(primaryType, conventionName(id), tagKeys, id.getDescription()), + registeredFamily(MetricType.GAUGE, conventionName(id) + "_max", tagKeys, id.getDescription())); + } + + private String conventionName(Meter.Id id) { + return getConventionName(id); + } + + private MicrometerCollector.RegisteredFamily registeredFamily(MetricType metricType, String name, + Collection labelNames, @Nullable String description) { + MetricFamilyDescriptor.Builder builder = MetricFamilyDescriptor.of(metricType, name) + .help(helpText(description)) + .labelNames(labelNames); + return new MicrometerCollector.RegisteredFamily(builder.build()); + } + + private String helpText(@Nullable String description) { + return prometheusConfig.descriptions() && description != null ? description : " "; } private Exemplars createExemplarsWithScaledValues(Exemplars exemplars) { @@ -572,11 +621,71 @@ private void onMeterRemoved(Meter meter) { } } - private MetricMetadata getMetadata(String name, @Nullable String description) { - String help = prometheusConfig.descriptions() && description != null ? description : " "; - // Unit is intentionally not set, see: - // https://github.com/OpenObservability/OpenMetrics/blob/1386544931307dff279688f332890c31b6c5de36/specification/OpenMetrics.md#unit - return new MetricMetadata(name, help, null); + private CounterSnapshot counterSnapshot(MicrometerCollector.Family family, String name, + String help) { + CounterSnapshot.Builder builder = CounterSnapshot.builder().name(name).help(help); + family.dataPointSnapshots.forEach(builder::dataPoint); + return builder.build(); + } + + private GaugeSnapshot gaugeSnapshot(MicrometerCollector.Family family, String name, + String help) { + GaugeSnapshot.Builder builder = GaugeSnapshot.builder().name(name).help(help); + family.dataPointSnapshots.forEach(builder::dataPoint); + return builder.build(); + } + + private InfoSnapshot infoSnapshot(MicrometerCollector.Family family, String name, + String help) { + InfoSnapshot.Builder builder = InfoSnapshot.builder().name(name).help(help); + family.dataPointSnapshots.forEach(builder::dataPoint); + return builder.build(); + } + + private SummarySnapshot summarySnapshot(MicrometerCollector.Family family, String name, + String help) { + SummarySnapshot.Builder builder = SummarySnapshot.builder().name(name).help(help); + family.dataPointSnapshots.forEach(builder::dataPoint); + return builder.build(); + } + + private io.prometheus.metrics.model.snapshots.HistogramSnapshot histogramSnapshot( + MicrometerCollector.Family family, String name, String help, + boolean gaugeHistogram) { + io.prometheus.metrics.model.snapshots.HistogramSnapshot.Builder builder = io.prometheus.metrics.model.snapshots.HistogramSnapshot + .builder() + .name(name) + .help(help) + .gaugeHistogram(gaugeHistogram); + family.dataPointSnapshots.forEach(builder::dataPoint); + return builder.build(); + } + + private String counterMetricName(String prometheusName, String originalName) { + return normalizeBaseName(prometheusName, originalName, ".bucket", "_bucket", ".created", "_created", ".info", + "_info", ".total", "_total"); + } + + private String infoMetricName(String prometheusName, String originalName) { + return normalizeBaseName(prometheusName, originalName, ".info", "_info"); + } + + private String gaugeMetricName(String prometheusName, String originalName) { + return normalizeBaseName(prometheusName, originalName, ".bucket", "_bucket", ".created", "_created", ".total", + "_total"); + } + + private static String normalizeBaseName(String name, String originalName, String... suffixMappings) { + for (int i = 0; i < suffixMappings.length; i += 2) { + if (originalName.endsWith(suffixMappings[i])) { + return stripSuffix(name, suffixMappings[i + 1]); + } + } + return name; + } + + private static String stripSuffix(String name, String suffix) { + return name.endsWith(suffix) ? name.substring(0, name.length() - suffix.length()) : name; } private void applyToCollector(Meter.Id id, Consumer consumer) { @@ -584,8 +693,14 @@ private void applyToCollector(Meter.Id id, Consumer consume if (existingCollector == null) { MicrometerCollector micrometerCollector = new MicrometerCollector(name, id); consumer.accept(micrometerCollector); - registry.register(micrometerCollector); - return micrometerCollector; + try { + registry.register(micrometerCollector); + return micrometerCollector; + } + catch (IllegalArgumentException e) { + meterRegistrationFailed(id, e.getMessage()); + return null; + } } if (!existingCollector.getOriginalId().getName().equals(id.getName())) { diff --git a/implementations/micrometer-registry-prometheus/src/test/java/io/micrometer/prometheusmetrics/MicrometerCollectorTest.java b/implementations/micrometer-registry-prometheus/src/test/java/io/micrometer/prometheusmetrics/MicrometerCollectorTest.java index e8fa44ffc0..904a9d70b8 100644 --- a/implementations/micrometer-registry-prometheus/src/test/java/io/micrometer/prometheusmetrics/MicrometerCollectorTest.java +++ b/implementations/micrometer-registry-prometheus/src/test/java/io/micrometer/prometheusmetrics/MicrometerCollectorTest.java @@ -21,9 +21,10 @@ import io.micrometer.core.instrument.Tag; import io.micrometer.core.instrument.Tags; import io.micrometer.core.instrument.config.NamingConvention; +import io.prometheus.metrics.model.registry.MetricType; import io.prometheus.metrics.model.snapshots.CounterSnapshot; import io.prometheus.metrics.model.snapshots.Labels; -import io.prometheus.metrics.model.snapshots.MetricMetadata; +import io.prometheus.metrics.model.snapshots.MetricFamilyDescriptor; import org.junit.jupiter.api.Test; import java.util.stream.Stream; @@ -46,10 +47,11 @@ void manyTags() { CounterSnapshot.CounterDataPointSnapshot sample = new CounterSnapshot.CounterDataPointSnapshot(1.0, Labels.of("k", Integer.toString(i)), null, 0); - collector.add(id, - (conventionName) -> Stream.of(new MicrometerCollector.Family<>(conventionName, - family -> new CounterSnapshot(family.metadata, family.dataPointSnapshots), - new MetricMetadata(conventionName), sample))); + collector.add(id, (conventionName) -> Stream.of(new MicrometerCollector.Family<>(conventionName, family -> { + CounterSnapshot.Builder builder = CounterSnapshot.builder().name(family.getConventionName()); + family.dataPointSnapshots.forEach(builder::dataPoint); + return builder.build(); + }, sample))); } // Threw StackOverflowException because of too many nested streams originally @@ -69,13 +71,17 @@ void sameValuesDifferentOrder() { Meter.Id sample2Id = id.withTags(Tags.of("k", "v2", "k2", "v1")); collector.add(sampleId, - (conventionName) -> Stream.of(new MicrometerCollector.Family<>(conventionName, - family -> new CounterSnapshot(family.metadata, family.dataPointSnapshots), - new MetricMetadata(conventionName), sample))); + (conventionName) -> Stream.of(new MicrometerCollector.Family<>(conventionName, family -> { + CounterSnapshot.Builder builder = CounterSnapshot.builder().name(family.getConventionName()); + family.dataPointSnapshots.forEach(builder::dataPoint); + return builder.build(); + }, sample))); collector.add(sample2Id, - (conventionName) -> Stream.of(new MicrometerCollector.Family<>(conventionName, - family -> new CounterSnapshot(family.metadata, family.dataPointSnapshots), - new MetricMetadata(conventionName), sample2))); + (conventionName) -> Stream.of(new MicrometerCollector.Family<>(conventionName, family -> { + CounterSnapshot.Builder builder = CounterSnapshot.builder().name(family.getConventionName()); + family.dataPointSnapshots.forEach(builder::dataPoint); + return builder.build(); + }, sample2))); assertThat(collector.collect().get(0).getDataPoints()).hasSize(2); } @@ -92,14 +98,16 @@ void sameMetricDifferentTagKeysCounter() { Labels.of(asList("k1", "k4"), asList("v1", "v4")), null, 0); Meter.Id id2 = id.replaceTags(Tags.of("k1", "v1", "k4", "v4")); - collector.add(id, - (conventionName) -> Stream.of(new MicrometerCollector.Family<>(conventionName, - family -> new CounterSnapshot(family.metadata, family.dataPointSnapshots), - new MetricMetadata(conventionName), sample))); - collector.add(id2, - (conventionName) -> Stream.of(new MicrometerCollector.Family<>(conventionName, - family -> new CounterSnapshot(family.metadata, family.dataPointSnapshots), - new MetricMetadata(conventionName), sample2))); + collector.add(id, (conventionName) -> Stream.of(new MicrometerCollector.Family<>(conventionName, family -> { + CounterSnapshot.Builder builder = CounterSnapshot.builder().name(family.getConventionName()); + family.dataPointSnapshots.forEach(builder::dataPoint); + return builder.build(); + }, sample))); + collector.add(id2, (conventionName) -> Stream.of(new MicrometerCollector.Family<>(conventionName, family -> { + CounterSnapshot.Builder builder = CounterSnapshot.builder().name(family.getConventionName()); + family.dataPointSnapshots.forEach(builder::dataPoint); + return builder.build(); + }, sample2))); assertThat(collector.collect().get(0).getDataPoints()).hasSize(2); } @@ -116,14 +124,16 @@ void oneSampleHasSubsetOfTagKeysOfAnotherSample() { Labels.of(asList("k1", "k2"), asList("v1", "v2")), null, 0); Meter.Id id2 = id.replaceTags(Tags.of("k1", "v1", "k2", "v2")); - collector.add(id, - (conventionName) -> Stream.of(new MicrometerCollector.Family<>(conventionName, - family -> new CounterSnapshot(family.metadata, family.dataPointSnapshots), - new MetricMetadata(conventionName), sample))); - collector.add(id2, - (conventionName) -> Stream.of(new MicrometerCollector.Family<>(conventionName, - family -> new CounterSnapshot(family.metadata, family.dataPointSnapshots), - new MetricMetadata(conventionName), sample2))); + collector.add(id, (conventionName) -> Stream.of(new MicrometerCollector.Family<>(conventionName, family -> { + CounterSnapshot.Builder builder = CounterSnapshot.builder().name(family.getConventionName()); + family.dataPointSnapshots.forEach(builder::dataPoint); + return builder.build(); + }, sample))); + collector.add(id2, (conventionName) -> Stream.of(new MicrometerCollector.Family<>(conventionName, family -> { + CounterSnapshot.Builder builder = CounterSnapshot.builder().name(family.getConventionName()); + family.dataPointSnapshots.forEach(builder::dataPoint); + return builder.build(); + }, sample2))); assertThat(collector.collect().get(0).getDataPoints()).hasSize(2); } @@ -140,16 +150,39 @@ void sameMetricNameWithNoTagsAndAListOfTags() { Labels.EMPTY, null, 0); Meter.Id id2 = id.replaceTags(Tags.empty()); - collector.add(id, - (conventionName) -> Stream.of(new MicrometerCollector.Family<>(conventionName, - family -> new CounterSnapshot(family.metadata, family.dataPointSnapshots), - new MetricMetadata(conventionName), sample))); - collector.add(id2, - (conventionName) -> Stream.of(new MicrometerCollector.Family<>(conventionName, - family -> new CounterSnapshot(family.metadata, family.dataPointSnapshots), - new MetricMetadata(conventionName), sample2))); + collector.add(id, (conventionName) -> Stream.of(new MicrometerCollector.Family<>(conventionName, family -> { + CounterSnapshot.Builder builder = CounterSnapshot.builder().name(family.getConventionName()); + family.dataPointSnapshots.forEach(builder::dataPoint); + return builder.build(); + }, sample))); + collector.add(id2, (conventionName) -> Stream.of(new MicrometerCollector.Family<>(conventionName, family -> { + CounterSnapshot.Builder builder = CounterSnapshot.builder().name(family.getConventionName()); + family.dataPointSnapshots.forEach(builder::dataPoint); + return builder.build(); + }, sample2))); assertThat(collector.collect().get(0).getDataPoints()).hasSize(2); } + @Test + void registrationDescriptorUsesPrometheusFamilyName() { + Meter.Id id = Metrics.counter("my.counter").getId(); + MicrometerCollector collector = new MicrometerCollector(id.getConventionName(convention), id); + + CounterSnapshot.CounterDataPointSnapshot sample = new CounterSnapshot.CounterDataPointSnapshot(1.0, + Labels.of("k", "v"), null, 0); + collector.add(id, (conventionName) -> Stream.of(new MicrometerCollector.Family<>("my_counter", family -> { + CounterSnapshot.Builder builder = CounterSnapshot.builder().name("my_counter"); + family.dataPointSnapshots.forEach(builder::dataPoint); + return builder.build(); + }, sample)), new MicrometerCollector.RegisteredFamily( + MetricFamilyDescriptor.counter("my_counter").help("help").labelNames(asList("k")).build())); + + assertThat(collector.getPrometheusNames()).containsExactly("my_counter"); + assertThat(collector.getMetricType("my_counter")).isEqualTo(MetricType.COUNTER); + assertThat(collector.getLabelNames("my_counter")).containsExactly("k"); + assertThat(collector.getMetadata("my_counter")).isNotNull(); + assertThat(collector.getMetadata("my_counter").getName()).isEqualTo("my_counter"); + } + } diff --git a/implementations/micrometer-registry-prometheus/src/test/java/io/micrometer/prometheusmetrics/PrometheusMeterRegistryTest.java b/implementations/micrometer-registry-prometheus/src/test/java/io/micrometer/prometheusmetrics/PrometheusMeterRegistryTest.java index 9148961be5..e2d18d523a 100644 --- a/implementations/micrometer-registry-prometheus/src/test/java/io/micrometer/prometheusmetrics/PrometheusMeterRegistryTest.java +++ b/implementations/micrometer-registry-prometheus/src/test/java/io/micrometer/prometheusmetrics/PrometheusMeterRegistryTest.java @@ -90,6 +90,19 @@ void differentMicrometerNameSamePrometheusNameFailsToRegister() { assertThat(failed.get()).isTrue(); } + @Test + void functionTimerCollisionUsesPrometheusRegistrationMetadata() { + registry.throwExceptionOnRegistrationFailure(); + + registry.more().timer("test", Tags.empty(), this, o -> 1, o -> 2, TimeUnit.SECONDS); + + assertThatThrownBy( + () -> Gauge.builder("test.seconds", new AtomicInteger(42), AtomicInteger::get).register(registry)) + .isInstanceOf(IllegalArgumentException.class) + .hasMessageContaining("same Prometheus name (test_seconds)") + .hasMessageContaining("would fail with an exception on scrape"); + } + @Test @Disabled("We don't detect this situation yet; scrape will fail") void differentTypesThatProduceSamePrometheusMetricFamilyFailsToRegister() {