Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Dependency injection into Logback Appenders with Spring Boot

Tags:

spring-boot

I'm using Spring Boot 1.5.2 with Logback, which is configured using a logback-spring.xml. There, I define an appender of a custom type (subclass of RollingFileAppender) and would like to get a pair of beans injected.

Is this possible? I naively tried annotating the appender @Component etc. but as it is created by Logback/Joran, it of course doesn't work. Is there a trick I can apply?

If not possible, what would be the canonical way of achieving my goal (inserting beans from the application context into an appender)?

like image 497
wujek Avatar asked Dec 24 '22 19:12

wujek


2 Answers

As mentioned also in the question, by default, Logback instantiates and manages the lifecycle of different logging components (appenders, etc) itself. It knows nothing of Spring. And, Logback typically configures itself way before Spring is started (as Spring also uses it for logging).

So, you cannot really use Spring to configure an instance of FileAppender (or some other rather fundamental appender) and then inject that into Logback.

However, in case your appender is not truly fundamental (or you are happy to ignore logging events during Spring Boot startup), you can follow the "simple" approach below. In case you would like to capture all events (including the ones during startup), keep on reading.

Simple approach (loses events during startup)

Create your appender as a Spring component:

import ch.qos.logback.classic.spi.ILoggingEvent;
import ch.qos.logback.core.UnsynchronizedAppenderBase;
import org.springframework.context.SmartLifecycle;
import org.springframework.stereotype.Component;

@Component
public class LogbackCustomAppender extends UnsynchronizedAppenderBase<ILoggingEvent> implements SmartLifecycle {

  @Override
  protected void append(ILoggingEvent event) {
    // TODO handle event here
  }

  @Override
  public boolean isRunning() {
    return isStarted();
  }

}

As you can see, it is annotated with @Component so that Spring will pick it up during classpath scanning. Also, it implements SmartLifecycle so that Spring will call Logback Lifecycle interface methods (luckily, start() and stop() methods have exactly the same signature, so we only need to implement isRunning() which will delegate to Logback isStarted()).

Now, by the end of Spring application context startup, we can retrieve the fully initialized LogbackCustomAppender instance. But Logback is blissfully unaware of this appender, so we need to register it with Logback.

One way of doing this is within your Spring Boot Application class:

@SpringBootApplication
@ComponentScan(basePackages = {"net.my.app"})
public class CustomApplication {

  public static void main(String[] args) throws Exception {
    ConfigurableApplicationContext context = SpringApplication.run(CustomApplication.class, args);
    context.start();
    addCustomAppender(context, (LoggerContext) LoggerFactory.getILoggerFactory());
  }

  private static void addCustomAppender(ConfigurableApplicationContext context, LoggerContext loggerContext) {
    LogbackErrorCollector customAppender = context.getBean(LogbackCustomAppender.class);
    Logger rootLogger = loggerContext.getLogger(Logger.ROOT_LOGGER_NAME);
    rootLogger.addAppender(customAppender);
  }

}

No need to change anything in your Logback configuration file.

More complicated approach (captures all events)

As mentioned above, you might be interested in not losing the events logged during Spring Boot startup.

For this, you could implement a placeholder appender (that would buffer startup events internally):

import ch.qos.logback.core.Appender;
import ch.qos.logback.core.UnsynchronizedAppenderBase;

import java.util.ArrayList;

public class BufferingAppenderWrapper<E> extends UnsynchronizedAppenderBase<E> {

  private final ArrayList<E> eventBuffer = new ArrayList<>(1024);
  private Appender<E> delegate;

  @Override
  protected void append(E event) {
    synchronized (eventBuffer) {
      if (delegate != null) {
        delegate.doAppend(event);
      }
      else {
        eventBuffer.add(event);
      }
    }
  }

  public void setDelegateAndReplayBuffer(Appender<E> delegate) {
    synchronized (eventBuffer) {
      this.delegate = delegate;
      for (E event : this.eventBuffer) {
        delegate.doAppend(event);
      }
      this.eventBuffer.clear();
    }
  }

}

We register that appender with Logback the usual way (e.g. logback.xml):

  <appender name="DELEGATE" class="my.app.BufferingAppenderWrapper" />

  <root level="INFO">
    <appender-ref ref="DELEGATE" />
  </root>

After Spring has started, look that appender up by name and register your Spring-configured appender with the placeholder (flushing the buffered events in the process):

@SpringBootApplication
@ComponentScan(basePackages = {"net.my.app"})
public class CustomApplication {

  public static void main(String[] args) throws Exception {
    ConfigurableApplicationContext context = SpringApplication.run(CustomApplication.class, args);
    context.start();
    addCustomAppender(context, (LoggerContext) LoggerFactory.getILoggerFactory());
  }

  private static void addCustomAppender(ConfigurableApplicationContext context, LoggerContext loggerContext) {
    LogbackErrorCollector customAppender = context.getBean(LogbackCustomAppender.class);
    Logger rootLogger = loggerContext.getLogger(Logger.ROOT_LOGGER_NAME);
    BufferingAppenderWrapper<ILoggingEvent> delegate = (BufferingAppenderWrapper<ILoggingEvent>) rootLogger.getAppender("DELEGATE");
    delegate.setDelegateAndReplayBuffer(customAppender);
  }

}

LogbackCustomAppender stays the same.

like image 137
Neeme Praks Avatar answered Dec 26 '22 09:12

Neeme Praks


It isn't possible to do what you are trying to do. Logback is initialised before the application context is created so there's nothing to perform the dependency injection.

Perhaps you could ask another question describing what you'd like your appender to be able to do? There may be a solution that doesn't involve injecting Spring-managed beans into it.

like image 27
Andy Wilkinson Avatar answered Dec 26 '22 09:12

Andy Wilkinson