Log4j API
Log4j is essentially composed of a logging API called Log4j API, and its reference implementation called Log4j Core.
What is a logging API and a logging implementation?
- Logging API
-
A logging API is an interface your code or your dependencies directly logs against. It is required at compile-time. It is implementation agnostic to ensure that your application can write logs, but is not tied to a specific logging implementation. Log4j API, SLF4J, JUL (Java Logging), JCL (Apache Commons Logging), JPL (Java Platform Logging) and JBoss Logging are major logging APIs.
- Logging implementation
-
A logging implementation is only required at runtime and can be changed without the need to recompile your software. Log4j Core, JUL (Java Logging), Logback are the most well-known logging implementations.
Are you looking for a crash course on how to use Log4j in your application or library? See Getting started. You can also check out Installation for the complete installation instructions. |
Log4j API provides
-
A logging API that libraries and applications can code to
-
Adapter components to create a logging implementation
This page tries to cover the most prominent Log4j API features.
Did you know that Log4j provides specialized APIs for Kotlin and Scala? Check out Log4j Kotlin and Log4j Scala projects for details. |
Introduction
To log, you need a Logger
instance which you will retrieve from the LogManager
.
These are all part of the log4j-api
module, which you can install as follows:
-
Maven
-
Gradle
<dependency>
<groupId>org.apache.logging.log4j</groupId>
<artifactId>log4j-api</artifactId>
<version>${log4j-api.version}</version>
</dependency>
implementation 'org.apache.logging.log4j:log4j-api:${log4j-api.version}'
You can use the Logger
instance to log by using methods like info()
, warn()
, error()
, etc.
These methods are named after the log levels they represent, a way to categorize log events by severity.
The log message can also contain placeholders written as {}
that will be replaced by the arguments passed to the method.
import org.apache.logging.log4j.Logger;
import org.apache.logging.log4j.LogManager;
public class DbTableService {
private static final Logger LOGGER = LogManager.getLogger(); (1)
public void truncateTable(String tableName) throws IOException {
LOGGER.warn("truncating table `{}`", tableName); (2)
db.truncate(tableName);
}
}
1 | The returned Logger instance is thread-safe and reusable.
Unless explicitly provided as an argument, getLogger() associates the returned Logger with the enclosing class, that is, DbTableService in this example. |
2 | The placeholder {} in the message will be replaced with the value of tableName |
The generated log event, which contain the user-provided log message and log level (i.e., WARN
), will be enriched with several other implicitly derived contextual information: timestamp, class & method name, line number, etc.
What happens to the generated log event will vary significantly depending on the configuration used. It can be pretty-printed to the console, written to a file, or get totally ignored due to insufficient severity or some other filtering.
Log levels are used to categorize log events by severity and control the verbosity of the logs.
Log4j contains various predefined levels, but the most common are DEBUG
, INFO
, WARN
, and ERROR
.
With them, you can filter out less important logs and focus on the most critical ones.
Previously we used Logger#warn()
to log a warning message, which could mean that something is not right, but the application can continue.
Log levels have a priority, and WARN
is less severe than ERROR
.
Exceptions are often also errors.
In this case, we might use the ERROR
log level.
Make sure to log exceptions that have diagnostics value.
This is simply done by passing the exception as the last argument to the log method:
LOGGER.warn("truncating table `{}`", tableName);
try {
db.truncate(tableName);
} catch (IOException exception) {
LOGGER.error("failed truncating table `{}`", tableName, exception); (1)
throw new IOException("failed truncating table: " + tableName, exception);
}
1 | By using error() instead of warn() , we signal that the operation failed. |
While there is only one placeholder in the message, we pass two arguments: tableName
and exception
.
Log4j will attach the last extra argument of type Throwable
in a separate field to the generated log event.
Log messages are often used interchangeably with log events.
While this simplification holds for several cases, it is not technically correct.
A log event, capturing the logging context (level, logger name, instant, etc.) along with the log message, is generated by the logging implementation (e.g., Log4j Core) when a user issues a log using a logger, e.g., Click for an introduction to log event fieldsLog events contain fields that can be classified into three categories:
For clarity’s sake let us look at a log event formatted as JSON:
|
Best practices
There are several widespread bad practices while using Log4j API. Let’s try to walk through the most common ones and see how to fix them.
Don’t use toString()
-
Don’t use
Object#toString()
in arguments, it is redundant!/* BAD! */ LOGGER.info("userId: {}", userId.toString());
-
Underlying message type and layout will deal with arguments:
/* GOOD */ LOGGER.info("userId: {}", userId);
Pass exception as the last extra argument
-
Don’t call
Throwable#printStackTrace()
! This not only circumvents the logging but can also leak sensitive information!/* BAD! */ exception.printStackTrace();
-
Don’t use
Throwable#getMessage()
! This prevents the log event from getting enriched with the exception./* BAD! */ LOGGER.info("failed", exception.getMessage()); /* BAD! */ LOGGER.info("failed for user ID `{}`: {}", userId, exception.getMessage());
-
Don’t provide both
Throwable#getMessage()
andThrowable
itself! This bloats the log message with a duplicate exception message./* BAD! */ LOGGER.info("failed for user ID `{}`: {}", userId, exception.getMessage(), exception);
-
Pass exception as the last extra argument:
/* GOOD */ LOGGER.error("failed", exception); /* GOOD */ LOGGER.error("failed for user ID `{}`", userId, exception);
Don’t use string concatenation
If you are using String
concatenation while logging, you are doing something very wrong and dangerous!
-
Don’t use
String
concatenation to format arguments! This circumvents the handling of arguments by message type and layout. More importantly, this approach is prone to attacks! ImagineuserId
being provided by the user with the following content:placeholders for non-existing args to trigger failure: {} {} {dangerousLookup}
/* BAD! */ LOGGER.info("failed for user ID: " + userId);
-
Use message parameters
/* GOOD */ LOGGER.info("failed for user ID `{}`", userId);
Use Supplier
s to pass computationally expensive arguments
If one or more arguments of the log statement are computationally expensive, it is not wise to evaluate them knowing that their results can be discarded. Consider the following example:
/* BAD! */ LOGGER.info("failed for user ID `{}` and role `{}`", userId, db.findUserRoleById(userId));
The database query (i.e., db.findUserNameById(userId)
) can be a significant bottleneck if the created the log event will be discarded anyway – maybe the INFO
level is not accepted for this logger, or due to some other filtering.
-
The old-school way of solving this problem is to level-guard the log statement:
/* OKAY */ if (LOGGER.isInfoEnabled()) { LOGGER.info(...); }
While this would work for cases where the message can be dropped due to insufficient level, this approach is still prone to other filtering cases; e.g., maybe the associated marker is not accepted.
-
Use
Supplier
s to pass arguments containing computationally expensive items:/* GOOD */ LOGGER.info("failed for user ID `{}` and role `{}`", () -> userId, () -> db.findUserRoleById(userId));
-
Use a
Supplier
to pass the message and its arguments containing computationally expensive items:/* GOOD */ LOGGER.info(() -> new ParameterizedMessage("failed for user ID `{}` and role `{}`", userId, db.findUserRoleById(userId)));
Loggers
Logger
s are the primary entry point for logging.
In this section we will introduce you to further details about Logger
s.
Refer to Architecture to see where |
Logger names
Most logging implementations use a hierarchical scheme for matching logger names with logging configuration.
In this scheme, the logger name hierarchy is represented by .
(dot) characters in the logger name, in a fashion very similar to the hierarchy used for Java package names.
For example, org.apache.logging.appender
and org.apache.logging.filter
both have org.apache.logging
as their parent.
In most cases, applications name their loggers by passing the current class’s name to LogManager.getLogger(…)
.
Because this usage is so common, Log4j provides that as the default when the logger name parameter is either omitted or is null.
For example, all Logger
-typed variables below will have a name of com.example.LoggerNameTest
:
public class LoggerNameTest {
Logger logger1 = LogManager.getLogger(LoggerNameTest.class);
Logger logger2 = LogManager.getLogger(LoggerNameTest.class.getName());
Logger logger3 = LogManager.getLogger();
}
We suggest you to use |
Logger message factories
Loggers translate
LOGGER.info("Hello, {}!", name);
calls to the appropriate canonical logging method:
LOGGER.log(Level.INFO, messageFactory.createMessage("Hello, {}!", new Object[] {name}));
Note that how Hello, {}!
should be encoded given the {name}
array as argument completely depends on the MessageFactory
employed.
Log4j allows users to customize this behaviour in several getLogger()
methods of LogManager
:
LogManager.getLogger() (1)
.info("Hello, {}!", name); (2)
LogManager.getLogger(StringFormatterMessageFactory.INSTANCE) (3)
.info("Hello, %s!", name); (4)
1 | Create a logger using the default message factory |
2 | Use default parameter placeholders, that is, {} style |
3 | Explicitly provide the message factory, that is, StringFormatterMessageFactory .
Note that there are several other getLogger() methods accepting a MessageFactory . |
4 | Note the placeholder change from {} to %s !
Passed Hello, %s! and name arguments will be implicitly translated to a String.format("Hello, %s!", name) call due to the employed StringFormatterMessageFactory . |
Log4j bundles several predefined message factories. Some common ones are accessible through convenient factory methods, which we will cover below.
Formatter logger
The Logger
instance returned by default replaces the occurrences of {}
placeholders with the toString()
output of the associated parameter.
If you need more control over how the parameters are formatted, you can also use the java.util.Formatter
format strings by obtaining your Logger
using LogManager#getFormatterLogger()
:
Logger logger = LogManager.getFormatterLogger();
logger.debug("Logging in user %s with birthday %s", user.getName(), user.getBirthdayCalendar());
logger.debug(
"Logging in user %1$s with birthday %2$tm %2$te,%2$tY", user.getName(), user.getBirthdayCalendar());
logger.debug("Integer.MAX_VALUE = %,d", Integer.MAX_VALUE);
logger.debug("Long.MAX_VALUE = %,d", Long.MAX_VALUE);
Loggers returned by getFormatterLogger()
are referred as formatter loggers.
printf()
method
Formatter loggers give fine-grained control over the output format, but have the drawback that the correct type must be specified.
For example, passing anything other than a decimal integer for a %d
format parameter gives an exception.
If your main usage is to use {}
-style parameters, but occasionally you need fine-grained control over the output format, you can use the Logger#printf()
method:
Logger logger = LogManager.getLogger("Foo");
logger.debug("Opening connection to {}...", someDataSource);
logger.printf(Level.INFO, "Hello, %s!", userName);
Formatter performance
Keep in mind that, contrary to the formatter logger, the default Log4j logger (i.e., {}
-style parameters) is heavily optimized for several use cases and can operate garbage-free when configured correctly.
You might reconsider your formatter logger usages for latency sensitive applications.
Event logger
EventLogger
is a convenience to log StructuredDataMessage
s, which format their content in a way compliant with the Syslog message format described in RFC 5424.
Event Logger is deprecated for removal!
We advise users to switch to plain |
Simple logger
Even though Log4j Core is the reference implementation of Log4j API, Log4j API itself also provides a very minimalist implementation: Simple Logger.
This is a convenience for environments where either a fully-fledged logging implementation is missing, or cannot be included for other reasons.
SimpleLogger
is the fallback Log4j API implementation if no other is available in the classpath.
Status logger
Status Logger is a standalone, self-sufficient Logger
implementation to record events that occur in the logging system (i.e., Log4j) itself.
It is the logging system used by Log4j for reporting status of its internals.
Users can use the status logger to either emit logs in their custom Log4j components, or troubleshoot a Log4j configuration.
Fluent API
The fluent API allows you to log using a fluent interface:
LOGGER.atInfo()
.withMarker(marker)
.withLocation()
.withThrowable(exception)
.log("Login for user `{}` failed", userId);
Fish tagging
Just as a fish can be tagged and have its movement tracked (aka. fish tagging [1]), stamping log events with a common tag or set of data elements allows the complete flow of a transaction or a request to be tracked. You can use them for several purposes, such as:
-
Provide extra information while serializing the log event
-
Allow filtering of information so that it does not overwhelm the system or the individuals who need to make use of it
Log4j provides fish tagging in several flavors:
Levels
Log levels are used to categorize log events by severity.
Log4j contains predefined levels, of which the most common are DEBUG
, INFO
, WARN
, and ERROR
.
Log4j also allows you to introduce your own custom levels too.
Markers
Markers are programmatic labels developers can associate to log statements:
public class MyApp {
private static final Logger LOGGER = LogManager.getLogger();
private static final Marker ACCOUNT_MARKER = MarkerManager.getMarker("ACCOUNT");
public void removeUser(String userId) {
logger.debug(ACCOUNT_MARKER, "Removing user with ID `{}`", userId);
// ...
}
}
Thread Context
Just like Java’s ThreadLocal
, Thread Context facilitates associating information with the executing thread and making this information accessible to the rest of the logging system.
Thread Context offers both
-
map-structured – referred to as Thread Context Map or Mapped Diagnostic Context (MDC)
-
stack-structured – referred to as Thread Context Stack or Nested Diagnostic Context (NDC)
storage:
ThreadContext.put("ipAddress", request.getRemoteAddr()); (1)
ThreadContext.put("hostName", request.getServerName()); (1)
ThreadContext.put("loginId", session.getAttribute("loginId")); (1)
void performWork() {
ThreadContext.push("performWork()"); (2)
LOGGER.debug("Performing work"); (3)
// Perform the work
ThreadContext.pop(); (4)
}
ThreadContext.clear(); (5)
1 | Adding properties to the thread context map |
2 | Pushing properties to the thread context stack |
3 | Added properties can later on be used to, for instance, filter the log event, provide extra information in the layout, etc. |
4 | Popping the last pushed property from the thread context stack |
5 | Clearing the thread context (for both stack and map!) |
Messages
Whereas almost every other logging API and implementation accepts only String
-typed input as message, Log4j generalizes this concept with a Message
contract.
Customizability of the message type enables users to have complete control over how a message is encoded by Log4j.
This liberal approach allows applications to choose the message type best fitting to their logging needs; they can log plain String
s, or custom PurchaseOrder
objects.
Log4j provides several predefined message types to cater for common use cases:
-
Simple
String
-typed messages:LOGGER.info("foo"); LOGGER.info(new SimpleMessage("foo"));
-
String
-typed parameterized messages:LOGGER.info("foo {} {}", "bar", "baz"); LOGGER.info(new ParameterizedMessage("foo {} {}", new Object[] {"bar", "baz"}));
-
Map
-typed messages:LOGGER.info(new StringMapMessage().with("key1", "val1").with("key2", "val2"));
Flow tracing
The Logger
class provides traceEntry()
, traceExit()
, catching()
, throwing()
methods that are quite useful for following the execution path of applications.
These methods generate log events that can be filtered separately from other debug logging.