From 0fcd9b6c2473cdbd10d9ff0834388e5f99025758 Mon Sep 17 00:00:00 2001 From: Dmytro Nosan Date: Mon, 21 Oct 2024 19:33:19 +0300 Subject: [PATCH] Introduce a property to enable or disable logging for each exporter. This property provides more fine-grained control over log export: - management.otlp.logging.export.enabled By default, it is set to null, but if defined, it takes precedence over the global management.logging.export.enabled property --- .../logging/ConditionalOnEnabledLogging.java | 51 +++++++ .../logging/OnEnabledLoggingCondition.java | 71 ++++++++++ .../otlp/OtlpLoggingAutoConfiguration.java | 2 + ...itional-spring-configuration-metadata.json | 11 ++ .../OnEnabledLoggingConditionTests.java | 130 ++++++++++++++++++ .../OtlpLoggingAutoConfigurationTests.java | 8 ++ 6 files changed, 273 insertions(+) create mode 100644 spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/logging/ConditionalOnEnabledLogging.java create mode 100644 spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/logging/OnEnabledLoggingCondition.java create mode 100644 spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/logging/OnEnabledLoggingConditionTests.java diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/logging/ConditionalOnEnabledLogging.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/logging/ConditionalOnEnabledLogging.java new file mode 100644 index 000000000000..772a0356aca6 --- /dev/null +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/logging/ConditionalOnEnabledLogging.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 exporter 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(OnEnabledLoggingCondition.class) +public @interface ConditionalOnEnabledLogging { + + /** + * 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/OnEnabledLoggingCondition.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/logging/OnEnabledLoggingCondition.java new file mode 100644 index 000000000000..b73d32ae0931 --- /dev/null +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/logging/OnEnabledLoggingCondition.java @@ -0,0 +1,71 @@ +/* + * 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 ConditionalOnEnabledLogging + */ +class OnEnabledLoggingCondition 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(ConditionalOnEnabledLogging.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(ConditionalOnEnabledLogging.class) + .because(GLOBAL_PROPERTY + " is " + globalLoggingEnabled)); + } + return ConditionOutcome.match(ConditionMessage.forCondition(ConditionalOnEnabledLogging.class) + .because("logging is enabled by default")); + } + + private static String getExporterName(AnnotatedTypeMetadata metadata) { + Map attributes = metadata.getAnnotationAttributes(ConditionalOnEnabledLogging.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/OtlpLoggingAutoConfiguration.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/logging/otlp/OtlpLoggingAutoConfiguration.java index ec169dab024c..e033650b4ba2 100644 --- a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/logging/otlp/OtlpLoggingAutoConfiguration.java +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/logging/otlp/OtlpLoggingAutoConfiguration.java @@ -20,6 +20,7 @@ import io.opentelemetry.exporter.otlp.http.logs.OtlpHttpLogRecordExporter; import io.opentelemetry.sdk.logs.SdkLoggerProvider; +import org.springframework.boot.actuate.autoconfigure.logging.ConditionalOnEnabledLogging; import org.springframework.boot.autoconfigure.AutoConfiguration; import org.springframework.boot.autoconfigure.EnableAutoConfiguration; import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; @@ -36,6 +37,7 @@ @ConditionalOnClass({ SdkLoggerProvider.class, OpenTelemetry.class, OtlpHttpLogRecordExporter.class }) @EnableConfigurationProperties(OtlpLoggingProperties.class) @Import({ OtlpLoggingConfigurations.ConnectionDetails.class, OtlpLoggingConfigurations.Exporters.class }) +@ConditionalOnEnabledLogging("otlp") public class OtlpLoggingAutoConfiguration { } 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 e05babb51f7f..478082038cbd 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 the auto-configuration for exporting log record data is enabled", + "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 for exporting OTLP log records is enabled." + }, { "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/OnEnabledLoggingConditionTests.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/logging/OnEnabledLoggingConditionTests.java new file mode 100644 index 000000000000..bd0d2ca98e12 --- /dev/null +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/logging/OnEnabledLoggingConditionTests.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 OnEnabledLoggingCondition}. + * + * @author Moritz Halbritter + * @author Dmytro Nosan + */ +class OnEnabledLoggingConditionTests { + + @Test + void shouldMatchIfNoPropertyIsSet() { + OnEnabledLoggingCondition condition = new OnEnabledLoggingCondition(); + ConditionOutcome outcome = condition.getMatchOutcome(mockConditionContext(), mockMetadata("")); + assertThat(outcome.isMatch()).isTrue(); + assertThat(outcome.getMessage()).isEqualTo("@ConditionalOnEnabledLogging logging is enabled by default"); + } + + @Test + void shouldNotMatchIfGlobalPropertyIsFalse() { + OnEnabledLoggingCondition condition = new OnEnabledLoggingCondition(); + ConditionOutcome outcome = condition.getMatchOutcome( + mockConditionContext(Map.of("management.logging.export.enabled", "false")), mockMetadata("")); + assertThat(outcome.isMatch()).isFalse(); + assertThat(outcome.getMessage()) + .isEqualTo("@ConditionalOnEnabledLogging management.logging.export.enabled is false"); + } + + @Test + void shouldMatchIfGlobalPropertyIsTrue() { + OnEnabledLoggingCondition condition = new OnEnabledLoggingCondition(); + ConditionOutcome outcome = condition.getMatchOutcome( + mockConditionContext(Map.of("management.logging.export.enabled", "true")), mockMetadata("")); + assertThat(outcome.isMatch()).isTrue(); + assertThat(outcome.getMessage()) + .isEqualTo("@ConditionalOnEnabledLogging management.logging.export.enabled is true"); + } + + @Test + void shouldNotMatchIfExporterPropertyIsFalse() { + OnEnabledLoggingCondition condition = new OnEnabledLoggingCondition(); + ConditionOutcome outcome = condition.getMatchOutcome( + mockConditionContext(Map.of("management.otlp.logging.export.enabled", "false")), mockMetadata("otlp")); + assertThat(outcome.isMatch()).isFalse(); + assertThat(outcome.getMessage()) + .isEqualTo("@ConditionalOnEnabledLogging management.otlp.logging.export.enabled is false"); + } + + @Test + void shouldMatchIfExporterPropertyIsTrue() { + OnEnabledLoggingCondition condition = new OnEnabledLoggingCondition(); + ConditionOutcome outcome = condition.getMatchOutcome( + mockConditionContext(Map.of("management.otlp.logging.export.enabled", "true")), mockMetadata("otlp")); + assertThat(outcome.isMatch()).isTrue(); + assertThat(outcome.getMessage()) + .isEqualTo("@ConditionalOnEnabledLogging management.otlp.logging.export.enabled is true"); + } + + @Test + void exporterPropertyShouldOverrideGlobalPropertyIfTrue() { + OnEnabledLoggingCondition condition = new OnEnabledLoggingCondition(); + 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("@ConditionalOnEnabledLogging management.otlp.logging.export.enabled is true"); + } + + @Test + void exporterPropertyShouldOverrideGlobalPropertyIfFalse() { + OnEnabledLoggingCondition condition = new OnEnabledLoggingCondition(); + 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("@ConditionalOnEnabledLogging 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(ConditionalOnEnabledLogging.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 261cae689813..6066c16afbed 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,14 @@ void shouldNotSupplyBeansIfDependencyIsMissing(String packageName) { }); } + @Test + void shouldBackOffWhenOtlpLoggingExportPropertyIsNotEnabled() { + this.contextRunner.withPropertyValues("management.otlp.logging.export.enabled=false").run((context) -> { + assertThat(context).doesNotHaveBean(OtlpLoggingConnectionDetails.class); + assertThat(context).doesNotHaveBean(LogRecordExporter.class); + }); + } + @Test void shouldBackOffWhenCustomHttpExporterIsDefined() { this.contextRunner.withUserConfiguration(CustomHttpExporterConfiguration.class)