diff --git a/implementations/micrometer-registry-otlp/build.gradle b/implementations/micrometer-registry-otlp/build.gradle index 4d7bc0e06a..89843fed4b 100644 --- a/implementations/micrometer-registry-otlp/build.gradle +++ b/implementations/micrometer-registry-otlp/build.gradle @@ -10,6 +10,7 @@ dependencies { testImplementation 'io.rest-assured:rest-assured' testImplementation 'org.testcontainers:junit-jupiter' testImplementation 'org.awaitility:awaitility' + testImplementation libs.mockitoCore5 } dockerTest { 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 3427ff2b31..6e58f37bd1 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 @@ -91,6 +91,8 @@ public class OtlpMeterRegistry extends PushMeterRegistry { private final TimeUnit baseTimeUnit; + private final String userAgentHeader; + // Time when the last scheduled rollOver has started. Applicable only for delta // flavour. private volatile long lastMeterRolloverStartTime = -1; @@ -117,15 +119,17 @@ public OtlpMeterRegistry(OtlpConfig config, Clock clock, ThreadFactory threadFac this(config, clock, threadFactory, new HttpUrlConnectionSender()); } + // VisibleForTesting // not public until we decide what we want to expose in public API // HttpSender may not be a good idea if we will support a non-HTTP transport - private OtlpMeterRegistry(OtlpConfig config, Clock clock, ThreadFactory threadFactory, HttpSender httpSender) { + OtlpMeterRegistry(OtlpConfig config, Clock clock, ThreadFactory threadFactory, HttpSender httpSender) { super(config, clock); this.config = config; this.baseTimeUnit = config.baseTimeUnit(); this.httpSender = httpSender; this.resource = Resource.newBuilder().addAllAttributes(getResourceAttributes()).build(); this.aggregationTemporality = config.aggregationTemporality(); + this.userAgentHeader = getUserAgentHeader(); config().namingConvention(NamingConvention.dot); start(threadFactory); } @@ -175,6 +179,7 @@ protected void publish() { .build()) .build(); HttpSender.Request.Builder httpRequest = this.httpSender.post(this.config.url()) + .withHeader("User-Agent", this.userAgentHeader) .withContent("application/x-protobuf", request.toByteArray()); this.config.headers().forEach(httpRequest::withHeader); HttpSender.Response response = httpRequest.send(); @@ -487,4 +492,11 @@ static double[] getSloWithPositiveInf(DistributionStatisticConfig distributionSt return sloWithPositiveInf; } + private String getUserAgentHeader() { + if (this.getClass().getPackage().getImplementationVersion() == null) { + return "Micrometer-OTLP-Exporter-Java"; + } + return "Micrometer-OTLP-Exporter-Java/" + this.getClass().getPackage().getImplementationVersion(); + } + } 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 e86e67d6c2..9d033f7ed8 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 @@ -15,14 +15,17 @@ */ package io.micrometer.registry.otlp; -import io.micrometer.core.instrument.*; +import io.micrometer.core.Issue; import io.micrometer.core.instrument.Timer; +import io.micrometer.core.instrument.*; import io.micrometer.core.instrument.distribution.DistributionStatisticConfig; -import io.micrometer.core.instrument.Timer; +import io.micrometer.core.instrument.util.NamedThreadFactory; +import io.micrometer.core.ipc.http.HttpSender; import io.opentelemetry.proto.metrics.v1.ExponentialHistogramDataPoint; import io.opentelemetry.proto.metrics.v1.HistogramDataPoint; import io.opentelemetry.proto.metrics.v1.Metric; import io.opentelemetry.proto.metrics.v1.NumberDataPoint; +import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import java.io.IOException; @@ -31,6 +34,8 @@ import java.util.concurrent.TimeUnit; import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.ArgumentMatchers.assertArg; +import static org.mockito.Mockito.*; import static uk.org.webcompere.systemstubs.SystemStubs.withEnvironmentVariables; /** @@ -47,16 +52,27 @@ abstract class OtlpMeterRegistryTest { protected static final Tag meterTag = Tag.of("key", "value"); - protected MockClock clock = new MockClock(); + protected MockClock clock; - OtlpMeterRegistry registry = new OtlpMeterRegistry(otlpConfig(), clock); + private HttpSender mockHttpSender; - OtlpMeterRegistry registryWithExponentialHistogram = new OtlpMeterRegistry(exponentialHistogramOtlpConfig(), clock); + OtlpMeterRegistry registry; + + OtlpMeterRegistry registryWithExponentialHistogram; abstract OtlpConfig otlpConfig(); abstract OtlpConfig exponentialHistogramOtlpConfig(); + @BeforeEach + void setUp() { + this.clock = new MockClock(); + this.mockHttpSender = mock(HttpSender.class); + this.registry = new OtlpMeterRegistry(otlpConfig(), this.clock, + new NamedThreadFactory("otlp-metrics-publisher"), this.mockHttpSender); + this.registryWithExponentialHistogram = new OtlpMeterRegistry(exponentialHistogramOtlpConfig(), clock); + } + // If the service.name was not specified, SDKs MUST fallback to 'unknown_service' @Test void unknownServiceByDefault() { @@ -129,6 +145,23 @@ void timeGauge() { + " time_unix_nano: 1000000\n" + " as_double: 0.024\n" + " }\n" + "}\n"); } + @Issue("#5577") + @Test + void httpHeaders() throws Throwable { + HttpSender.Request.Builder builder = HttpSender.Request.build(otlpConfig().url(), this.mockHttpSender); + when(mockHttpSender.post(otlpConfig().url())).thenReturn(builder); + + when(mockHttpSender.send(isA(HttpSender.Request.class))).thenReturn(new HttpSender.Response(200, "")); + + writeToMetric(TimeGauge.builder("gauge.time", this, TimeUnit.MICROSECONDS, o -> 24).register(registry)); + registry.publish(); + + verify(this.mockHttpSender).send(assertArg(request -> { + assertThat(request.getRequestHeaders().get("User-Agent")).startsWith("Micrometer-OTLP-Exporter-Java"); + assertThat(request.getRequestHeaders()).containsEntry("Content-Type", "application/x-protobuf"); + })); + } + @Test void distributionWithPercentileShouldWriteSummary() { Timer.Builder timer = Timer.builder("timer")