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,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()
*/
Expand Down Expand Up @@ -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()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -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);
Expand Down Expand Up @@ -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()
Expand Down Expand Up @@ -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<String, HistogramFlavor> 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<String, Integer> perMeterMapping, Meter.Id id);

}

@Nullable
private static <T> T lookup(Map<String, T> values, Meter.Id id) {
if (values.isEmpty()) {
return null;
}
return doLookup(values, id);
}

@Nullable
private static <T> T doLookup(Map<String, T> 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}.
*
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