9

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)?

wujek
  • 10,112
  • 12
  • 52
  • 88

5 Answers5

15

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.

Neeme Praks
  • 8,956
  • 5
  • 47
  • 47
  • 1
    We can declare eventBuffer as ConcurrentLinkedQueue and get rid of they synchronized in the append method – Rips Sep 06 '21 at 01:48
4

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.

Andy Wilkinson
  • 108,729
  • 24
  • 257
  • 242
1

Using logback-extensions you can create your appenders in a spring application context file or in a spring config factory.

diegomtassis
  • 3,557
  • 2
  • 19
  • 29
  • Last commit: Sep 10, 2015. I had decided not to use this before I asked, but thanks anyway. – wujek Apr 04 '17 at 14:57
  • Last commit: 8 Feb 2018. That library is from the maintainer of Logback, so I have no doubts about the quality. I guess lack of activity shows that it is feature complete and does not require active development (as Logback and Spring are both also very mature projects). – Neeme Praks Apr 08 '19 at 20:09
0

Try defining a bean like this and calling the static getBean method on it, instead of using dependency injection:

@Component
public class BeanFinderGeneral implements ApplicationContextAware {

   private static ApplicationContext applicationContext;

   public static <T> T getBean(Class<T> clazz) {
       return  applicationContext.getBean(clazz);
   }

   @Override
   public void setApplicationContext(ApplicationContext pApplicationContext) throws BeansException {
       applicationContext = pApplicationContext;
   }
}
PaulNUK
  • 4,774
  • 2
  • 30
  • 58
-1

In Spring boot you can write a configuration class and create a Bean of your logback class as below:

@Component
@Configuration
public class LogBackObjectBuilder {

@Bean
public RollingFileAppender myRollingFileAppender() {
    return new YOUR-SUB-CLASS-OF-RollingFileAppender();
}
}

Just having this class scanned by spring will cause this Bean to be created and injected in the context. I hope I understood your question right. You want your custom appender to be injected in the application context.

htulsiani
  • 344
  • 1
  • 8