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,18 +229,40 @@ 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 #histogramFlavorPerMeter(Meter.Id)} uses the result of this method
* to look up the {@link HistogramFlavor} by {@link Meter.Id} (exact match on the
* Meter's name by default). This means that this method provides the data while
* {@link #histogramFlavorPerMeter(Meter.Id)} 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 #histogramFlavorPerMeter(Meter.Id)
* @see #histogramFlavor()
*/
default Map<String, HistogramFlavor> histogramFlavorPerMeter() {
return getStringMap(this, "histogramFlavorPerMeter", HistogramFlavor::fromString)
.orElse(Collections.emptyMap());
}

/**
* Looks up the histogram flavor to use on a per-meter level. This will override the
* {@link #histogramFlavor()} lookup behavior for matching Meters. By default, the key
* is used to do an exact match on the Meter's name in the map that
* {@link #histogramFlavorPerMeter()} returns. This means that
* {@link #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}
* @since 1.15.0
* @see #histogramFlavorPerMeter()
* @see #histogramFlavor()
*/
default HistogramFlavor histogramFlavorPerMeter(Meter.Id id) {
return histogramFlavorPerMeter().getOrDefault(id.getName(), histogramFlavor());
Copy link
Member Author

Choose a reason for hiding this comment

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

As mentioned offline, I think we should make a type for the function that is this method's signature and make it configurable on the registry Builder rather than in the OtlpConfig. The reason is to keep logic in the registry and config in the config interface - this feels a bit too much like logic for how the config is used. Probably it would be most usable for most people for the default implementation to copy the longest starts-with match wins behavior they have in Spring Boot for some similar types of configuration properties. If that were the case, I suspect the vast majority of users would not want to customize the logic anyway, so the fact it needs to be done via the builder is not much of an issue. Thoughts?

}

/**
* Max scale to use for exponential histograms, if configured.
* @return maxScale
Expand All @@ -266,18 +289,41 @@ 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 #maxBucketsPerMeter(Meter.Id)} uses the result of this method to
* look up the max bucket count by {@link Meter.Id} (exact match on the Meter's name
* by default). This means that this method provides the data while
* {@link #maxBucketsPerMeter(Meter.Id)} 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 #maxBucketsPerMeter(Meter.Id)
* @see #maxBucketCount()
*/
default Map<String, Integer> maxBucketsPerMeter() {
return getStringMap(this, "maxBucketsPerMeter", Integer::parseInt).orElse(Collections.emptyMap());
}

/**
* Looks up the max bucket count to use on a per-meter level. This will override the
* {@link #maxBucketCount()} lookup behavior for matching Meters. By default, the key
* is used to do an exact match on the Meter's name in the map that
* {@link #maxBucketsPerMeter()} returns. This means that
* {@link #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}
* @since 1.15.0
* @see #maxBucketsPerMeter()
* @see #maxBucketCount()
*/
default Integer maxBucketsPerMeter(Meter.Id id) {
return maxBucketsPerMeter().getOrDefault(id.getName(), maxBucketCount());
}

@Override
default Validated<?> validate() {
return checkAll(this, c -> PushRegistryConfig.validate(c), checkRequired("url", OtlpConfig::url),
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 @@ -430,13 +430,15 @@ private Histogram getHistogram(Meter.Id id, DistributionStatisticConfig distribu
}

private int getMaxBuckets(Meter.Id id) {
return config.maxBucketsPerMeter().getOrDefault(id.getName(), config.maxBucketCount());
Integer perMeterMaxBuckets = config.maxBucketsPerMeter(id);
return perMeterMaxBuckets == null ? config.maxBucketCount() : perMeterMaxBuckets;
}

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

final double[] serviceLevelObjectiveBoundaries = distributionStatisticConfig
.getServiceLevelObjectiveBoundaries();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,8 @@
*/
package io.micrometer.registry.otlp;

import io.micrometer.core.instrument.Meter;
import io.micrometer.core.instrument.Tags;
import io.micrometer.core.instrument.config.InvalidConfigurationException;
import org.junit.jupiter.api.Test;

Expand All @@ -25,7 +27,6 @@
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 uk.org.webcompere.systemstubs.SystemStubs.withEnvironmentVariable;
Expand Down Expand Up @@ -284,9 +285,41 @@ 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(idWithName("a.b.c")))
.isEqualTo(HistogramFlavor.EXPLICIT_BUCKET_HISTOGRAM);
assertThat(otlpConfig.histogramFlavorPerMeter(idWithName("expo")))
.isEqualTo(HistogramFlavor.BASE2_EXPONENTIAL_BUCKET_HISTOGRAM);
}

@Test
void customHistogramFlavorPerMeterFunction() {
Map<String, String> properties = new HashMap<>();
properties.put("otlp.histogramFlavorPerMeter",
"a.b.c=explicit_bucket_histogram,expo=base2_exponential_bucket_histogram");
OtlpConfig otlpConfig = new OtlpConfig() {
@Override
public String get(String key) {
return properties.get(key);
}

@Override
public HistogramFlavor histogramFlavorPerMeter(Meter.Id id) {
for (Map.Entry<String, HistogramFlavor> entry : histogramFlavorPerMeter().entrySet()) {
if (id.getName().startsWith(entry.getKey())) {
return entry.getValue();
}
}
return histogramFlavor();
}
};
assertThat(otlpConfig.validate().isValid()).isTrue();
assertThat(otlpConfig.histogramFlavorPerMeter(idWithName("something"))).isEqualTo(otlpConfig.histogramFlavor());
assertThat(otlpConfig.histogramFlavorPerMeter(idWithName("a.b.c")))
.isEqualTo(HistogramFlavor.EXPLICIT_BUCKET_HISTOGRAM);
assertThat(otlpConfig.histogramFlavorPerMeter(idWithName("expo")))
.isEqualTo(HistogramFlavor.BASE2_EXPONENTIAL_BUCKET_HISTOGRAM);
assertThat(otlpConfig.histogramFlavorPerMeter(idWithName("expo.other")))
.isEqualTo(HistogramFlavor.BASE2_EXPONENTIAL_BUCKET_HISTOGRAM);
}

@Test
Expand All @@ -295,7 +328,11 @@ void maxBucketsPerMeter() {
properties.put("otlp.maxBucketsPerMeter", "a.b.c = 10");
OtlpConfig otlpConfig = properties::get;
assertThat(otlpConfig.validate().isValid()).isTrue();
assertThat(otlpConfig.maxBucketsPerMeter()).containsExactly(entry("a.b.c", 10));
assertThat(otlpConfig.maxBucketsPerMeter(idWithName("a.b.c"))).isEqualTo(10);
}

Meter.Id idWithName(String name) {
return new Meter.Id(name, Tags.empty(), null, null, Meter.Type.OTHER);
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -427,8 +427,9 @@ public HistogramFlavor histogramFlavor() {
}

@Override
public Map<String, HistogramFlavor> histogramFlavorPerMeter() {
return Collections.singletonMap("expo", HistogramFlavor.BASE2_EXPONENTIAL_BUCKET_HISTOGRAM);
public HistogramFlavor histogramFlavorPerMeter(Meter.Id id) {
return id.getName().equals("expo") ? HistogramFlavor.BASE2_EXPONENTIAL_BUCKET_HISTOGRAM
: histogramFlavor();
}
};
OtlpMeterRegistry meterRegistry = new OtlpMeterRegistry(config, clock);
Expand Down Expand Up @@ -477,8 +478,8 @@ public int maxBucketCount() {
}

@Override
public Map<String, Integer> maxBucketsPerMeter() {
return Collections.singletonMap("low.variation", 15);
public Integer maxBucketsPerMeter(Meter.Id id) {
return id.getName().equals("low.variation") ? 15 : maxBucketCount();
}
};
OtlpMeterRegistry meterRegistry = new OtlpMeterRegistry(config, clock);
Expand Down