diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/logs/OpenTelemetryAutoConfiguration.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/logs/OpenTelemetryAutoConfiguration.java new file mode 100644 index 000000000000..9e41b658923b --- /dev/null +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/logs/OpenTelemetryAutoConfiguration.java @@ -0,0 +1,62 @@ +/* + * 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.logs; + +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 Logs. + * + * @author Toshiaki Maki + * @since 3.4.0 + */ +@AutoConfiguration("openTelemetryLogsAutoConfiguration") +@ConditionalOnClass({ SdkLoggerProvider.class, OpenTelemetry.class }) +public class OpenTelemetryAutoConfiguration { + + @Bean + @ConditionalOnMissingBean + public BatchLogRecordProcessor batchLogRecordProcessor(ObjectProvider logRecordExporters) { + return BatchLogRecordProcessor.builder(LogRecordExporter.composite(logRecordExporters.orderedStream().toList())) + .build(); + } + + @Bean + @ConditionalOnMissingBean + public 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/logs/SdkLoggerProviderBuilderCustomizer.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/logs/SdkLoggerProviderBuilderCustomizer.java new file mode 100644 index 000000000000..47d9ac382a3f --- /dev/null +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/logs/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.logs; + +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/logs/otlp/OtlpLogsAutoConfiguration.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/logs/otlp/OtlpLogsAutoConfiguration.java new file mode 100644 index 000000000000..3bf302a9d167 --- /dev/null +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/logs/otlp/OtlpLogsAutoConfiguration.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.logs.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 Logs. + * + * @author Toshiaki Maki + * @since 3.4.0 + */ +@AutoConfiguration +@ConditionalOnClass({ SdkLoggerProvider.class, OpenTelemetry.class, OtlpHttpLogRecordExporter.class }) +@EnableConfigurationProperties(OtlpProperties.class) +@Import({ OtlpLogsConfigurations.ConnectionDetails.class, OtlpLogsConfigurations.Exporters.class }) +public class OtlpLogsAutoConfiguration { + +} diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/logs/otlp/OtlpLogsConfigurations.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/logs/otlp/OtlpLogsConfigurations.java new file mode 100644 index 000000000000..f739549a4b89 --- /dev/null +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/logs/otlp/OtlpLogsConfigurations.java @@ -0,0 +1,87 @@ +/* + * 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.logs.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 OtlpLogsAutoConfiguration}. + * + * @author Toshiaki Maki + * @since 3.4.0 + */ +public class OtlpLogsConfigurations { + + @Configuration(proxyBeanMethods = false) + static class ConnectionDetails { + + @Bean + @ConditionalOnMissingBean + @ConditionalOnProperty(prefix = "management.otlp.logs", name = "endpoint") + OtlpLogsConnectionDetails otlpLogsConnectionDetails(OtlpProperties properties) { + return new PropertiesOtlpLogsConnectionDetails(properties); + } + + /** + * Adapts {@link OtlpProperties} to {@link OtlpLogsConnectionDetails}. + */ + static class PropertiesOtlpLogsConnectionDetails implements OtlpLogsConnectionDetails { + + private final OtlpProperties properties; + + PropertiesOtlpLogsConnectionDetails(OtlpProperties properties) { + this.properties = properties; + } + + @Override + public String getUrl() { + return this.properties.getEndpoint(); + } + + } + + } + + @Configuration(proxyBeanMethods = false) + static class Exporters { + + @ConditionalOnMissingBean(value = OtlpHttpLogRecordExporter.class, + type = "io.opentelemetry.exporter.otlp.logs.OtlpGrpcLogRecordExporter") + @ConditionalOnBean(OtlpLogsConnectionDetails.class) + @Bean + OtlpHttpLogRecordExporter otlpHttpLogRecordExporter(OtlpProperties properties, + OtlpLogsConnectionDetails connectionDetails) { + OtlpHttpLogRecordExporterBuilder builder = OtlpHttpLogRecordExporter.builder() + .setEndpoint(connectionDetails.getUrl()) + .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/logs/otlp/OtlpLogsConnectionDetails.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/logs/otlp/OtlpLogsConnectionDetails.java new file mode 100644 index 000000000000..83ee7d1648e0 --- /dev/null +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/logs/otlp/OtlpLogsConnectionDetails.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.logs.otlp; + +import org.springframework.boot.autoconfigure.service.connection.ConnectionDetails; + +/** + * Details required to establish a connection to an OpenTelemetry logs service. + * + * @author Toshiaki Maki + * @since 3.4.0 + */ +public interface OtlpLogsConnectionDetails extends ConnectionDetails { + + /** + * Address to where logs will be published. + * @return the address to where logs will be published + */ + String getUrl(); + +} diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/logs/otlp/OtlpProperties.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/logs/otlp/OtlpProperties.java new file mode 100644 index 000000000000..e520ccf2dce2 --- /dev/null +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/logs/otlp/OtlpProperties.java @@ -0,0 +1,103 @@ +/* + * 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.logs.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.logs") +public class OtlpProperties { + + /** + * 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 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 void setHeaders(Map headers) { + this.headers = 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/logs/otlp/package-info.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/logs/otlp/package-info.java new file mode 100644 index 000000000000..5c5a7d42cb97 --- /dev/null +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/logs/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 logs with OTLP. + */ +package org.springframework.boot.actuate.autoconfigure.logs.otlp; diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/logs/package-info.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/logs/package-info.java new file mode 100644 index 000000000000..6db2a10b3e31 --- /dev/null +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/logs/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 Logs. + */ +package org.springframework.boot.actuate.autoconfigure.logs; 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 d3a6fc7ead54..77b1d8aa837c 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.logs.OpenTelemetryAutoConfiguration +org.springframework.boot.actuate.autoconfigure.logs.otlp.OtlpLogsAutoConfiguration 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/logs/OpenTelemetryAutoConfigurationTests.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/logs/OpenTelemetryAutoConfigurationTests.java new file mode 100644 index 000000000000..08415d359b07 --- /dev/null +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/logs/OpenTelemetryAutoConfigurationTests.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.logs; + +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 OpenTelemetryAutoConfiguration}. + * + * @author Toshiaki Maki + */ +class OpenTelemetryAutoConfigurationTests { + + private final ApplicationContextRunner contextRunner; + + OpenTelemetryAutoConfigurationTests() { + this.contextRunner = new ApplicationContextRunner().withConfiguration(AutoConfigurations.of( + org.springframework.boot.actuate.autoconfigure.opentelemetry.OpenTelemetryAutoConfiguration.class, + OpenTelemetryAutoConfiguration.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/logs/otlp/OtlpLogsAutoConfigurationIntegrationTests.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/logs/otlp/OtlpLogsAutoConfigurationIntegrationTests.java new file mode 100644 index 000000000000..933076186a13 --- /dev/null +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/logs/otlp/OtlpLogsAutoConfigurationIntegrationTests.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.logs.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.logs.OpenTelemetryAutoConfiguration; +import org.springframework.boot.autoconfigure.AutoConfigurations; +import org.springframework.boot.test.context.runner.ApplicationContextRunner; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Integration tests for {@link OtlpLogsAutoConfiguration}. + * + * @author Toshiaki Maki + */ +public class OtlpLogsAutoConfigurationIntegrationTests { + + private final ApplicationContextRunner contextRunner = new ApplicationContextRunner() + .withPropertyValues("spring.application.name=otlp-logs-test", + "management.otlp.logs.headers.Authorization=Bearer my-token") + .withConfiguration(AutoConfigurations.of( + org.springframework.boot.actuate.autoconfigure.opentelemetry.OpenTelemetryAutoConfiguration.class, + OpenTelemetryAutoConfiguration.class, OtlpLogsAutoConfiguration.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.logs.endpoint=http://localhost:%d/v1/logs".formatted(this.mockWebServer.getPort())) + .run((context) -> { + SdkLoggerProvider loggerProvider = context.getBean(SdkLoggerProvider.class); + loggerProvider.get("test") + .logRecordBuilder() + .setSeverity(Severity.INFO) + .setSeverityText("INFO") + .setBody("Hello") + .setTimestamp(Instant.now()) + .emit(); + 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()) { + String bodyString = body.readString(StandardCharsets.UTF_8); + assertThat(bodyString).contains("otlp-logs-test"); + assertThat(bodyString).contains("test"); + assertThat(bodyString).contains("INFO"); + + assertThat(bodyString).contains("Hello"); + } + }); + } + + @Test + void httpLogRecordExporterCanBeConfiguredToUseGzipCompression() { + this.mockWebServer.enqueue(new MockResponse()); + this.contextRunner + .withPropertyValues( + "management.otlp.logs.endpoint=http://localhost:%d/v1/logs".formatted(this.mockWebServer.getPort()), + "management.otlp.logs.compression=gzip") + .run((context) -> { + SdkLoggerProvider loggerProvider = context.getBean(SdkLoggerProvider.class); + loggerProvider.get("test") + .logRecordBuilder() + .setBody("Hello") + .setSeverity(Severity.INFO) + .setSeverityText("INFO") + .setTimestamp(Instant.now()) + .emit(); + 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)); + String bodyString = uncompressed.readString(StandardCharsets.UTF_8); + assertThat(bodyString).contains("otlp-logs-test"); + assertThat(bodyString).contains("test"); + assertThat(bodyString).contains("INFO"); + assertThat(bodyString).contains("Hello"); + } + }); + } + +} diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/logs/otlp/OtlpLogsAutoConfigurationTests.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/logs/otlp/OtlpLogsAutoConfigurationTests.java new file mode 100644 index 000000000000..ac5d4bd62fa5 --- /dev/null +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/logs/otlp/OtlpLogsAutoConfigurationTests.java @@ -0,0 +1,130 @@ +/* + * 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.logs.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.logs.otlp.OtlpLogsConfigurations.ConnectionDetails.PropertiesOtlpLogsConnectionDetails; +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 OtlpLogsAutoConfiguration}. + * + * @author Toshiaki Maki + */ +class OtlpLogsAutoConfigurationTests { + + private final ApplicationContextRunner contextRunner = new ApplicationContextRunner() + .withConfiguration(AutoConfigurations.of(OtlpLogsAutoConfiguration.class)); + + @Test + void shouldNotSupplyBeansIfPropertyIsNotSet() { + this.contextRunner.run((context) -> { + assertThat(context).doesNotHaveBean(OtlpLogsConnectionDetails.class); + assertThat(context).doesNotHaveBean(OtlpHttpLogRecordExporter.class); + }); + } + + @Test + void shouldSupplyBeans() { + this.contextRunner.withPropertyValues("management.otlp.logs.endpoint=http://localhost:4318/v1/logs") + .run((context) -> { + assertThat(context).hasSingleBean(OtlpLogsConnectionDetails.class); + 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(OtlpLogsConnectionDetails.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(OtlpLogsConnectionDetails.class) + .doesNotHaveBean(PropertiesOtlpLogsConnectionDetails.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 OtlpLogsConnectionDetails customOtlpLogsConnectionDetails() { + return () -> "https://otel.example.com/v1/logs"; + } + + } + +}