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

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., LOGGER.info("Hello, world!"). Hence, log events are compound objects containing log messages.

Click for an introduction to log event fields

Log events contain fields that can be classified into three categories:

  1. Some fields are provided explicitly, in a Logger method call. The most important are the log level and the log message, which is a description of what happened, and it is addressed to humans.

  2. Some fields are contextual (e.g., Thread Context) and are either provided explicitly by developers of other parts of the application, or is injected by Java instrumentation.

  3. The last category of fields is those that are computed automatically by the logging implementation employed.

For clarity’s sake let us look at a log event formatted as JSON:

{
  (1)
  "log.level": "INFO",
  "message": "Unable to insert data into my_table.",
  "error.type": "java.lang.RuntimeException",
  "error.message": null,
  "error.stack_trace": [
    {
      "class": "com.example.Main",
      "method": "doQuery",
      "file.name": "Main.java",
      "file.line": 36
    },
    {
      "class": "com.example.Main",
      "method": "main",
      "file.name": "Main.java",
      "file.line": 25
    }
  ],
  "marker": "SQL",
  "log.logger": "com.example.Main",
  (2)
  "tags": [
    "SQL query"
  ],
  "labels": {
    "span_id": "3df85580-f001-4fb2-9e6e-3066ed6ddbb1",
    "trace_id": "1b1f8fc9-1a0c-47b0-a06f-af3c1dd1edf9"
  },
  (3)
  "@timestamp": "2024-05-23T09:32:24.163Z",
  "log.origin.class": "com.example.Main",
  "log.origin.method": "doQuery",
  "log.origin.file.name": "Main.java",
  "log.origin.file.line": 36,
  "process.thread.id": 1,
  "process.thread.name": "main",
  "process.thread.priority": 5
}
1 Explicitly supplied fields:
log.level

The level of the event, either explicitly provided as an argument to the logger call, or implied by the name of the logger method

message

The log message that describes what happened

error.*

An optional Throwable explicitly passed as an argument to the logger call

marker

An optional marker explicitly passed as an argument to the logger call

log.logger

The logger name provided explicitly to LogManager.getLogger() or inferred by Log4j API

2 Contextual fields:
tags

The Thread Context stack

labels

The Thread Context map

3 Logging backend specific fields. In case you are using Log4j Core, the following fields can be automatically generated:
@timestamp

The instant of the logger call

log.origin.*

The location of the logger call in the source code

process.thread.*

The name of the Java thread, where the logger is called

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() and Throwable 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! Imagine userId 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 Suppliers 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 Suppliers 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

Loggers are the primary entry point for logging. In this section we will introduce you to further details about Loggers.

Refer to Architecture to see where Loggers stand in the big picture.

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 LogManager.getLogger() without any arguments since it delivers the same functionality with less characters and is not prone to copy-paste errors.

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 StructuredDataMessages, 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 Logger instead.

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 Strings, 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.


1. Fish tagging is first described by Neil Harrison in the "Patterns for Logging Diagnostic Messages" chapter of "Pattern Languages of Program Design 3" edited by R. Martin, D. Riehle, and F. Buschmann in 1997.