Auto-configure Otel's MeterProvider where appropriate

Closes gh-44833
This commit is contained in:
Moritz Halbritter
2025-03-21 14:18:29 +01:00
parent bb6b477fc5
commit 1bdb7dae41
4 changed files with 119 additions and 13 deletions

View File

@@ -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> 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> 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();
}

View File

@@ -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> 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> 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();
}

View File

@@ -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<MeterProvider> meterProviderSupplier = (Supplier<MeterProvider>) 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<MeterProvider> meterProviderSupplier = (Supplier<MeterProvider>) 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";
}

View File

@@ -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<MeterProvider> meterProviderSupplier = (Supplier<MeterProvider>) 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<MeterProvider> meterProviderSupplier = (Supplier<MeterProvider>) 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 {