Dynamically add appender with slf4j and log4j2

42,255

Solution 1

I think you are having a similar scenario as ours. A more complex logging in production, but a simpler one during JUnit testing, so that we can assert that there has been no errors.

There are cleaner solutions using builders if you are using log4j2 > 2.4 (but then, no support for Java6), but this is the one that I have got working with log4j2 2.3:

@Test
public void testClass() {
    LoggerContext loggerContext = (LoggerContext) LogManager.getContext(false);

    Configuration configuration = loggerContext.getConfiguration();
    LoggerConfig rootLoggerConfig = configuration.getLoggerConfig("");
    ListAppender listAppender = new ListAppender("testAppender");

    rootLoggerConfig.addAppender(listAppender, Level.ALL, null);

    new TestClass();    //this is doing writing an error like org.slf4j.LoggerFactory.getLogger(TestClass.class).error("testing this");

    assertEquals(1, listAppender.getEvents().size());
}

Important to note that we need to pass "false" when calling getContext, as otherwise it seems not to be getting the same context as slf4j.

Solution 2

Accessing and manipulating log4j2 over slf4j by code/at runtime:

import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.core.LoggerContext;
import org.apache.logging.log4j.core.config.Configuration;
import org.apache.logging.log4j.core.config.LoggerConfig;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

public class Log4j2OverSlf4jConfigurator {

    final private static Logger LOGGER = LoggerFactory.getLogger(Log4j2OverSlf4jConfigurator.class);

    public static void main(final String[] args) {
        LOGGER.info("Starting");
        LoggerContext loggerContext = (LoggerContext) LogManager.getContext(false);
        Configuration configuration = loggerContext.getConfiguration();

        LOGGER.info("Filepath: {}", configuration.getConfigurationSource().getLocation());
        // Log4j root logger has no name attribute -> name == ""
        LoggerConfig rootLoggerConfig = configuration.getLoggerConfig("");

        rootLoggerConfig.getAppenders().forEach((name, appender) -> {
            LOGGER.info("Appender {}: {}", name, appender.getLayout().toString());
            // rootLoggerConfig.removeAppender(a.getName());
        });

        rootLoggerConfig.getAppenderRefs().forEach(ar -> {
            System.out.println("AppenderReference: " + ar.getRef());
        });

        // adding appenders
        configuration.addAppender(null);
    }
}

Reference: https://logging.apache.org/log4j/2.x/manual/customconfig.html

Solution 3

If anyone else stumbles over this, with slf4j version 1.7.26 a builder pattern is required which has some changes in relate to the accepted answer. The following implementation works in java 8+ and scala 2.13 (slightly differs from the answer of lqbweb):

Java:

import org.apache.logging.log4j.LogManager
import org.apache.logging.log4j.Level
import org.apache.logging.log4j.core.LoggerContext

LoggerContext loggerContext = (LoggerContext) LogManager.getContext(false)

Configuration configuration = loggerContext.getConfiguration()
LoggerConfig rootLoggerConfig = configuration.getLoggerConfig("")

PatternLayout.Builder layoutBuilder = PatternLayout.newBuilder()
layoutBuilder.withPattern("%d{HH:mm:ss.SSS} %-5level %logger{36} - %msg%n")

RandomAccessFileAppender.Builder<?> appenderBuilder =
(RandomAccessFileAppender.Builder<?>) RandomAccessFileAppender
      .newBuilder()

appenderBuilder.setLayout(layoutBuilder.build())
appenderBuilder.withBufferedIo(true)
appenderBuilder.setFileName(logFile.getAbsolutePath)
appenderBuilder.setName("OutputDirFileLogger")

RandomAccessFileAppender appender = appenderBuilder.build()

appender.start() // important to make the appender usable instantly 


rootLoggerConfig.addAppender(appender, Level.DEBUG, null)

scala:

    import org.apache.logging.log4j.LogManager
    import org.apache.logging.log4j.Level
    import org.apache.logging.log4j.core.LoggerContext

    val loggerContext = LogManager.getContext(false).asInstanceOf[LoggerContext]

    val configuration: Configuration = loggerContext.getConfiguration
    val rootLoggerConfig: LoggerConfig = configuration.getLoggerConfig("")

    val layoutBuilder: PatternLayout.Builder = PatternLayout.newBuilder()
    layoutBuilder.withPattern("%d{HH:mm:ss.SSS} %-5level %logger{36} - %msg%n")

    val appenderBuilder: RandomAccessFileAppender.Builder[_] = RandomAccessFileAppender
      .newBuilder()
      .asInstanceOf[RandomAccessFileAppender.Builder[_]]

    appenderBuilder.setLayout(layoutBuilder.build())
    appenderBuilder.withBufferedIo(true)
    appenderBuilder.setFileName(logFile.getAbsolutePath)
    appenderBuilder.setName("OutputDirFileLogger")

    val appender: RandomAccessFileAppender = appenderBuilder.build()
    appender.start()

    rootLoggerConfig.addAppender(appender, Level.DEBUG, null) 
Share:
42,255

Related videos on Youtube

Daniele Torino
Author by

Daniele Torino

Updated on January 14, 2022

Comments

  • Daniele Torino
    Daniele Torino over 2 years

    I want to dynamically create an appender and add it to a logger. However, this seems not to be possible with slf4j. I can add my appender to a log4j logger but then I fail to retrieve the logger with the slf4j LoggerFactoy.

    What I want to do: I create a test class (not a jUnit test) and pass a logger in the constructor for the test class to use. Every instance of the test class needs it's own logger and appender that saves the log so it can be later used in an HTML report.

    What I tried (for simplicity I created a jUnit test):

      import static org.junit.Assert.assertEquals;
    
      import java.util.LinkedList;
      import java.util.List;
    
      import org.apache.logging.log4j.core.LogEvent;
      import org.junit.Test;
      import org.slf4j.helpers.Log4jLoggerFactory;
    
      import ch.fides.fusion.logging.ListAppender;
    
      public class ListAppenderTest {
    
          @Test
          public void test() {
    
              String testName = "test1";
    
              // the log messages are to be inserted in this list
              List<LogEvent> testLog = new LinkedList<>();
    
              // create log4j logger
              org.apache.logging.log4j.core.Logger log4jlogger = (org.apache.logging.log4j.core.Logger) org.apache.logging.log4j.LogManager
                                              .getLogger("Test:" + testName);
    
              // create appender and add it to the logger
              ListAppender listAppender = new ListAppender("Test:" + testName + ":MemoryAppender", testLog);
              log4jlogger.addAppender(listAppender);
    
              // get the slf4j logger
              org.slf4j.helpers.Log4jLoggerFactory loggerFactory = new Log4jLoggerFactory();
              org.slf4j.Logger testLogger = loggerFactory.getLogger("Test:" + testName);
    
              // test it
              final String TEST_MESSAGE = "test message";
              testLogger.info(TEST_MESSAGE);
    
              assertEquals(1, testLog.size());
              LogEvent logEvent = testLog.get(0);
              assertEquals(TEST_MESSAGE, logEvent.getMessage().getFormattedMessage() );
          }
    
      }
    

    and this is my very basic appender:

     package ch.fides.fusion.logging;
    
      import java.util.List;
    
      import org.apache.logging.log4j.core.LogEvent;
      import org.apache.logging.log4j.core.appender.AbstractAppender;
    
      public class ListAppender extends AbstractAppender {
    
          private final List<LogEvent> log;
    
          public ListAppender(String name, List<LogEvent> testLog) {
              super(name, null, null);
              this.log = testLog;
          }
    
          @Override
          public void append(LogEvent logEvent) {
              log.add(new TestLogEvent(logEvent));
          }
    
      }
    

    What can I do to get this to work? Maybe I am approaching this from the wrong angle but I would like to avoid creating my own logger class. Any help is greatly appreciated.

    • mark
      mark almost 9 years
      HI Daniele, did you get this to work? If you did would you post your solution? Cheers Mark.
    • Daniele Torino
      Daniele Torino almost 9 years
      At the time I had no other Idea than writing a custom class that implemented the Logger interface. It's not really what I was looking for but it did the job. Unfortunately I am working on a different project now and I didn't have the time to review the other solutions here.
  • Daniele Torino
    Daniele Torino about 10 years
    Thank you for the info. Creating an appender was pretty easy, that is not my problem. The problem is creating a logger and appender dynamically and then accessing that logger over slf4j.
  • lqbweb
    lqbweb almost 8 years
    this will not work with slf4j, you need to call getContext like getContext(false), otherwise you get a different context.
  • Cedric Reichenbach
    Cedric Reichenbach almost 6 years
    "Important to note that we need to pass "false" when calling getContext, as otherwise it seems not to be getting the same context as slf4j." Ah, this is really crucial here!
  • Charlie
    Charlie over 4 years
    Might want to clarify that getLoggerConfig("") is key and that getRootLogger() doesn't do the trick.
  • Makan Tayebi
    Makan Tayebi about 4 years
    how do you go about adding the Appender? slf4j doesn't care when I add the appenderObj to the config. actually everything here references the log4j and you only only only print the stuff from the slf4j logger Object. I'm not sure if this answer recognizes the difference between slf4j and log4j
  • Johnyb
    Johnyb over 3 years
    Agree, few years later this is still the only source i found for this and it does not work
  • Benjamin Peter
    Benjamin Peter almost 3 years
    Note that the existing logger config is applied as well and messages that are filtered out before hitting the root logger are missing. Change your log config or modify it here as well.