diff --git a/implementations/micrometer-registry-otlp/src/main/java/io/micrometer/registry/otlp/OtlpConfig.java b/implementations/micrometer-registry-otlp/src/main/java/io/micrometer/registry/otlp/OtlpConfig.java index f1a4e6263b..d326cc6f5d 100644 --- a/implementations/micrometer-registry-otlp/src/main/java/io/micrometer/registry/otlp/OtlpConfig.java +++ b/implementations/micrometer-registry-otlp/src/main/java/io/micrometer/registry/otlp/OtlpConfig.java @@ -15,12 +15,13 @@ */ package io.micrometer.registry.otlp; +import io.micrometer.core.instrument.Meter; import io.micrometer.core.instrument.config.InvalidConfigurationException; import io.micrometer.core.instrument.config.validate.Validated; import io.micrometer.core.instrument.push.PushRegistryConfig; -import java.time.Duration; import java.net.URLDecoder; +import java.time.Duration; import java.util.*; import java.util.concurrent.TimeUnit; import java.util.stream.Collectors; @@ -228,10 +229,16 @@ default HistogramFlavor histogramFlavor() { } /** - * Configures the histogram flavor to use on a per-meter level. This will override the - * {@link #histogramFlavor()} configuration for matching Meters. The key is used to do - * an exact match on the Meter's name. - * @return mapping of meter name to histogram flavor + * Configures the histogram flavor mapping to use on a per-meter level. This can + * override the {@link #histogramFlavor()} configuration for matching Meters. + * {@link OtlpMeterRegistry} uses the result of this method to look up the + * {@link HistogramFlavor} by {@link Meter.Id}. The longest dot-separated match wins. + * For example, if the returned Map has keys {@literal http} and + * {@literal http.server}, an ID with a name {@literal http.server.requests} would + * match with the entry having key {@literal http.server}, whereas an ID with name + * {@literal http.client.requests} would match with the entry having the key + * {@literal http}. + * @return mapping of meter name (or prefix) to histogram flavor * @since 1.15.0 * @see #histogramFlavor() */ @@ -266,10 +273,15 @@ default int maxBucketCount() { } /** - * Configures the max bucket count to use on a per-meter level. This will override the - * {@link #maxBucketCount()} configuration for matching Meters. The key is used to do - * an exact match on the Meter's name. This has no effect on a meter if it does not - * have an exponential bucket histogram configured. + * Configures the max bucket count mapping to use on a per-meter level. This can + * override the {@link #maxBucketCount()} configuration for matching Meters. + * {@link OtlpMeterRegistry} uses the result of this method to look up the max bucket + * count by {@link Meter.Id}. The longest dot-separated match wins. For example, if + * the returned Map has keys {@literal http} and {@literal http.server}, an ID with a + * name {@literal http.server.requests} would match with the entry having key + * {@literal http.server}, whereas an ID with name {@literal http.client.requests} + * would match with the entry having the key {@literal http}. This has no effect on a + * meter if it does not have an exponential bucket histogram configured. * @return mapping of meter name to max bucket count * @since 1.15.0 * @see #maxBucketCount() diff --git a/implementations/micrometer-registry-otlp/src/main/java/io/micrometer/registry/otlp/OtlpMeterRegistry.java b/implementations/micrometer-registry-otlp/src/main/java/io/micrometer/registry/otlp/OtlpMeterRegistry.java index b38646cd7a..2e6cb4d624 100644 --- a/implementations/micrometer-registry-otlp/src/main/java/io/micrometer/registry/otlp/OtlpMeterRegistry.java +++ b/implementations/micrometer-registry-otlp/src/main/java/io/micrometer/registry/otlp/OtlpMeterRegistry.java @@ -16,10 +16,11 @@ package io.micrometer.registry.otlp; import io.micrometer.common.lang.Nullable; +import io.micrometer.common.util.StringUtils; import io.micrometer.common.util.internal.logging.InternalLogger; import io.micrometer.common.util.internal.logging.InternalLoggerFactory; -import io.micrometer.core.instrument.Timer; import io.micrometer.core.instrument.*; +import io.micrometer.core.instrument.Timer; import io.micrometer.core.instrument.config.NamingConvention; import io.micrometer.core.instrument.distribution.*; import io.micrometer.core.instrument.distribution.pause.PauseDetector; @@ -82,6 +83,10 @@ public class OtlpMeterRegistry extends PushMeterRegistry { private final OtlpMetricsSender metricsSender; + private final HistogramFlavorPerMeterLookup histogramFlavorPerMeterLookup; + + private final MaxBucketsPerMeterLookup maxBucketsPerMeterLookup; + private final Resource resource; private final AggregationTemporality aggregationTemporality; @@ -120,6 +125,8 @@ private OtlpMeterRegistry(OtlpConfig config, Clock clock, ThreadFactory threadFa this.config = config; this.baseTimeUnit = config.baseTimeUnit(); this.metricsSender = metricsSender; + this.histogramFlavorPerMeterLookup = HistogramFlavorPerMeterLookup.DEFAULT; + this.maxBucketsPerMeterLookup = MaxBucketsPerMeterLookup.DEFAULT; this.resource = Resource.newBuilder().addAllAttributes(getResourceAttributes()).build(); this.aggregationTemporality = config.aggregationTemporality(); config().namingConvention(NamingConvention.dot); @@ -430,14 +437,16 @@ private Histogram getHistogram(Meter.Id id, DistributionStatisticConfig distribu } private int getMaxBuckets(Meter.Id id) { - return config.maxBucketsPerMeter().getOrDefault(id.getName(), config.maxBucketCount()); + Integer maxBuckets = maxBucketsPerMeterLookup.getMaxBuckets(config.maxBucketsPerMeter(), id); + return (maxBuckets == null) ? config.maxBucketCount() : maxBuckets; } private HistogramFlavor histogramFlavor(Meter.Id id, OtlpConfig otlpConfig, DistributionStatisticConfig distributionStatisticConfig) { - HistogramFlavor preferredHistogramFlavor = otlpConfig.histogramFlavorPerMeter() - .getOrDefault(id.getName(), otlpConfig.histogramFlavor()); - + HistogramFlavor preferredHistogramFlavor = histogramFlavorPerMeterLookup + .getHistogramFlavor(otlpConfig.histogramFlavorPerMeter(), id); + preferredHistogramFlavor = preferredHistogramFlavor == null ? otlpConfig.histogramFlavor() + : preferredHistogramFlavor; final double[] serviceLevelObjectiveBoundaries = distributionStatisticConfig .getServiceLevelObjectiveBoundaries(); if (distributionStatisticConfig.isPublishingHistogram() @@ -497,6 +506,91 @@ static double[] getSloWithPositiveInf(DistributionStatisticConfig distributionSt return sloWithPositiveInf; } + /** + * Overridable lookup mechanism for {@link HistogramFlavor}. + */ + // VisibleForTesting + @FunctionalInterface + interface HistogramFlavorPerMeterLookup { + + /** + * Default implementation. + */ + HistogramFlavorPerMeterLookup DEFAULT = OtlpMeterRegistry::lookup; + + /** + * Looks up the histogram flavor to use on a per-meter level. This will override + * the default {@link OtlpConfig#histogramFlavor()} for matching Meters. + * {@link OtlpConfig#histogramFlavorPerMeter()} provides the data while this + * method provides the logic for the lookup, and you can override them + * independently. + * @param perMeterMapping configured mapping data + * @param id the {@link Meter.Id} the {@link HistogramFlavor} is configured for + * @return the histogram flavor mapped to the {@link Meter.Id} or {@code null} if + * mapping is undefined + * @see OtlpConfig#histogramFlavorPerMeter() + * @see OtlpConfig#histogramFlavor() + */ + @Nullable + HistogramFlavor getHistogramFlavor(Map perMeterMapping, Meter.Id id); + + } + + /** + * Overridable lookup mechanism for max bucket count. This has no effect on a meter if + * it does not have an exponential bucket histogram configured. + */ + // VisibleForTesting + @FunctionalInterface + interface MaxBucketsPerMeterLookup { + + /** + * Default implementation. + */ + MaxBucketsPerMeterLookup DEFAULT = OtlpMeterRegistry::lookup; + + /** + * Looks up the max bucket count to use on a per-meter level. This will override + * the default {@link OtlpConfig#maxBucketCount()} for matching Meters. + * {@link OtlpConfig#maxBucketsPerMeter()} provides the data while this method + * provides the logic for the lookup, and you can override them independently. + * This has no effect on a meter if it does not have an exponential bucket + * histogram configured. + * @param perMeterMapping configured mapping data + * @param id the {@link Meter.Id} the max bucket count is configured for + * @return the max bucket count mapped to the {@link Meter.Id} or {@code null} if + * the mapping is undefined + * @see OtlpConfig#maxBucketsPerMeter() + * @see OtlpConfig#maxBucketCount() + */ + @Nullable + Integer getMaxBuckets(Map perMeterMapping, Meter.Id id); + + } + + @Nullable + private static T lookup(Map values, Meter.Id id) { + if (values.isEmpty()) { + return null; + } + return doLookup(values, id); + } + + @Nullable + private static T doLookup(Map values, Meter.Id id) { + String name = id.getName(); + while (StringUtils.isNotEmpty(name)) { + T result = values.get(name); + if (result != null) { + return result; + } + int lastDot = name.lastIndexOf('.'); + name = (lastDot != -1) ? name.substring(0, lastDot) : ""; + } + + return null; + } + /** * Builder for {@link OtlpMeterRegistry}. * diff --git a/implementations/micrometer-registry-otlp/src/test/java/io/micrometer/registry/otlp/OtlpConfigTest.java b/implementations/micrometer-registry-otlp/src/test/java/io/micrometer/registry/otlp/OtlpConfigTest.java index f0efa86663..4b02ce3d37 100644 --- a/implementations/micrometer-registry-otlp/src/test/java/io/micrometer/registry/otlp/OtlpConfigTest.java +++ b/implementations/micrometer-registry-otlp/src/test/java/io/micrometer/registry/otlp/OtlpConfigTest.java @@ -25,9 +25,9 @@ import java.util.concurrent.TimeUnit; import java.util.stream.Stream; -import static java.util.Map.entry; -import static org.assertj.core.api.Assertions.assertThat; -import static org.assertj.core.api.Assertions.assertThatThrownBy; +import static io.micrometer.registry.otlp.HistogramFlavor.BASE2_EXPONENTIAL_BUCKET_HISTOGRAM; +import static io.micrometer.registry.otlp.HistogramFlavor.EXPLICIT_BUCKET_HISTOGRAM; +import static org.assertj.core.api.Assertions.*; import static uk.org.webcompere.systemstubs.SystemStubs.withEnvironmentVariable; import static uk.org.webcompere.systemstubs.SystemStubs.withEnvironmentVariables; @@ -257,15 +257,14 @@ void histogramPreference() { OtlpConfig otlpConfig = properties::get; assertThat(otlpConfig.validate().isValid()).isTrue(); - assertThat(otlpConfig.histogramFlavor()).isEqualTo(HistogramFlavor.BASE2_EXPONENTIAL_BUCKET_HISTOGRAM); + assertThat(otlpConfig.histogramFlavor()).isEqualTo(BASE2_EXPONENTIAL_BUCKET_HISTOGRAM); } @Test void histogramPreferenceConfigTakesPrecedenceOverEnvVars() throws Exception { OtlpConfig config = k -> "base2_exponential_bucket_histogram"; withEnvironmentVariable("OTEL_EXPORTER_OTLP_METRICS_DEFAULT_HISTOGRAM_AGGREGATION", "explicit_bucket_histogram") - .execute(() -> assertThat(config.histogramFlavor()) - .isEqualTo(HistogramFlavor.BASE2_EXPONENTIAL_BUCKET_HISTOGRAM)); + .execute(() -> assertThat(config.histogramFlavor()).isEqualTo(BASE2_EXPONENTIAL_BUCKET_HISTOGRAM)); } @Test @@ -273,8 +272,7 @@ void histogramPreferenceUseEnvVarWhenConfigNotSet() throws Exception { OtlpConfig config = k -> null; withEnvironmentVariable("OTEL_EXPORTER_OTLP_METRICS_DEFAULT_HISTOGRAM_AGGREGATION", "base2_exponential_bucket_histogram") - .execute(() -> assertThat(config.histogramFlavor()) - .isEqualTo(HistogramFlavor.BASE2_EXPONENTIAL_BUCKET_HISTOGRAM)); + .execute(() -> assertThat(config.histogramFlavor()).isEqualTo(BASE2_EXPONENTIAL_BUCKET_HISTOGRAM)); } @Test @@ -284,9 +282,8 @@ void histogramFlavorPerMeter() { "a.b.c=explicit_bucket_histogram ,expo =base2_exponential_bucket_histogram"); OtlpConfig otlpConfig = properties::get; assertThat(otlpConfig.validate().isValid()).isTrue(); - assertThat(otlpConfig.histogramFlavorPerMeter()).containsExactly( - entry("a.b.c", HistogramFlavor.EXPLICIT_BUCKET_HISTOGRAM), - entry("expo", HistogramFlavor.BASE2_EXPONENTIAL_BUCKET_HISTOGRAM)); + assertThat(otlpConfig.histogramFlavorPerMeter()).containsExactly(entry("a.b.c", EXPLICIT_BUCKET_HISTOGRAM), + entry("expo", BASE2_EXPONENTIAL_BUCKET_HISTOGRAM)); } @Test diff --git a/implementations/micrometer-registry-otlp/src/test/java/io/micrometer/registry/otlp/OtlpMeterRegistryTest.java b/implementations/micrometer-registry-otlp/src/test/java/io/micrometer/registry/otlp/OtlpMeterRegistryTest.java index 5a59a0bb91..123b121a50 100644 --- a/implementations/micrometer-registry-otlp/src/test/java/io/micrometer/registry/otlp/OtlpMeterRegistryTest.java +++ b/implementations/micrometer-registry-otlp/src/test/java/io/micrometer/registry/otlp/OtlpMeterRegistryTest.java @@ -16,8 +16,8 @@ package io.micrometer.registry.otlp; import io.micrometer.core.Issue; -import io.micrometer.core.instrument.Timer; import io.micrometer.core.instrument.*; +import io.micrometer.core.instrument.Timer; import io.micrometer.core.instrument.distribution.DistributionStatisticConfig; import io.micrometer.core.ipc.http.HttpSender; import io.opentelemetry.proto.metrics.v1.ExponentialHistogramDataPoint; @@ -33,6 +33,8 @@ import java.util.concurrent.TimeUnit; import java.util.stream.IntStream; +import static io.micrometer.registry.otlp.HistogramFlavor.BASE2_EXPONENTIAL_BUCKET_HISTOGRAM; +import static io.micrometer.registry.otlp.HistogramFlavor.EXPLICIT_BUCKET_HISTOGRAM; import static org.assertj.core.api.Assertions.assertThat; import static org.mockito.ArgumentMatchers.assertArg; import static org.mockito.Mockito.*; @@ -409,7 +411,33 @@ void testGetSloWithPositiveInf() { abstract void testMetricsStartAndEndTime(); @Test - void perMeterHistogramFlavorOverridesGeneralHistogramFlavor() { + void defaultHistogramFlavorShouldBeUsedIfNoOverrides() { + OtlpConfig config = new OtlpConfig() { + @Override + public String get(String key) { + return null; + } + + @Override + public AggregationTemporality aggregationTemporality() { + return otlpConfig().aggregationTemporality(); + } + }; + OtlpMeterRegistry meterRegistry = OtlpMeterRegistry.builder(config).clock(clock).build(); + + Timer timer = Timer.builder("test.timer").publishPercentileHistogram().register(meterRegistry); + assertThat(writeToMetric(timer).getDataCase().getNumber()).isEqualTo(Metric.DataCase.HISTOGRAM.getNumber()); + + meterRegistry.clear(); + + DistributionSummary ds = DistributionSummary.builder("test.ds") + .publishPercentileHistogram() + .register(meterRegistry); + assertThat(writeToMetric(ds).getDataCase().getNumber()).isEqualTo(Metric.DataCase.HISTOGRAM.getNumber()); + } + + @Test + void globalHistogramFlavorShouldBeUsedIfNoPerMeterConfig() { OtlpConfig config = new OtlpConfig() { @Override public String get(String key) { @@ -423,20 +451,57 @@ public AggregationTemporality aggregationTemporality() { @Override public HistogramFlavor histogramFlavor() { - return HistogramFlavor.EXPLICIT_BUCKET_HISTOGRAM; + return BASE2_EXPONENTIAL_BUCKET_HISTOGRAM; + } + }; + OtlpMeterRegistry meterRegistry = OtlpMeterRegistry.builder(config).clock(clock).build(); + Timer timer = Timer.builder("test.timer").publishPercentileHistogram().register(meterRegistry); + assertThat(writeToMetric(timer).getDataCase().getNumber()) + .isEqualTo(Metric.DataCase.EXPONENTIAL_HISTOGRAM.getNumber()); + + meterRegistry.clear(); + + DistributionSummary ds = DistributionSummary.builder("test.ds") + .publishPercentileHistogram() + .register(meterRegistry); + assertThat(writeToMetric(ds).getDataCase().getNumber()) + .isEqualTo(Metric.DataCase.EXPONENTIAL_HISTOGRAM.getNumber()); + } + + @Test + void perMeterHistogramFlavorShouldBeUsedFromConfigIfNoLookupOverrides() { + OtlpConfig config = new OtlpConfig() { + @Override + public String get(String key) { + return null; + } + + @Override + public AggregationTemporality aggregationTemporality() { + return otlpConfig().aggregationTemporality(); + } + + @Override + public HistogramFlavor histogramFlavor() { + return EXPLICIT_BUCKET_HISTOGRAM; } @Override public Map histogramFlavorPerMeter() { - return Collections.singletonMap("expo", HistogramFlavor.BASE2_EXPONENTIAL_BUCKET_HISTOGRAM); + Map histogramFlavors = new HashMap<>(); + histogramFlavors.put("expo", BASE2_EXPONENTIAL_BUCKET_HISTOGRAM); + return histogramFlavors; } }; - OtlpMeterRegistry meterRegistry = new OtlpMeterRegistry(config, clock); + OtlpMeterRegistry meterRegistry = OtlpMeterRegistry.builder(config).clock(clock).build(); + Timer expo = Timer.builder("expo").publishPercentileHistogram().register(meterRegistry); + Timer expoOther = Timer.builder("expo.other").publishPercentileHistogram().register(meterRegistry); Timer other = Timer.builder("other").publishPercentileHistogram().register(meterRegistry); - assertThat(writeToMetric(expo).getDataCase().getNumber()) .isEqualTo(Metric.DataCase.EXPONENTIAL_HISTOGRAM.getNumber()); + assertThat(writeToMetric(expoOther).getDataCase().getNumber()) + .isEqualTo(Metric.DataCase.EXPONENTIAL_HISTOGRAM.getNumber()); assertThat(writeToMetric(other).getDataCase().getNumber()).isEqualTo(Metric.DataCase.HISTOGRAM.getNumber()); meterRegistry.clear(); @@ -444,17 +509,21 @@ public Map histogramFlavorPerMeter() { DistributionSummary expo2 = DistributionSummary.builder("expo") .publishPercentileHistogram() .register(meterRegistry); + DistributionSummary expoOther2 = DistributionSummary.builder("expo.other") + .publishPercentileHistogram() + .register(meterRegistry); DistributionSummary other2 = DistributionSummary.builder("other") .publishPercentileHistogram() .register(meterRegistry); - assertThat(writeToMetric(expo2).getDataCase().getNumber()) .isEqualTo(Metric.DataCase.EXPONENTIAL_HISTOGRAM.getNumber()); + assertThat(writeToMetric(expoOther2).getDataCase().getNumber()) + .isEqualTo(Metric.DataCase.EXPONENTIAL_HISTOGRAM.getNumber()); assertThat(writeToMetric(other2).getDataCase().getNumber()).isEqualTo(Metric.DataCase.HISTOGRAM.getNumber()); } @Test - void maxBucketsPerMeter() { + void globalMaxBucketsShouldBeUsedIfNoPerMeterConfig() { OtlpConfig config = new OtlpConfig() { @Override public String get(String key) { @@ -468,7 +537,52 @@ public AggregationTemporality aggregationTemporality() { @Override public HistogramFlavor histogramFlavor() { - return HistogramFlavor.BASE2_EXPONENTIAL_BUCKET_HISTOGRAM; + return BASE2_EXPONENTIAL_BUCKET_HISTOGRAM; + } + + @Override + public int maxBucketCount() { + return 56; + } + }; + OtlpMeterRegistry meterRegistry = OtlpMeterRegistry.builder(config).clock(clock).build(); + Timer timer = Timer.builder("test.timer").publishPercentileHistogram().register(meterRegistry); + IntStream.range(1, 111).forEach(i -> timer.record(i, TimeUnit.MILLISECONDS)); + + clock.add(config.step()); + + assertThat(writeToMetric(timer).getExponentialHistogram().getDataPoints(0).getPositive().getBucketCountsList()) + .hasSize(56); + + meterRegistry.clear(); + + DistributionSummary ds = DistributionSummary.builder("test.ds") + .publishPercentileHistogram() + .register(meterRegistry); + IntStream.range(1, 111).forEach(ds::record); + + clock.add(config.step()); + + assertThat(writeToMetric(ds).getExponentialHistogram().getDataPoints(0).getPositive().getBucketCountsList()) + .hasSize(56); + } + + @Test + void perMeterMaxBucketsShouldBeUsedFromConfigIfNoLookupOverrides() { + OtlpConfig config = new OtlpConfig() { + @Override + public String get(String key) { + return null; + } + + @Override + public AggregationTemporality aggregationTemporality() { + return otlpConfig().aggregationTemporality(); + } + + @Override + public HistogramFlavor histogramFlavor() { + return BASE2_EXPONENTIAL_BUCKET_HISTOGRAM; } @Override @@ -478,14 +592,20 @@ public int maxBucketCount() { @Override public Map maxBucketsPerMeter() { - return Collections.singletonMap("low.variation", 15); + Map maxBuckets = new HashMap<>(); + maxBuckets.put("low.variation", 15); + return maxBuckets; } }; - OtlpMeterRegistry meterRegistry = new OtlpMeterRegistry(config, clock); + OtlpMeterRegistry meterRegistry = OtlpMeterRegistry.builder(config).clock(clock).build(); + Timer lowVariation = Timer.builder("low.variation").publishPercentileHistogram().register(meterRegistry); + Timer lowVariationOther = Timer.builder("low.variation.other") + .publishPercentileHistogram() + .register(meterRegistry); Timer other = Timer.builder("other").publishPercentileHistogram().register(meterRegistry); - List.of(lowVariation, other) + List.of(lowVariation, lowVariationOther, other) .forEach(t -> IntStream.range(1, 111).forEach(i -> t.record(i, TimeUnit.MILLISECONDS))); clock.add(config.step()); @@ -493,6 +613,10 @@ public Map maxBucketsPerMeter() { .getDataPoints(0) .getPositive() .getBucketCountsList()).hasSize(15); + assertThat(writeToMetric(lowVariationOther).getExponentialHistogram() + .getDataPoints(0) + .getPositive() + .getBucketCountsList()).hasSize(15); assertThat(writeToMetric(other).getExponentialHistogram().getDataPoints(0).getPositive().getBucketCountsList()) .hasSize(56); @@ -501,21 +625,54 @@ public Map maxBucketsPerMeter() { DistributionSummary lowVariation2 = DistributionSummary.builder("low.variation") .publishPercentileHistogram() .register(meterRegistry); + DistributionSummary lowVariationOther2 = DistributionSummary.builder("low.variation.other") + .publishPercentileHistogram() + .register(meterRegistry); DistributionSummary other2 = DistributionSummary.builder("other") .publishPercentileHistogram() .register(meterRegistry); - List.of(lowVariation2, other2).forEach(t -> IntStream.range(1, 111).forEach(t::record)); + List.of(lowVariation2, lowVariationOther2, other2).forEach(t -> IntStream.range(1, 111).forEach(t::record)); clock.add(config.step()); assertThat(writeToMetric(lowVariation2).getExponentialHistogram() .getDataPoints(0) .getPositive() .getBucketCountsList()).hasSize(15); + assertThat(writeToMetric(lowVariationOther2).getExponentialHistogram() + .getDataPoints(0) + .getPositive() + .getBucketCountsList()).hasSize(15); assertThat(writeToMetric(other2).getExponentialHistogram().getDataPoints(0).getPositive().getBucketCountsList()) .hasSize(56); } + @Test + void longestMatchWinsByDefaultHistogramFlavorPerMeter() { + Map histogramFlavorPerMeter = new HashMap<>(); + histogramFlavorPerMeter.put("http", EXPLICIT_BUCKET_HISTOGRAM); + histogramFlavorPerMeter.put("http.server", BASE2_EXPONENTIAL_BUCKET_HISTOGRAM); + + assertThat(OtlpMeterRegistry.HistogramFlavorPerMeterLookup.DEFAULT.getHistogramFlavor(histogramFlavorPerMeter, + createIdWithName("http.server.requests"))) + .isEqualTo(BASE2_EXPONENTIAL_BUCKET_HISTOGRAM); + } + + @Test + void longestMatchWinsByDefaultMaxBucketsPerMeter() { + Map maxBucketsPerMeter = new HashMap<>(); + maxBucketsPerMeter.put("http", 10); + maxBucketsPerMeter.put("http.server", 20); + + assertThat(OtlpMeterRegistry.MaxBucketsPerMeterLookup.DEFAULT.getMaxBuckets(maxBucketsPerMeter, + createIdWithName("http.server.requests"))) + .isEqualTo(20); + } + + private Meter.Id createIdWithName(String name) { + return new Meter.Id(name, Tags.empty(), null, null, Meter.Type.OTHER); + } + protected Metric writeToMetric(Meter meter) { OtlpMetricConverter otlpMetricConverter = new OtlpMetricConverter(clock, otlpConfig().step(), registry.getBaseTimeUnit(), otlpConfig().aggregationTemporality(),