Skip to content

Slf4JBridgeHandler.uninstall called too early in context close, swallowing JUL log output.Β #33697

Open

Description

@sijskes

Behavior

JUL logging output in @PreDestroy methods is swallowed, not logged.

Expected behavior

JUL logging output in @PreDestroy methods logged.

Description

LogbackLoggingSystem.cleanup() -> Slf4JLoggingSystem.cleanup() -> Slf4JBridgeHandler.uninstall()

is called before @PreDestroy methods.

therefore JUL logging in @PreDestroy is swallowed, not logged.

This is because the uninstall was registered as a SmartLifeCycle, in LoggingApplicationListener.onApplicationPreparedEvent.

And SmartLifeCycle.stop() is called early in the AbstractApplicationContext.doClose(), before the destroyBeans().

Versions

  • spring-boot-2.6.13.jar / spring-boot-2.7.7.jar
  • jul-to-slf4j-1.7.36.jar

Context

  • WAR deployment
  • Apache Tomcat Version 9.0.53

Misc

There are multiple call paths reaching LoggingApplicationListener.cleanupLoggingSystem(). Also one for onContextClosedEvent(). but this contains shortcut code for exactly this scenario. So it looks like the SmartLifeCycle shutdown route is intentional, but unclear to me why the shutdown of Slf4JBridgeHandler should happen so early in the shutdown.

Activity

  1. wilkinsona commented on Jan 5, 2023

    @wilkinsona
    Member

    This behaviour was improved in 45ad155 but it sounds like it didn't go far enough for your needs. I'm not sure if we'll be able to improve things further, but we can take a look. To help us to do so, please provide a complete yet minimal sample that reproduces the problem. You can share such a sample with us by pushing it to a separate repository on GitHub or by zipping it up and attaching it to this issue.

    Please note that Spring Boot 2.6.x is no longer eligible for open source support. Any fix for this issue will only be made in 2.7.x at the earliest.

  2. sijskes commented on Jan 5, 2023

    @sijskes
    Author

    When it turns out that it might not be easy to fix this in AbstractApplicationContext.doClose(), we could discuss the need of calling the Slf4JLoggingSystem.removeJdkLoggingBridgeHandler() in Slf4JLoggingSystem.cleanup(). All it does is removing a reference from the JUL logging handlers lists, no listener calling or cleaning resources (so it looks).

    It would be easily fixed by adding a property which shortcuts this behaviour, but i'm not familiar with how easy this is perceived in the whole spring project.

    I will try to bump the version to a higher level, this will take some time. If this doesnt fix things, i will build minimal sample.

  3. sijskes commented on Jan 5, 2023

    @sijskes
    Author

    upgraded tot spring boot 2.7.6.

    what stands out here, that although Slf4JLoggingSystem decides not to install SLF4JBridgeHandler in configureJdkLoggingBridgeHandler() it still decides to uninstall it. That to me seems asymmetric.

    So any change of handling SLF4JBridgeHandler in the class derived from SpringBootServletInitializer is impossible.

  4. sijskes commented on Jan 6, 2023

    @sijskes
    Author

    logbugdemo.zip

    minimal example demonstrating this bug

  5. sijskes commented on Jan 6, 2023

    @sijskes
    Author

    stackdump of preDestroy();

    Screenshot at 2023-01-05 21-55-46

    stackdump of cleanup();

    Screenshot at 2023-01-05 22-00-40

    proposed changes:

    • do not shutdown logging subsystem, that you did not start yourself. (re: Slf4JLoggingSystem)
    • shutdown logging at the latest moment (re: AbstractApplicationContext)

    The first one is the most fundamental one, if applied it allows you to control the logging bridge at application level.

  6. sijskes commented on Jan 6, 2023

    @sijskes
    Author

    when deployed with tomcat the Slf4JLoggingSystem.isJulUsingASingleConsoleHandlerAtMost() returns false dependent on the logging configuration used in the tomcat server.

  7. 1 remaining item

  8. added this to the 3.2.x milestone on Jun 4, 2024
  9. modified the milestones: 3.2.x, 3.3.x on Nov 20, 2024
  10. modified the milestones: 3.3.x, 3.4.x on Jun 18, 2025
  11. modified the milestones: 3.4.x, 3.5.x on Dec 16, 2025
  12. daguimu commented on Apr 23, 2026

    @daguimu

    I would like to try a fix for this and I have read through the current code paths (LogbackLoggingSystem.configureJdkLoggingBridgeHandler / cleanUp, LoggingApplicationListener.Lifecycle after commit 45ad155). Before writing a PR I wanted to pitch the direction since the behavioral change is user-visible.

    As I read it, the remaining problem has two distinct parts:

    1. Install/uninstall asymmetry. configureJdkLoggingBridgeHandler() only installs the bridge when isBridgeJulIntoSlf4j() is true (bridge class available AND JUL has at most a single default ConsoleHandler). cleanUp() calls removeJdkLoggingBridgeHandler() whenever isBridgeHandlerAvailable() is true, i.e. whenever the class is on the classpath. If an application sets up its own JUL handlers before Spring Boot starts, we do not install, but on shutdown we still call SLF4JBridgeHandler.uninstall() and removeDefaultRootHandler(), which can clobber the user's handlers.

    2. Timing of the bridge uninstall. Even when we did install it, the uninstall currently runs from LoggingApplicationListener.Lifecycle.stop(), which fires during stopBeans() in AbstractApplicationContext.doClose(). That is before destroyBeans() runs, so a bean's @PreDestroy-emitted JUL output has no bridge to Slf4j and is swallowed. (This is the scenario in the issue β€” the reproducer under logbugdemo.zip shows exactly that stack order.)

    Possible fixes, in increasing order of scope:

    • (a) Symmetry only. Track whether we called SLF4JBridgeHandler.install() in configureJdkLoggingBridgeHandler() and guard the removeJdkLoggingBridgeHandler() call in cleanUp() on that flag. Small, clearly correct, and covers the asymmetry case. Does not address @PreDestroy timing.
    • (b) Timing only. Move the bridge-handler uninstall out of Lifecycle.stop() into a DisposableBean (or a @PreDestroy with an explicitly low ordering) so that it runs after user @PreDestroy methods. The existing Lifecycle still handles the rest of logging system cleanup so the web-server-shutdown improvement from JUL-based logging performed during close of application context is lostΒ #9457 / 45ad155 is preserved.
    • (c) Both. Apply (a) and (b) together.

    My instinct is (c) with the bridge-uninstall moved to a DisposableBean registered after the LOGGING_LIFECYCLE_BEAN_NAME registration in registerLoggerGroupsIfNecessary, guarded by the install flag from (a). That keeps the behavioral change narrow (only affects the bridge handler; other logging cleanup timing is unchanged) and is symmetric.

    Would any of these directions be acceptable as a PR, or do you see a different approach you'd prefer? Happy to scope down to (a) if (b)/(c) touches too much of the lifecycle model.

  13. wilkinsona commented on Apr 30, 2026

    @wilkinsona
    Member

    Thanks for taking a look at this, @daguimu. Beans are destroyed in reverse registration order (see DefaultSingletonBeanRegistry.destroySingletons()) and, therefore, @Order or Ordered have no effect. This makes guaranteeing the order and being the last bean to be destroyed rather tricky. It might be possible to register a bean really early so it's destroyed really late, but I'm not yet sure how we could do so in a robust and predictable manner. Your comment is food for thought though, so thank you for that.

  14. modified the milestones: 3.5.x, 4.0.x on Jun 25, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    Type

    No type

    Projects

    No projects

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions