There's been three versions of Log4j in one week to address security flaws, all of them due to the same lookups feature. This feature is somewhat unique to Log4j 2.x, so maybe it wouldn't be a bad idea to ditch Log4j in favor of an , hopefully safer, alternate logger?
Dependencing on the project, changing the logger might range from easy peasy to a multi-week task. I'm ready to bet that in many (most?) cases, it'd actually be quite easy, so let's explore how to do it, using Logback as the target (there aren't that many alternatives actually).
EDIT(2022-04-30): fixed logback.xml
sample for logging to a file: reordered appenders (thanks to Phil Harron for reporting it).
Prerequisites
So first, in which cases would it be relatively easy to move off?
If you're in a situation where you only depend on the APIs exposed by log4j-api
(put simply: LogManager
, Logger
, Level
, and possibly ThreadContext
and/or Marker
),
or even use Slf4j instead (with log4j-slf4j-impl
),
you're in good conditions, but it's not enough.
Another thing to be considered in addition to the logging code itself is whether you expose your logging configuration to users (and how: configuration files? JMX?): migrating away from Log4j will obviously change the way you (they) configure logging.
And finally, logging frameworks being extensible,
have a look at which such extensions you're using,
and whether they have alternatives for other loggers.
For example, if you're using sentry-log4j2
,
know that there's an equivalent sentry-logback
for Logback.
I'll assume a very simple, but totally realistic, setup (realistic because that's what I've been using in most apps I've written over the past few years).
Changing dependencies
Putting aside configuration for a moment, let's have a look at what needs to be changed in the project dependencies.
If you're using Slf4j as your logging API,
then you'll switch log4j-slf4j-impl
to logback-classic
.
If you're using the Log4j API directly,
then your final setup should have log4j-api
, log4j-to-slf4j
, and logback-classic
.
If you had other adapters or bridges, replace them accordingly:
From | To |
---|---|
log4j-jcl |
jcl-over-slf4j |
log4j-1.2-api |
log4j-over-slf4j |
log4j-jul |
jul-to-slf4j |
log4j-jpl |
No alternative, but you can copy the one from Slf4j 2 |
At this point, you should no longer have log4j-core
in your dependencies,
either directly or transitively.
A note on dependency management
It's important to have only one logger in your runtime dependencies, and make sure you don't have conflicts between adapters/bridges. Louis Jacomet wrote a great blog post over at Gradle's blog on the subject.
If you're using Maven, use mvn dependency:tree
to figure out which dependencies you have, and where they come from;
then use dependency exclusions if needed to remove unwanted transitive dependencies
as you replace them with the appropriate adapter, bridge or implementation.
If you're using Gradle, I cannot recommend Louis Jacomet's logging-capabilities
plugin enough!
If you're already using it, don't forget to switch from
loggingCapabilities {
enforceLog4J2()
}
to
loggingCapabilities {
enforceLogback()
}
Migrating configuration files
The next step is migrating configuration files so you can get an equivalent behavior.
I'll take a couple simple examples, again taken from real applications.
Logging to the console
For apps running in Docker containers, or sometimes through systemd, it's useful to have the application log directly to the console.
This Log4j 2 configuration:
<Configuration>
<Appenders>
<Console name="Console" target="SYSTEM_OUT">
<PatternLayout pattern="%d{ISO8601_OFFSET_DATE_TIME_HHCMM}{Europe/Paris} %p %c{1.} [%t] %m%n"/>
</Console>
</Appenders>
<Loggers>
<Root level="info">
<AppenderRef ref="Console"/>
</Root>
</Loggers>
</Configuration>
will become for Logback:
<configuration>
<shutdownHook/>
<appender name="Console" class="ch.qos.logback.core.ConsoleAppender">
<encoder>
<pattern>%d{"yyyy-MM-dd'T'HH:mm:ss,SSSXXX", Europe/Paris} [%thread] %p %c{1} [%t] %m%n</pattern>
</encoder>
</appender>
<root level="info">
<appender-ref ref="Console" />
</root>
</configuration>
Logging to a file
For applications that prefer logging to files, with a rolling strategy (here also using an asynchronous logger), this Log4j 2 configuration file:
<Configuration>
<Appenders>
<RollingFile name="LogFile"
fileName="/var/log/myapp/myapp.log"
filePattern="/var/log/myapp/myapp-%d{yyyy-MM-dd}.log.gz">
<PatternLayout>
<Pattern>%d %p %c{1.} [%t] %m%n</Pattern>
</PatternLayout>
<Policies>
<TimeBasedTriggeringPolicy />
</Policies>
</RollingFile>
<Async name="AsyncLogFile">
<AppenderRef ref="LogFile" />
</Async>
</Appenders>
<Loggers>
<Root level="info">
<AppenderRef ref="AsyncLogFile" />
</Root>
</Loggers>
</Configuration>
will become for Logback:
<configuration>
<shutdownHook/>
<appender name="LogFile" class="ch.qos.logback.core.rolling.RollingFileAppender">
<file>/var/log/myapp/myapp.log</file>
<rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
<fileNamePattern>/var/log/myapp/myapp-%d{yyy-MM-dd}.log.gz</fileNamePattern>
<maxHistory>7</maxHistory>
</rollingPolicy>
<encoder>
<pattern>%d %p %c{1} [%t] %m%n</pattern>
</encoder>
</appender>
<appender name="AsyncLogFile" class="ch.qos.logback.classic.AsyncAppender">
<appender-ref ref="LogFile" />
</appender>
<root level="info">
<appender-ref ref="AsyncLogFile" />
</root>
</configuration>
Sending logs to Sentry
The easiest way to use Sentry is to configure it as a log appender, in general in addition to some other appender(s) as seen above.
The following Log4j 2 configuration file snippet:
<Configuration>
<Appenders>
<!-- … -->
<Sentry name="Sentry"
minimumEventLevel="WARN"
minimumBreadcrumbLevel="DEBUG"
/>
</Appenders>
<Loggers>
<Root level="info">
<!-- … -->
<AppenderRef ref="Sentry" level="warn" />
</Root>
</Loggers>
</Configuration>
will become for Logback:
<configuration>
<!-- … -->
<appender name="Sentry" class="io.sentry.logback.SentryAppender">
<minimumEventLevel>WARN</minimumEventLevel>
<minimumBreadcrumbLevel>DEBUG</minimumBreadcrumbLevel>
</appender>
<root level="info">
<!-- … -->
<appender-ref ref="Sentry" />
</root>
</configuration>
Other things to note
If you're using a log4j2.component.properties
file,
you'll have to replace it with explicit System.setProperty()
in code as early as possible
(before Logback is initialized),
or with other equivalent ways to achieve the same.
In this file, in apps I've written,
I've been using log4j2.isWebapp=false
and log4j.contextSelector=org.apache.logging.log4j.core.async.AsyncLoggerContextSelector
to make all appenders asynchronous.
There's no equivalent to log4j2.isWebapp
,
because Logback does not change its behavior depending on the presence of the servlet class in the classpath.
As for the AsyncLoggerContextSelector
, you'd have to explicitly use AsyncAppender
s in the configuration file
(there might be a way to configure Logback/Joran to automatically wrap all appenders with an AsyncAppender
but let's be explicit).
Linking the dots
The last thing to do is making sure the application uses the configuration file.
The way Log4j 2 and Logback search for their configuration file is quite similar: first a system property, then files in the classpath, and finally fallback to the console.
If you're using the system property,
change it from log4j2.configurationFile
to logback.configurationFile
in all your Docker entrypoints, systemd service units, shell scripts, etc.
If you're using a file on the classpath,
it'll have to be named logback.xml
rather than log4j2.xml
(or logback-test.xml
rather than log4j2-test.xml
).
Conclusion
Those 3 steps (dependencies, configuration file, finding the configuration file) should be enough for many, if not most, applications for migrating off of Log4j 2. I'm not saying this is a good thing and you should do it, but if, in the light of this cascade of flaws in Log4j 2, you envisioned switching away, those would be the steps to follow.