diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/logging/ConditionalOnEnabledLoggingExport.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/logging/ConditionalOnEnabledLoggingExport.java new file mode 100644 index 0000000000..b32d8f9c49 --- /dev/null +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/logging/ConditionalOnEnabledLoggingExport.java @@ -0,0 +1,51 @@ +/* + * 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; + +import java.lang.annotation.Documented; +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +import org.springframework.context.annotation.Conditional; + +/** + * {@link Conditional @Conditional} that checks whether logging export is enabled. It + * matches if the value of the {@code management.logging.export.enabled} property is + * {@code true} or if it is not configured. If the {@link #value() logging exporter name} + * is set, the {@code management..logging.export.enabled} property can be used to + * control the behavior for the specific logging exporter. In that case, the + * exporter-specific property takes precedence over the global property. + * + * @author Moritz Halbritter + * @author Dmytro Nosan + * @since 3.4.0 + */ +@Retention(RetentionPolicy.RUNTIME) +@Target({ ElementType.TYPE, ElementType.METHOD }) +@Documented +@Conditional(OnEnabledLoggingExportCondition.class) +public @interface ConditionalOnEnabledLoggingExport { + + /** + * Name of the logging exporter. + * @return the name of the logging exporter + */ + String value() default ""; + +} diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/logging/OnEnabledLoggingExportCondition.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/logging/OnEnabledLoggingExportCondition.java new file mode 100644 index 0000000000..eb328c8165 --- /dev/null +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/logging/OnEnabledLoggingExportCondition.java @@ -0,0 +1,72 @@ +/* + * 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; + +import java.util.Map; + +import org.springframework.boot.autoconfigure.condition.ConditionMessage; +import org.springframework.boot.autoconfigure.condition.ConditionOutcome; +import org.springframework.boot.autoconfigure.condition.SpringBootCondition; +import org.springframework.context.annotation.ConditionContext; +import org.springframework.core.type.AnnotatedTypeMetadata; +import org.springframework.util.StringUtils; + +/** + * {@link SpringBootCondition} to check whether logging exporter is enabled. + * + * @author Moritz Halbritter + * @author Dmytro Nosan + * @see ConditionalOnEnabledLoggingExport + */ +class OnEnabledLoggingExportCondition extends SpringBootCondition { + + private static final String GLOBAL_PROPERTY = "management.logging.export.enabled"; + + private static final String EXPORTER_PROPERTY = "management.%s.logging.export.enabled"; + + @Override + public ConditionOutcome getMatchOutcome(ConditionContext context, AnnotatedTypeMetadata metadata) { + String loggingExporter = getExporterName(metadata); + if (StringUtils.hasLength(loggingExporter)) { + Boolean exporterLoggingEnabled = context.getEnvironment() + .getProperty(EXPORTER_PROPERTY.formatted(loggingExporter), Boolean.class); + if (exporterLoggingEnabled != null) { + return new ConditionOutcome(exporterLoggingEnabled, + ConditionMessage.forCondition(ConditionalOnEnabledLoggingExport.class) + .because(EXPORTER_PROPERTY.formatted(loggingExporter) + " is " + exporterLoggingEnabled)); + } + } + Boolean globalLoggingEnabled = context.getEnvironment().getProperty(GLOBAL_PROPERTY, Boolean.class); + if (globalLoggingEnabled != null) { + return new ConditionOutcome(globalLoggingEnabled, + ConditionMessage.forCondition(ConditionalOnEnabledLoggingExport.class) + .because(GLOBAL_PROPERTY + " is " + globalLoggingEnabled)); + } + return ConditionOutcome.match(ConditionMessage.forCondition(ConditionalOnEnabledLoggingExport.class) + .because("is enabled by default")); + } + + private static String getExporterName(AnnotatedTypeMetadata metadata) { + Map attributes = metadata + .getAnnotationAttributes(ConditionalOnEnabledLoggingExport.class.getName()); + if (attributes == null) { + return null; + } + return (String) attributes.get("value"); + } + +} diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/logging/otlp/OtlpLoggingConfigurations.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/logging/otlp/OtlpLoggingConfigurations.java index b2b1b300f9..14b491bd23 100644 --- a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/logging/otlp/OtlpLoggingConfigurations.java +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/logging/otlp/OtlpLoggingConfigurations.java @@ -23,6 +23,7 @@ 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.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; @@ -76,6 +77,7 @@ final class OtlpLoggingConfigurations { @Configuration(proxyBeanMethods = false) @ConditionalOnMissingBean({ OtlpGrpcLogRecordExporter.class, OtlpHttpLogRecordExporter.class }) @ConditionalOnBean(OtlpLoggingConnectionDetails.class) + @ConditionalOnEnabledLoggingExport("otlp") static class Exporters { @Bean diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/resources/META-INF/additional-spring-configuration-metadata.json b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/resources/META-INF/additional-spring-configuration-metadata.json index e05babb51f..855ab8e56d 100644 --- a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/resources/META-INF/additional-spring-configuration-metadata.json +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/resources/META-INF/additional-spring-configuration-metadata.json @@ -309,6 +309,12 @@ "description": "Whether to enable SSL certificate info.", "defaultValue": false }, + { + "name": "management.logging.export.enabled", + "type": "java.lang.Boolean", + "description": "Whether auto-configuration of logging is enabled to export logs.", + "defaultValue": true + }, { "name": "management.metrics.binders.files.enabled", "type": "java.lang.Boolean", @@ -2067,6 +2073,11 @@ "description": "Whether auto-configuration of Micrometer annotations is enabled.", "defaultValue": false }, + { + "name": "management.otlp.logging.export.enabled", + "type": "java.lang.Boolean", + "description": "Whether auto-configuration of logging is enabled to export OTLP logs." + }, { "name": "management.otlp.tracing.export.enabled", "type": "java.lang.Boolean", diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/logging/OnEnabledLoggingExportConditionTests.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/logging/OnEnabledLoggingExportConditionTests.java new file mode 100644 index 0000000000..1bca2e1071 --- /dev/null +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/logging/OnEnabledLoggingExportConditionTests.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.logging; + +import java.util.Collections; +import java.util.Map; + +import org.junit.jupiter.api.Test; + +import org.springframework.boot.autoconfigure.condition.ConditionOutcome; +import org.springframework.context.annotation.ConditionContext; +import org.springframework.core.type.AnnotatedTypeMetadata; +import org.springframework.mock.env.MockEnvironment; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.BDDMockito.given; +import static org.mockito.Mockito.mock; + +/** + * Tests for {@link OnEnabledLoggingExportCondition}. + * + * @author Moritz Halbritter + * @author Dmytro Nosan + */ +class OnEnabledLoggingExportConditionTests { + + @Test + void shouldMatchIfNoPropertyIsSet() { + OnEnabledLoggingExportCondition condition = new OnEnabledLoggingExportCondition(); + ConditionOutcome outcome = condition.getMatchOutcome(mockConditionContext(), mockMetadata("")); + assertThat(outcome.isMatch()).isTrue(); + assertThat(outcome.getMessage()).isEqualTo("@ConditionalOnEnabledLoggingExport is enabled by default"); + } + + @Test + void shouldNotMatchIfGlobalPropertyIsFalse() { + OnEnabledLoggingExportCondition condition = new OnEnabledLoggingExportCondition(); + ConditionOutcome outcome = condition.getMatchOutcome( + mockConditionContext(Map.of("management.logging.export.enabled", "false")), mockMetadata("")); + assertThat(outcome.isMatch()).isFalse(); + assertThat(outcome.getMessage()) + .isEqualTo("@ConditionalOnEnabledLoggingExport management.logging.export.enabled is false"); + } + + @Test + void shouldMatchIfGlobalPropertyIsTrue() { + OnEnabledLoggingExportCondition condition = new OnEnabledLoggingExportCondition(); + ConditionOutcome outcome = condition.getMatchOutcome( + mockConditionContext(Map.of("management.logging.export.enabled", "true")), mockMetadata("")); + assertThat(outcome.isMatch()).isTrue(); + assertThat(outcome.getMessage()) + .isEqualTo("@ConditionalOnEnabledLoggingExport management.logging.export.enabled is true"); + } + + @Test + void shouldNotMatchIfExporterPropertyIsFalse() { + OnEnabledLoggingExportCondition condition = new OnEnabledLoggingExportCondition(); + ConditionOutcome outcome = condition.getMatchOutcome( + mockConditionContext(Map.of("management.otlp.logging.export.enabled", "false")), mockMetadata("otlp")); + assertThat(outcome.isMatch()).isFalse(); + assertThat(outcome.getMessage()) + .isEqualTo("@ConditionalOnEnabledLoggingExport management.otlp.logging.export.enabled is false"); + } + + @Test + void shouldMatchIfExporterPropertyIsTrue() { + OnEnabledLoggingExportCondition condition = new OnEnabledLoggingExportCondition(); + ConditionOutcome outcome = condition.getMatchOutcome( + mockConditionContext(Map.of("management.otlp.logging.export.enabled", "true")), mockMetadata("otlp")); + assertThat(outcome.isMatch()).isTrue(); + assertThat(outcome.getMessage()) + .isEqualTo("@ConditionalOnEnabledLoggingExport management.otlp.logging.export.enabled is true"); + } + + @Test + void exporterPropertyShouldOverrideGlobalPropertyIfTrue() { + OnEnabledLoggingExportCondition condition = new OnEnabledLoggingExportCondition(); + ConditionOutcome outcome = condition.getMatchOutcome(mockConditionContext( + Map.of("management.logging.enabled", "false", "management.otlp.logging.export.enabled", "true")), + mockMetadata("otlp")); + assertThat(outcome.isMatch()).isTrue(); + assertThat(outcome.getMessage()) + .isEqualTo("@ConditionalOnEnabledLoggingExport management.otlp.logging.export.enabled is true"); + } + + @Test + void exporterPropertyShouldOverrideGlobalPropertyIfFalse() { + OnEnabledLoggingExportCondition condition = new OnEnabledLoggingExportCondition(); + ConditionOutcome outcome = condition.getMatchOutcome(mockConditionContext( + Map.of("management.logging.enabled", "true", "management.otlp.logging.export.enabled", "false")), + mockMetadata("otlp")); + assertThat(outcome.isMatch()).isFalse(); + assertThat(outcome.getMessage()) + .isEqualTo("@ConditionalOnEnabledLoggingExport management.otlp.logging.export.enabled is false"); + } + + private ConditionContext mockConditionContext() { + return mockConditionContext(Collections.emptyMap()); + } + + private ConditionContext mockConditionContext(Map properties) { + ConditionContext context = mock(ConditionContext.class); + MockEnvironment environment = new MockEnvironment(); + properties.forEach(environment::setProperty); + given(context.getEnvironment()).willReturn(environment); + return context; + } + + private AnnotatedTypeMetadata mockMetadata(String exporter) { + AnnotatedTypeMetadata metadata = mock(AnnotatedTypeMetadata.class); + given(metadata.getAnnotationAttributes(ConditionalOnEnabledLoggingExport.class.getName())) + .willReturn(Map.of("value", exporter)); + return metadata; + } + +} diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/logging/otlp/OtlpLoggingAutoConfigurationTests.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/logging/otlp/OtlpLoggingAutoConfigurationTests.java index 261cae6898..34a70bca0d 100644 --- a/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/logging/otlp/OtlpLoggingAutoConfigurationTests.java +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/logging/otlp/OtlpLoggingAutoConfigurationTests.java @@ -73,6 +73,28 @@ class OtlpLoggingAutoConfigurationTests { }); } + @Test + void shouldBackOffWhenLoggingExportPropertyIsNotEnabled() { + this.contextRunner + .withPropertyValues("management.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 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() { this.contextRunner.withUserConfiguration(CustomHttpExporterConfiguration.class)