diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/logging/opentelemetry/OpenTelemetryLoggingAutoConfiguration.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/logging/opentelemetry/OpenTelemetryLoggingAutoConfiguration.java new file mode 100644 index 00000000000..934be85562a --- /dev/null +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/logging/opentelemetry/OpenTelemetryLoggingAutoConfiguration.java @@ -0,0 +1,61 @@ +/* + * Copyright 2012-2024 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.opentelemetry; + +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 logRecordExporters) { + return BatchLogRecordProcessor.builder(LogRecordExporter.composite(logRecordExporters.orderedStream().toList())) + .build(); + } + + @Bean + @ConditionalOnMissingBean + SdkLoggerProvider otelSdkLoggerProvider(Resource resource, ObjectProvider logRecordProcessors, + ObjectProvider customizers) { + SdkLoggerProviderBuilder builder = SdkLoggerProvider.builder().setResource(resource); + logRecordProcessors.orderedStream().forEach(builder::addLogRecordProcessor); + customizers.orderedStream().forEach((customizer) -> customizer.customize(builder)); + return builder.build(); + } + +} diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/logging/opentelemetry/SdkLoggerProviderBuilderCustomizer.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/logging/opentelemetry/SdkLoggerProviderBuilderCustomizer.java new file mode 100644 index 00000000000..d58daf3ca3a --- /dev/null +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/logging/opentelemetry/SdkLoggerProviderBuilderCustomizer.java @@ -0,0 +1,38 @@ +/* + * Copyright 2012-2024 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.opentelemetry; + +import io.opentelemetry.sdk.logs.SdkLoggerProvider; +import io.opentelemetry.sdk.logs.SdkLoggerProviderBuilder; + +/** + * Callback interface that can be used to customize the {@link SdkLoggerProviderBuilder} + * that is used to create the auto-configured {@link SdkLoggerProvider}. + * + * @author Toshiaki Maki + * @since 3.4.0 + */ +@FunctionalInterface +public interface SdkLoggerProviderBuilderCustomizer { + + /** + * Customize the given {@code builder}. + * @param builder the builder to customize + */ + void customize(SdkLoggerProviderBuilder builder); + +} diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/logging/opentelemetry/otlp/OtlpLoggingAutoConfiguration.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/logging/opentelemetry/otlp/OtlpLoggingAutoConfiguration.java new file mode 100644 index 00000000000..d398d6766d8 --- /dev/null +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/logging/opentelemetry/otlp/OtlpLoggingAutoConfiguration.java @@ -0,0 +1,41 @@ +/* + * Copyright 2012-2024 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.opentelemetry.otlp; + +import io.opentelemetry.api.OpenTelemetry; +import io.opentelemetry.exporter.otlp.http.logs.OtlpHttpLogRecordExporter; +import io.opentelemetry.sdk.logs.SdkLoggerProvider; + +import org.springframework.boot.autoconfigure.AutoConfiguration; +import org.springframework.boot.autoconfigure.EnableAutoConfiguration; +import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; +import org.springframework.boot.context.properties.EnableConfigurationProperties; +import org.springframework.context.annotation.Import; + +/** + * {@link EnableAutoConfiguration Auto-configuration} for OTLP logging. + * + * @author Toshiaki Maki + * @since 3.4.0 + */ +@AutoConfiguration +@ConditionalOnClass({ SdkLoggerProvider.class, OpenTelemetry.class, OtlpHttpLogRecordExporter.class }) +@EnableConfigurationProperties(OtlpLoggingProperties.class) +@Import({ OtlpLoggingConfigurations.ConnectionDetails.class, OtlpLoggingConfigurations.Exporters.class }) +public class OtlpLoggingAutoConfiguration { + +} diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/logging/opentelemetry/otlp/OtlpLoggingConfigurations.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/logging/opentelemetry/otlp/OtlpLoggingConfigurations.java new file mode 100644 index 00000000000..7558efc6446 --- /dev/null +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/logging/opentelemetry/otlp/OtlpLoggingConfigurations.java @@ -0,0 +1,89 @@ +/* + * Copyright 2012-2024 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.opentelemetry.otlp; + +import java.util.Locale; + +import io.opentelemetry.exporter.otlp.http.logs.OtlpHttpLogRecordExporter; +import io.opentelemetry.exporter.otlp.http.logs.OtlpHttpLogRecordExporterBuilder; + +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; + +/** + * Configurations imported by {@link OtlpLoggingAutoConfiguration}. + * + * @author Toshiaki Maki + */ +final class OtlpLoggingConfigurations { + + private OtlpLoggingConfigurations() { + } + + @Configuration(proxyBeanMethods = false) + static class ConnectionDetails { + + @Bean + @ConditionalOnMissingBean + @ConditionalOnProperty(prefix = "management.otlp.logging", name = "endpoint") + OtlpLoggingConnectionDetails otlpLogsConnectionDetails(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 getEndpoint() { + return this.properties.getEndpoint(); + } + + } + + } + + @Configuration(proxyBeanMethods = false) + static class Exporters { + + @ConditionalOnMissingBean(value = OtlpHttpLogRecordExporter.class, + type = "io.opentelemetry.exporter.otlp.logs.OtlpGrpcLogRecordExporter") + @ConditionalOnBean(OtlpLoggingConnectionDetails.class) + @Bean + OtlpHttpLogRecordExporter otlpHttpLogRecordExporter(OtlpLoggingProperties properties, + OtlpLoggingConnectionDetails connectionDetails) { + OtlpHttpLogRecordExporterBuilder builder = OtlpHttpLogRecordExporter.builder() + .setEndpoint(connectionDetails.getEndpoint()) + .setCompression(properties.getCompression().name().toLowerCase(Locale.US)) + .setTimeout(properties.getTimeout()); + properties.getHeaders().forEach(builder::addHeader); + return builder.build(); + } + + } + +} diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/logging/opentelemetry/otlp/OtlpLoggingConnectionDetails.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/logging/opentelemetry/otlp/OtlpLoggingConnectionDetails.java new file mode 100644 index 00000000000..f4d1dfb35a5 --- /dev/null +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/logging/opentelemetry/otlp/OtlpLoggingConnectionDetails.java @@ -0,0 +1,35 @@ +/* + * Copyright 2012-2024 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.opentelemetry.otlp; + +import org.springframework.boot.autoconfigure.service.connection.ConnectionDetails; + +/** + * Details required to establish a connection to an OpenTelemetry logging service. + * + * @author Toshiaki Maki + * @since 3.4.0 + */ +public interface OtlpLoggingConnectionDetails extends ConnectionDetails { + + /** + * Address to where logs will be published. + * @return the address to where logs will be published + */ + String getEndpoint(); + +} diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/logging/opentelemetry/otlp/OtlpLoggingProperties.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/logging/opentelemetry/otlp/OtlpLoggingProperties.java new file mode 100644 index 00000000000..b7d484a9466 --- /dev/null +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/logging/opentelemetry/otlp/OtlpLoggingProperties.java @@ -0,0 +1,99 @@ +/* + * Copyright 2012-2024 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.opentelemetry.otlp; + +import java.time.Duration; +import java.util.HashMap; +import java.util.Map; + +import org.springframework.boot.context.properties.ConfigurationProperties; + +/** + * Configuration properties for exporting logs using OTLP. + * + * @author Jonatan Ivanov + * @since 3.4.0 + */ +@ConfigurationProperties("management.otlp.logging") +public class OtlpLoggingProperties { + + /** + * URL to the OTel collector's HTTP API. + */ + private String endpoint; + + /** + * 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. + */ + private Duration timeout = Duration.ofSeconds(10); + + /** + * Method used to compress the payload. + */ + private Compression compression = Compression.NONE; + + /** + * Custom HTTP headers you want to pass to the collector, for example auth headers. + */ + private final Map headers = new HashMap<>(); + + public String getEndpoint() { + return this.endpoint; + } + + public void setEndpoint(String endpoint) { + this.endpoint = endpoint; + } + + public Duration getTimeout() { + return this.timeout; + } + + public void setTimeout(Duration timeout) { + this.timeout = timeout; + } + + public Compression getCompression() { + return this.compression; + } + + public void setCompression(Compression compression) { + this.compression = compression; + } + + public Map getHeaders() { + return this.headers; + } + + public enum Compression { + + /** + * Gzip compression. + */ + GZIP, + + /** + * No compression. + */ + NONE + + } + +} diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/logging/opentelemetry/otlp/package-info.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/logging/opentelemetry/otlp/package-info.java new file mode 100644 index 00000000000..167fb211c09 --- /dev/null +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/logging/opentelemetry/otlp/package-info.java @@ -0,0 +1,20 @@ +/* + * Copyright 2012-2023 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. + */ + +/** + * Auto-configuration for OpenTelemetry logging with OTLP. + */ +package org.springframework.boot.actuate.autoconfigure.logging.opentelemetry.otlp; diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/logging/opentelemetry/package-info.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/logging/opentelemetry/package-info.java new file mode 100644 index 00000000000..63ec3087114 --- /dev/null +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/logging/opentelemetry/package-info.java @@ -0,0 +1,20 @@ +/* + * Copyright 2012-2019 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. + */ + +/** + * Auto-configuration for OpenTelemetry logging. + */ +package org.springframework.boot.actuate.autoconfigure.logging.opentelemetry; diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports index d3a6fc7ead5..3fcc8bf0204 100644 --- a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports @@ -35,6 +35,8 @@ org.springframework.boot.actuate.autoconfigure.ldap.LdapHealthContributorAutoCon org.springframework.boot.actuate.autoconfigure.liquibase.LiquibaseEndpointAutoConfiguration org.springframework.boot.actuate.autoconfigure.logging.LogFileWebEndpointAutoConfiguration org.springframework.boot.actuate.autoconfigure.logging.LoggersEndpointAutoConfiguration +org.springframework.boot.actuate.autoconfigure.logging.opentelemetry.OpenTelemetryLoggingAutoConfiguration +org.springframework.boot.actuate.autoconfigure.logging.opentelemetry.otlp.OtlpLoggingAutoConfiguration org.springframework.boot.actuate.autoconfigure.mail.MailHealthContributorAutoConfiguration org.springframework.boot.actuate.autoconfigure.management.HeapDumpWebEndpointAutoConfiguration org.springframework.boot.actuate.autoconfigure.management.ThreadDumpEndpointAutoConfiguration diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/logging/opentelemetry/OpenTelemetryLoggingAutoConfigurationTests.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/logging/opentelemetry/OpenTelemetryLoggingAutoConfigurationTests.java new file mode 100644 index 00000000000..00ddf95e3f5 --- /dev/null +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/logging/opentelemetry/OpenTelemetryLoggingAutoConfigurationTests.java @@ -0,0 +1,226 @@ +/* + * Copyright 2012-2024 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.opentelemetry; + +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.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( + org.springframework.boot.actuate.autoconfigure.opentelemetry.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 shouldAllowMultipleLogRecordExporter() { + this.contextRunner.withUserConfiguration(MultipleLogRecordExporterConfig.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 shouldAllowMultipleLogRecordProcessorInAdditionToBatchLogRecordProcessor() { + this.contextRunner.withUserConfiguration(MultipleLogRecordProcessorConfig.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 shouldAllowMultipleSdkLoggerProviderBuilderCustomizer() { + this.contextRunner.withUserConfiguration(MultipleSdkLoggerProviderBuilderCustomizerConfig.class) + .run((context) -> { + assertThat(context).hasSingleBean(SdkLoggerProvider.class); + assertThat(context.getBeansOfType(NoopSdkLoggerProviderBuilderCustomizer.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 MultipleLogRecordExporterConfig { + + @Bean + public LogRecordExporter customLogRecordExporter1() { + return new NoopLogRecordExporter(); + } + + @Bean + public LogRecordExporter customLogRecordExporter2() { + return new NoopLogRecordExporter(); + } + + } + + @Configuration(proxyBeanMethods = false) + public static class MultipleLogRecordProcessorConfig { + + @Bean + public LogRecordProcessor customLogRecordProcessor1() { + return new NoopLogRecordProcessor(); + } + + @Bean + public LogRecordProcessor customLogRecordProcessor2() { + return new NoopLogRecordProcessor(); + } + + } + + @Configuration(proxyBeanMethods = false) + public static class MultipleSdkLoggerProviderBuilderCustomizerConfig { + + @Bean + public SdkLoggerProviderBuilderCustomizer customSdkLoggerProviderBuilderCustomizer1() { + return new NoopSdkLoggerProviderBuilderCustomizer(); + } + + @Bean + public SdkLoggerProviderBuilderCustomizer customSdkLoggerProviderBuilderCustomizer2() { + return new NoopSdkLoggerProviderBuilderCustomizer(); + } + + } + + static class NoopLogRecordExporter implements LogRecordExporter { + + @Override + public CompletableResultCode export(Collection 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(); + } + + } + +} diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/logging/opentelemetry/otlp/OtlpLoggingAutoConfigurationIntegrationTests.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/logging/opentelemetry/otlp/OtlpLoggingAutoConfigurationIntegrationTests.java new file mode 100644 index 00000000000..71b4b55c9dd --- /dev/null +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/logging/opentelemetry/otlp/OtlpLoggingAutoConfigurationIntegrationTests.java @@ -0,0 +1,128 @@ +/* + * Copyright 2012-2024 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.opentelemetry.otlp; + +import java.io.IOException; +import java.nio.charset.StandardCharsets; +import java.time.Instant; +import java.util.concurrent.TimeUnit; + +import io.opentelemetry.api.logs.Severity; +import io.opentelemetry.sdk.logs.SdkLoggerProvider; +import okhttp3.mockwebserver.MockResponse; +import okhttp3.mockwebserver.MockWebServer; +import okhttp3.mockwebserver.RecordedRequest; +import okio.Buffer; +import okio.GzipSource; +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.opentelemetry.OpenTelemetryLoggingAutoConfiguration; +import org.springframework.boot.actuate.autoconfigure.opentelemetry.OpenTelemetryAutoConfiguration; +import org.springframework.boot.autoconfigure.AutoConfigurations; +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}. + * + * @author Toshiaki Maki + */ +public class OtlpLoggingAutoConfigurationIntegrationTests { + + 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)); + + private final MockWebServer mockWebServer = new MockWebServer(); + + @BeforeEach + void setUp() throws IOException { + this.mockWebServer.start(); + } + + @AfterEach + void tearDown() throws IOException { + this.mockWebServer.close(); + } + + @Test + void httpLogRecordExporterShouldUseProtobufAndNoCompressionByDefault() { + this.mockWebServer.enqueue(new MockResponse()); + this.contextRunner + .withPropertyValues("management.otlp.logging.endpoint=http://localhost:%d/v1/logs" + .formatted(this.mockWebServer.getPort())) + .run((context) -> { + logMessage(context); + RecordedRequest request = this.mockWebServer.takeRequest(10, TimeUnit.SECONDS); + assertThat(request).isNotNull(); + assertThat(request.getRequestLine()).contains("/v1/logs"); + assertThat(request.getHeader("Content-Type")).isEqualTo("application/x-protobuf"); + assertThat(request.getHeader("Content-Encoding")).isNull(); + assertThat(request.getBodySize()).isPositive(); + try (Buffer body = request.getBody()) { + assertLogMessage(body); + } + }); + } + + @Test + 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") + .run((context) -> { + logMessage(context); + RecordedRequest request = this.mockWebServer.takeRequest(10, TimeUnit.SECONDS); + assertThat(request).isNotNull(); + assertThat(request.getRequestLine()).contains("/v1/logs"); + assertThat(request.getHeader("Content-Type")).isEqualTo("application/x-protobuf"); + assertThat(request.getHeader("Content-Encoding")).isEqualTo("gzip"); + assertThat(request.getBodySize()).isPositive(); + try (Buffer uncompressed = new Buffer(); Buffer body = request.getBody()) { + uncompressed.writeAll(new GzipSource(body)); + assertLogMessage(uncompressed); + } + }); + } + + private static void logMessage(ApplicationContext context) { + SdkLoggerProvider loggerProvider = context.getBean(SdkLoggerProvider.class); + loggerProvider.get("test") + .logRecordBuilder() + .setSeverity(Severity.INFO) + .setSeverityText("INFO") + .setBody("Hello") + .setTimestamp(Instant.now()) + .emit(); + } + + private static void assertLogMessage(Buffer body) { + String string = body.readString(StandardCharsets.UTF_8); + assertThat(string).contains("otlp-logs-test"); + assertThat(string).contains("test"); + assertThat(string).contains("INFO"); + assertThat(string).contains("Hello"); + } + +} diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/logging/opentelemetry/otlp/OtlpLoggingAutoConfigurationTests.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/logging/opentelemetry/otlp/OtlpLoggingAutoConfigurationTests.java new file mode 100644 index 00000000000..afe42035c0c --- /dev/null +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/logging/opentelemetry/otlp/OtlpLoggingAutoConfigurationTests.java @@ -0,0 +1,132 @@ +/* + * Copyright 2012-2024 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.opentelemetry.otlp; + +import io.opentelemetry.exporter.otlp.http.logs.OtlpHttpLogRecordExporter; +import io.opentelemetry.exporter.otlp.logs.OtlpGrpcLogRecordExporter; +import io.opentelemetry.sdk.logs.export.LogRecordExporter; +import okhttp3.HttpUrl; +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.opentelemetry.otlp.OtlpLoggingConfigurations.ConnectionDetails.PropertiesOtlpLoggingConnectionDetails; +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 OtlpLoggingAutoConfiguration}. + * + * @author Toshiaki Maki + */ +class OtlpLoggingAutoConfigurationTests { + + private final ApplicationContextRunner contextRunner = new ApplicationContextRunner() + .withConfiguration(AutoConfigurations.of(OtlpLoggingAutoConfiguration.class)); + + @Test + void shouldNotSupplyBeansIfPropertyIsNotSet() { + this.contextRunner.run((context) -> { + assertThat(context).doesNotHaveBean(OtlpLoggingConnectionDetails.class); + assertThat(context).doesNotHaveBean(OtlpHttpLogRecordExporter.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.getEndpoint()).isEqualTo("http://localhost:4318/v1/logs"); + assertThat(context).hasSingleBean(OtlpHttpLogRecordExporter.class) + .hasSingleBean(LogRecordExporter.class); + }); + } + + @ParameterizedTest + @ValueSource(strings = { "io.opentelemetry.sdk.logs", "io.opentelemetry.api", + "io.opentelemetry.exporter.otlp.http.logs" }) + void shouldNotSupplyBeansIfDependencyIsMissing(String packageName) { + this.contextRunner.withClassLoader(new FilteredClassLoader(packageName)).run((context) -> { + assertThat(context).doesNotHaveBean(OtlpLoggingConnectionDetails.class); + assertThat(context).doesNotHaveBean(OtlpHttpLogRecordExporter.class); + }); + } + + @Test + void shouldBackOffWhenCustomHttpExporterIsDefined() { + this.contextRunner.withUserConfiguration(CustomHttpExporterConfiguration.class) + .run((context) -> assertThat(context).hasBean("customOtlpHttpLogRecordExporter") + .hasSingleBean(LogRecordExporter.class)); + } + + @Test + void shouldBackOffWhenCustomGrpcExporterIsDefined() { + this.contextRunner.withUserConfiguration(CustomGrpcExporterConfiguration.class) + .run((context) -> assertThat(context).hasBean("customOtlpGrpcLogRecordExporter") + .hasSingleBean(LogRecordExporter.class)); + } + + @Test + void shouldBackOffWhenCustomOtlpLogsConnectionDetailsIsDefined() { + this.contextRunner.withUserConfiguration(CustomOtlpLogsConnectionDetails.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")); + }); + + } + + @Configuration(proxyBeanMethods = false) + public static class CustomHttpExporterConfiguration { + + @Bean + public OtlpHttpLogRecordExporter customOtlpHttpLogRecordExporter() { + return OtlpHttpLogRecordExporter.builder().build(); + } + + } + + @Configuration(proxyBeanMethods = false) + public static class CustomGrpcExporterConfiguration { + + @Bean + public OtlpGrpcLogRecordExporter customOtlpGrpcLogRecordExporter() { + return OtlpGrpcLogRecordExporter.builder().build(); + } + + } + + @Configuration(proxyBeanMethods = false) + public static class CustomOtlpLogsConnectionDetails { + + @Bean + public OtlpLoggingConnectionDetails customOtlpLogsConnectionDetails() { + return () -> "https://otel.example.com/v1/logs"; + } + + } + +}