From b6b9237f2c0f8b3767c04d017bfd0a5e5778d18d Mon Sep 17 00:00:00 2001 From: Dmytro Nosan Date: Thu, 19 Dec 2024 17:25:10 +0200 Subject: [PATCH] Reset StatusLogger fallback listener stream on initialization Update `Log4J2LoggingSystem` so that the `StatusLogger` fallback listener has its print stream reset on each initialization. This allows output capture to work with the status listener. Fixes gh-43578 Co-authored-by: Phillip Webb --- .../ConfigureClasspathToPreferLog4j2.java | 4 +-- .../logging/log4j2/Log4J2LoggingSystem.java | 19 ++++++++++- .../DuplicateJsonMembersCustomizer.java | 32 +++++++++++++++++++ .../src/main/resources/application.properties | 3 ++ ...g4j2StructuredLoggingApplicationTests.java | 8 ++++- 5 files changed, 62 insertions(+), 4 deletions(-) create mode 100644 spring-boot-tests/spring-boot-smoke-tests/spring-boot-smoke-test-structured-logging-log4j2/src/main/java/smoketest/structuredlogging/log4j2/DuplicateJsonMembersCustomizer.java diff --git a/spring-boot-project/spring-boot-tools/spring-boot-test-support/src/main/java/org/springframework/boot/testsupport/logging/ConfigureClasspathToPreferLog4j2.java b/spring-boot-project/spring-boot-tools/spring-boot-test-support/src/main/java/org/springframework/boot/testsupport/logging/ConfigureClasspathToPreferLog4j2.java index c5486fe9ebb1..71ae7da62e05 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-test-support/src/main/java/org/springframework/boot/testsupport/logging/ConfigureClasspathToPreferLog4j2.java +++ b/spring-boot-project/spring-boot-tools/spring-boot-test-support/src/main/java/org/springframework/boot/testsupport/logging/ConfigureClasspathToPreferLog4j2.java @@ -35,8 +35,8 @@ @Target(ElementType.TYPE) @Documented @ClassPathExclusions("log4j-to-slf4j-*.jar") -@ClassPathOverrides({ "org.apache.logging.log4j:log4j-core:2.19.0", - "org.apache.logging.log4j:log4j-slf4j-impl:2.19.0" }) +@ClassPathOverrides({ "org.apache.logging.log4j:log4j-core:2.24.3", + "org.apache.logging.log4j:log4j-slf4j-impl:2.24.3" }) public @interface ConfigureClasspathToPreferLog4j2 { } diff --git a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/logging/log4j2/Log4J2LoggingSystem.java b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/logging/log4j2/Log4J2LoggingSystem.java index 355db15256e4..bec896152093 100644 --- a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/logging/log4j2/Log4J2LoggingSystem.java +++ b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/logging/log4j2/Log4J2LoggingSystem.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2024 the original author or authors. + * 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. @@ -47,6 +47,7 @@ import org.apache.logging.log4j.core.util.AuthorizationProvider; import org.apache.logging.log4j.core.util.NameUtil; import org.apache.logging.log4j.jul.Log4jBridgeHandler; +import org.apache.logging.log4j.status.StatusLogger; import org.apache.logging.log4j.util.PropertiesUtil; import org.springframework.boot.context.properties.bind.BindResult; @@ -213,6 +214,7 @@ public void initialize(LoggingInitializationContext initializationContext, Strin if (isAlreadyInitialized(loggerContext)) { return; } + resetFallbackListenerStream(StatusLogger.getLogger()); Environment environment = initializationContext.getEnvironment(); if (environment != null) { getLoggerContext().putObject(ENVIRONMENT_KEY, environment); @@ -224,6 +226,21 @@ public void initialize(LoggingInitializationContext initializationContext, Strin markAsInitialized(loggerContext); } + /** + * Reset the stream used by the fallback listener to the current system out. This + * allows the fallback lister to work with any captured output streams in a similar + * way to the {@code follow} attribute of the {@code literal Console} appender. + * @param statusLogger the status logger to update + */ + private void resetFallbackListenerStream(StatusLogger statusLogger) { + try { + statusLogger.getFallbackListener().setStream(System.out); + } + catch (NoSuchMethodError ex) { + // Ignore for older versions of Log4J + } + } + @Override protected void loadDefaults(LoggingInitializationContext initializationContext, LogFile logFile) { String location = getPackagedConfigFile((logFile != null) ? "log4j2-file.xml" : "log4j2.xml"); diff --git a/spring-boot-tests/spring-boot-smoke-tests/spring-boot-smoke-test-structured-logging-log4j2/src/main/java/smoketest/structuredlogging/log4j2/DuplicateJsonMembersCustomizer.java b/spring-boot-tests/spring-boot-smoke-tests/spring-boot-smoke-test-structured-logging-log4j2/src/main/java/smoketest/structuredlogging/log4j2/DuplicateJsonMembersCustomizer.java new file mode 100644 index 000000000000..4ef5170e1448 --- /dev/null +++ b/spring-boot-tests/spring-boot-smoke-tests/spring-boot-smoke-test-structured-logging-log4j2/src/main/java/smoketest/structuredlogging/log4j2/DuplicateJsonMembersCustomizer.java @@ -0,0 +1,32 @@ +/* + * 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 smoketest.structuredlogging.log4j2; + +import java.util.Objects; + +import org.springframework.boot.json.JsonWriter.Members; +import org.springframework.boot.logging.structured.StructuredLoggingJsonMembersCustomizer; + +public class DuplicateJsonMembersCustomizer implements StructuredLoggingJsonMembersCustomizer { + + @Override + public void customize(Members members) { + members.add("test").as(Objects::toString); + members.add("test").as(Objects::toString); + } + +} diff --git a/spring-boot-tests/spring-boot-smoke-tests/spring-boot-smoke-test-structured-logging-log4j2/src/main/resources/application.properties b/spring-boot-tests/spring-boot-smoke-tests/spring-boot-smoke-test-structured-logging-log4j2/src/main/resources/application.properties index d943aa294c98..9ccfe04c7266 100644 --- a/spring-boot-tests/spring-boot-smoke-tests/spring-boot-smoke-test-structured-logging-log4j2/src/main/resources/application.properties +++ b/spring-boot-tests/spring-boot-smoke-tests/spring-boot-smoke-test-structured-logging-log4j2/src/main/resources/application.properties @@ -2,3 +2,6 @@ logging.structured.format.console=ecs #--- spring.config.activate.on-profile=custom logging.structured.format.console=smoketest.structuredlogging.log4j2.CustomStructuredLogFormatter +#--- +spring.config.activate.on-profile=on-error +logging.structured.json.customizer=smoketest.structuredlogging.log4j2.DuplicateJsonMembersCustomizer diff --git a/spring-boot-tests/spring-boot-smoke-tests/spring-boot-smoke-test-structured-logging-log4j2/src/test/java/smoketest/structuredlogging/log4j2/SampleLog4j2StructuredLoggingApplicationTests.java b/spring-boot-tests/spring-boot-smoke-tests/spring-boot-smoke-test-structured-logging-log4j2/src/test/java/smoketest/structuredlogging/log4j2/SampleLog4j2StructuredLoggingApplicationTests.java index 1c3bd6e8b6d9..70005f289a3f 100644 --- a/spring-boot-tests/spring-boot-smoke-tests/spring-boot-smoke-test-structured-logging-log4j2/src/test/java/smoketest/structuredlogging/log4j2/SampleLog4j2StructuredLoggingApplicationTests.java +++ b/spring-boot-tests/spring-boot-smoke-tests/spring-boot-smoke-test-structured-logging-log4j2/src/test/java/smoketest/structuredlogging/log4j2/SampleLog4j2StructuredLoggingApplicationTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2024 the original author or authors. + * 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. @@ -62,4 +62,10 @@ void custom(CapturedOutput output) { assertThat(output).contains("epoch=").contains("msg=\"Starting SampleLog4j2StructuredLoggingApplication"); } + @Test + void shouldCaptureCustomizerError(CapturedOutput output) { + SampleLog4j2StructuredLoggingApplication.main(new String[] { "--spring.profiles.active=on-error" }); + assertThat(output).contains("The name 'test' has already been written"); + } + }