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..d636ba58ef 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,11 +16,14 @@ 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 org.jspecify.annotations.Nullable; import java.util.*; import java.util.concurrent.ConcurrentHashMap; @@ -56,6 +59,21 @@ public void add(Meter.Id id, Child child) { children.put(id, child); } + public void add(Meter.Id id, Function>> samples, + Function> descriptors) { + children.put(id, new Child() { + @Override + public Stream> samples(String conventionName) { + return samples.apply(conventionName); + } + + @Override + public Stream descriptors(String conventionName) { + return descriptors.apply(conventionName); + } + }); + } + public void remove(Meter.Id id) { children.remove(id); } @@ -91,10 +109,52 @@ public MetricSnapshots collect() { return new MetricSnapshots(metricSnapshots); } + @Override + public List getPrometheusNames() { + return new ArrayList<>(descriptorsByName().keySet()); + } + + @Override + public @Nullable MetricType getMetricType(String prometheusName) { + List descriptors = descriptorsByName().get(prometheusName); + return descriptors == null || descriptors.isEmpty() ? null : descriptors.get(0).getType(); + } + + @Override + public @Nullable Set getLabelNames(String prometheusName) { + List descriptors = descriptorsByName().get(prometheusName); + if (descriptors == null || descriptors.isEmpty()) { + return null; + } + Set labelNames = new LinkedHashSet<>(); + descriptors.forEach(descriptor -> labelNames.addAll(descriptor.getLabelNames())); + return labelNames; + } + + @Override + public @Nullable MetricMetadata getMetadata(String prometheusName) { + List descriptors = descriptorsByName().get(prometheusName); + return descriptors == null || descriptors.isEmpty() ? null : descriptors.get(0).getMetadata(); + } + + private Map> descriptorsByName() { + Map> descriptors = new LinkedHashMap<>(); + for (Child child : children.values()) { + child.descriptors(conventionName) + .forEach(descriptor -> descriptors.computeIfAbsent(descriptor.getPrometheusName(), name -> new ArrayList<>()) + .add(descriptor)); + } + return descriptors; + } + interface Child { Stream> samples(String conventionName); + default Stream descriptors(String conventionName) { + return Stream.empty(); + } + } static class Family { @@ -107,6 +167,11 @@ static class Family { final Function, MetricSnapshot> metricSnapshotFactory; + Family(String conventionName, Function, MetricSnapshot> metricSnapshotFactory, + MetricFamilyDescriptor descriptor, T... dataPointSnapshots) { + this(conventionName, metricSnapshotFactory, descriptor.getMetadata(), dataPointSnapshots); + } + Family(String conventionName, Function, MetricSnapshot> metricSnapshotFactory, MetricMetadata metadata, T... dataPointSnapshots) { this.conventionName = conventionName; 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..71c6aae4d5 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; @@ -219,8 +220,11 @@ public Counter newCounter(Meter.Id 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)))); + getDescriptor(MetricType.COUNTER, conventionName, id.getDescription(), tagKeys), + new CounterDataPointSnapshot(counter.count(), + Labels.of(tagKeys, tagValues), counter.exemplar(), createdTimestampMillis))), + (conventionName) -> Stream + .of(getDescriptor(MetricType.COUNTER, conventionName, id.getDescription(), tagKeys))); }); return counter; } @@ -255,7 +259,8 @@ 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, + getDescriptor(MetricType.SUMMARY, conventionName, id.getDescription(), tagKeys), + new SummaryDataPointSnapshot(count, sum, quantiles, Labels.of(tagKeys, tagValues), exemplars, createdTimestampMillis))); } else { @@ -285,7 +290,7 @@ public DistributionSummary newDistributionSummary(Meter.Id id, families.add(new MicrometerCollector.Family<>(conventionName, family -> new io.prometheus.metrics.model.snapshots.HistogramSnapshot(family.metadata, family.dataPointSnapshots), - getMetadata(conventionName, id.getDescription()), + getDescriptor(MetricType.HISTOGRAM, conventionName, id.getDescription(), tagKeys), new HistogramDataPointSnapshot(ClassicHistogramBuckets.of(buckets, counts), sum, Labels.of(tagKeys, tagValues), exemplars, createdTimestampMillis))); @@ -301,10 +306,15 @@ 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()), + getDescriptor(MetricType.GAUGE, conventionName + "_max", id.getDescription(), tagKeys), new GaugeDataPointSnapshot(summary.max(), Labels.of(tagKeys, tagValues), null))); return families.build(); + }, (conventionName) -> { + MetricType summaryType = distributionStatisticConfig.isPublishingHistogram() ? MetricType.HISTOGRAM + : MetricType.SUMMARY; + return Stream.of(getDescriptor(summaryType, conventionName, id.getDescription(), tagKeys), + getDescriptor(MetricType.GAUGE, conventionName + "_max", id.getDescription(), tagKeys)); }); }); @@ -317,7 +327,7 @@ protected io.micrometer.core.instrument.Timer newTimer(Meter.Id id, PrometheusTimer timer = new PrometheusTimer(id, clock, distributionStatisticConfig, pauseDetector, exemplarSamplerFactory); applyToCollector(id, (collector) -> addDistributionStatisticSamples(id, collector, timer, timer::exemplars, - tagValues(id), false)); + tagValues(id), false, distributionStatisticConfig.isPublishingHistogram())); return timer; } @@ -332,15 +342,19 @@ protected io.micrometer.core.instrument.Gauge newGauge(Meter.Id id, @Nullabl 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))))); + getDescriptor(MetricType.INFO, conventionName, id.getDescription(), tagKeys), + new InfoDataPointSnapshot(Labels.of(tagKeys, tagValues)))), + (conventionName) -> Stream + .of(getDescriptor(MetricType.INFO, conventionName, id.getDescription(), tagKeys))); } 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)))); + getDescriptor(MetricType.GAUGE, conventionName, id.getDescription(), tagKeys), + new GaugeDataPointSnapshot(gauge.value(), Labels.of(tagKeys, tagValues), null))), + (conventionName) -> Stream + .of(getDescriptor(MetricType.GAUGE, conventionName, id.getDescription(), tagKeys))); } }); return gauge; @@ -350,7 +364,7 @@ protected io.micrometer.core.instrument.Gauge newGauge(Meter.Id id, @Nullabl protected LongTaskTimer newLongTaskTimer(Meter.Id id, DistributionStatisticConfig distributionStatisticConfig) { LongTaskTimer ltt = new DefaultLongTaskTimer(id, clock, getBaseTimeUnit(), distributionStatisticConfig, true); applyToCollector(id, (collector) -> addDistributionStatisticSamples(id, collector, ltt, () -> Exemplars.EMPTY, - tagValues(id), true)); + tagValues(id), true, distributionStatisticConfig.isPublishingHistogram())); return ltt; } @@ -366,9 +380,11 @@ protected FunctionTimer newFunctionTimer(Meter.Id id, T obj, ToLongFunction< collector.add(id, (conventionName) -> Stream.of(new MicrometerCollector.Family<>(conventionName, family -> new SummarySnapshot(family.metadata, family.dataPointSnapshots), - getMetadata(conventionName, id.getDescription()), + getDescriptor(MetricType.SUMMARY, conventionName, id.getDescription(), tagKeys), new SummaryDataPointSnapshot((long) ft.count(), ft.totalTime(getBaseTimeUnit()), - Quantiles.EMPTY, Labels.of(tagKeys, tagValues), null, createdTimestampMillis)))); + Quantiles.EMPTY, Labels.of(tagKeys, tagValues), null, createdTimestampMillis))), + (conventionName) -> Stream + .of(getDescriptor(MetricType.SUMMARY, conventionName, id.getDescription(), tagKeys))); }); return ft; } @@ -383,8 +399,11 @@ protected FunctionCounter newFunctionCounter(Meter.Id id, T obj, ToDoubleFun 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)))); + getDescriptor(MetricType.COUNTER, conventionName, id.getDescription(), tagKeys), + new CounterDataPointSnapshot(fc.count(), + Labels.of(tagKeys, tagValues), null, createdTimestampMillis))), + (conventionName) -> Stream + .of(getDescriptor(MetricType.COUNTER, conventionName, id.getDescription(), tagKeys))); }); return fc; } @@ -431,6 +450,41 @@ protected Meter newMeter(Meter.Id id, Meter.Type type, Iterable mea } } return families.build(); + }, (conventionName) -> { + Stream.Builder descriptors = Stream.builder(); + List statKeys = new ArrayList<>(tagKeys); + statKeys.add("statistic"); + for (Measurement measurement : measurements) { + switch (measurement.getStatistic()) { + case TOTAL: + case TOTAL_TIME: + descriptors.add(getDescriptor(MetricType.COUNTER, conventionName + "_sum", + id.getDescription(), statKeys)); + break; + case COUNT: + descriptors.add( + getDescriptor(MetricType.COUNTER, conventionName, id.getDescription(), statKeys)); + break; + case MAX: + descriptors.add(getDescriptor(MetricType.GAUGE, conventionName + "_max", + id.getDescription(), statKeys)); + break; + case VALUE: + case UNKNOWN: + descriptors.add(getDescriptor(MetricType.GAUGE, conventionName + "_value", + id.getDescription(), statKeys)); + break; + case ACTIVE_TASKS: + descriptors.add(getDescriptor(MetricType.GAUGE, conventionName + "_active_count", + id.getDescription(), statKeys)); + break; + case DURATION: + descriptors.add(getDescriptor(MetricType.GAUGE, conventionName + "_duration_sum", + id.getDescription(), statKeys)); + break; + } + } + return descriptors.build(); }); }); @@ -442,7 +496,7 @@ private MicrometerCollector.Family customCounterFamily long createdTimestampMillis = clock.wallTime(); return new MicrometerCollector.Family<>(conventionName + suffix, family -> new CounterSnapshot(family.metadata, family.dataPointSnapshots), - getMetadata(conventionName + suffix, id.getDescription()), + getDescriptor(MetricType.COUNTER, conventionName + suffix, id.getDescription(), labelNames(labels)), new CounterDataPointSnapshot(value, labels, null, createdTimestampMillis)); } @@ -450,7 +504,7 @@ private MicrometerCollector.Family customGaugeFamily(Met String suffix, Labels labels, double value) { return new MicrometerCollector.Family<>(conventionName + suffix, family -> new GaugeSnapshot(family.metadata, family.dataPointSnapshots), - getMetadata(conventionName + suffix, id.getDescription()), + getDescriptor(MetricType.GAUGE, conventionName + suffix, id.getDescription(), labelNames(labels)), new GaugeDataPointSnapshot(value, labels, null)); } @@ -468,7 +522,7 @@ public PrometheusRegistry getPrometheusRegistry() { private void addDistributionStatisticSamples(Meter.Id id, MicrometerCollector collector, HistogramSupport histogramSupport, Supplier exemplarsSupplier, List tagValues, - boolean forLongTaskTimer) { + boolean forLongTaskTimer, boolean publishingHistogram) { long createdTimestampMillis = clock.wallTime(); List tagKeys = tagKeys(id); collector.add(id, (conventionName) -> { @@ -493,7 +547,8 @@ 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, + getDescriptor(MetricType.SUMMARY, conventionName, id.getDescription(), tagKeys), + new SummaryDataPointSnapshot(count, sum, quantiles, Labels.of(tagKeys, tagValues), exemplars, createdTimestampMillis))); } else { @@ -523,7 +578,7 @@ private void addDistributionStatisticSamples(Meter.Id id, MicrometerCollector co families.add(new MicrometerCollector.Family<>(conventionName, family -> new io.prometheus.metrics.model.snapshots.HistogramSnapshot(forLongTaskTimer, family.metadata, family.dataPointSnapshots), - getMetadata(conventionName, id.getDescription()), + getDescriptor(MetricType.HISTOGRAM, conventionName, id.getDescription(), tagKeys), new HistogramDataPointSnapshot(ClassicHistogramBuckets.of(buckets, counts), sum, Labels.of(tagKeys, tagValues), exemplars, createdTimestampMillis))); @@ -539,10 +594,15 @@ 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))); + getDescriptor(MetricType.GAUGE, conventionName + "_max", id.getDescription(), tagKeys), + new GaugeDataPointSnapshot(histogramSnapshot.max(getBaseTimeUnit()), Labels.of(tagKeys, tagValues), + null))); return families.build(); + }, (conventionName) -> { + MetricType histogramSupportType = publishingHistogram ? MetricType.HISTOGRAM : MetricType.SUMMARY; + return Stream.of(getDescriptor(histogramSupportType, conventionName, id.getDescription(), tagKeys), + getDescriptor(MetricType.GAUGE, conventionName + "_max", id.getDescription(), tagKeys)); }); } @@ -572,11 +632,19 @@ private void onMeterRemoved(Meter meter) { } } - private MetricMetadata getMetadata(String name, @Nullable String description) { - String help = prometheusConfig.descriptions() && description != null ? description : " "; + private List labelNames(Labels labels) { + return StreamSupport.stream(labels.spliterator(), false).map(Label::getName).collect(toList()); + } + + private MetricFamilyDescriptor getDescriptor(MetricType type, String name, @Nullable String description, + List labelNames) { // Unit is intentionally not set, see: // https://github.com/OpenObservability/OpenMetrics/blob/1386544931307dff279688f332890c31b6c5de36/specification/OpenMetrics.md#unit - return new MetricMetadata(name, help, null); + return MetricFamilyDescriptor.of(type, name).help(getHelp(description)).labelNames(labelNames).build(); + } + + private String getHelp(@Nullable String description) { + return prometheusConfig.descriptions() && description != null ? description : " "; } private void applyToCollector(Meter.Id id, Consumer consumer) { @@ -584,8 +652,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 ex) { + meterRegistrationFailed(id, ex.getMessage()); + return null; + } } if (!existingCollector.getOriginalId().getName().equals(id.getName())) { diff --git a/implementations/micrometer-registry-prometheus/src/main/java/io/micrometer/prometheusmetrics/PrometheusNamingConvention.java b/implementations/micrometer-registry-prometheus/src/main/java/io/micrometer/prometheusmetrics/PrometheusNamingConvention.java index 292a0dbf59..eca7ff55c3 100644 --- a/implementations/micrometer-registry-prometheus/src/main/java/io/micrometer/prometheusmetrics/PrometheusNamingConvention.java +++ b/implementations/micrometer-registry-prometheus/src/main/java/io/micrometer/prometheusmetrics/PrometheusNamingConvention.java @@ -30,6 +30,9 @@ */ public class PrometheusNamingConvention implements NamingConvention { + private static final String[] RESERVED_SUFFIXES = { "_total", "_created", "_bucket", "_info", ".total", ".created", + ".bucket", ".info" }; + private final String timerSuffix; public PrometheusNamingConvention() { @@ -75,7 +78,28 @@ else if (!conventionName.endsWith("_seconds")) { break; } - return PrometheusNaming.sanitizeMetricName(conventionName); + return stripReservedSuffixes(PrometheusNaming.sanitizeMetricName(conventionName)); + } + + private static String stripReservedSuffixes(String metricName) { + String sanitizedName = metricName; + boolean stripped = true; + while (stripped) { + stripped = false; + for (String reservedSuffix : RESERVED_SUFFIXES) { + if (sanitizedName.equals(reservedSuffix)) { + return reservedSuffix.substring(1); + } + } + for (String reservedSuffix : RESERVED_SUFFIXES) { + if (sanitizedName.endsWith(reservedSuffix)) { + sanitizedName = sanitizedName.substring(0, sanitizedName.length() - reservedSuffix.length()); + stripped = true; + break; + } + } + } + return sanitizedName; } /**