From 1bdb7dae41d24435cc41465d695d916e5f3349d1 Mon Sep 17 00:00:00 2001 From: Moritz Halbritter Date: Fri, 21 Mar 2025 14:18:29 +0100 Subject: [PATCH] Auto-configure Otel's MeterProvider where appropriate Closes gh-44833 --- .../otlp/OtlpLoggingConfigurations.java | 8 ++- .../otlp/OtlpTracingConfigurations.java | 8 ++- .../OtlpLoggingAutoConfigurationTests.java | 65 ++++++++++++++++--- .../OtlpTracingAutoConfigurationTests.java | 51 ++++++++++++++- 4 files changed, 119 insertions(+), 13 deletions(-) diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/logging/otlp/OtlpLoggingConfigurations.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/logging/otlp/OtlpLoggingConfigurations.java index 24e5e21b6e..d3c93f7716 100644 --- a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/logging/otlp/OtlpLoggingConfigurations.java +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/logging/otlp/OtlpLoggingConfigurations.java @@ -18,11 +18,13 @@ package org.springframework.boot.actuate.autoconfigure.logging.otlp; import java.util.Locale; +import io.opentelemetry.api.metrics.MeterProvider; import io.opentelemetry.exporter.otlp.http.logs.OtlpHttpLogRecordExporter; import io.opentelemetry.exporter.otlp.http.logs.OtlpHttpLogRecordExporterBuilder; import io.opentelemetry.exporter.otlp.logs.OtlpGrpcLogRecordExporter; import io.opentelemetry.exporter.otlp.logs.OtlpGrpcLogRecordExporterBuilder; +import org.springframework.beans.factory.ObjectProvider; import org.springframework.boot.actuate.autoconfigure.logging.ConditionalOnEnabledLoggingExport; import org.springframework.boot.autoconfigure.condition.ConditionalOnBean; import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; @@ -83,26 +85,28 @@ final class OtlpLoggingConfigurations { @Bean @ConditionalOnProperty(name = "management.otlp.logging.transport", havingValue = "http", matchIfMissing = true) OtlpHttpLogRecordExporter otlpHttpLogRecordExporter(OtlpLoggingProperties properties, - OtlpLoggingConnectionDetails connectionDetails) { + OtlpLoggingConnectionDetails connectionDetails, ObjectProvider meterProvider) { OtlpHttpLogRecordExporterBuilder builder = OtlpHttpLogRecordExporter.builder() .setEndpoint(connectionDetails.getUrl(Transport.HTTP)) .setTimeout(properties.getTimeout()) .setConnectTimeout(properties.getConnectTimeout()) .setCompression(properties.getCompression().name().toLowerCase(Locale.US)); properties.getHeaders().forEach(builder::addHeader); + meterProvider.ifAvailable(builder::setMeterProvider); return builder.build(); } @Bean @ConditionalOnProperty(name = "management.otlp.logging.transport", havingValue = "grpc") OtlpGrpcLogRecordExporter otlpGrpcLogRecordExporter(OtlpLoggingProperties properties, - OtlpLoggingConnectionDetails connectionDetails) { + OtlpLoggingConnectionDetails connectionDetails, ObjectProvider meterProvider) { OtlpGrpcLogRecordExporterBuilder builder = OtlpGrpcLogRecordExporter.builder() .setEndpoint(connectionDetails.getUrl(Transport.GRPC)) .setTimeout(properties.getTimeout()) .setConnectTimeout(properties.getConnectTimeout()) .setCompression(properties.getCompression().name().toLowerCase(Locale.US)); properties.getHeaders().forEach(builder::addHeader); + meterProvider.ifAvailable(builder::setMeterProvider); return builder.build(); } diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/tracing/otlp/OtlpTracingConfigurations.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/tracing/otlp/OtlpTracingConfigurations.java index 949b137b55..892f544cec 100644 --- a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/tracing/otlp/OtlpTracingConfigurations.java +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/tracing/otlp/OtlpTracingConfigurations.java @@ -18,11 +18,13 @@ package org.springframework.boot.actuate.autoconfigure.tracing.otlp; import java.util.Locale; +import io.opentelemetry.api.metrics.MeterProvider; import io.opentelemetry.exporter.otlp.http.trace.OtlpHttpSpanExporter; import io.opentelemetry.exporter.otlp.http.trace.OtlpHttpSpanExporterBuilder; import io.opentelemetry.exporter.otlp.trace.OtlpGrpcSpanExporter; import io.opentelemetry.exporter.otlp.trace.OtlpGrpcSpanExporterBuilder; +import org.springframework.beans.factory.ObjectProvider; import org.springframework.boot.actuate.autoconfigure.tracing.ConditionalOnEnabledTracing; import org.springframework.boot.autoconfigure.condition.ConditionalOnBean; import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; @@ -81,26 +83,28 @@ class OtlpTracingConfigurations { @Bean @ConditionalOnProperty(name = "management.otlp.tracing.transport", havingValue = "http", matchIfMissing = true) OtlpHttpSpanExporter otlpHttpSpanExporter(OtlpTracingProperties properties, - OtlpTracingConnectionDetails connectionDetails) { + OtlpTracingConnectionDetails connectionDetails, ObjectProvider meterProvider) { OtlpHttpSpanExporterBuilder builder = OtlpHttpSpanExporter.builder() .setEndpoint(connectionDetails.getUrl(Transport.HTTP)) .setTimeout(properties.getTimeout()) .setConnectTimeout(properties.getConnectTimeout()) .setCompression(properties.getCompression().name().toLowerCase(Locale.ROOT)); properties.getHeaders().forEach(builder::addHeader); + meterProvider.ifAvailable(builder::setMeterProvider); return builder.build(); } @Bean @ConditionalOnProperty(name = "management.otlp.tracing.transport", havingValue = "grpc") OtlpGrpcSpanExporter otlpGrpcSpanExporter(OtlpTracingProperties properties, - OtlpTracingConnectionDetails connectionDetails) { + OtlpTracingConnectionDetails connectionDetails, ObjectProvider meterProvider) { OtlpGrpcSpanExporterBuilder builder = OtlpGrpcSpanExporter.builder() .setEndpoint(connectionDetails.getUrl(Transport.GRPC)) .setTimeout(properties.getTimeout()) .setConnectTimeout(properties.getConnectTimeout()) .setCompression(properties.getCompression().name().toLowerCase(Locale.ROOT)); properties.getHeaders().forEach(builder::addHeader); + meterProvider.ifAvailable(builder::setMeterProvider); return builder.build(); } diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/logging/otlp/OtlpLoggingAutoConfigurationTests.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/logging/otlp/OtlpLoggingAutoConfigurationTests.java index 34a70bca0d..017f3dea55 100644 --- a/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/logging/otlp/OtlpLoggingAutoConfigurationTests.java +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/logging/otlp/OtlpLoggingAutoConfigurationTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2024 the original author or authors. + * Copyright 2012-2025 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -16,8 +16,13 @@ package org.springframework.boot.actuate.autoconfigure.logging.otlp; +import java.util.function.Supplier; + +import io.opentelemetry.api.metrics.MeterProvider; import io.opentelemetry.exporter.otlp.http.logs.OtlpHttpLogRecordExporter; +import io.opentelemetry.exporter.otlp.http.logs.OtlpHttpLogRecordExporterBuilder; import io.opentelemetry.exporter.otlp.logs.OtlpGrpcLogRecordExporter; +import io.opentelemetry.exporter.otlp.logs.OtlpGrpcLogRecordExporterBuilder; import io.opentelemetry.sdk.logs.export.LogRecordExporter; import okhttp3.HttpUrl; import org.junit.jupiter.api.Test; @@ -30,6 +35,7 @@ import org.springframework.boot.test.context.FilteredClassLoader; import org.springframework.boot.test.context.runner.ApplicationContextRunner; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; +import org.springframework.test.util.ReflectionTestUtils; import static org.assertj.core.api.Assertions.assertThat; @@ -37,6 +43,7 @@ import static org.assertj.core.api.Assertions.assertThat; * Tests for {@link OtlpLoggingAutoConfiguration}. * * @author Toshiaki Maki + * @author Moritz Halbritter */ class OtlpLoggingAutoConfigurationTests { @@ -118,7 +125,6 @@ class OtlpLoggingAutoConfigurationTests { assertThat(otlpHttpLogRecordExporter).extracting("delegate.httpSender.url") .isEqualTo(HttpUrl.get("https://otel.example.com/v1/logs")); }); - } @Test @@ -155,31 +161,74 @@ class OtlpLoggingAutoConfigurationTests { }); } + @Test + @SuppressWarnings("unchecked") + void httpShouldUseMeterProviderIfSet() { + this.contextRunner.withUserConfiguration(MeterProviderConfiguration.class) + .withPropertyValues("management.otlp.logging.endpoint=http://localhost:4318/v1/logs") + .run((context) -> { + OtlpHttpLogRecordExporter otlpHttpLogRecordExporter = context.getBean(OtlpHttpLogRecordExporter.class); + OtlpHttpLogRecordExporterBuilder builder = otlpHttpLogRecordExporter.toBuilder(); + Supplier meterProviderSupplier = (Supplier) ReflectionTestUtils + .getField(ReflectionTestUtils.getField(builder, "delegate"), "meterProviderSupplier"); + assertThat(meterProviderSupplier).isNotNull(); + assertThat(meterProviderSupplier.get()).isSameAs(MeterProviderConfiguration.meterProvider); + }); + } + + @Test + @SuppressWarnings("unchecked") + void grpcShouldUseMeterProviderIfSet() { + this.contextRunner.withUserConfiguration(MeterProviderConfiguration.class) + .withPropertyValues("management.otlp.logging.endpoint=http://localhost:4318/v1/logs", + "management.otlp.logging.transport=grpc") + .run((context) -> { + OtlpGrpcLogRecordExporter otlpGrpcLogRecordExporter = context.getBean(OtlpGrpcLogRecordExporter.class); + OtlpGrpcLogRecordExporterBuilder builder = otlpGrpcLogRecordExporter.toBuilder(); + Supplier meterProviderSupplier = (Supplier) ReflectionTestUtils + .getField(ReflectionTestUtils.getField(builder, "delegate"), "meterProviderSupplier"); + assertThat(meterProviderSupplier).isNotNull(); + assertThat(meterProviderSupplier.get()).isSameAs(MeterProviderConfiguration.meterProvider); + }); + } + @Configuration(proxyBeanMethods = false) - public static class CustomHttpExporterConfiguration { + private static final class MeterProviderConfiguration { + + static final MeterProvider meterProvider = (instrumentationScopeName) -> null; @Bean - public OtlpHttpLogRecordExporter customOtlpHttpLogRecordExporter() { + MeterProvider meterProvider() { + return meterProvider; + } + + } + + @Configuration(proxyBeanMethods = false) + private static final class CustomHttpExporterConfiguration { + + @Bean + OtlpHttpLogRecordExporter customOtlpHttpLogRecordExporter() { return OtlpHttpLogRecordExporter.builder().build(); } } @Configuration(proxyBeanMethods = false) - public static class CustomGrpcExporterConfiguration { + private static final class CustomGrpcExporterConfiguration { @Bean - public OtlpGrpcLogRecordExporter customOtlpGrpcLogRecordExporter() { + OtlpGrpcLogRecordExporter customOtlpGrpcLogRecordExporter() { return OtlpGrpcLogRecordExporter.builder().build(); } } @Configuration(proxyBeanMethods = false) - public static class CustomOtlpLoggingConnectionDetails { + private static final class CustomOtlpLoggingConnectionDetails { @Bean - public OtlpLoggingConnectionDetails customOtlpLoggingConnectionDetails() { + OtlpLoggingConnectionDetails customOtlpLoggingConnectionDetails() { return (transport) -> "https://otel.example.com/v1/logs"; } diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/tracing/otlp/OtlpTracingAutoConfigurationTests.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/tracing/otlp/OtlpTracingAutoConfigurationTests.java index 174592638a..4cdeb7dc64 100644 --- a/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/tracing/otlp/OtlpTracingAutoConfigurationTests.java +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/tracing/otlp/OtlpTracingAutoConfigurationTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2024 the original author or authors. + * Copyright 2012-2025 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -16,8 +16,13 @@ package org.springframework.boot.actuate.autoconfigure.tracing.otlp; +import java.util.function.Supplier; + +import io.opentelemetry.api.metrics.MeterProvider; import io.opentelemetry.exporter.otlp.http.trace.OtlpHttpSpanExporter; +import io.opentelemetry.exporter.otlp.http.trace.OtlpHttpSpanExporterBuilder; import io.opentelemetry.exporter.otlp.trace.OtlpGrpcSpanExporter; +import io.opentelemetry.exporter.otlp.trace.OtlpGrpcSpanExporterBuilder; import io.opentelemetry.sdk.trace.export.SpanExporter; import okhttp3.HttpUrl; import org.junit.jupiter.api.Test; @@ -28,6 +33,7 @@ import org.springframework.boot.test.context.FilteredClassLoader; import org.springframework.boot.test.context.runner.ApplicationContextRunner; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; +import org.springframework.test.util.ReflectionTestUtils; import static org.assertj.core.api.Assertions.assertThat; @@ -147,6 +153,49 @@ class OtlpTracingAutoConfigurationTests { }); } + @Test + @SuppressWarnings("unchecked") + void httpShouldUseMeterProviderIfSet() { + this.contextRunner.withUserConfiguration(MeterProviderConfiguration.class) + .withPropertyValues("management.otlp.tracing.endpoint=http://localhost:4318/v1/traces") + .run((context) -> { + OtlpHttpSpanExporter otlpHttpSpanExporter = context.getBean(OtlpHttpSpanExporter.class); + OtlpHttpSpanExporterBuilder builder = otlpHttpSpanExporter.toBuilder(); + Supplier meterProviderSupplier = (Supplier) ReflectionTestUtils + .getField(ReflectionTestUtils.getField(builder, "delegate"), "meterProviderSupplier"); + assertThat(meterProviderSupplier).isNotNull(); + assertThat(meterProviderSupplier.get()).isSameAs(MeterProviderConfiguration.meterProvider); + }); + } + + @Test + @SuppressWarnings("unchecked") + void grpcShouldUseMeterProviderIfSet() { + this.contextRunner.withUserConfiguration(MeterProviderConfiguration.class) + .withPropertyValues("management.otlp.tracing.endpoint=http://localhost:4318/v1/traces", + "management.otlp.tracing.transport=grpc") + .run((context) -> { + OtlpGrpcSpanExporter otlpGrpcSpanExporter = context.getBean(OtlpGrpcSpanExporter.class); + OtlpGrpcSpanExporterBuilder builder = otlpGrpcSpanExporter.toBuilder(); + Supplier meterProviderSupplier = (Supplier) ReflectionTestUtils + .getField(ReflectionTestUtils.getField(builder, "delegate"), "meterProviderSupplier"); + assertThat(meterProviderSupplier).isNotNull(); + assertThat(meterProviderSupplier.get()).isSameAs(MeterProviderConfiguration.meterProvider); + }); + } + + @Configuration(proxyBeanMethods = false) + private static final class MeterProviderConfiguration { + + static final MeterProvider meterProvider = (instrumentationScopeName) -> null; + + @Bean + MeterProvider meterProvider() { + return meterProvider; + } + + } + @Configuration(proxyBeanMethods = false) private static final class CustomHttpExporterConfiguration {