Dynamically add appender with slf4j and log4j2
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)
Related videos on Youtube
Daniele Torino
Updated on January 14, 2022Comments
-
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 almost 9 yearsHI Daniele, did you get this to work? If you did would you post your solution? Cheers Mark.
-
Daniele Torino almost 9 yearsAt 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 about 10 yearsThank 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 almost 8 yearsthis will not work with slf4j, you need to call getContext like getContext(false), otherwise you get a different context.
-
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 over 4 yearsMight want to clarify that
getLoggerConfig("")
is key and thatgetRootLogger()
doesn't do the trick. -
Makan Tayebi about 4 yearshow 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 over 3 yearsAgree, few years later this is still the only source i found for this and it does not work
-
Benjamin Peter almost 3 yearsNote 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.