From 258eb29978bcefc5bd14514b5bf50509cd73167b Mon Sep 17 00:00:00 2001 From: Dmytro Nosan Date: Wed, 22 Jan 2025 16:26:53 +0200 Subject: [PATCH 1/2] Register Logback StatusListener when using custom Logback file Update `LogbackLoggingSystem` so that the `OnErrorConsoleStatusListener` is also registered when loading a custom Logback configuration file. See gh-43931 Signed-off-by: Dmytro Nosan --- .../logging/logback/LogbackLoggingSystem.java | 4 +- .../logback/LogbackLoggingSystemTests.java | 58 ++++++++++++------- .../logback-include-status-listener.xml | 13 +++++ .../src/main/resources/application.properties | 4 ++ .../src/main/resources/custom-logback.xml | 11 ++++ ...mpleStructuredLoggingApplicationTests.java | 7 +++ 6 files changed, 75 insertions(+), 22 deletions(-) create mode 100644 spring-boot-project/spring-boot/src/test/resources/logback-include-status-listener.xml create mode 100644 spring-boot-tests/spring-boot-smoke-tests/spring-boot-smoke-test-structured-logging/src/main/resources/custom-logback.xml diff --git a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/logging/logback/LogbackLoggingSystem.java b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/logging/logback/LogbackLoggingSystem.java index 7fcc812cd8..3b7cad7428 100644 --- a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/logging/logback/LogbackLoggingSystem.java +++ b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/logging/logback/LogbackLoggingSystem.java @@ -216,6 +216,7 @@ public class LogbackLoggingSystem extends AbstractLoggingSystem implements BeanF LoggerContext loggerContext = getLoggerContext(); stopAndReset(loggerContext); withLoggingSuppressed(() -> putInitializationContextObjects(loggerContext, initializationContext)); + addOnErrorConsoleStatusListener(loggerContext); SpringBootJoranConfigurator configurator = new SpringBootJoranConfigurator(initializationContext); configurator.setContext(loggerContext); boolean configuredUsingAotGeneratedArtifacts = configurator.configureUsingAotGeneratedArtifacts(); @@ -261,6 +262,7 @@ public class LogbackLoggingSystem extends AbstractLoggingSystem implements BeanF if (initializationContext != null) { applySystemProperties(initializationContext.getEnvironment(), logFile); } + addOnErrorConsoleStatusListener(loggerContext); try { Resource resource = ApplicationResourceLoader.get().getResource(location); configureByResourceUrl(initializationContext, loggerContext, resource.getURL()); @@ -493,7 +495,7 @@ public class LogbackLoggingSystem extends AbstractLoggingSystem implements BeanF private void addOnErrorConsoleStatusListener(LoggerContext context) { FilteringStatusListener listener = new FilteringStatusListener(new OnErrorConsoleStatusListener(), - Status.ERROR); + Status.WARN); listener.setContext(context); if (context.getStatusManager().add(listener)) { listener.start(); diff --git a/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/logging/logback/LogbackLoggingSystemTests.java b/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/logging/logback/LogbackLoggingSystemTests.java index 966b0f593b..79133332c0 100644 --- a/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/logging/logback/LogbackLoggingSystemTests.java +++ b/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/logging/logback/LogbackLoggingSystemTests.java @@ -43,7 +43,6 @@ import ch.qos.logback.core.rolling.SizeAndTimeBasedRollingPolicy; import ch.qos.logback.core.status.OnConsoleStatusListener; import ch.qos.logback.core.status.OnErrorConsoleStatusListener; import ch.qos.logback.core.status.Status; -import ch.qos.logback.core.status.StatusListener; import ch.qos.logback.core.util.DynamicClassLoadingException; import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.BeforeEach; @@ -656,10 +655,8 @@ class LogbackLoggingSystemTests extends AbstractLoggingSystemTests { .contains("SizeAndTimeBasedFileNamingAndTriggeringPolicy") .contains("DebugLogbackConfigurator"); LoggerContext loggerContext = this.logger.getLoggerContext(); - List statusListeners = loggerContext.getStatusManager().getCopyOfStatusListenerList(); - assertThat(statusListeners).hasSize(1); - StatusListener statusListener = statusListeners.get(0); - assertThat(statusListener).isInstanceOf(OnConsoleStatusListener.class); + assertThat(loggerContext.getStatusManager().getCopyOfStatusListenerList()) + .allSatisfy((listener) -> assertThat(listener).isInstanceOf(OnConsoleStatusListener.class)); } finally { System.clearProperty("logback.debug"); @@ -671,25 +668,35 @@ class LogbackLoggingSystemTests extends AbstractLoggingSystemTests { this.loggingSystem.beforeInitialize(); initialize(this.initializationContext, null, getLogFile(tmpDir() + "/tmp.log", null)); LoggerContext loggerContext = this.logger.getLoggerContext(); - List statusListeners = loggerContext.getStatusManager().getCopyOfStatusListenerList(); - assertThat(statusListeners).hasSize(1); - StatusListener statusListener = statusListeners.get(0); - assertThat(statusListener).isInstanceOf(FilteringStatusListener.class); - assertThat(statusListener).hasFieldOrPropertyWithValue("levelThreshold", Status.ERROR); - assertThat(statusListener).extracting("delegate").isInstanceOf(OnErrorConsoleStatusListener.class); - AppenderBase appender = new AppenderBase<>() { - - @Override - protected void append(ILoggingEvent eventObject) { - throw new IllegalStateException("Fail to append"); - } - - }; - this.logger.addAppender(appender); + assertThat(loggerContext.getStatusManager().getCopyOfStatusListenerList()).allSatisfy((listener) -> { + assertThat(listener).isInstanceOf(FilteringStatusListener.class); + assertThat(listener).hasFieldOrPropertyWithValue("levelThreshold", Status.WARN); + assertThat(listener).extracting("delegate").isInstanceOf(OnErrorConsoleStatusListener.class); + }); + AlwaysFailAppender appender = new AlwaysFailAppender(); appender.setContext(loggerContext); appender.start(); + this.logger.addAppender(appender); this.logger.info("Hello world"); - assertThat(output).contains("Fail to append").contains("Hello world"); + assertThat(output).contains("Always Fail Appender").contains("Hello world"); + } + + @Test + void logbackErrorStatusListenerShouldBeRegisteredWhenUsingCustomLogbackXml(CapturedOutput output) { + this.loggingSystem.beforeInitialize(); + initialize(this.initializationContext, "classpath:logback-include-defaults.xml", null); + LoggerContext loggerContext = this.logger.getLoggerContext(); + assertThat(loggerContext.getStatusManager().getCopyOfStatusListenerList()).allSatisfy((listener) -> { + assertThat(listener).isInstanceOf(FilteringStatusListener.class); + assertThat(listener).hasFieldOrPropertyWithValue("levelThreshold", Status.WARN); + assertThat(listener).extracting("delegate").isInstanceOf(OnErrorConsoleStatusListener.class); + }); + AlwaysFailAppender appender = new AlwaysFailAppender(); + appender.setContext(loggerContext); + appender.start(); + this.logger.addAppender(appender); + this.logger.info("Hello world"); + assertThat(output).contains("Always Fail Appender").contains("Hello world"); } @Test @@ -1042,4 +1049,13 @@ class LogbackLoggingSystemTests extends AbstractLoggingSystemTests { return (SizeAndTimeBasedRollingPolicy) getFileAppender().getRollingPolicy(); } + private static final class AlwaysFailAppender extends AppenderBase { + + @Override + protected void append(ILoggingEvent eventObject) { + throw new RuntimeException("Always Fail Appender"); + } + + } + } diff --git a/spring-boot-project/spring-boot/src/test/resources/logback-include-status-listener.xml b/spring-boot-project/spring-boot/src/test/resources/logback-include-status-listener.xml new file mode 100644 index 0000000000..6be18d7a37 --- /dev/null +++ b/spring-boot-project/spring-boot/src/test/resources/logback-include-status-listener.xml @@ -0,0 +1,13 @@ + + + + + + + [%p] - %m%n + + + + + + diff --git a/spring-boot-tests/spring-boot-smoke-tests/spring-boot-smoke-test-structured-logging/src/main/resources/application.properties b/spring-boot-tests/spring-boot-smoke-tests/spring-boot-smoke-test-structured-logging/src/main/resources/application.properties index 000769604f..6a26dd0049 100644 --- a/spring-boot-tests/spring-boot-smoke-tests/spring-boot-smoke-test-structured-logging/src/main/resources/application.properties +++ b/spring-boot-tests/spring-boot-smoke-tests/spring-boot-smoke-test-structured-logging/src/main/resources/application.properties @@ -10,3 +10,7 @@ logging.structured.format.console=smoketest.structuredlogging.CustomStructuredLo #--- spring.config.activate.on-profile=on-error logging.structured.json.customizer=smoketest.structuredlogging.DuplicateJsonMembersCustomizer +#--- +logging.config=classpath:custom-logback.xml +spring.config.activate.on-profile=on-error-custom-logback-file +logging.structured.json.customizer=smoketest.structuredlogging.DuplicateJsonMembersCustomizer diff --git a/spring-boot-tests/spring-boot-smoke-tests/spring-boot-smoke-test-structured-logging/src/main/resources/custom-logback.xml b/spring-boot-tests/spring-boot-smoke-tests/spring-boot-smoke-test-structured-logging/src/main/resources/custom-logback.xml new file mode 100644 index 0000000000..fd274d8cba --- /dev/null +++ b/spring-boot-tests/spring-boot-smoke-tests/spring-boot-smoke-test-structured-logging/src/main/resources/custom-logback.xml @@ -0,0 +1,11 @@ + + + + ecs + UTF-8 + + + + + + diff --git a/spring-boot-tests/spring-boot-smoke-tests/spring-boot-smoke-test-structured-logging/src/test/java/smoketest/structuredlogging/SampleStructuredLoggingApplicationTests.java b/spring-boot-tests/spring-boot-smoke-tests/spring-boot-smoke-test-structured-logging/src/test/java/smoketest/structuredlogging/SampleStructuredLoggingApplicationTests.java index 2d60bb8c8f..a6f8b8fbd1 100644 --- a/spring-boot-tests/spring-boot-smoke-tests/spring-boot-smoke-test-structured-logging/src/test/java/smoketest/structuredlogging/SampleStructuredLoggingApplicationTests.java +++ b/spring-boot-tests/spring-boot-smoke-tests/spring-boot-smoke-test-structured-logging/src/test/java/smoketest/structuredlogging/SampleStructuredLoggingApplicationTests.java @@ -71,4 +71,11 @@ class SampleStructuredLoggingApplicationTests { assertThat(output).contains("The name 'test' has already been written"); } + @Test + void shouldCaptureCustomizerErrorWhenUsingCustomLogbackFile(CapturedOutput output) { + SampleStructuredLoggingApplication + .main(new String[] { "--spring.profiles.active=on-error-custom-logback-file" }); + assertThat(output).contains("The name 'test' has already been written"); + } + } From 85369508263f4fee57b5688e92e739dc58a48d73 Mon Sep 17 00:00:00 2001 From: Phillip Webb Date: Mon, 27 Jan 2025 21:20:23 -0800 Subject: [PATCH 2/2] Polish Logback StatusListener code Introduce a `SystemStatusListener` class to simplify Logback status listener registration for both debug and regular output. See gh-43931 --- .../logback/FilteringStatusListener.java | 80 ------------ .../logging/logback/LogbackLoggingSystem.java | 29 +---- .../logging/logback/SystemStatusListener.java | 65 ++++++++++ .../logback/FilteringStatusListenerTests.java | 121 ------------------ .../logback/LogbackLoggingSystemTests.java | 19 ++- .../logback/SystemStatusListenerTests.java | 106 +++++++++++++++ 6 files changed, 185 insertions(+), 235 deletions(-) delete mode 100644 spring-boot-project/spring-boot/src/main/java/org/springframework/boot/logging/logback/FilteringStatusListener.java create mode 100644 spring-boot-project/spring-boot/src/main/java/org/springframework/boot/logging/logback/SystemStatusListener.java delete mode 100644 spring-boot-project/spring-boot/src/test/java/org/springframework/boot/logging/logback/FilteringStatusListenerTests.java create mode 100644 spring-boot-project/spring-boot/src/test/java/org/springframework/boot/logging/logback/SystemStatusListenerTests.java diff --git a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/logging/logback/FilteringStatusListener.java b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/logging/logback/FilteringStatusListener.java deleted file mode 100644 index 9917c6fb9a..0000000000 --- a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/logging/logback/FilteringStatusListener.java +++ /dev/null @@ -1,80 +0,0 @@ -/* - * Copyright 2012-2025 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.logging.logback; - -import ch.qos.logback.core.spi.ContextAwareBase; -import ch.qos.logback.core.spi.LifeCycle; -import ch.qos.logback.core.status.Status; -import ch.qos.logback.core.status.StatusListener; - -/** - * Logback {@link StatusListener} that filters {@link Status} by its logging level and - * delegates to the underlying {@code StatusListener}. - * - * @author Dmytro Nosan - */ -class FilteringStatusListener extends ContextAwareBase implements StatusListener, LifeCycle { - - private final StatusListener delegate; - - private final int levelThreshold; - - /** - * Creates a new {@link FilteringStatusListener}. - * @param delegate the {@link StatusListener} to delegate to - * @param levelThreshold the minimum log level accepted for delegation - */ - FilteringStatusListener(StatusListener delegate, int levelThreshold) { - this.delegate = delegate; - this.levelThreshold = levelThreshold; - } - - @Override - public void addStatusEvent(Status status) { - if (status.getLevel() >= this.levelThreshold) { - this.delegate.addStatusEvent(status); - } - } - - @Override - public boolean isResetResistant() { - return this.delegate.isResetResistant(); - } - - @Override - public void start() { - if (this.delegate instanceof LifeCycle lifeCycle) { - lifeCycle.start(); - } - } - - @Override - public void stop() { - if (this.delegate instanceof LifeCycle lifeCycle) { - lifeCycle.stop(); - } - } - - @Override - public boolean isStarted() { - if (this.delegate instanceof LifeCycle lifeCycle) { - return lifeCycle.isStarted(); - } - return true; - } - -} diff --git a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/logging/logback/LogbackLoggingSystem.java b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/logging/logback/LogbackLoggingSystem.java index 3b7cad7428..6b1cad70f4 100644 --- a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/logging/logback/LogbackLoggingSystem.java +++ b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/logging/logback/LogbackLoggingSystem.java @@ -35,11 +35,8 @@ import ch.qos.logback.classic.spi.TurboFilterList; import ch.qos.logback.classic.turbo.TurboFilter; import ch.qos.logback.core.joran.spi.JoranException; import ch.qos.logback.core.spi.FilterReply; -import ch.qos.logback.core.status.OnConsoleStatusListener; -import ch.qos.logback.core.status.OnErrorConsoleStatusListener; import ch.qos.logback.core.status.Status; import ch.qos.logback.core.status.StatusUtil; -import ch.qos.logback.core.util.StatusListenerConfigHelper; import ch.qos.logback.core.util.StatusPrinter2; import org.slf4j.ILoggerFactory; import org.slf4j.Logger; @@ -216,7 +213,7 @@ public class LogbackLoggingSystem extends AbstractLoggingSystem implements BeanF LoggerContext loggerContext = getLoggerContext(); stopAndReset(loggerContext); withLoggingSuppressed(() -> putInitializationContextObjects(loggerContext, initializationContext)); - addOnErrorConsoleStatusListener(loggerContext); + SystemStatusListener.addTo(loggerContext); SpringBootJoranConfigurator configurator = new SpringBootJoranConfigurator(initializationContext); configurator.setContext(loggerContext); boolean configuredUsingAotGeneratedArtifacts = configurator.configureUsingAotGeneratedArtifacts(); @@ -231,21 +228,16 @@ public class LogbackLoggingSystem extends AbstractLoggingSystem implements BeanF LoggerContext loggerContext = getLoggerContext(); stopAndReset(loggerContext); withLoggingSuppressed(() -> { - putInitializationContextObjects(loggerContext, initializationContext); boolean debug = Boolean.getBoolean("logback.debug"); - if (debug) { - StatusListenerConfigHelper.addOnConsoleListenerInstance(loggerContext, new OnConsoleStatusListener()); - } - else { - addOnErrorConsoleStatusListener(loggerContext); - } + putInitializationContextObjects(loggerContext, initializationContext); + SystemStatusListener.addTo(loggerContext, debug); Environment environment = initializationContext.getEnvironment(); // Apply system properties directly in case the same JVM runs multiple apps new LogbackLoggingSystemProperties(environment, getDefaultValueResolver(environment), loggerContext::putProperty) .apply(logFile); - LogbackConfigurator configurator = debug ? new DebugLogbackConfigurator(loggerContext) - : new LogbackConfigurator(loggerContext); + LogbackConfigurator configurator = (!debug) ? new LogbackConfigurator(loggerContext) + : new DebugLogbackConfigurator(loggerContext); new DefaultLogbackConfiguration(logFile).apply(configurator); loggerContext.setPackagingDataEnabled(true); loggerContext.start(); @@ -262,7 +254,7 @@ public class LogbackLoggingSystem extends AbstractLoggingSystem implements BeanF if (initializationContext != null) { applySystemProperties(initializationContext.getEnvironment(), logFile); } - addOnErrorConsoleStatusListener(loggerContext); + SystemStatusListener.addTo(loggerContext); try { Resource resource = ApplicationResourceLoader.get().getResource(location); configureByResourceUrl(initializationContext, loggerContext, resource.getURL()); @@ -493,15 +485,6 @@ public class LogbackLoggingSystem extends AbstractLoggingSystem implements BeanF } } - private void addOnErrorConsoleStatusListener(LoggerContext context) { - FilteringStatusListener listener = new FilteringStatusListener(new OnErrorConsoleStatusListener(), - Status.WARN); - listener.setContext(context); - if (context.getStatusManager().add(listener)) { - listener.start(); - } - } - void setStatusPrinterStream(PrintStream stream) { this.statusPrinter.setPrintStream(stream); } diff --git a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/logging/logback/SystemStatusListener.java b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/logging/logback/SystemStatusListener.java new file mode 100644 index 0000000000..89a257e88e --- /dev/null +++ b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/logging/logback/SystemStatusListener.java @@ -0,0 +1,65 @@ +/* + * Copyright 2012-2025 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.logging.logback; + +import java.io.PrintStream; + +import ch.qos.logback.classic.LoggerContext; +import ch.qos.logback.core.status.OnPrintStreamStatusListenerBase; +import ch.qos.logback.core.status.Status; +import ch.qos.logback.core.status.StatusListener; + +/** + * {@link StatusListener} used to print appropriate status messages to {@link System#out} + * or {@link System#err}. + * + * @author Dmytro Nosan + * @author Phillip Webb + */ +final class SystemStatusListener extends OnPrintStreamStatusListenerBase { + + private final boolean debug; + + private SystemStatusListener(boolean debug) { + this.debug = debug; + } + + @Override + public void addStatusEvent(Status status) { + if (this.debug || status.getLevel() >= Status.WARN) { + super.addStatusEvent(status); + } + } + + @Override + protected PrintStream getPrintStream() { + return (!this.debug) ? System.err : System.out; + } + + static void addTo(LoggerContext loggerContext) { + addTo(loggerContext, false); + } + + static void addTo(LoggerContext loggerContext, boolean debug) { + SystemStatusListener listener = new SystemStatusListener(debug); + listener.setContext(loggerContext); + if (loggerContext.getStatusManager().add(listener)) { + listener.start(); + } + } + +} diff --git a/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/logging/logback/FilteringStatusListenerTests.java b/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/logging/logback/FilteringStatusListenerTests.java deleted file mode 100644 index e47a6889cc..0000000000 --- a/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/logging/logback/FilteringStatusListenerTests.java +++ /dev/null @@ -1,121 +0,0 @@ -/* - * Copyright 2012-2025 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.logging.logback; - -import java.util.ArrayList; -import java.util.List; - -import ch.qos.logback.core.spi.LifeCycle; -import ch.qos.logback.core.status.ErrorStatus; -import ch.qos.logback.core.status.InfoStatus; -import ch.qos.logback.core.status.Status; -import ch.qos.logback.core.status.StatusListener; -import ch.qos.logback.core.status.WarnStatus; -import org.junit.jupiter.api.Test; - -import static org.assertj.core.api.Assertions.assertThat; - -/** - * Tests for {@link FilteringStatusListener}. - * - * @author Dmytro Nosan - */ -class FilteringStatusListenerTests { - - private final DelegateStatusListener delegate = new DelegateStatusListener(); - - @Test - void shouldFilterOutInfoStatus() { - FilteringStatusListener listener = createListener(Status.WARN); - InfoStatus info = new InfoStatus("info", getClass()); - WarnStatus warn = new WarnStatus("warn", getClass()); - ErrorStatus error = new ErrorStatus("error", getClass()); - listener.addStatusEvent(info); - listener.addStatusEvent(warn); - listener.addStatusEvent(error); - assertThat(this.delegate.getStatuses()).containsExactly(warn, error); - } - - @Test - void shouldStartUnderlyingStatusListener() { - FilteringStatusListener listener = createListener(); - assertThat(this.delegate.isStarted()).isFalse(); - listener.start(); - assertThat(this.delegate.isStarted()).isTrue(); - } - - @Test - void shouldStopUnderlyingStatusListener() { - FilteringStatusListener listener = createListener(); - this.delegate.start(); - assertThat(this.delegate.isStarted()).isTrue(); - listener.stop(); - assertThat(this.delegate.isStarted()).isFalse(); - } - - @Test - void shouldUseResetResistantValueFromUnderlyingStatusListener() { - FilteringStatusListener listener = createListener(); - assertThat(listener.isResetResistant()).isEqualTo(this.delegate.isResetResistant()); - } - - private FilteringStatusListener createListener() { - return new FilteringStatusListener(this.delegate, Status.INFO); - } - - private FilteringStatusListener createListener(int levelThreshold) { - return new FilteringStatusListener(this.delegate, levelThreshold); - } - - private static final class DelegateStatusListener implements StatusListener, LifeCycle { - - private final List statuses = new ArrayList<>(); - - private boolean started = false; - - @Override - public void addStatusEvent(Status status) { - this.statuses.add(status); - } - - List getStatuses() { - return this.statuses; - } - - @Override - public boolean isResetResistant() { - return true; - } - - @Override - public void start() { - this.started = true; - } - - @Override - public void stop() { - this.started = false; - } - - @Override - public boolean isStarted() { - return this.started; - } - - } - -} diff --git a/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/logging/logback/LogbackLoggingSystemTests.java b/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/logging/logback/LogbackLoggingSystemTests.java index 79133332c0..db6f34ce9f 100644 --- a/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/logging/logback/LogbackLoggingSystemTests.java +++ b/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/logging/logback/LogbackLoggingSystemTests.java @@ -40,9 +40,6 @@ import ch.qos.logback.core.encoder.LayoutWrappingEncoder; import ch.qos.logback.core.joran.spi.JoranException; import ch.qos.logback.core.rolling.RollingFileAppender; import ch.qos.logback.core.rolling.SizeAndTimeBasedRollingPolicy; -import ch.qos.logback.core.status.OnConsoleStatusListener; -import ch.qos.logback.core.status.OnErrorConsoleStatusListener; -import ch.qos.logback.core.status.Status; import ch.qos.logback.core.util.DynamicClassLoadingException; import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.BeforeEach; @@ -655,8 +652,10 @@ class LogbackLoggingSystemTests extends AbstractLoggingSystemTests { .contains("SizeAndTimeBasedFileNamingAndTriggeringPolicy") .contains("DebugLogbackConfigurator"); LoggerContext loggerContext = this.logger.getLoggerContext(); - assertThat(loggerContext.getStatusManager().getCopyOfStatusListenerList()) - .allSatisfy((listener) -> assertThat(listener).isInstanceOf(OnConsoleStatusListener.class)); + assertThat(loggerContext.getStatusManager().getCopyOfStatusListenerList()).allSatisfy((listener) -> { + assertThat(listener).isInstanceOf(SystemStatusListener.class); + assertThat(listener).hasFieldOrPropertyWithValue("debug", true); + }); } finally { System.clearProperty("logback.debug"); @@ -669,9 +668,8 @@ class LogbackLoggingSystemTests extends AbstractLoggingSystemTests { initialize(this.initializationContext, null, getLogFile(tmpDir() + "/tmp.log", null)); LoggerContext loggerContext = this.logger.getLoggerContext(); assertThat(loggerContext.getStatusManager().getCopyOfStatusListenerList()).allSatisfy((listener) -> { - assertThat(listener).isInstanceOf(FilteringStatusListener.class); - assertThat(listener).hasFieldOrPropertyWithValue("levelThreshold", Status.WARN); - assertThat(listener).extracting("delegate").isInstanceOf(OnErrorConsoleStatusListener.class); + assertThat(listener).isInstanceOf(SystemStatusListener.class); + assertThat(listener).hasFieldOrPropertyWithValue("debug", false); }); AlwaysFailAppender appender = new AlwaysFailAppender(); appender.setContext(loggerContext); @@ -687,9 +685,8 @@ class LogbackLoggingSystemTests extends AbstractLoggingSystemTests { initialize(this.initializationContext, "classpath:logback-include-defaults.xml", null); LoggerContext loggerContext = this.logger.getLoggerContext(); assertThat(loggerContext.getStatusManager().getCopyOfStatusListenerList()).allSatisfy((listener) -> { - assertThat(listener).isInstanceOf(FilteringStatusListener.class); - assertThat(listener).hasFieldOrPropertyWithValue("levelThreshold", Status.WARN); - assertThat(listener).extracting("delegate").isInstanceOf(OnErrorConsoleStatusListener.class); + assertThat(listener).isInstanceOf(SystemStatusListener.class); + assertThat(listener).hasFieldOrPropertyWithValue("debug", false); }); AlwaysFailAppender appender = new AlwaysFailAppender(); appender.setContext(loggerContext); diff --git a/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/logging/logback/SystemStatusListenerTests.java b/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/logging/logback/SystemStatusListenerTests.java new file mode 100644 index 0000000000..a3ef3bf659 --- /dev/null +++ b/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/logging/logback/SystemStatusListenerTests.java @@ -0,0 +1,106 @@ +/* + * Copyright 2012-2025 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.logging.logback; + +import java.util.function.Supplier; + +import ch.qos.logback.classic.LoggerContext; +import ch.qos.logback.core.status.ErrorStatus; +import ch.qos.logback.core.status.InfoStatus; +import ch.qos.logback.core.status.Status; +import ch.qos.logback.core.status.StatusListener; +import ch.qos.logback.core.status.StatusManager; +import ch.qos.logback.core.status.WarnStatus; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.ArgumentCaptor; + +import org.springframework.boot.testsupport.system.CapturedOutput; +import org.springframework.boot.testsupport.system.OutputCaptureExtension; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.BDDMockito.given; +import static org.mockito.BDDMockito.then; +import static org.mockito.Mockito.mock; + +/** + * Tests for {@link SystemStatusListener}. + * + * @author Dmytro Nosan + * @author Phillip Webb + */ +@ExtendWith(OutputCaptureExtension.class) +class SystemStatusListenerTests { + + private static final String TEST_MESSAGE = "testtesttest"; + + @Test + void addStatusWithInfoLevelWhenNoDebugDoesNotPrint(CapturedOutput output) { + addStatus(false, () -> new InfoStatus(TEST_MESSAGE, null)); + assertThat(output.getOut()).doesNotContain(TEST_MESSAGE); + assertThat(output.getErr()).doesNotContain(TEST_MESSAGE); + } + + @Test + void addStatusWithWarningLevelWhenNoDebugPrintsToSystemErr(CapturedOutput output) { + addStatus(false, () -> new WarnStatus(TEST_MESSAGE, null)); + assertThat(output.getOut()).doesNotContain(TEST_MESSAGE); + assertThat(output.getErr()).contains(TEST_MESSAGE); + } + + @Test + void addStatusWithErrorLevelWhenNoDebugPrintsToSystemErr(CapturedOutput output) { + addStatus(false, () -> new ErrorStatus(TEST_MESSAGE, null)); + assertThat(output.getOut()).doesNotContain(TEST_MESSAGE); + assertThat(output.getErr()).contains(TEST_MESSAGE); + } + + @Test + void addStatusWithInfoLevelWhenDebugPrintsToSystemOut(CapturedOutput output) { + addStatus(true, () -> new InfoStatus(TEST_MESSAGE, null)); + assertThat(output.getOut()).contains(TEST_MESSAGE); + assertThat(output.getErr()).doesNotContain(TEST_MESSAGE); + } + + @Test + void addStatusWithWarningLevelWhenDebugPrintsToSystemOut(CapturedOutput output) { + addStatus(true, () -> new WarnStatus(TEST_MESSAGE, null)); + assertThat(output.getOut()).contains(TEST_MESSAGE); + assertThat(output.getErr()).doesNotContain(TEST_MESSAGE); + } + + @Test + void addStatusWithErrorLevelWhenDebugPrintsToSystemOut(CapturedOutput output) { + addStatus(true, () -> new ErrorStatus(TEST_MESSAGE, null)); + assertThat(output.getOut()).contains(TEST_MESSAGE); + assertThat(output.getErr()).doesNotContain(TEST_MESSAGE); + } + + private void addStatus(boolean debug, Supplier statusFactory) { + StatusManager statusManager = mock(StatusManager.class); + given(statusManager.add(any(StatusListener.class))).willReturn(true); + LoggerContext loggerContext = mock(LoggerContext.class); + given(loggerContext.getStatusManager()).willReturn(statusManager); + SystemStatusListener.addTo(loggerContext, debug); + ArgumentCaptor listener = ArgumentCaptor.forClass(StatusListener.class); + then(statusManager).should().add(listener.capture()); + assertThat(listener.getValue()).extracting("context").isSameAs(loggerContext); + listener.getValue().addStatusEvent(statusFactory.get()); + } + +}