Create spring-boot-opentelemetry module

Renames `management.otlp.logging` properties to
`management.opentelemetry.logging`.

Issue: 46149
This commit is contained in:
Phillip Webb 2025-05-27 15:27:27 -07:00
parent 3909b9d483
commit ded9701db5
57 changed files with 930 additions and 779 deletions

View File

@ -119,6 +119,7 @@ include "spring-boot-project:spring-boot-metrics"
include "spring-boot-project:spring-boot-mongodb"
include "spring-boot-project:spring-boot-mustache"
include "spring-boot-project:spring-boot-netty"
include "spring-boot-project:spring-boot-opentelemetry"
include "spring-boot-project:spring-boot-parent"
include "spring-boot-project:spring-boot-pulsar"
include "spring-boot-project:spring-boot-quartz"

View File

@ -56,6 +56,7 @@ dependencies {
optional(project(":spring-boot-project:spring-boot-jsonb"))
optional(project(":spring-boot-project:spring-boot-kafka"))
optional(project(":spring-boot-project:spring-boot-metrics"))
optional(project(":spring-boot-project:spring-boot-opentelemetry"))
optional(project(":spring-boot-project:spring-boot-r2dbc"))
optional(project(":spring-boot-project:spring-boot-restclient"))
optional(project(":spring-boot-project:spring-boot-security-oauth2-client"))

View File

@ -1,61 +0,0 @@
/*
* Copyright 2012-present 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.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.boot.actuate.autoconfigure.logging;
import io.opentelemetry.api.OpenTelemetry;
import io.opentelemetry.sdk.logs.LogRecordProcessor;
import io.opentelemetry.sdk.logs.SdkLoggerProvider;
import io.opentelemetry.sdk.logs.SdkLoggerProviderBuilder;
import io.opentelemetry.sdk.logs.export.BatchLogRecordProcessor;
import io.opentelemetry.sdk.logs.export.LogRecordExporter;
import io.opentelemetry.sdk.resources.Resource;
import org.springframework.beans.factory.ObjectProvider;
import org.springframework.boot.autoconfigure.AutoConfiguration;
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
import org.springframework.context.annotation.Bean;
/**
* {@link EnableAutoConfiguration Auto-configuration} for OpenTelemetry logging.
*
* @author Toshiaki Maki
* @since 3.4.0
*/
@AutoConfiguration
@ConditionalOnClass({ SdkLoggerProvider.class, OpenTelemetry.class })
public class OpenTelemetryLoggingAutoConfiguration {
@Bean
@ConditionalOnMissingBean
BatchLogRecordProcessor batchLogRecordProcessor(ObjectProvider<LogRecordExporter> logRecordExporters) {
return BatchLogRecordProcessor.builder(LogRecordExporter.composite(logRecordExporters.orderedStream().toList()))
.build();
}
@Bean
@ConditionalOnMissingBean
SdkLoggerProvider otelSdkLoggerProvider(Resource resource, ObjectProvider<LogRecordProcessor> logRecordProcessors,
ObjectProvider<SdkLoggerProviderBuilderCustomizer> customizers) {
SdkLoggerProviderBuilder builder = SdkLoggerProvider.builder().setResource(resource);
logRecordProcessors.orderedStream().forEach(builder::addLogRecordProcessor);
customizers.orderedStream().forEach((customizer) -> customizer.customize(builder));
return builder.build();
}
}

View File

@ -1,112 +0,0 @@
/*
* Copyright 2012-present 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.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
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;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.util.Assert;
/**
* Configurations imported by {@link OtlpLoggingAutoConfiguration}.
*
* @author Toshiaki Maki
*/
final class OtlpLoggingConfigurations {
@Configuration(proxyBeanMethods = false)
static class ConnectionDetails {
@Bean
@ConditionalOnMissingBean
@ConditionalOnProperty("management.otlp.logging.endpoint")
OtlpLoggingConnectionDetails otlpLoggingConnectionDetails(OtlpLoggingProperties properties) {
return new PropertiesOtlpLoggingConnectionDetails(properties);
}
/**
* Adapts {@link OtlpLoggingProperties} to {@link OtlpLoggingConnectionDetails}.
*/
static class PropertiesOtlpLoggingConnectionDetails implements OtlpLoggingConnectionDetails {
private final OtlpLoggingProperties properties;
PropertiesOtlpLoggingConnectionDetails(OtlpLoggingProperties properties) {
this.properties = properties;
}
@Override
public String getUrl(Transport transport) {
Assert.state(transport == this.properties.getTransport(),
"Requested transport %s doesn't match configured transport %s".formatted(transport,
this.properties.getTransport()));
return this.properties.getEndpoint();
}
}
}
@Configuration(proxyBeanMethods = false)
@ConditionalOnMissingBean({ OtlpGrpcLogRecordExporter.class, OtlpHttpLogRecordExporter.class })
@ConditionalOnBean(OtlpLoggingConnectionDetails.class)
@ConditionalOnEnabledLoggingExport("otlp")
static class Exporters {
@Bean
@ConditionalOnProperty(name = "management.otlp.logging.transport", havingValue = "http", matchIfMissing = true)
OtlpHttpLogRecordExporter otlpHttpLogRecordExporter(OtlpLoggingProperties properties,
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, 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

@ -24,7 +24,6 @@ import io.micrometer.registry.otlp.OtlpMetricsSender;
import org.springframework.beans.factory.ObjectProvider;
import org.springframework.boot.actuate.autoconfigure.metrics.export.ConditionalOnEnabledMetricsExport;
import org.springframework.boot.actuate.autoconfigure.metrics.export.simple.SimpleMetricsExportAutoConfiguration;
import org.springframework.boot.actuate.autoconfigure.opentelemetry.OpenTelemetryProperties;
import org.springframework.boot.autoconfigure.AutoConfiguration;
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
import org.springframework.boot.autoconfigure.condition.ConditionalOnBean;
@ -35,6 +34,7 @@ import org.springframework.boot.autoconfigure.thread.Threading;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.boot.metrics.autoconfigure.CompositeMeterRegistryAutoConfiguration;
import org.springframework.boot.metrics.autoconfigure.MetricsAutoConfiguration;
import org.springframework.boot.opentelemetry.autoconfigure.OpenTelemetryProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.core.env.Environment;
import org.springframework.core.task.VirtualThreadTaskExecutor;

View File

@ -28,8 +28,8 @@ import io.micrometer.registry.otlp.OtlpConfig;
import org.springframework.boot.actuate.autoconfigure.metrics.export.otlp.OtlpMetricsProperties.Meter;
import org.springframework.boot.actuate.autoconfigure.metrics.export.properties.StepRegistryPropertiesConfigAdapter;
import org.springframework.boot.actuate.autoconfigure.opentelemetry.OpenTelemetryProperties;
import org.springframework.boot.actuate.autoconfigure.opentelemetry.OpenTelemetryResourceAttributes;
import org.springframework.boot.opentelemetry.autoconfigure.OpenTelemetryProperties;
import org.springframework.boot.opentelemetry.autoconfigure.OpenTelemetryResourceAttributes;
import org.springframework.core.env.Environment;
import org.springframework.util.CollectionUtils;

View File

@ -11,10 +11,6 @@ org.springframework.boot.actuate.autoconfigure.context.ShutdownEndpointAutoConfi
org.springframework.boot.actuate.autoconfigure.endpoint.jackson.JacksonEndpointAutoConfiguration
org.springframework.boot.actuate.autoconfigure.endpoint.jmx.JmxEndpointAutoConfiguration
org.springframework.boot.actuate.autoconfigure.env.EnvironmentEndpointAutoConfiguration
org.springframework.boot.actuate.autoconfigure.logging.LogFileWebEndpointAutoConfiguration
org.springframework.boot.actuate.autoconfigure.logging.LoggersEndpointAutoConfiguration
org.springframework.boot.actuate.autoconfigure.logging.OpenTelemetryLoggingAutoConfiguration
org.springframework.boot.actuate.autoconfigure.logging.otlp.OtlpLoggingAutoConfiguration
org.springframework.boot.actuate.autoconfigure.management.HeapDumpWebEndpointAutoConfiguration
org.springframework.boot.actuate.autoconfigure.management.ThreadDumpEndpointAutoConfiguration
org.springframework.boot.actuate.autoconfigure.metrics.data.RepositoryMetricsAutoConfiguration
@ -44,7 +40,6 @@ org.springframework.boot.actuate.autoconfigure.observation.ObservationAutoConfig
org.springframework.boot.actuate.autoconfigure.observation.web.client.HttpClientObservationsAutoConfiguration
org.springframework.boot.actuate.autoconfigure.observation.web.reactive.WebFluxObservationAutoConfiguration
org.springframework.boot.actuate.autoconfigure.observation.web.servlet.WebMvcObservationAutoConfiguration
org.springframework.boot.actuate.autoconfigure.opentelemetry.OpenTelemetryAutoConfiguration
org.springframework.boot.actuate.autoconfigure.r2dbc.ConnectionFactoryHealthContributorAutoConfiguration
org.springframework.boot.actuate.autoconfigure.r2dbc.R2dbcObservationAutoConfiguration
org.springframework.boot.actuate.autoconfigure.sbom.SbomEndpointAutoConfiguration

View File

@ -1,226 +0,0 @@
/*
* Copyright 2012-present 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.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.boot.actuate.autoconfigure.logging;
import java.util.Collection;
import java.util.concurrent.atomic.AtomicInteger;
import io.opentelemetry.context.Context;
import io.opentelemetry.sdk.common.CompletableResultCode;
import io.opentelemetry.sdk.logs.LogRecordProcessor;
import io.opentelemetry.sdk.logs.ReadWriteLogRecord;
import io.opentelemetry.sdk.logs.SdkLoggerProvider;
import io.opentelemetry.sdk.logs.SdkLoggerProviderBuilder;
import io.opentelemetry.sdk.logs.data.LogRecordData;
import io.opentelemetry.sdk.logs.export.BatchLogRecordProcessor;
import io.opentelemetry.sdk.logs.export.LogRecordExporter;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.ValueSource;
import org.springframework.boot.actuate.autoconfigure.opentelemetry.OpenTelemetryAutoConfiguration;
import org.springframework.boot.autoconfigure.AutoConfigurations;
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 static org.assertj.core.api.Assertions.assertThat;
/**
* Tests for {@link OpenTelemetryLoggingAutoConfiguration}.
*
* @author Toshiaki Maki
*/
class OpenTelemetryLoggingAutoConfigurationTests {
private final ApplicationContextRunner contextRunner;
OpenTelemetryLoggingAutoConfigurationTests() {
this.contextRunner = new ApplicationContextRunner().withConfiguration(AutoConfigurations
.of(OpenTelemetryAutoConfiguration.class, OpenTelemetryLoggingAutoConfiguration.class));
}
@Test
void shouldSupplyBeans() {
this.contextRunner.run((context) -> {
assertThat(context).hasSingleBean(BatchLogRecordProcessor.class);
assertThat(context).hasSingleBean(SdkLoggerProvider.class);
});
}
@ParameterizedTest
@ValueSource(strings = { "io.opentelemetry.sdk.logs", "io.opentelemetry.api" })
void shouldNotSupplyBeansIfDependencyIsMissing(String packageName) {
this.contextRunner.withClassLoader(new FilteredClassLoader(packageName)).run((context) -> {
assertThat(context).doesNotHaveBean(BatchLogRecordProcessor.class);
assertThat(context).doesNotHaveBean(SdkLoggerProvider.class);
});
}
@Test
void shouldBackOffOnCustomBeans() {
this.contextRunner.withUserConfiguration(CustomConfig.class).run((context) -> {
assertThat(context).hasBean("customBatchLogRecordProcessor").hasSingleBean(BatchLogRecordProcessor.class);
assertThat(context.getBeansOfType(LogRecordProcessor.class)).hasSize(1);
assertThat(context).hasBean("customSdkLoggerProvider").hasSingleBean(SdkLoggerProvider.class);
});
}
@Test
void shouldAllowMultipleLogRecordExporters() {
this.contextRunner.withUserConfiguration(MultipleLogRecordExportersConfig.class).run((context) -> {
assertThat(context).hasSingleBean(BatchLogRecordProcessor.class);
assertThat(context.getBeansOfType(LogRecordExporter.class)).hasSize(2);
assertThat(context).hasBean("customLogRecordExporter1");
assertThat(context).hasBean("customLogRecordExporter2");
});
}
@Test
void shouldAllowMultipleLogRecordProcessorsInAdditionToBatchLogRecordProcessor() {
this.contextRunner.withUserConfiguration(MultipleLogRecordProcessorsConfig.class).run((context) -> {
assertThat(context).hasSingleBean(BatchLogRecordProcessor.class);
assertThat(context).hasSingleBean(SdkLoggerProvider.class);
assertThat(context.getBeansOfType(LogRecordProcessor.class)).hasSize(3);
assertThat(context).hasBean("batchLogRecordProcessor");
assertThat(context).hasBean("customLogRecordProcessor1");
assertThat(context).hasBean("customLogRecordProcessor2");
});
}
@Test
void shouldAllowMultipleSdkLoggerProviderBuilderCustomizers() {
this.contextRunner.withUserConfiguration(MultipleSdkLoggerProviderBuilderCustomizersConfig.class)
.run((context) -> {
assertThat(context).hasSingleBean(SdkLoggerProvider.class);
assertThat(context.getBeansOfType(SdkLoggerProviderBuilderCustomizer.class)).hasSize(2);
assertThat(context).hasBean("customSdkLoggerProviderBuilderCustomizer1");
assertThat(context).hasBean("customSdkLoggerProviderBuilderCustomizer2");
assertThat(context
.getBean("customSdkLoggerProviderBuilderCustomizer1", NoopSdkLoggerProviderBuilderCustomizer.class)
.called()).isEqualTo(1);
assertThat(context
.getBean("customSdkLoggerProviderBuilderCustomizer2", NoopSdkLoggerProviderBuilderCustomizer.class)
.called()).isEqualTo(1);
});
}
@Configuration(proxyBeanMethods = false)
public static class CustomConfig {
@Bean
public BatchLogRecordProcessor customBatchLogRecordProcessor() {
return BatchLogRecordProcessor.builder(new NoopLogRecordExporter()).build();
}
@Bean
public SdkLoggerProvider customSdkLoggerProvider() {
return SdkLoggerProvider.builder().build();
}
}
@Configuration(proxyBeanMethods = false)
public static class MultipleLogRecordExportersConfig {
@Bean
public LogRecordExporter customLogRecordExporter1() {
return new NoopLogRecordExporter();
}
@Bean
public LogRecordExporter customLogRecordExporter2() {
return new NoopLogRecordExporter();
}
}
@Configuration(proxyBeanMethods = false)
public static class MultipleLogRecordProcessorsConfig {
@Bean
public LogRecordProcessor customLogRecordProcessor1() {
return new NoopLogRecordProcessor();
}
@Bean
public LogRecordProcessor customLogRecordProcessor2() {
return new NoopLogRecordProcessor();
}
}
@Configuration(proxyBeanMethods = false)
public static class MultipleSdkLoggerProviderBuilderCustomizersConfig {
@Bean
public SdkLoggerProviderBuilderCustomizer customSdkLoggerProviderBuilderCustomizer1() {
return new NoopSdkLoggerProviderBuilderCustomizer();
}
@Bean
public SdkLoggerProviderBuilderCustomizer customSdkLoggerProviderBuilderCustomizer2() {
return new NoopSdkLoggerProviderBuilderCustomizer();
}
}
static class NoopLogRecordExporter implements LogRecordExporter {
@Override
public CompletableResultCode export(Collection<LogRecordData> logs) {
return CompletableResultCode.ofSuccess();
}
@Override
public CompletableResultCode flush() {
return CompletableResultCode.ofSuccess();
}
@Override
public CompletableResultCode shutdown() {
return CompletableResultCode.ofSuccess();
}
}
static class NoopLogRecordProcessor implements LogRecordProcessor {
@Override
public void onEmit(Context context, ReadWriteLogRecord logRecord) {
}
}
static class NoopSdkLoggerProviderBuilderCustomizer implements SdkLoggerProviderBuilderCustomizer {
final AtomicInteger called = new AtomicInteger(0);
@Override
public void customize(SdkLoggerProviderBuilder builder) {
this.called.incrementAndGet();
}
int called() {
return this.called.get();
}
}
}

View File

@ -26,7 +26,7 @@ import org.junit.jupiter.api.Test;
import org.springframework.boot.actuate.autoconfigure.metrics.export.otlp.OtlpMetricsExportAutoConfiguration.PropertiesOtlpMetricsConnectionDetails;
import org.springframework.boot.actuate.autoconfigure.metrics.export.otlp.OtlpMetricsProperties.Meter;
import org.springframework.boot.actuate.autoconfigure.opentelemetry.OpenTelemetryProperties;
import org.springframework.boot.opentelemetry.autoconfigure.OpenTelemetryProperties;
import org.springframework.mock.env.MockEnvironment;
import static org.assertj.core.api.Assertions.assertThat;

View File

@ -1,190 +0,0 @@
/*
* Copyright 2012-present 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.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.boot.actuate.autoconfigure.opentelemetry;
import io.opentelemetry.api.OpenTelemetry;
import io.opentelemetry.api.common.AttributeKey;
import io.opentelemetry.context.propagation.ContextPropagators;
import io.opentelemetry.sdk.OpenTelemetrySdk;
import io.opentelemetry.sdk.logs.SdkLoggerProvider;
import io.opentelemetry.sdk.metrics.SdkMeterProvider;
import io.opentelemetry.sdk.resources.Resource;
import io.opentelemetry.sdk.trace.SdkTracerProvider;
import org.junit.jupiter.api.Test;
import org.springframework.boot.autoconfigure.AutoConfiguration;
import org.springframework.boot.autoconfigure.AutoConfigurations;
import org.springframework.boot.context.annotation.ImportCandidates;
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 static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.entry;
import static org.mockito.Mockito.mock;
/**
* Tests for {@link OpenTelemetryAutoConfiguration}.
*
* @author Moritz Halbritter
*/
class OpenTelemetryAutoConfigurationTests {
private final ApplicationContextRunner runner = new ApplicationContextRunner()
.withConfiguration(AutoConfigurations.of(OpenTelemetryAutoConfiguration.class));
@Test
void isRegisteredInAutoConfigurationImports() {
assertThat(ImportCandidates.load(AutoConfiguration.class, null).getCandidates())
.contains(OpenTelemetryAutoConfiguration.class.getName());
}
@Test
void shouldProvideBeans() {
this.runner.run((context) -> {
assertThat(context).hasSingleBean(OpenTelemetrySdk.class);
assertThat(context).hasSingleBean(Resource.class);
});
}
@Test
void shouldBackOffIfOpenTelemetryIsNotOnClasspath() {
this.runner.withClassLoader(new FilteredClassLoader("io.opentelemetry")).run((context) -> {
assertThat(context).doesNotHaveBean(OpenTelemetrySdk.class);
assertThat(context).doesNotHaveBean(Resource.class);
});
}
@Test
void backsOffOnUserSuppliedBeans() {
this.runner.withUserConfiguration(UserConfiguration.class).run((context) -> {
assertThat(context).hasSingleBean(OpenTelemetry.class);
assertThat(context).hasBean("customOpenTelemetry");
assertThat(context).hasSingleBean(Resource.class);
assertThat(context).hasBean("customResource");
});
}
@Test
void shouldApplySpringApplicationNameToResource() {
this.runner.withPropertyValues("spring.application.name=my-application").run((context) -> {
Resource resource = context.getBean(Resource.class);
assertThat(resource.getAttributes().asMap())
.contains(entry(AttributeKey.stringKey("service.name"), "my-application"));
});
}
@Test
void shouldApplySpringApplicationGroupToResource() {
this.runner.withPropertyValues("spring.application.group=my-group").run((context) -> {
Resource resource = context.getBean(Resource.class);
assertThat(resource.getAttributes().asMap())
.contains(entry(AttributeKey.stringKey("service.group"), "my-group"));
});
}
@Test
void shouldNotApplySpringApplicationGroupIfNotSet() {
this.runner.run((context) -> {
Resource resource = context.getBean(Resource.class);
assertThat(resource.getAttributes().asMap()).doesNotContainKey(AttributeKey.stringKey("service.group"));
});
}
@Test
void shouldApplyServiceNamespaceIfApplicationGroupIsSet() {
this.runner.withPropertyValues("spring.application.group=my-group").run((context) -> {
Resource resource = context.getBean(Resource.class);
assertThat(resource.getAttributes().asMap()).containsEntry(AttributeKey.stringKey("service.namespace"),
"my-group");
});
}
@Test
void shouldNotApplyServiceNamespaceIfApplicationGroupIsNotSet() {
this.runner.run(((context) -> {
Resource resource = context.getBean(Resource.class);
assertThat(resource.getAttributes().asMap()).doesNotContainKey(AttributeKey.stringKey("service.namespace"));
}));
}
@Test
void shouldFallbackToDefaultApplicationNameIfSpringApplicationNameIsNotSet() {
this.runner.run((context) -> {
Resource resource = context.getBean(Resource.class);
assertThat(resource.getAttributes().asMap())
.contains(entry(AttributeKey.stringKey("service.name"), "unknown_service"));
});
}
@Test
void shouldApplyResourceAttributesFromProperties() {
this.runner.withPropertyValues("management.opentelemetry.resource-attributes.region=us-west").run((context) -> {
Resource resource = context.getBean(Resource.class);
assertThat(resource.getAttributes().asMap()).contains(entry(AttributeKey.stringKey("region"), "us-west"));
});
}
@Test
void shouldRegisterSdkTracerProviderIfAvailable() {
this.runner.withBean(SdkTracerProvider.class, () -> SdkTracerProvider.builder().build()).run((context) -> {
OpenTelemetry openTelemetry = context.getBean(OpenTelemetry.class);
assertThat(openTelemetry.getTracerProvider()).isNotNull();
});
}
@Test
void shouldRegisterContextPropagatorsIfAvailable() {
this.runner.withBean(ContextPropagators.class, ContextPropagators::noop).run((context) -> {
OpenTelemetry openTelemetry = context.getBean(OpenTelemetry.class);
assertThat(openTelemetry.getPropagators()).isNotNull();
});
}
@Test
void shouldRegisterSdkLoggerProviderIfAvailable() {
this.runner.withBean(SdkLoggerProvider.class, () -> SdkLoggerProvider.builder().build()).run((context) -> {
OpenTelemetry openTelemetry = context.getBean(OpenTelemetry.class);
assertThat(openTelemetry.getLogsBridge()).isNotNull();
});
}
@Test
void shouldRegisterSdkMeterProviderIfAvailable() {
this.runner.withBean(SdkMeterProvider.class, () -> SdkMeterProvider.builder().build()).run((context) -> {
OpenTelemetry openTelemetry = context.getBean(OpenTelemetry.class);
assertThat(openTelemetry.getMeterProvider()).isNotNull();
});
}
@Configuration(proxyBeanMethods = false)
static class UserConfiguration {
@Bean
OpenTelemetry customOpenTelemetry() {
return mock(OpenTelemetry.class);
}
@Bean
Resource customResource() {
return Resource.getDefault();
}
}
}

View File

@ -30,8 +30,8 @@ import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.EnumSource;
import org.slf4j.MDC;
import org.springframework.boot.actuate.autoconfigure.opentelemetry.OpenTelemetryAutoConfiguration;
import org.springframework.boot.autoconfigure.AutoConfigurations;
import org.springframework.boot.opentelemetry.autoconfigure.OpenTelemetrySdkAutoConfiguration;
import org.springframework.boot.test.context.runner.ApplicationContextRunner;
import org.springframework.boot.testsupport.classpath.ForkedClassPath;
import org.springframework.context.ApplicationContext;
@ -173,7 +173,7 @@ class BaggagePropagationIntegrationTests {
@Override
public ApplicationContextRunner get() {
return new ApplicationContextRunner().withInitializer(new OtelApplicationContextInitializer())
.withConfiguration(AutoConfigurations.of(OpenTelemetryAutoConfiguration.class,
.withConfiguration(AutoConfigurations.of(OpenTelemetrySdkAutoConfiguration.class,
org.springframework.boot.actuate.autoconfigure.tracing.OpenTelemetryTracingAutoConfiguration.class))
.withPropertyValues("management.tracing.baggage.remote-fields=x-vcap-request-id,country-code,bp",
"management.tracing.baggage.correlation.fields=country-code,bp");
@ -199,7 +199,7 @@ class BaggagePropagationIntegrationTests {
@Override
public ApplicationContextRunner get() {
return new ApplicationContextRunner().withInitializer(new OtelApplicationContextInitializer())
.withConfiguration(AutoConfigurations.of(OpenTelemetryAutoConfiguration.class,
.withConfiguration(AutoConfigurations.of(OpenTelemetrySdkAutoConfiguration.class,
org.springframework.boot.actuate.autoconfigure.tracing.OpenTelemetryTracingAutoConfiguration.class))
.withPropertyValues("management.tracing.propagation.type=W3C",
"management.tracing.baggage.remote-fields=x-vcap-request-id,country-code,bp",
@ -239,7 +239,7 @@ class BaggagePropagationIntegrationTests {
@Override
public ApplicationContextRunner get() {
return new ApplicationContextRunner().withInitializer(new OtelApplicationContextInitializer())
.withConfiguration(AutoConfigurations.of(OpenTelemetryAutoConfiguration.class,
.withConfiguration(AutoConfigurations.of(OpenTelemetrySdkAutoConfiguration.class,
org.springframework.boot.actuate.autoconfigure.tracing.OpenTelemetryTracingAutoConfiguration.class))
.withPropertyValues("management.tracing.propagation.type=B3",
"management.tracing.baggage.remote-fields=x-vcap-request-id,country-code,bp",
@ -253,7 +253,7 @@ class BaggagePropagationIntegrationTests {
@Override
public ApplicationContextRunner get() {
return new ApplicationContextRunner().withInitializer(new OtelApplicationContextInitializer())
.withConfiguration(AutoConfigurations.of(OpenTelemetryAutoConfiguration.class,
.withConfiguration(AutoConfigurations.of(OpenTelemetrySdkAutoConfiguration.class,
org.springframework.boot.actuate.autoconfigure.tracing.OpenTelemetryTracingAutoConfiguration.class))
.withPropertyValues("management.tracing.propagation.type=B3_MULTI",
"management.tracing.baggage.remote-fields=x-vcap-request-id,country-code,bp",

View File

@ -90,7 +90,7 @@ class OpenTelemetryTracingAutoConfigurationTests {
private final ApplicationContextRunner contextRunner = new ApplicationContextRunner()
.withConfiguration(AutoConfigurations.of(
org.springframework.boot.actuate.autoconfigure.opentelemetry.OpenTelemetryAutoConfiguration.class,
org.springframework.boot.opentelemetry.autoconfigure.OpenTelemetrySdkAutoConfiguration.class,
OpenTelemetryTracingAutoConfiguration.class));
@Test

View File

@ -47,10 +47,10 @@ import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.springframework.boot.actuate.autoconfigure.observation.ObservationAutoConfiguration;
import org.springframework.boot.actuate.autoconfigure.opentelemetry.OpenTelemetryAutoConfiguration;
import org.springframework.boot.actuate.autoconfigure.tracing.MicrometerTracingAutoConfiguration;
import org.springframework.boot.actuate.autoconfigure.tracing.otlp.OtlpTracingAutoConfigurationIntegrationTests.MockGrpcServer.RecordedGrpcRequest;
import org.springframework.boot.autoconfigure.AutoConfigurations;
import org.springframework.boot.opentelemetry.autoconfigure.OpenTelemetrySdkAutoConfiguration;
import org.springframework.boot.test.context.runner.ApplicationContextRunner;
import static org.assertj.core.api.Assertions.assertThat;
@ -65,7 +65,7 @@ class OtlpTracingAutoConfigurationIntegrationTests {
private final ApplicationContextRunner contextRunner = new ApplicationContextRunner()
.withPropertyValues("management.tracing.sampling.probability=1.0")
.withConfiguration(AutoConfigurations.of(ObservationAutoConfiguration.class,
MicrometerTracingAutoConfiguration.class, OpenTelemetryAutoConfiguration.class,
MicrometerTracingAutoConfiguration.class, OpenTelemetrySdkAutoConfiguration.class,
org.springframework.boot.actuate.autoconfigure.tracing.OpenTelemetryTracingAutoConfiguration.class,
OtlpTracingAutoConfiguration.class));

View File

@ -4,4 +4,6 @@ org.springframework.boot.actuate.autoconfigure.health.HealthContributorAutoConfi
org.springframework.boot.actuate.autoconfigure.health.HealthEndpointAutoConfiguration
org.springframework.boot.actuate.autoconfigure.info.InfoContributorAutoConfiguration
org.springframework.boot.actuate.autoconfigure.info.InfoEndpointAutoConfiguration
org.springframework.boot.actuate.autoconfigure.logging.LogFileWebEndpointAutoConfiguration
org.springframework.boot.actuate.autoconfigure.logging.LoggersEndpointAutoConfiguration
org.springframework.boot.actuate.autoconfigure.web.server.ManagementContextAutoConfiguration

View File

@ -2048,6 +2048,7 @@ bom {
"spring-boot-mustache",
"spring-boot-neo4j",
"spring-boot-netty",
"spring-boot-opentelemetry",
"spring-boot-properties-migrator",
"spring-boot-pulsar",
"spring-boot-quartz",

View File

@ -32,6 +32,7 @@ dependencies {
dockerTestImplementation(project(":spring-boot-project:spring-boot-jdbc"))
dockerTestImplementation(project(":spring-boot-project:spring-boot-flyway"))
dockerTestImplementation(project(":spring-boot-project:spring-boot-liquibase"))
dockerTestImplementation(project(":spring-boot-project:spring-boot-opentelemetry"))
dockerTestImplementation(project(":spring-boot-project:spring-boot-pulsar"))
dockerTestImplementation(project(":spring-boot-project:spring-boot-r2dbc"))
dockerTestImplementation(project(":spring-boot-project:spring-boot-tools:spring-boot-test-support-docker"))
@ -45,6 +46,7 @@ dependencies {
dockerTestRuntimeOnly("com.clickhouse:clickhouse-r2dbc")
dockerTestRuntimeOnly("com.microsoft.sqlserver:mssql-jdbc")
dockerTestRuntimeOnly("com.oracle.database.r2dbc:oracle-r2dbc")
dockerTestRuntimeOnly("io.opentelemetry:opentelemetry-exporter-otlp")
dockerTestRuntimeOnly("io.r2dbc:r2dbc-mssql")
dockerTestRuntimeOnly("org.postgresql:postgresql")
dockerTestRuntimeOnly("org.postgresql:r2dbc-postgresql")
@ -67,6 +69,7 @@ dependencies {
optional(project(":spring-boot-project:spring-boot-liquibase"))
optional(project(":spring-boot-project:spring-boot-mongodb"))
optional(project(":spring-boot-project:spring-boot-neo4j"))
optional(project(":spring-boot-project:spring-boot-opentelemetry"))
optional(project(":spring-boot-project:spring-boot-pulsar"))
optional(project(":spring-boot-project:spring-boot-r2dbc"))
optional(project(":spring-boot-project:spring-boot-zipkin"))

View File

@ -16,9 +16,9 @@
package org.springframework.boot.docker.compose.service.connection.otlp;
import org.springframework.boot.actuate.autoconfigure.logging.otlp.OtlpLoggingConnectionDetails;
import org.springframework.boot.actuate.autoconfigure.logging.otlp.Transport;
import org.springframework.boot.docker.compose.service.connection.test.DockerComposeTest;
import org.springframework.boot.opentelemetry.actuate.autoconfigure.logging.OpenTelemetryLoggingConnectionDetails;
import org.springframework.boot.opentelemetry.actuate.autoconfigure.logging.Transport;
import org.springframework.boot.testsupport.container.TestImage;
import static org.assertj.core.api.Assertions.assertThat;
@ -32,7 +32,7 @@ import static org.assertj.core.api.Assertions.assertThat;
class GrafanaOpenTelemetryLoggingDockerComposeConnectionDetailsFactoryIntegrationTests {
@DockerComposeTest(composeFile = "otlp-compose.yaml", image = TestImage.GRAFANA_OTEL_LGTM)
void runCreatesConnectionDetails(OtlpLoggingConnectionDetails connectionDetails) {
void runCreatesConnectionDetails(OpenTelemetryLoggingConnectionDetails connectionDetails) {
assertThat(connectionDetails.getUrl(Transport.HTTP)).startsWith("http://").endsWith("/v1/logs");
assertThat(connectionDetails.getUrl(Transport.GRPC)).startsWith("http://").endsWith("/v1/logs");
}

View File

@ -16,9 +16,9 @@
package org.springframework.boot.docker.compose.service.connection.otlp;
import org.springframework.boot.actuate.autoconfigure.logging.otlp.OtlpLoggingConnectionDetails;
import org.springframework.boot.actuate.autoconfigure.logging.otlp.Transport;
import org.springframework.boot.docker.compose.service.connection.test.DockerComposeTest;
import org.springframework.boot.opentelemetry.actuate.autoconfigure.logging.OpenTelemetryLoggingConnectionDetails;
import org.springframework.boot.opentelemetry.actuate.autoconfigure.logging.Transport;
import org.springframework.boot.testsupport.container.TestImage;
import static org.assertj.core.api.Assertions.assertThat;
@ -32,7 +32,7 @@ import static org.assertj.core.api.Assertions.assertThat;
class OpenTelemetryLoggingDockerComposeConnectionDetailsFactoryIntegrationTests {
@DockerComposeTest(composeFile = "otlp-compose.yaml", image = TestImage.OPENTELEMETRY)
void runCreatesConnectionDetails(OtlpLoggingConnectionDetails connectionDetails) {
void runCreatesConnectionDetails(OpenTelemetryLoggingConnectionDetails connectionDetails) {
assertThat(connectionDetails.getUrl(Transport.HTTP)).startsWith("http://").endsWith("/v1/logs");
assertThat(connectionDetails.getUrl(Transport.GRPC)).startsWith("http://").endsWith("/v1/logs");
}

View File

@ -16,20 +16,20 @@
package org.springframework.boot.docker.compose.service.connection.otlp;
import org.springframework.boot.actuate.autoconfigure.logging.otlp.OtlpLoggingConnectionDetails;
import org.springframework.boot.actuate.autoconfigure.logging.otlp.Transport;
import org.springframework.boot.docker.compose.core.RunningService;
import org.springframework.boot.docker.compose.service.connection.DockerComposeConnectionDetailsFactory;
import org.springframework.boot.docker.compose.service.connection.DockerComposeConnectionSource;
import org.springframework.boot.opentelemetry.actuate.autoconfigure.logging.OpenTelemetryLoggingConnectionDetails;
import org.springframework.boot.opentelemetry.actuate.autoconfigure.logging.Transport;
/**
* {@link DockerComposeConnectionDetailsFactory} to create
* {@link OtlpLoggingConnectionDetails} for an OTLP service.
* {@link OpenTelemetryLoggingConnectionDetails} for an OTLP service.
*
* @author Eddú Meléndez
*/
class OpenTelemetryLoggingDockerComposeConnectionDetailsFactory
extends DockerComposeConnectionDetailsFactory<OtlpLoggingConnectionDetails> {
extends DockerComposeConnectionDetailsFactory<OpenTelemetryLoggingConnectionDetails> {
private static final String[] OPENTELEMETRY_IMAGE_NAMES = { "otel/opentelemetry-collector-contrib",
"grafana/otel-lgtm" };
@ -40,16 +40,17 @@ class OpenTelemetryLoggingDockerComposeConnectionDetailsFactory
OpenTelemetryLoggingDockerComposeConnectionDetailsFactory() {
super(OPENTELEMETRY_IMAGE_NAMES,
"org.springframework.boot.actuate.autoconfigure.logging.otlp.OtlpLoggingAutoConfiguration");
"org.springframework.boot.opentelemetry.actuate.autoconfigure.logging.OpenTelemetryLoggingExportAutoConfiguration");
}
@Override
protected OtlpLoggingConnectionDetails getDockerComposeConnectionDetails(DockerComposeConnectionSource source) {
protected OpenTelemetryLoggingConnectionDetails getDockerComposeConnectionDetails(
DockerComposeConnectionSource source) {
return new OpenTelemetryLoggingDockerComposeConnectionDetails(source.getRunningService());
}
private static final class OpenTelemetryLoggingDockerComposeConnectionDetails extends DockerComposeConnectionDetails
implements OtlpLoggingConnectionDetails {
implements OpenTelemetryLoggingConnectionDetails {
private final String host;

View File

@ -128,6 +128,7 @@ dependencies {
autoConfiguration(project(path: ":spring-boot-project:spring-boot-mustache", configuration: "autoConfigurationMetadata"))
autoConfiguration(project(path: ":spring-boot-project:spring-boot-neo4j", configuration: "autoConfigurationMetadata"))
autoConfiguration(project(path: ":spring-boot-project:spring-boot-netty", configuration: "autoConfigurationMetadata"))
autoConfiguration(project(path: ":spring-boot-project:spring-boot-opentelemetry", configuration: "autoConfigurationMetadata"))
autoConfiguration(project(path: ":spring-boot-project:spring-boot-pulsar", configuration: "autoConfigurationMetadata"))
autoConfiguration(project(path: ":spring-boot-project:spring-boot-quartz", configuration: "autoConfigurationMetadata"))
autoConfiguration(project(path: ":spring-boot-project:spring-boot-r2dbc", configuration: "autoConfigurationMetadata"))
@ -212,6 +213,7 @@ dependencies {
configurationProperties(project(path: ":spring-boot-project:spring-boot-mustache", configuration: "configurationPropertiesMetadata"))
configurationProperties(project(path: ":spring-boot-project:spring-boot-neo4j", configuration: "configurationPropertiesMetadata"))
configurationProperties(project(path: ":spring-boot-project:spring-boot-netty", configuration: "configurationPropertiesMetadata"))
configurationProperties(project(path: ":spring-boot-project:spring-boot-opentelemetry", configuration: "configurationPropertiesMetadata"))
configurationProperties(project(path: ":spring-boot-project:spring-boot-pulsar", configuration: "configurationPropertiesMetadata"))
configurationProperties(project(path: ":spring-boot-project:spring-boot-quartz", configuration: "configurationPropertiesMetadata"))
configurationProperties(project(path: ":spring-boot-project:spring-boot-r2dbc", configuration: "configurationPropertiesMetadata"))

View File

@ -42,9 +42,10 @@ You have to provide the location of the OpenTelemetry logs endpoint to configure
[configprops,yaml]
----
management:
otlp:
opentelemetry:
logging:
endpoint: "https://otlp.example.com:4318/v1/logs"
export:
endpoint: "https://otlp.example.com:4318/v1/logs"
----
NOTE: The OpenTelemetry Logback appender and Log4j appender are not part of Spring Boot.

View File

@ -110,7 +110,7 @@ The following service connections are currently supported:
| javadoc:org.springframework.boot.neo4j.autoconfigure.Neo4jConnectionDetails[]
| Containers named "neo4j" or "bitnami/neo4j"
| javadoc:org.springframework.boot.actuate.autoconfigure.logging.otlp.OtlpLoggingConnectionDetails[]
| javadoc:org.springframework.boot.opentelemetry.actuate.autoconfigure.logging.OpenTelemetryLoggingConnectionDetails[]
| Containers named "otel/opentelemetry-collector-contrib", "grafana/otel-lgtm"
| javadoc:org.springframework.boot.actuate.autoconfigure.metrics.export.otlp.OtlpMetricsConnectionDetails[]

View File

@ -155,7 +155,7 @@ The following service connection factories are provided in the `spring-boot-test
| javadoc:org.springframework.boot.neo4j.autoconfigure.Neo4jConnectionDetails[]
| Containers of type javadoc:{url-testcontainers-neo4j-javadoc}/org.testcontainers.containers.Neo4jContainer[]
| javadoc:org.springframework.boot.actuate.autoconfigure.logging.otlp.OtlpLoggingConnectionDetails[]
| javadoc:org.springframework.boot.opentelemetry.actuate.autoconfigure.logging.OpenTelemetryLoggingConnectionDetails[]
| Containers named "otel/opentelemetry-collector-contrib" or of type javadoc:org.testcontainers.grafana.LgtmStackContainer[]
| javadoc:org.springframework.boot.actuate.autoconfigure.metrics.export.otlp.OtlpMetricsConnectionDetails[]

View File

@ -0,0 +1,44 @@
/*
* Copyright 2012-present 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.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
plugins {
id "java-library"
id "org.springframework.boot.auto-configuration"
id "org.springframework.boot.configuration-properties"
id "org.springframework.boot.deployed"
id "org.springframework.boot.docker-test"
id "org.springframework.boot.optional-dependencies"
}
description = "Spring Boot Open Telemetry"
dependencies {
api(project(":spring-boot-project:spring-boot"))
api("io.opentelemetry:opentelemetry-api")
api("io.opentelemetry:opentelemetry-sdk")
optional(project(":spring-boot-project:spring-boot-autoconfigure"))
optional(project(":spring-boot-project:spring-boot-actuator-autoconfigure"))
optional("io.opentelemetry:opentelemetry-exporter-otlp")
testImplementation(project(":spring-boot-project:spring-boot-test"))
testImplementation(project(":spring-boot-project:spring-boot-tools:spring-boot-test-support"))
testImplementation("com.squareup.okhttp3:mockwebserver")
testRuntimeOnly("ch.qos.logback:logback-classic")
testRuntimeOnly("io.grpc:grpc-api:1.72.0")
}

View File

@ -14,17 +14,18 @@
* limitations under the License.
*/
package org.springframework.boot.actuate.autoconfigure.logging.otlp;
package org.springframework.boot.opentelemetry.actuate.autoconfigure.logging;
import org.springframework.boot.autoconfigure.service.connection.ConnectionDetails;
/**
* Details required to establish a connection to an OpenTelemetry logging service.
* Details required for actuator to establish a connection to an OpenTelemetry logging
* service.
*
* @author Toshiaki Maki
* @since 3.4.0
* @since 4.0.0
*/
public interface OtlpLoggingConnectionDetails extends ConnectionDetails {
public interface OpenTelemetryLoggingConnectionDetails extends ConnectionDetails {
/**
* Address to where logs will be published.

View File

@ -0,0 +1,63 @@
/*
* Copyright 2012-present 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.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.boot.opentelemetry.actuate.autoconfigure.logging;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.util.Assert;
/**
* {@link Configuration @Configuration} for {@link OpenTelemetryLoggingConnectionDetails}.
*
* @author Toshiaki Maki
*/
@Configuration(proxyBeanMethods = false)
class OpenTelemetryLoggingConnectionDetailsConfiguration {
@Bean
@ConditionalOnMissingBean
@ConditionalOnProperty("management.opentelemetry.logging.export.endpoint")
PropertiesOpenTelemetryLoggingConnectionDetails openTelemetryLoggingConnectionDetails(
OpenTelemetryLoggingExportProperties properties) {
return new PropertiesOpenTelemetryLoggingConnectionDetails(properties);
}
/**
* Adapts {@link OpenTelemetryLoggingExportProperties} to
* {@link OpenTelemetryLoggingConnectionDetails}.
*/
static class PropertiesOpenTelemetryLoggingConnectionDetails implements OpenTelemetryLoggingConnectionDetails {
private final OpenTelemetryLoggingExportProperties properties;
PropertiesOpenTelemetryLoggingConnectionDetails(OpenTelemetryLoggingExportProperties properties) {
this.properties = properties;
}
@Override
public String getUrl(Transport transport) {
Assert.state(transport == this.properties.getTransport(),
"Requested transport %s doesn't match configured transport %s".formatted(transport,
this.properties.getTransport()));
return this.properties.getEndpoint();
}
}
}

View File

@ -14,12 +14,12 @@
* limitations under the License.
*/
package org.springframework.boot.actuate.autoconfigure.logging.otlp;
package org.springframework.boot.opentelemetry.actuate.autoconfigure.logging;
import io.opentelemetry.api.OpenTelemetry;
import io.opentelemetry.exporter.otlp.http.logs.OtlpHttpLogRecordExporter;
import io.opentelemetry.sdk.logs.SdkLoggerProvider;
import org.springframework.boot.actuate.autoconfigure.logging.ConditionalOnEnabledLoggingExport;
import org.springframework.boot.autoconfigure.AutoConfiguration;
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
@ -27,15 +27,16 @@ import org.springframework.boot.context.properties.EnableConfigurationProperties
import org.springframework.context.annotation.Import;
/**
* {@link EnableAutoConfiguration Auto-configuration} for OTLP logging.
* {@link EnableAutoConfiguration Auto-configuration} for OpenTelemetry logging exports.
*
* @author Toshiaki Maki
* @since 3.4.0
* @since 4.0.0
*/
@AutoConfiguration
@ConditionalOnClass({ SdkLoggerProvider.class, OpenTelemetry.class, OtlpHttpLogRecordExporter.class })
@EnableConfigurationProperties(OtlpLoggingProperties.class)
@Import({ OtlpLoggingConfigurations.ConnectionDetails.class, OtlpLoggingConfigurations.Exporters.class })
public class OtlpLoggingAutoConfiguration {
@ConditionalOnClass({ ConditionalOnEnabledLoggingExport.class, OpenTelemetry.class, SdkLoggerProvider.class })
@ConditionalOnEnabledLoggingExport("opentelemetry")
@EnableConfigurationProperties(OpenTelemetryLoggingExportProperties.class)
@Import({ OpenTelemetryLoggingConnectionDetailsConfiguration.class, OpenTelemetryLoggingTransportConfiguration.class })
public class OpenTelemetryLoggingExportAutoConfiguration {
}

View File

@ -14,7 +14,7 @@
* limitations under the License.
*/
package org.springframework.boot.actuate.autoconfigure.logging.otlp;
package org.springframework.boot.opentelemetry.actuate.autoconfigure.logging;
import java.time.Duration;
import java.util.HashMap;
@ -23,13 +23,13 @@ import java.util.Map;
import org.springframework.boot.context.properties.ConfigurationProperties;
/**
* Configuration properties for exporting logs using OTLP.
* Configuration properties for exporting logs using OpenTelemetry.
*
* @author Jonatan Ivanov
* @since 3.4.0
* @since 4.0.0
*/
@ConfigurationProperties("management.otlp.logging")
public class OtlpLoggingProperties {
@ConfigurationProperties("management.opentelemetry.logging.export")
public class OpenTelemetryLoggingExportProperties {
/**
* URL to the OTel collector's HTTP API.

View File

@ -0,0 +1,75 @@
/*
* Copyright 2012-present 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.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.boot.opentelemetry.actuate.autoconfigure.logging;
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.autoconfigure.condition.ConditionalOnBean;
import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
/**
* {@link Configuration @Configuration} for OpenTelemetry log record exporters.
*
* @author Toshiaki Maki
*/
@Configuration(proxyBeanMethods = false)
@ConditionalOnClass(OtlpHttpLogRecordExporter.class)
@ConditionalOnMissingBean({ OtlpGrpcLogRecordExporter.class, OtlpHttpLogRecordExporter.class })
@ConditionalOnBean(OpenTelemetryLoggingConnectionDetails.class)
class OpenTelemetryLoggingTransportConfiguration {
@Bean
@ConditionalOnProperty(name = "management.opentelemetry.logging.export.transport", havingValue = "http",
matchIfMissing = true)
OtlpHttpLogRecordExporter otlpHttpLogRecordExporter(OpenTelemetryLoggingExportProperties properties,
OpenTelemetryLoggingConnectionDetails 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.opentelemetry.logging.export.transport", havingValue = "grpc")
OtlpGrpcLogRecordExporter otlpGrpcLogRecordExporter(OpenTelemetryLoggingExportProperties properties,
OpenTelemetryLoggingConnectionDetails 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

@ -14,13 +14,13 @@
* limitations under the License.
*/
package org.springframework.boot.actuate.autoconfigure.logging.otlp;
package org.springframework.boot.opentelemetry.actuate.autoconfigure.logging;
/**
* Transport used to send OTLP data.
* Transport used to send OTLP log data.
*
* @author Moritz Halbritter
* @since 3.4.0
* @since 4.0.0
*/
public enum Transport {

View File

@ -15,6 +15,6 @@
*/
/**
* Auto-configuration for exporting logs with OTLP.
* Auto-configuration for exporting logs with OpenTelemetry.
*/
package org.springframework.boot.actuate.autoconfigure.logging.otlp;
package org.springframework.boot.opentelemetry.actuate.autoconfigure.logging;

View File

@ -14,7 +14,7 @@
* limitations under the License.
*/
package org.springframework.boot.actuate.autoconfigure.opentelemetry;
package org.springframework.boot.opentelemetry.autoconfigure;
import java.util.HashMap;
import java.util.Map;

View File

@ -14,7 +14,7 @@
* limitations under the License.
*/
package org.springframework.boot.actuate.autoconfigure.opentelemetry;
package org.springframework.boot.opentelemetry.autoconfigure;
import java.io.ByteArrayOutputStream;
import java.nio.charset.StandardCharsets;
@ -39,9 +39,9 @@ import org.springframework.util.StringUtils;
* Resource Specification</a>
*
* @author Dmytro Nosan
* @since 3.5.0
* @since 4.0.0
*/
public final class OpenTelemetryResourceAttributes {
public class OpenTelemetryResourceAttributes {
/**
* Default value for service name if {@code service.name} is not set.
@ -52,7 +52,7 @@ public final class OpenTelemetryResourceAttributes {
private final Map<String, String> resourceAttributes;
private final Function<String, String> getEnv;
private final Function<String, String> systemEnvironment;
/**
* Creates a new instance of {@link OpenTelemetryResourceAttributes}.
@ -67,14 +67,14 @@ public final class OpenTelemetryResourceAttributes {
* Creates a new {@link OpenTelemetryResourceAttributes} instance.
* @param environment the environment
* @param resourceAttributes user-provided resource attributes to be used
* @param getEnv a function to retrieve environment variables by name
* @param systemEnvironment a function to retrieve environment variables by name
*/
OpenTelemetryResourceAttributes(Environment environment, Map<String, String> resourceAttributes,
Function<String, String> getEnv) {
Function<String, String> systemEnvironment) {
Assert.notNull(environment, "'environment' must not be null");
this.environment = environment;
this.resourceAttributes = (resourceAttributes != null) ? resourceAttributes : Collections.emptyMap();
this.getEnv = (getEnv != null) ? getEnv : System::getenv;
this.systemEnvironment = (systemEnvironment != null) ? systemEnvironment : System::getenv;
}
/**
@ -150,7 +150,7 @@ public final class OpenTelemetryResourceAttributes {
}
private String getEnv(String name) {
return this.getEnv.apply(name);
return this.systemEnvironment.apply(name);
}
/**
@ -166,17 +166,17 @@ public final class OpenTelemetryResourceAttributes {
return value;
}
byte[] bytes = value.getBytes(StandardCharsets.UTF_8);
ByteArrayOutputStream bos = new ByteArrayOutputStream(bytes.length);
ByteArrayOutputStream out = new ByteArrayOutputStream(bytes.length);
for (int i = 0; i < bytes.length; i++) {
byte b = bytes[i];
if (b != '%') {
bos.write(b);
out.write(b);
continue;
}
int u = decodeHex(bytes, i + 1);
int l = decodeHex(bytes, i + 2);
if (u >= 0 && l >= 0) {
bos.write((u << 4) + l);
out.write((u << 4) + l);
}
else {
throw new IllegalArgumentException(
@ -185,7 +185,7 @@ public final class OpenTelemetryResourceAttributes {
}
i += 2;
}
return bos.toString(StandardCharsets.UTF_8);
return out.toString(StandardCharsets.UTF_8);
}
private static int decodeHex(byte[] bytes, int index) {

View File

@ -14,13 +14,17 @@
* limitations under the License.
*/
package org.springframework.boot.actuate.autoconfigure.opentelemetry;
package org.springframework.boot.opentelemetry.autoconfigure;
import io.opentelemetry.api.OpenTelemetry;
import io.opentelemetry.context.propagation.ContextPropagators;
import io.opentelemetry.sdk.OpenTelemetrySdk;
import io.opentelemetry.sdk.OpenTelemetrySdkBuilder;
import io.opentelemetry.sdk.logs.LogRecordProcessor;
import io.opentelemetry.sdk.logs.SdkLoggerProvider;
import io.opentelemetry.sdk.logs.SdkLoggerProviderBuilder;
import io.opentelemetry.sdk.logs.export.BatchLogRecordProcessor;
import io.opentelemetry.sdk.logs.export.LogRecordExporter;
import io.opentelemetry.sdk.metrics.SdkMeterProvider;
import io.opentelemetry.sdk.resources.Resource;
import io.opentelemetry.sdk.resources.ResourceBuilder;
@ -33,37 +37,41 @@ import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.env.Environment;
/**
* {@link EnableAutoConfiguration Auto-configuration} for OpenTelemetry.
* {@link EnableAutoConfiguration Auto-configuration} for the OpenTelemetry SDK.
*
* @author Moritz Halbritter
* @since 3.2.0
* @since 4.0.0
*/
@AutoConfiguration
@ConditionalOnClass(OpenTelemetrySdk.class)
@ConditionalOnClass({ OpenTelemetry.class, OpenTelemetrySdk.class })
@EnableConfigurationProperties(OpenTelemetryProperties.class)
public class OpenTelemetryAutoConfiguration {
public class OpenTelemetrySdkAutoConfiguration {
OpenTelemetrySdkAutoConfiguration() {
}
@Bean
@ConditionalOnMissingBean(OpenTelemetry.class)
OpenTelemetrySdk openTelemetry(ObjectProvider<SdkTracerProvider> tracerProvider,
ObjectProvider<ContextPropagators> propagators, ObjectProvider<SdkLoggerProvider> loggerProvider,
ObjectProvider<SdkMeterProvider> meterProvider) {
OpenTelemetrySdk openTelemetrySdk(ObjectProvider<SdkTracerProvider> openTelemetrySdkTracerProvider,
ObjectProvider<ContextPropagators> openTelemetryContextPropagators,
ObjectProvider<SdkLoggerProvider> openTelemetrySdkLoggerProvider,
ObjectProvider<SdkMeterProvider> openTelemetrySdkMeterProvider) {
OpenTelemetrySdkBuilder builder = OpenTelemetrySdk.builder();
tracerProvider.ifAvailable(builder::setTracerProvider);
propagators.ifAvailable(builder::setPropagators);
loggerProvider.ifAvailable(builder::setLoggerProvider);
meterProvider.ifAvailable(builder::setMeterProvider);
openTelemetrySdkTracerProvider.ifAvailable(builder::setTracerProvider);
openTelemetryContextPropagators.ifAvailable(builder::setPropagators);
openTelemetrySdkLoggerProvider.ifAvailable(builder::setLoggerProvider);
openTelemetrySdkMeterProvider.ifAvailable(builder::setMeterProvider);
return builder.build();
}
@Bean
@ConditionalOnMissingBean
Resource openTelemetryResource(Environment environment, OpenTelemetryProperties properties) {
Resource resource = Resource.getDefault();
return resource.merge(toResource(environment, properties));
return Resource.getDefault().merge(toResource(environment, properties));
}
private Resource toResource(Environment environment, OpenTelemetryProperties properties) {
@ -72,4 +80,30 @@ public class OpenTelemetryAutoConfiguration {
return builder.build();
}
@Configuration(proxyBeanMethods = false)
@ConditionalOnClass(SdkLoggerProvider.class)
static class LoggerConfiguration {
@Bean
@ConditionalOnMissingBean
BatchLogRecordProcessor openTelemetryBatchLogRecordProcessor(
ObjectProvider<LogRecordExporter> logRecordExporters) {
LogRecordExporter exporter = LogRecordExporter.composite(logRecordExporters.orderedStream().toList());
return BatchLogRecordProcessor.builder(exporter).build();
}
@Bean
@ConditionalOnMissingBean
SdkLoggerProvider openTelemetrySdkLoggerProvider(Resource openTelemetryResource,
ObjectProvider<LogRecordProcessor> logRecordProcessors,
ObjectProvider<SdkLoggerProviderBuilderCustomizer> customizers) {
SdkLoggerProviderBuilder builder = SdkLoggerProvider.builder();
builder.setResource(openTelemetryResource);
logRecordProcessors.orderedStream().forEach(builder::addLogRecordProcessor);
customizers.orderedStream().forEach((customizer) -> customizer.customize(builder));
return builder.build();
}
}
}

View File

@ -14,7 +14,7 @@
* limitations under the License.
*/
package org.springframework.boot.actuate.autoconfigure.logging;
package org.springframework.boot.opentelemetry.autoconfigure;
import io.opentelemetry.sdk.logs.SdkLoggerProvider;
import io.opentelemetry.sdk.logs.SdkLoggerProviderBuilder;
@ -24,7 +24,7 @@ import io.opentelemetry.sdk.logs.SdkLoggerProviderBuilder;
* that is used to create the auto-configured {@link SdkLoggerProvider}.
*
* @author Toshiaki Maki
* @since 3.4.0
* @since 4.0.0
*/
@FunctionalInterface
public interface SdkLoggerProviderBuilderCustomizer {

View File

@ -17,4 +17,4 @@
/**
* Auto-configuration for OpenTelemetry.
*/
package org.springframework.boot.actuate.autoconfigure.opentelemetry;
package org.springframework.boot.opentelemetry.autoconfigure;

View File

@ -0,0 +1,98 @@
{
"groups": [],
"properties": [
{
"name": "management.logging.export.enabled",
"type": "java.lang.Boolean",
"description": "Whether auto-configuration of logging is enabled to export logs.",
"defaultValue": true,
"deprecation": {
"replacement": "management.opentelemetry.logging.export.enabled",
"level": "error"
}
},
{
"name": "management.otlp.logging",
"type": "org.springframework.boot.opentelemetry.actuate.autoconfigure.logging.OpenTelemetryLoggingExportProperties",
"sourceType": "org.springframework.boot.opentelemetry.actuate.autoconfigure.logging.OpenTelemetryLoggingExportProperties",
"deprecation": {
"replacement": "management.opentelemetry.logging.export",
"level": "error"
}
},
{
"name": "management.otlp.logging.compression",
"type": "org.springframework.boot.opentelemetry.actuate.autoconfigure.logging.OpenTelemetryLoggingExportProperties$Compression",
"description": "Method used to compress the payload.",
"sourceType": "org.springframework.boot.opentelemetry.actuate.autoconfigure.logging.OpenTelemetryLoggingExportProperties",
"defaultValue": "none",
"deprecation": {
"replacement": "management.opentelemetry.logging.export.compression",
"level": "error"
}
},
{
"name": "management.otlp.logging.connect-timeout",
"type": "java.time.Duration",
"description": "Connect timeout for the OTel collector connection.",
"sourceType": "org.springframework.boot.opentelemetry.actuate.autoconfigure.logging.OpenTelemetryLoggingExportProperties",
"defaultValue": "10s",
"deprecation": {
"replacement": "management.opentelemetry.logging.export.connect-timeout",
"level": "error"
}
},
{
"name": "management.otlp.logging.endpoint",
"type": "java.lang.String",
"description": "URL to the OTel collector's HTTP API.",
"sourceType": "org.springframework.boot.opentelemetry.actuate.autoconfigure.logging.OpenTelemetryLoggingExportProperties",
"deprecation": {
"replacement": "management.opentelemetry.logging.export.endpoint",
"level": "error"
}
},
{
"name": "management.otlp.logging.export.enabled",
"type": "java.lang.Boolean",
"description": "Whether auto-configuration of logging is enabled to export OTLP logs.",
"deprecation": {
"replacement": "management.opentelemetry.logging.export.enabled",
"level": "error"
}
},
{
"name": "management.otlp.logging.headers",
"type": "java.util.Map<java.lang.String,java.lang.String>",
"description": "Custom HTTP headers you want to pass to the collector, for example auth headers.",
"sourceType": "org.springframework.boot.opentelemetry.actuate.autoconfigure.logging.OpenTelemetryLoggingExportProperties",
"deprecation": {
"replacement": "management.opentelemetry.logging.export.headers",
"level": "error"
}
},
{
"name": "management.otlp.logging.timeout",
"type": "java.time.Duration",
"description": "Call timeout for the OTel Collector to process an exported batch of data. This timeout spans the entire call: resolving DNS, connecting, writing the request body, server processing, and reading the response body. If the call requires redirects or retries all must complete within one timeout period.",
"sourceType": "org.springframework.boot.opentelemetry.actuate.autoconfigure.logging.OpenTelemetryLoggingExportProperties",
"defaultValue": "10s",
"deprecation": {
"replacement": "management.opentelemetry.logging.export.timeout",
"level": "error"
}
},
{
"name": "management.otlp.logging.transport",
"type": "org.springframework.boot.opentelemetry.actuate.autoconfigure.logging.Transport",
"description": "Transport used to send the logs.",
"sourceType": "org.springframework.boot.opentelemetry.actuate.autoconfigure.logging.OpenTelemetryLoggingExportProperties",
"defaultValue": "http",
"deprecation": {
"replacement": "management.opentelemetry.logging.export.transport",
"level": "error"
}
}
],
"hints": []
}

View File

@ -0,0 +1,2 @@
org.springframework.boot.opentelemetry.actuate.autoconfigure.logging.OpenTelemetryLoggingExportAutoConfiguration
org.springframework.boot.opentelemetry.autoconfigure.OpenTelemetrySdkAutoConfiguration

View File

@ -14,7 +14,7 @@
* limitations under the License.
*/
package org.springframework.boot.actuate.autoconfigure.logging.otlp;
package org.springframework.boot.opentelemetry.actuate.autoconfigure.logging;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
@ -32,26 +32,25 @@ import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.springframework.boot.actuate.autoconfigure.logging.OpenTelemetryLoggingAutoConfiguration;
import org.springframework.boot.actuate.autoconfigure.opentelemetry.OpenTelemetryAutoConfiguration;
import org.springframework.boot.autoconfigure.AutoConfigurations;
import org.springframework.boot.opentelemetry.autoconfigure.OpenTelemetrySdkAutoConfiguration;
import org.springframework.boot.test.context.runner.ApplicationContextRunner;
import org.springframework.context.ApplicationContext;
import static org.assertj.core.api.Assertions.assertThat;
/**
* Integration tests for {@link OtlpLoggingAutoConfiguration}.
* Integration tests for {@link OpenTelemetryLoggingExportAutoConfiguration}.
*
* @author Toshiaki Maki
*/
class OtlpLoggingAutoConfigurationIntegrationTests {
class OpenTelemetryLoggingExportAutoConfigurationIntegrationTests {
private final ApplicationContextRunner contextRunner = new ApplicationContextRunner()
.withPropertyValues("spring.application.name=otlp-logs-test",
"management.otlp.logging.headers.Authorization=Bearer my-token")
.withConfiguration(AutoConfigurations.of(OpenTelemetryAutoConfiguration.class,
OpenTelemetryLoggingAutoConfiguration.class, OtlpLoggingAutoConfiguration.class));
"management.opentelemetry.logging.export.headers.Authorization=Bearer my-token")
.withConfiguration(AutoConfigurations.of(OpenTelemetrySdkAutoConfiguration.class,
OpenTelemetryLoggingExportAutoConfiguration.class));
private final MockWebServer mockWebServer = new MockWebServer();
@ -69,7 +68,7 @@ class OtlpLoggingAutoConfigurationIntegrationTests {
void httpLogRecordExporterShouldUseProtobufAndNoCompressionByDefault() {
this.mockWebServer.enqueue(new MockResponse());
this.contextRunner
.withPropertyValues("management.otlp.logging.endpoint=http://localhost:%d/v1/logs"
.withPropertyValues("management.opentelemetry.logging.export.endpoint=http://localhost:%d/v1/logs"
.formatted(this.mockWebServer.getPort()))
.run((context) -> {
logMessage(context);
@ -89,8 +88,8 @@ class OtlpLoggingAutoConfigurationIntegrationTests {
void httpLogRecordExporterCanBeConfiguredToUseGzipCompression() {
this.mockWebServer.enqueue(new MockResponse());
this.contextRunner
.withPropertyValues("management.otlp.logging.endpoint=http://localhost:%d/v1/logs"
.formatted(this.mockWebServer.getPort()), "management.otlp.logging.compression=gzip")
.withPropertyValues("management.opentelemetry.logging.export.endpoint=http://localhost:%d/v1/logs"
.formatted(this.mockWebServer.getPort()), "management.opentelemetry.logging.export.compression=gzip")
.run((context) -> {
logMessage(context);
RecordedRequest request = this.mockWebServer.takeRequest(10, TimeUnit.SECONDS);

View File

@ -14,13 +14,15 @@
* limitations under the License.
*/
package org.springframework.boot.actuate.autoconfigure.logging.otlp;
package org.springframework.boot.opentelemetry.actuate.autoconfigure.logging;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.function.Supplier;
import io.opentelemetry.api.metrics.MeterProvider;
import io.opentelemetry.exporter.otlp.http.logs.OtlpHttpLogRecordExporter;
import io.opentelemetry.exporter.otlp.logs.OtlpGrpcLogRecordExporter;
import io.opentelemetry.sdk.logs.SdkLoggerProviderBuilder;
import io.opentelemetry.sdk.logs.export.LogRecordExporter;
import okhttp3.HttpUrl;
import org.assertj.core.api.InstanceOfAssertFactories;
@ -28,8 +30,12 @@ import org.junit.jupiter.api.Test;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.ValueSource;
import org.springframework.boot.actuate.autoconfigure.logging.otlp.OtlpLoggingConfigurations.ConnectionDetails.PropertiesOtlpLoggingConnectionDetails;
import org.springframework.boot.autoconfigure.AutoConfiguration;
import org.springframework.boot.autoconfigure.AutoConfigurations;
import org.springframework.boot.context.annotation.ImportCandidates;
import org.springframework.boot.opentelemetry.actuate.autoconfigure.logging.OpenTelemetryLoggingConnectionDetailsConfiguration.PropertiesOpenTelemetryLoggingConnectionDetails;
import org.springframework.boot.opentelemetry.autoconfigure.OpenTelemetrySdkAutoConfiguration;
import org.springframework.boot.opentelemetry.autoconfigure.SdkLoggerProviderBuilderCustomizer;
import org.springframework.boot.test.context.FilteredClassLoader;
import org.springframework.boot.test.context.runner.ApplicationContextRunner;
import org.springframework.context.annotation.Bean;
@ -38,131 +44,146 @@ import org.springframework.context.annotation.Configuration;
import static org.assertj.core.api.Assertions.assertThat;
/**
* Tests for {@link OtlpLoggingAutoConfiguration}.
* Tests for {@link OpenTelemetryLoggingExportAutoConfiguration}.
*
* @author Toshiaki Maki
* @author Moritz Halbritter
*/
class OtlpLoggingAutoConfigurationTests {
class OpenTelemetryLoggingExportAutoConfigurationTests {
private final ApplicationContextRunner contextRunner = new ApplicationContextRunner()
.withConfiguration(AutoConfigurations.of(OtlpLoggingAutoConfiguration.class));
private final ApplicationContextRunner contextRunner;
@Test
void shouldNotSupplyBeansIfPropertyIsNotSet() {
this.contextRunner.run((context) -> {
assertThat(context).doesNotHaveBean(OtlpLoggingConnectionDetails.class);
assertThat(context).doesNotHaveBean(OtlpHttpLogRecordExporter.class);
});
OpenTelemetryLoggingExportAutoConfigurationTests() {
this.contextRunner = new ApplicationContextRunner().withConfiguration(AutoConfigurations
.of(OpenTelemetrySdkAutoConfiguration.class, OpenTelemetryLoggingExportAutoConfiguration.class));
}
@Test
void shouldSupplyBeans() {
this.contextRunner.withPropertyValues("management.otlp.logging.endpoint=http://localhost:4318/v1/logs")
.run((context) -> {
assertThat(context).hasSingleBean(OtlpLoggingConnectionDetails.class);
OtlpLoggingConnectionDetails connectionDetails = context.getBean(OtlpLoggingConnectionDetails.class);
assertThat(connectionDetails.getUrl(Transport.HTTP)).isEqualTo("http://localhost:4318/v1/logs");
assertThat(context).hasSingleBean(OtlpHttpLogRecordExporter.class)
.hasSingleBean(LogRecordExporter.class);
});
void registeredInAutoConfigurationImports() {
assertThat(ImportCandidates.load(AutoConfiguration.class, null).getCandidates())
.contains(OpenTelemetryLoggingExportAutoConfiguration.class.getName());
}
@ParameterizedTest
@ValueSource(strings = { "io.opentelemetry.sdk.logs", "io.opentelemetry.api",
"io.opentelemetry.exporter.otlp.http.logs" })
void shouldNotSupplyBeansIfDependencyIsMissing(String packageName) {
void whenOpenTelemetryIsNotOnClasspathDoesNotProvideBeans(String packageName) {
this.contextRunner.withClassLoader(new FilteredClassLoader(packageName)).run((context) -> {
assertThat(context).doesNotHaveBean(OtlpLoggingConnectionDetails.class);
assertThat(context).doesNotHaveBean(OpenTelemetryLoggingConnectionDetails.class);
assertThat(context).doesNotHaveBean(OtlpHttpLogRecordExporter.class);
});
}
@Test
void shouldBackOffWhenLoggingExportPropertyIsNotEnabled() {
void whenHasEndpointPropertyProvidesBeans() {
this.contextRunner
.withPropertyValues("management.opentelemetry.logging.export.endpoint=http://localhost:4318/v1/logs")
.run((context) -> {
assertThat(context).hasSingleBean(OpenTelemetryLoggingConnectionDetails.class);
OpenTelemetryLoggingConnectionDetails connectionDetails = context
.getBean(OpenTelemetryLoggingConnectionDetails.class);
assertThat(connectionDetails.getUrl(Transport.HTTP)).isEqualTo("http://localhost:4318/v1/logs");
assertThat(context).hasSingleBean(OtlpHttpLogRecordExporter.class);
assertThat(context).hasSingleBean(LogRecordExporter.class);
});
}
@Test
void whenHasNoEndpointPropertyDoesNotProvideBeans() {
this.contextRunner.run((context) -> {
assertThat(context).doesNotHaveBean(OpenTelemetryLoggingConnectionDetails.class);
assertThat(context).doesNotHaveBean(OtlpHttpLogRecordExporter.class);
});
}
@Test
void whenOpenTelemetryLoggingExportEnabledPropertyIsFalseProvidesExpectedBeans() {
this.contextRunner
.withPropertyValues("management.opentelemetry.logging.export.enabled=false",
"management.opentelemetry.logging.export.endpoint=http://localhost:4318/v1/logs")
.run((context) -> {
assertThat(context).doesNotHaveBean(OpenTelemetryLoggingConnectionDetails.class);
assertThat(context).doesNotHaveBean(LogRecordExporter.class);
});
}
@Test
void whenLoggingExportEnabledPropertyIsFalseNoProvideExpectedBeans() {
this.contextRunner
.withPropertyValues("management.logging.export.enabled=false",
"management.otlp.logging.endpoint=http://localhost:4318/v1/logs")
"management.opentelemetry.logging.export.endpoint=http://localhost:4318/v1/logs")
.run((context) -> {
assertThat(context).hasSingleBean(OtlpLoggingConnectionDetails.class);
assertThat(context).doesNotHaveBean(OpenTelemetryLoggingConnectionDetails.class);
assertThat(context).doesNotHaveBean(LogRecordExporter.class);
});
}
@Test
void shouldBackOffWhenOtlpLoggingExportPropertyIsNotEnabled() {
this.contextRunner
.withPropertyValues("management.otlp.logging.export.enabled=false",
"management.otlp.logging.endpoint=http://localhost:4318/v1/logs")
.run((context) -> {
assertThat(context).hasSingleBean(OtlpLoggingConnectionDetails.class);
assertThat(context).doesNotHaveBean(LogRecordExporter.class);
});
}
@Test
void shouldBackOffWhenCustomHttpExporterIsDefined() {
void whenHasCustomHttpExporterDoesNotProvideExporterBean() {
this.contextRunner.withUserConfiguration(CustomHttpExporterConfiguration.class)
.run((context) -> assertThat(context).hasBean("customOtlpHttpLogRecordExporter")
.hasSingleBean(LogRecordExporter.class));
}
@Test
void shouldBackOffWhenCustomGrpcExporterIsDefined() {
void whenHasCustomGrpcExporterDoesNotProvideExporterBean() {
this.contextRunner.withUserConfiguration(CustomGrpcExporterConfiguration.class)
.run((context) -> assertThat(context).hasBean("customOtlpGrpcLogRecordExporter")
.hasSingleBean(LogRecordExporter.class));
}
// FIXME
@Test
void shouldBackOffWhenCustomOtlpLoggingConnectionDetailsIsDefined() {
this.contextRunner.withUserConfiguration(CustomOtlpLoggingConnectionDetails.class).run((context) -> {
assertThat(context).hasSingleBean(OtlpLoggingConnectionDetails.class)
.doesNotHaveBean(PropertiesOtlpLoggingConnectionDetails.class);
OtlpHttpLogRecordExporter otlpHttpLogRecordExporter = context.getBean(OtlpHttpLogRecordExporter.class);
assertThat(otlpHttpLogRecordExporter).extracting("delegate.httpSender.url")
.isEqualTo(HttpUrl.get("https://otel.example.com/v1/logs"));
});
void whenHasCustomLoggingConnectionDetailsDoesNotProvideExporterBean() {
this.contextRunner.withUserConfiguration(CustomOtlpLoggingConnectionDetailsConfiguration.class)
.run((context) -> {
assertThat(context).hasSingleBean(OpenTelemetryLoggingConnectionDetails.class)
.doesNotHaveBean(PropertiesOpenTelemetryLoggingConnectionDetails.class);
OtlpHttpLogRecordExporter otlpHttpLogRecordExporter = context.getBean(OtlpHttpLogRecordExporter.class);
assertThat(otlpHttpLogRecordExporter).extracting("delegate.httpSender.url")
.isEqualTo(HttpUrl.get("https://otel.example.com/v1/logs"));
});
}
@Test
void shouldUseHttpExporterIfTransportIsNotSet() {
this.contextRunner.withPropertyValues("management.otlp.logging.endpoint=http://localhost:4318/v1/logs")
void whenHasNoTransportPropertySetUsesHttpExporter() {
this.contextRunner
.withPropertyValues("management.opentelemetry.logging.export.endpoint=http://localhost:4318/v1/logs")
.run((context) -> {
assertThat(context).hasSingleBean(OtlpHttpLogRecordExporter.class)
.hasSingleBean(LogRecordExporter.class);
assertThat(context).hasSingleBean(OtlpHttpLogRecordExporter.class);
assertThat(context).hasSingleBean(LogRecordExporter.class);
assertThat(context).doesNotHaveBean(OtlpGrpcLogRecordExporter.class);
});
}
@Test
void shouldUseHttpExporterIfTransportIsSetToHttp() {
void whenHasTransportPropertySetToHttpUsesHttpExporter() {
this.contextRunner
.withPropertyValues("management.otlp.logging.endpoint=http://localhost:4318/v1/logs",
"management.otlp.logging.transport=http")
.withPropertyValues("management.opentelemetry.logging.export.endpoint=http://localhost:4318/v1/logs",
"management.opentelemetry.logging.export.transport=http")
.run((context) -> {
assertThat(context).hasSingleBean(OtlpHttpLogRecordExporter.class)
.hasSingleBean(LogRecordExporter.class);
assertThat(context).hasSingleBean(OtlpHttpLogRecordExporter.class);
assertThat(context).hasSingleBean(LogRecordExporter.class);
assertThat(context).doesNotHaveBean(OtlpGrpcLogRecordExporter.class);
});
}
@Test
void shouldUseGrpcExporterIfTransportIsSetToGrpc() {
void whenHasTransportPropertySetToGrpcUsesGrpcExporter() {
this.contextRunner
.withPropertyValues("management.otlp.logging.endpoint=http://localhost:4318/v1/logs",
"management.otlp.logging.transport=grpc")
.withPropertyValues("management.opentelemetry.logging.export.endpoint=http://localhost:4318/v1/logs",
"management.opentelemetry.logging.export.transport=grpc")
.run((context) -> {
assertThat(context).hasSingleBean(OtlpGrpcLogRecordExporter.class)
.hasSingleBean(LogRecordExporter.class);
assertThat(context).hasSingleBean(OtlpGrpcLogRecordExporter.class);
assertThat(context).hasSingleBean(LogRecordExporter.class);
assertThat(context).doesNotHaveBean(OtlpHttpLogRecordExporter.class);
});
}
@Test
void httpShouldUseMeterProviderIfSet() {
void whenHasMeterProviderBeanAddsItToHttpExporter() {
this.contextRunner.withUserConfiguration(MeterProviderConfiguration.class)
.withPropertyValues("management.otlp.logging.endpoint=http://localhost:4318/v1/logs")
.withPropertyValues("management.opentelemetry.logging.export.endpoint=http://localhost:4318/v1/logs")
.run((context) -> {
OtlpHttpLogRecordExporter otlpHttpLogRecordExporter = context.getBean(OtlpHttpLogRecordExporter.class);
assertThat(otlpHttpLogRecordExporter.toBuilder())
@ -173,10 +194,10 @@ class OtlpLoggingAutoConfigurationTests {
}
@Test
void grpcShouldUseMeterProviderIfSet() {
void whenHasMeterProviderBeanAddsItToGrpcExporter() {
this.contextRunner.withUserConfiguration(MeterProviderConfiguration.class)
.withPropertyValues("management.otlp.logging.endpoint=http://localhost:4318/v1/logs",
"management.otlp.logging.transport=grpc")
.withPropertyValues("management.opentelemetry.logging.export.endpoint=http://localhost:4318/v1/logs",
"management.opentelemetry.logging.export.transport=grpc")
.run((context) -> {
OtlpGrpcLogRecordExporter otlpGrpcLogRecordExporter = context.getBean(OtlpGrpcLogRecordExporter.class);
assertThat(otlpGrpcLogRecordExporter.toBuilder())
@ -186,6 +207,36 @@ class OtlpLoggingAutoConfigurationTests {
});
}
@Configuration(proxyBeanMethods = false)
public static class MultipleSdkLoggerProviderBuilderCustomizersConfig {
@Bean
public SdkLoggerProviderBuilderCustomizer customSdkLoggerProviderBuilderCustomizer1() {
return new NoopSdkLoggerProviderBuilderCustomizer();
}
@Bean
public SdkLoggerProviderBuilderCustomizer customSdkLoggerProviderBuilderCustomizer2() {
return new NoopSdkLoggerProviderBuilderCustomizer();
}
}
static class NoopSdkLoggerProviderBuilderCustomizer implements SdkLoggerProviderBuilderCustomizer {
final AtomicInteger called = new AtomicInteger(0);
@Override
public void customize(SdkLoggerProviderBuilder builder) {
this.called.incrementAndGet();
}
int called() {
return this.called.get();
}
}
@Configuration(proxyBeanMethods = false)
private static final class MeterProviderConfiguration {
@ -219,10 +270,10 @@ class OtlpLoggingAutoConfigurationTests {
}
@Configuration(proxyBeanMethods = false)
private static final class CustomOtlpLoggingConnectionDetails {
private static final class CustomOtlpLoggingConnectionDetailsConfiguration {
@Bean
OtlpLoggingConnectionDetails customOtlpLoggingConnectionDetails() {
OpenTelemetryLoggingConnectionDetails customOtlpLoggingConnectionDetails() {
return (transport) -> "https://otel.example.com/v1/logs";
}

View File

@ -14,7 +14,7 @@
* limitations under the License.
*/
package org.springframework.boot.actuate.autoconfigure.opentelemetry;
package org.springframework.boot.opentelemetry.autoconfigure;
import org.junit.jupiter.api.Test;

View File

@ -14,7 +14,7 @@
* limitations under the License.
*/
package org.springframework.boot.actuate.autoconfigure.opentelemetry;
package org.springframework.boot.opentelemetry.autoconfigure;
import java.util.LinkedHashMap;
import java.util.Map;
@ -99,7 +99,7 @@ class OpenTelemetryResourceAttributesTests {
@SuppressWarnings("unchecked")
void systemGetEnvShouldBeUsedAsDefaultEnvFunction() {
OpenTelemetryResourceAttributes attributes = new OpenTelemetryResourceAttributes(this.environment, null);
Function<String, String> getEnv = assertThat(attributes).extracting("getEnv")
Function<String, String> getEnv = assertThat(attributes).extracting("systemEnvironment")
.asInstanceOf(InstanceOfAssertFactories.type(Function.class))
.actual();
System.getenv().forEach((key, value) -> assertThat(getEnv.apply(key)).isEqualTo(value));

View File

@ -0,0 +1,364 @@
/*
* Copyright 2012-present 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.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.boot.opentelemetry.autoconfigure;
import java.util.Collection;
import java.util.concurrent.atomic.AtomicInteger;
import io.opentelemetry.api.OpenTelemetry;
import io.opentelemetry.api.common.AttributeKey;
import io.opentelemetry.context.Context;
import io.opentelemetry.context.propagation.ContextPropagators;
import io.opentelemetry.sdk.OpenTelemetrySdk;
import io.opentelemetry.sdk.common.CompletableResultCode;
import io.opentelemetry.sdk.logs.LogRecordProcessor;
import io.opentelemetry.sdk.logs.ReadWriteLogRecord;
import io.opentelemetry.sdk.logs.SdkLoggerProvider;
import io.opentelemetry.sdk.logs.SdkLoggerProviderBuilder;
import io.opentelemetry.sdk.logs.data.LogRecordData;
import io.opentelemetry.sdk.logs.export.BatchLogRecordProcessor;
import io.opentelemetry.sdk.logs.export.LogRecordExporter;
import io.opentelemetry.sdk.metrics.SdkMeterProvider;
import io.opentelemetry.sdk.resources.Resource;
import io.opentelemetry.sdk.trace.SdkTracerProvider;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.ValueSource;
import org.springframework.boot.autoconfigure.AutoConfiguration;
import org.springframework.boot.autoconfigure.AutoConfigurations;
import org.springframework.boot.context.annotation.ImportCandidates;
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 static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.entry;
import static org.mockito.Mockito.mock;
/**
* Tests for {@link OpenTelemetrySdkAutoConfiguration}.
*
* @author Moritz Halbritter
* @author Toshiaki Maki
* @author Phillip Webb
*/
class OpenTelemetrySdkAutoConfigurationTests {
private final ApplicationContextRunner contextRunner = new ApplicationContextRunner()
.withConfiguration(AutoConfigurations.of(OpenTelemetrySdkAutoConfiguration.class));
@Test
void registeredInAutoConfigurationImports() {
assertThat(ImportCandidates.load(AutoConfiguration.class, null).getCandidates())
.contains(OpenTelemetrySdkAutoConfiguration.class.getName());
}
@Test
void providesBeans() {
this.contextRunner.run((context) -> {
assertThat(context).hasSingleBean(OpenTelemetrySdk.class);
assertThat(context).hasSingleBean(Resource.class);
assertThat(context).hasSingleBean(BatchLogRecordProcessor.class);
assertThat(context).hasSingleBean(SdkLoggerProvider.class);
});
}
@ParameterizedTest
@ValueSource(strings = { "io.opentelemetry", "io.opentelemetry.api" })
void whenOpenTelemetryIsNotOnClasspathDoesNotProvideBeans(String packageName) {
this.contextRunner.withClassLoader(new FilteredClassLoader(packageName)).run((context) -> {
assertThat(context).doesNotHaveBean(OpenTelemetrySdk.class);
assertThat(context).doesNotHaveBean(Resource.class);
assertThat(context).doesNotHaveBean(BatchLogRecordProcessor.class);
assertThat(context).doesNotHaveBean(SdkLoggerProvider.class);
});
}
@Test
void whenOpenTelemetryLogsIsNotOnClasspathDoesNotProvideBeans() {
this.contextRunner.withClassLoader(new FilteredClassLoader("io.opentelemetry.sdk.logs")).run((context) -> {
assertThat(context).hasSingleBean(OpenTelemetrySdk.class);
assertThat(context).hasSingleBean(Resource.class);
assertThat(context).doesNotHaveBean(BatchLogRecordProcessor.class);
assertThat(context).doesNotHaveBean(SdkLoggerProvider.class);
});
}
@Test
void whenHasUserSuppliedBeansDoesNotProvideBeans() {
this.contextRunner.withUserConfiguration(UserConfiguration.class).run((context) -> {
assertThat(context).hasSingleBean(OpenTelemetry.class);
assertThat(context).hasBean("customOpenTelemetry");
assertThat(context).hasSingleBean(Resource.class);
assertThat(context).hasBean("customResource");
assertThat(context).hasSingleBean(BatchLogRecordProcessor.class);
assertThat(context).hasBean("customBatchLogRecordProcessor").hasSingleBean(BatchLogRecordProcessor.class);
assertThat(context).hasSingleBean(LogRecordProcessor.class);
assertThat(context).hasBean("customSdkLoggerProvider").hasSingleBean(SdkLoggerProvider.class);
});
}
@Test
void whenHasApplicationNamePropertyProvidesServiceNameResourceAttribute() {
this.contextRunner.withPropertyValues("spring.application.name=my-application").run((context) -> {
Resource resource = context.getBean(Resource.class);
assertThat(resource.getAttributes().asMap())
.contains(entry(AttributeKey.stringKey("service.name"), "my-application"));
});
}
@Test
void whenHasApplicationGroupPropertyProvidesServiceGroupResourceAttribute() {
this.contextRunner.withPropertyValues("spring.application.group=my-group").run((context) -> {
Resource resource = context.getBean(Resource.class);
assertThat(resource.getAttributes().asMap())
.contains(entry(AttributeKey.stringKey("service.group"), "my-group"));
});
}
@Test
void whenHasApplicationGroupPropertyProvidesServiceNamespaceResourceAttribute() {
this.contextRunner.withPropertyValues("spring.application.group=my-group").run((context) -> {
Resource resource = context.getBean(Resource.class);
assertThat(resource.getAttributes().asMap()).containsEntry(AttributeKey.stringKey("service.namespace"),
"my-group");
});
}
@Test
void whenHasNoApplicationGroupPropertyProvidesNoServiceGroupResourceAttribute() {
this.contextRunner.run((context) -> {
Resource resource = context.getBean(Resource.class);
assertThat(resource.getAttributes().asMap()).doesNotContainKey(AttributeKey.stringKey("service.group"));
});
}
@Test
void whenHasNoApplicationGroupPropertyProvidesNoServiceNamespaceResourceAttribute() {
this.contextRunner.run(((context) -> {
Resource resource = context.getBean(Resource.class);
assertThat(resource.getAttributes().asMap()).doesNotContainKey(AttributeKey.stringKey("service.namespace"));
}));
}
@Test
void whenHasNoApplicationNamePropertyProvidesDefaultApplicationName() {
this.contextRunner.run((context) -> {
Resource resource = context.getBean(Resource.class);
assertThat(resource.getAttributes().asMap())
.contains(entry(AttributeKey.stringKey("service.name"), "unknown_service"));
});
}
@Test
void whenHasResourceAttributesPropertyProvidesResourceAttributes() {
this.contextRunner.withPropertyValues("management.opentelemetry.resource-attributes.region=us-west")
.run((context) -> {
Resource resource = context.getBean(Resource.class);
assertThat(resource.getAttributes().asMap())
.contains(entry(AttributeKey.stringKey("region"), "us-west"));
});
}
@Test
void whenHasSdkTracerProviderBeanProvidesTracerProvider() {
this.contextRunner.withBean(SdkTracerProvider.class, () -> SdkTracerProvider.builder().build())
.run((context) -> {
OpenTelemetry openTelemetry = context.getBean(OpenTelemetry.class);
assertThat(openTelemetry.getTracerProvider()).isNotNull();
});
}
@Test
void whenHasContextPropagatorsBeanProvidesPropagators() {
this.contextRunner.withBean(ContextPropagators.class, ContextPropagators::noop).run((context) -> {
OpenTelemetry openTelemetry = context.getBean(OpenTelemetry.class);
assertThat(openTelemetry.getPropagators()).isNotNull();
});
}
@Test
void whenHasSdkLoggerProviderBeanProvidesLogsBridge() {
this.contextRunner.withBean(SdkLoggerProvider.class, () -> SdkLoggerProvider.builder().build())
.run((context) -> {
OpenTelemetry openTelemetry = context.getBean(OpenTelemetry.class);
assertThat(openTelemetry.getLogsBridge()).isNotNull();
});
}
@Test
void whenHasSdkMeterProviderProvidesMeterProvider() {
this.contextRunner.withBean(SdkMeterProvider.class, () -> SdkMeterProvider.builder().build()).run((context) -> {
OpenTelemetry openTelemetry = context.getBean(OpenTelemetry.class);
assertThat(openTelemetry.getMeterProvider()).isNotNull();
});
}
@Test
void whenHasMultipleLogRecordExportersProvidesBatchLogRecordProcessor() {
this.contextRunner.withUserConfiguration(MultipleLogRecordExportersConfiguration.class).run((context) -> {
assertThat(context).hasSingleBean(BatchLogRecordProcessor.class);
assertThat(context.getBeansOfType(LogRecordExporter.class)).hasSize(2);
assertThat(context).hasBean("customLogRecordExporter1");
assertThat(context).hasBean("customLogRecordExporter2");
});
}
@Test
void whenHasMultipleLogRecordProcessorsStillProvidesBatchLogRecordProcessor() {
this.contextRunner.withUserConfiguration(MultipleLogRecordProcessorsConfiguration.class).run((context) -> {
assertThat(context).hasSingleBean(BatchLogRecordProcessor.class);
assertThat(context).hasSingleBean(SdkLoggerProvider.class);
assertThat(context.getBeansOfType(LogRecordProcessor.class)).hasSize(3);
assertThat(context).hasBean("openTelemetryBatchLogRecordProcessor");
assertThat(context).hasBean("customLogRecordProcessor1");
assertThat(context).hasBean("customLogRecordProcessor2");
});
}
@Test
void whenHasMultipleSdkLoggerProviderBuilderCustomizersCallsCustomizeMethod() {
this.contextRunner.withUserConfiguration(MultipleSdkLoggerProviderBuilderCustomizersConfiguration.class)
.run((context) -> {
assertThat(context).hasSingleBean(SdkLoggerProvider.class);
assertThat(context.getBeansOfType(SdkLoggerProviderBuilderCustomizer.class)).hasSize(2);
assertThat(context).hasBean("customSdkLoggerProviderBuilderCustomizer1");
assertThat(context).hasBean("customSdkLoggerProviderBuilderCustomizer2");
assertThat(context
.getBean("customSdkLoggerProviderBuilderCustomizer1", NoopSdkLoggerProviderBuilderCustomizer.class)
.called()).isEqualTo(1);
assertThat(context
.getBean("customSdkLoggerProviderBuilderCustomizer2", NoopSdkLoggerProviderBuilderCustomizer.class)
.called()).isEqualTo(1);
});
}
@Configuration(proxyBeanMethods = false)
static class UserConfiguration {
@Bean
OpenTelemetry customOpenTelemetry() {
return mock(OpenTelemetry.class);
}
@Bean
Resource customResource() {
return Resource.getDefault();
}
@Bean
BatchLogRecordProcessor customBatchLogRecordProcessor() {
return BatchLogRecordProcessor.builder(new NoopLogRecordExporter()).build();
}
@Bean
SdkLoggerProvider customSdkLoggerProvider() {
return SdkLoggerProvider.builder().build();
}
}
@Configuration(proxyBeanMethods = false)
static class MultipleLogRecordExportersConfiguration {
@Bean
LogRecordExporter customLogRecordExporter1() {
return new NoopLogRecordExporter();
}
@Bean
LogRecordExporter customLogRecordExporter2() {
return new NoopLogRecordExporter();
}
}
@Configuration(proxyBeanMethods = false)
static class MultipleLogRecordProcessorsConfiguration {
@Bean
LogRecordProcessor customLogRecordProcessor1() {
return new NoopLogRecordProcessor();
}
@Bean
LogRecordProcessor customLogRecordProcessor2() {
return new NoopLogRecordProcessor();
}
}
@Configuration(proxyBeanMethods = false)
static class MultipleSdkLoggerProviderBuilderCustomizersConfiguration {
@Bean
SdkLoggerProviderBuilderCustomizer customSdkLoggerProviderBuilderCustomizer1() {
return new NoopSdkLoggerProviderBuilderCustomizer();
}
@Bean
SdkLoggerProviderBuilderCustomizer customSdkLoggerProviderBuilderCustomizer2() {
return new NoopSdkLoggerProviderBuilderCustomizer();
}
}
static class NoopLogRecordExporter implements LogRecordExporter {
@Override
public CompletableResultCode export(Collection<LogRecordData> logs) {
return CompletableResultCode.ofSuccess();
}
@Override
public CompletableResultCode flush() {
return CompletableResultCode.ofSuccess();
}
@Override
public CompletableResultCode shutdown() {
return CompletableResultCode.ofSuccess();
}
}
static class NoopLogRecordProcessor implements LogRecordProcessor {
@Override
public void onEmit(Context context, ReadWriteLogRecord logRecord) {
}
}
static class NoopSdkLoggerProviderBuilderCustomizer implements SdkLoggerProviderBuilderCustomizer {
final AtomicInteger called = new AtomicInteger(0);
@Override
public void customize(SdkLoggerProviderBuilder builder) {
this.called.incrementAndGet();
}
int called() {
return this.called.get();
}
}
}

View File

@ -44,6 +44,7 @@ dependencies {
dockerTestImplementation("org.testcontainers:junit-jupiter")
optional(project(":spring-boot-project:spring-boot-actuator-autoconfigure-all"))
optional(project(":spring-boot-project:spring-boot-opentelemetry"))
optional(project(":spring-boot-project:spring-boot-tx"))
optional(project(":spring-boot-project:spring-boot-zipkin"))
optional("org.springframework:spring-test")

View File

@ -22,10 +22,10 @@ import org.testcontainers.junit.jupiter.Container;
import org.testcontainers.junit.jupiter.Testcontainers;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.actuate.autoconfigure.logging.otlp.OtlpLoggingAutoConfiguration;
import org.springframework.boot.actuate.autoconfigure.logging.otlp.OtlpLoggingConnectionDetails;
import org.springframework.boot.actuate.autoconfigure.logging.otlp.Transport;
import org.springframework.boot.autoconfigure.ImportAutoConfiguration;
import org.springframework.boot.opentelemetry.actuate.autoconfigure.logging.OpenTelemetryLoggingConnectionDetails;
import org.springframework.boot.opentelemetry.actuate.autoconfigure.logging.OpenTelemetryLoggingExportAutoConfiguration;
import org.springframework.boot.opentelemetry.actuate.autoconfigure.logging.Transport;
import org.springframework.boot.testcontainers.service.connection.ServiceConnection;
import org.springframework.boot.testsupport.container.TestImage;
import org.springframework.context.annotation.Configuration;
@ -47,7 +47,7 @@ class GrafanaOpenTelemetryLoggingContainerConnectionDetailsFactoryIntegrationTes
static final LgtmStackContainer container = TestImage.container(LgtmStackContainer.class);
@Autowired
private OtlpLoggingConnectionDetails connectionDetails;
private OpenTelemetryLoggingConnectionDetails connectionDetails;
@Test
void connectionCanBeMadeToOpenTelemetryContainer() {
@ -58,7 +58,7 @@ class GrafanaOpenTelemetryLoggingContainerConnectionDetailsFactoryIntegrationTes
}
@Configuration(proxyBeanMethods = false)
@ImportAutoConfiguration(OtlpLoggingAutoConfiguration.class)
@ImportAutoConfiguration(OpenTelemetryLoggingExportAutoConfiguration.class)
static class TestConfiguration {
}

View File

@ -22,10 +22,10 @@ import org.testcontainers.junit.jupiter.Container;
import org.testcontainers.junit.jupiter.Testcontainers;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.actuate.autoconfigure.logging.otlp.OtlpLoggingConnectionDetails;
import org.springframework.boot.actuate.autoconfigure.logging.otlp.Transport;
import org.springframework.boot.actuate.autoconfigure.tracing.otlp.OtlpTracingAutoConfiguration;
import org.springframework.boot.autoconfigure.ImportAutoConfiguration;
import org.springframework.boot.opentelemetry.actuate.autoconfigure.logging.OpenTelemetryLoggingConnectionDetails;
import org.springframework.boot.opentelemetry.actuate.autoconfigure.logging.Transport;
import org.springframework.boot.testcontainers.service.connection.ServiceConnection;
import org.springframework.boot.testsupport.container.TestImage;
import org.springframework.context.annotation.Configuration;
@ -48,7 +48,7 @@ class OpenTelemetryLoggingContainerConnectionDetailsFactoryIntegrationTests {
.withExposedPorts(4317, 4318);
@Autowired
private OtlpLoggingConnectionDetails connectionDetails;
private OpenTelemetryLoggingConnectionDetails connectionDetails;
@Test
void connectionCanBeMadeToOpenTelemetryContainer() {

View File

@ -18,36 +18,36 @@ package org.springframework.boot.testcontainers.service.connection.otlp;
import org.testcontainers.grafana.LgtmStackContainer;
import org.springframework.boot.actuate.autoconfigure.logging.otlp.OtlpLoggingConnectionDetails;
import org.springframework.boot.actuate.autoconfigure.logging.otlp.Transport;
import org.springframework.boot.opentelemetry.actuate.autoconfigure.logging.OpenTelemetryLoggingConnectionDetails;
import org.springframework.boot.opentelemetry.actuate.autoconfigure.logging.Transport;
import org.springframework.boot.testcontainers.service.connection.ContainerConnectionDetailsFactory;
import org.springframework.boot.testcontainers.service.connection.ContainerConnectionSource;
import org.springframework.boot.testcontainers.service.connection.ServiceConnection;
/**
* {@link ContainerConnectionDetailsFactory} to create
* {@link OtlpLoggingConnectionDetails} from a
* {@link OpenTelemetryLoggingConnectionDetails} from a
* {@link ServiceConnection @ServiceConnection}-annotated {@link LgtmStackContainer} using
* the {@code "grafana/otel-lgtm"} image.
*
* @author Eddú Meléndez
*/
class GrafanaOpenTelemetryLoggingContainerConnectionDetailsFactory
extends ContainerConnectionDetailsFactory<LgtmStackContainer, OtlpLoggingConnectionDetails> {
extends ContainerConnectionDetailsFactory<LgtmStackContainer, OpenTelemetryLoggingConnectionDetails> {
GrafanaOpenTelemetryLoggingContainerConnectionDetailsFactory() {
super(ANY_CONNECTION_NAME,
"org.springframework.boot.actuate.autoconfigure.logging.otlp.OtlpLoggingAutoConfiguration");
"org.springframework.boot.opentelemetry.actuate.autoconfigure.logging.OpenTelemetryLoggingExportAutoConfiguration");
}
@Override
protected OtlpLoggingConnectionDetails getContainerConnectionDetails(
protected OpenTelemetryLoggingConnectionDetails getContainerConnectionDetails(
ContainerConnectionSource<LgtmStackContainer> source) {
return new OpenTelemetryLoggingContainerConnectionDetails(source);
}
private static final class OpenTelemetryLoggingContainerConnectionDetails
extends ContainerConnectionDetails<LgtmStackContainer> implements OtlpLoggingConnectionDetails {
extends ContainerConnectionDetails<LgtmStackContainer> implements OpenTelemetryLoggingConnectionDetails {
private OpenTelemetryLoggingContainerConnectionDetails(ContainerConnectionSource<LgtmStackContainer> source) {
super(source);

View File

@ -19,15 +19,15 @@ package org.springframework.boot.testcontainers.service.connection.otlp;
import org.testcontainers.containers.Container;
import org.testcontainers.containers.GenericContainer;
import org.springframework.boot.actuate.autoconfigure.logging.otlp.OtlpLoggingConnectionDetails;
import org.springframework.boot.actuate.autoconfigure.logging.otlp.Transport;
import org.springframework.boot.opentelemetry.actuate.autoconfigure.logging.OpenTelemetryLoggingConnectionDetails;
import org.springframework.boot.opentelemetry.actuate.autoconfigure.logging.Transport;
import org.springframework.boot.testcontainers.service.connection.ContainerConnectionDetailsFactory;
import org.springframework.boot.testcontainers.service.connection.ContainerConnectionSource;
import org.springframework.boot.testcontainers.service.connection.ServiceConnection;
/**
* {@link ContainerConnectionDetailsFactory} to create
* {@link OtlpLoggingConnectionDetails} from a
* {@link OpenTelemetryLoggingConnectionDetails} from a
* {@link ServiceConnection @ServiceConnection}-annotated {@link GenericContainer} using
* the {@code "otel/opentelemetry-collector-contrib"} image.
*
@ -35,7 +35,7 @@ import org.springframework.boot.testcontainers.service.connection.ServiceConnect
* @author Moritz Halbritter
*/
class OpenTelemetryLoggingContainerConnectionDetailsFactory
extends ContainerConnectionDetailsFactory<Container<?>, OtlpLoggingConnectionDetails> {
extends ContainerConnectionDetailsFactory<Container<?>, OpenTelemetryLoggingConnectionDetails> {
private static final int OTLP_GRPC_PORT = 4317;
@ -43,17 +43,17 @@ class OpenTelemetryLoggingContainerConnectionDetailsFactory
OpenTelemetryLoggingContainerConnectionDetailsFactory() {
super("otel/opentelemetry-collector-contrib",
"org.springframework.boot.actuate.autoconfigure.logging.otlp.OtlpLoggingAutoConfiguration");
"org.springframework.boot.opentelemetry.actuate.autoconfigure.logging.OpenTelemetryLoggingExportAutoConfiguration");
}
@Override
protected OtlpLoggingConnectionDetails getContainerConnectionDetails(
protected OpenTelemetryLoggingConnectionDetails getContainerConnectionDetails(
ContainerConnectionSource<Container<?>> source) {
return new OpenTelemetryLoggingContainerConnectionDetails(source);
}
private static final class OpenTelemetryLoggingContainerConnectionDetails
extends ContainerConnectionDetails<Container<?>> implements OtlpLoggingConnectionDetails {
extends ContainerConnectionDetails<Container<?>> implements OpenTelemetryLoggingConnectionDetails {
private OpenTelemetryLoggingContainerConnectionDetails(ContainerConnectionSource<Container<?>> source) {
super(source);