Skip to content

Make per-meter OTLP configuration more flexible #6102

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 8 commits into from
Apr 15, 2025
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -228,11 +229,18 @@ 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.
* Configures the histogram flavor mapping to use on a per-meter level. This can
* override the {@link #histogramFlavor()} configuration for matching Meters. By
* default, {@link OtlpMeterRegistry} (through
* {@link io.micrometer.registry.otlp.OtlpMeterRegistry.HistogramFlavorPerMeterLookup})
* uses the result of this method to look up the {@link HistogramFlavor} by
* {@link Meter.Id} (prefix match on the Meter's name by default). This means that
* this method provides the data while
* {@link io.micrometer.registry.otlp.OtlpMeterRegistry.HistogramFlavorPerMeterLookup}
* provides the logic for the lookup, and you can override them independently.
* @return mapping of meter name to histogram flavor
* @since 1.15.0
* @see io.micrometer.registry.otlp.OtlpMeterRegistry.HistogramFlavorPerMeterLookup
* @see #histogramFlavor()
*/
default Map<String, HistogramFlavor> histogramFlavorPerMeter() {
Expand Down Expand Up @@ -266,12 +274,20 @@ 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. By
* default, {@link OtlpMeterRegistry} (through
* {@link io.micrometer.registry.otlp.OtlpMeterRegistry.MaxBucketsPerMeterLookup})
* uses the result of this method to look up the max bucket count by {@link Meter.Id}
* (prefix match on the Meter's name by default). This means that this method provides
* the data while
* {@link io.micrometer.registry.otlp.OtlpMeterRegistry.MaxBucketsPerMeterLookup}
* 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.
* @return mapping of meter name to max bucket count
* @since 1.15.0
* @see io.micrometer.registry.otlp.OtlpMeterRegistry.MaxBucketsPerMeterLookup
* @see #maxBucketCount()
*/
default Map<String, Integer> maxBucketsPerMeter() {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,8 +18,8 @@
import io.micrometer.common.lang.Nullable;
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;
Expand Down Expand Up @@ -82,6 +82,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;
Expand Down Expand Up @@ -111,15 +115,19 @@ public OtlpMeterRegistry(OtlpConfig config, Clock clock) {
* @since 1.14.0
*/
public OtlpMeterRegistry(OtlpConfig config, Clock clock, ThreadFactory threadFactory) {
this(config, clock, threadFactory, new OtlpHttpMetricsSender(new HttpUrlConnectionSender()));
this(config, clock, threadFactory, new OtlpHttpMetricsSender(new HttpUrlConnectionSender()),
OtlpMeterRegistry::histogramFlavorPerMeter, OtlpMeterRegistry::maxBucketsPerMeter);
}

private OtlpMeterRegistry(OtlpConfig config, Clock clock, ThreadFactory threadFactory,
OtlpMetricsSender metricsSender) {
OtlpMetricsSender metricsSender, HistogramFlavorPerMeterLookup histogramFlavorPerMeterLookup,
MaxBucketsPerMeterLookup maxBucketsPerMeterLookup) {
super(config, clock);
this.config = config;
this.baseTimeUnit = config.baseTimeUnit();
this.metricsSender = metricsSender;
this.histogramFlavorPerMeterLookup = histogramFlavorPerMeterLookup;
this.maxBucketsPerMeterLookup = maxBucketsPerMeterLookup;
this.resource = Resource.newBuilder().addAllAttributes(getResourceAttributes()).build();
this.aggregationTemporality = config.aggregationTemporality();
config().namingConvention(NamingConvention.dot);
Expand Down Expand Up @@ -430,14 +438,12 @@ private Histogram getHistogram(Meter.Id id, DistributionStatisticConfig distribu
}

private int getMaxBuckets(Meter.Id id) {
return config.maxBucketsPerMeter().getOrDefault(id.getName(), config.maxBucketCount());
return maxBucketsPerMeterLookup.getMaxBuckets(config, id);
}

private HistogramFlavor histogramFlavor(Meter.Id id, OtlpConfig otlpConfig,
DistributionStatisticConfig distributionStatisticConfig) {
HistogramFlavor preferredHistogramFlavor = otlpConfig.histogramFlavorPerMeter()
.getOrDefault(id.getName(), otlpConfig.histogramFlavor());

HistogramFlavor preferredHistogramFlavor = histogramFlavorPerMeterLookup.getHistogramFlavor(otlpConfig, id);
final double[] serviceLevelObjectiveBoundaries = distributionStatisticConfig
.getServiceLevelObjectiveBoundaries();
if (distributionStatisticConfig.isPublishingHistogram()
Expand Down Expand Up @@ -497,6 +503,76 @@ static double[] getSloWithPositiveInf(DistributionStatisticConfig distributionSt
return sloWithPositiveInf;
}

private static HistogramFlavor histogramFlavorPerMeter(OtlpConfig config, Meter.Id id) {
for (Map.Entry<String, HistogramFlavor> entry : config.histogramFlavorPerMeter().entrySet()) {
if (id.getName().startsWith(entry.getKey())) {
return entry.getValue();
}
}
return config.histogramFlavor();
}

private static Integer maxBucketsPerMeter(OtlpConfig config, Meter.Id id) {
for (Map.Entry<String, Integer> entry : config.maxBucketsPerMeter().entrySet()) {
if (id.getName().startsWith(entry.getKey())) {
return entry.getValue();
}
}
return config.maxBucketCount();
}

/**
* Overridable lookup mechanism for {@link HistogramFlavor}.
*
* @since 1.15.0
*/
@FunctionalInterface
public interface HistogramFlavorPerMeterLookup {

/**
* Looks up the histogram flavor to use on a per-meter level. This will override
* the {@link OtlpConfig#histogramFlavor()} for matching Meters. The default
* implementation in {@link OtlpMeterRegistry} does a prefix match on the Meter's
* name in the map that {@link OtlpConfig#histogramFlavorPerMeter()} returns. This
* means that {@link OtlpConfig#histogramFlavorPerMeter()} provides the data while
* this method provides the logic for the lookup, and you can override them
* independently.
* @param id the {@link Meter.Id} the {@link HistogramFlavor} is configured for
* @return the histogram flavor mapped to the {@link Meter.Id}
* @see OtlpConfig#histogramFlavorPerMeter()
* @see OtlpConfig#histogramFlavor()
*/
HistogramFlavor getHistogramFlavor(OtlpConfig config, Meter.Id id);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should we make this and MaxBucketsPerMeterLookup#getMaxBuckets @Nullable?
In the current case the lookup should provide a fallback, if we make it nullable we can do that for the user.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think allowing nulls and falling back to the default value makes sense.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I've updated the signature along with incorporating this change such that the mapping function should not deal with the default value other than to return null. This feels cleaner than the expectation of implementations calling specific methods on OtlpConfig


}

/**
* Overridable lookup mechanism for max bucket count. This has no effect on a meter if
* it does not have an exponential bucket histogram configured.
*
* @since 1.15.0
*/
@FunctionalInterface
public interface MaxBucketsPerMeterLookup {

/**
* Looks up the max bucket count to use on a per-meter level. This will override
* the {@link OtlpConfig#maxBucketCount()} for matching Meters. The default
* implementation in {@link OtlpMeterRegistry} does a prefix match on the Meter's
* name in the map that {@link OtlpConfig#maxBucketsPerMeter()} returns. This
* means that {@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 id the {@link Meter.Id} the max bucket count is configured for
* @return the max bucket count mapped to the {@link Meter.Id}
* @see OtlpConfig#maxBucketsPerMeter()
* @see OtlpConfig#maxBucketCount()
*/
Integer getMaxBuckets(OtlpConfig config, Meter.Id id);

}

/**
* Builder for {@link OtlpMeterRegistry}.
*
Expand All @@ -512,9 +588,15 @@ public static class Builder {

private OtlpMetricsSender metricsSender;

private HistogramFlavorPerMeterLookup histogramFlavorPerMeterLookup;

private MaxBucketsPerMeterLookup maxBucketsPerMeterLookup;

private Builder(OtlpConfig otlpConfig) {
this.otlpConfig = otlpConfig;
this.metricsSender = new OtlpHttpMetricsSender(new HttpUrlConnectionSender());
this.histogramFlavorPerMeterLookup = OtlpMeterRegistry::histogramFlavorPerMeter;
this.maxBucketsPerMeterLookup = OtlpMeterRegistry::maxBucketsPerMeter;
}

/** Override the default clock. */
Expand All @@ -540,8 +622,19 @@ public Builder metricsSender(OtlpMetricsSender metricsSender) {
return this;
}

public Builder histogramFlavorPerMeterLookup(HistogramFlavorPerMeterLookup histogramFlavorPerMeterLookup) {
this.histogramFlavorPerMeterLookup = histogramFlavorPerMeterLookup;
return this;
}

public Builder maxBucketsPerMeterLookup(MaxBucketsPerMeterLookup maxBucketsPerMeterLookup) {
this.maxBucketsPerMeterLookup = maxBucketsPerMeterLookup;
return this;
}

public OtlpMeterRegistry build() {
return new OtlpMeterRegistry(otlpConfig, clock, threadFactory, metricsSender);
return new OtlpMeterRegistry(otlpConfig, clock, threadFactory, metricsSender, histogramFlavorPerMeterLookup,
maxBucketsPerMeterLookup);
}

}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand Down Expand Up @@ -257,24 +257,22 @@ 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
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
Expand All @@ -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
Expand Down
Loading