Messages

Unlike other logging APIs, which either restrict the description of log events to (possibly interpolated) Java Strings or allow generic Java Objects, the Log4j API encapsulates every log message into the logging-specific Message interface, before passing it to the logging implementation. Such an approach opens to developers a wide range of customization possibilities.

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

Usage

While internally Log4j uses Message objects, the Logger interface provides various shortcut methods to create the most commonly used messages:

  • To create a SimpleMessage from a String argument, the following logger calls are equivalent:

    LOGGER.error("Houston, we have a problem.", exception);
    LOGGER.error(new SimpleMessage("Houston, we have a problem."), exception);
  • To create a ParameterizedMessage from a format String and an array of object parameters, the following logger calls are equivalent:

    LOGGER.error("Unable process user with ID `{}`", userId, exception);
    LOGGER.error(new ParameterizedMessage("Unable process user with ID `{}`", userId), exception);

In most cases, this is sufficient.

Nex to use cases sufficed with String-based messages, the Message interface abstraction also allows users to log custom objects. This effectively provides logging convenience in certain use cases. For instance, imagine a scenario that uses a domain event to signal authentication failures:

record LoginFailureEvent(String userName, InetSocketAddress remoteAddress) {}

When the developer wants to log a message reporting the event, we can see that the string construction becomes more challenging to read:

LOGGER.info(
        "Connection closed by authenticating user {} {} port {} [preauth]",
        event.userName(),
        event.remoteAddress().getHostName(),
        event.remoteAddress().getPort());

By extending the Message interface, developers can simplify the reporting of a login failure:

record LoginFailureEvent(String userName, InetSocketAddress remoteAddress) implements Message { (1)
    @Override
    public String getFormattedMessage() { (2)
        return "Connection closed by authenticating user " + userName() + " "
                + remoteAddress().getHostName() + " port " + remoteAddress().getPort() + " [preauth]";
    }
    // Other methods
}
1 Domain model needs to implement the Message interface
2 getFormattedMessage() provides the String to be logged

As a result, logging of LoginFailureEvent instances can be simplified as follows:

LOGGER.info(event);

Collection

This section explains predefined Log4j Message implementations addressing certain use cases. We will group this collection into following titles:

String-based types

This section explains message types intended for human-readable String-typed output.

FormattedMessage

FormattedMessage is intended as a generic entry point to actual message implementations that use pattern-based formatting. It works as follows:

  1. If the input is a valid MessageFormat pattern, use MessageFormatMessage

  2. If the input is a valid String.format() pattern, use StringFormattedMessage

  3. Otherwise, use ParameterizedMessage

Due to checks involved, FormattedMessage has an extra performance overhead compared to directly using a concrete Message implementation.

LocalizedMessage

LocalizedMessage incorporates a ResourceBundle, and allows the message pattern parameter to be the key to the message pattern in the bundle. If no bundle is specified, LocalizedMessage will attempt to locate a bundle with the name of the Logger used to log the event. The message retrieved from the bundle will be formatted using a FormattedMessage.

LocalizedMessage is primarily provided for compatibility with Log4j 1. We advise you to perform log message localization at the representation layer of your application, e.g., the client UI.

MessageFormatMessage

MessageFormatMessage formats its input using Java’s MessageFormat.

While MessageFormatMessage offers more flexibility compared to ParameterizedMessage, the latter is engineered for performance, e.g., it is garbage-free. You are recommended to use ParameterizedMessage for performance-sensitive setups.

ObjectMessage

ObjectMessage is a wrapper Message implementation to log custom domain model instances. It formats an input Object by calling its toString() method. If the object is found to be extending from StringBuilderFormattable, it uses formatTo(StringBuilder) instead.

ObjectMessage can be thought as a convenience for ParameterizedMessage such that the following message instances are analogous:

  • new ObjectMessage(obj)

  • new ParameterizedMessage("{}", obj)

That is,

  • They will both be formatted in the same way

  • Message#getParameters() will return an Object[] containing only obj

Hence, ObjectMessage is intended more as a marker interface to indicate the single value it encapsulates.

ReusableObjectMessage provides functionally equivalent to ObjectMessage, plus methods to replace its content to enable Garbage-free logging. When garbage-free logging is enabled, loggers will use this instead of ObjectMessage.

ParameterizedMessage

ParameterizedMessage accepts a formatting pattern containing {} placeholders and a list of arguments. It formats the message such that each {} placeholder in the pattern is replaced with the corresponding argument.

ReusableParameterizedMessage provides functionally equivalent to ParameterizedMessage, plus methods to replace its content to enable Garbage-free logging. When garbage-free logging is enabled, loggers will use this instead of ParameterizedMessage.

SimpleMessage

SimpleMessage encapsulates a String or CharSequence that requires no formatting.

ReusableSimpleMessage provides functionally equivalent to SimpleMessage, plus methods to replace its content to enable Garbage-free logging. When garbage-free logging is enabled, loggers will use this instead of SimpleMessage.

StringFormattedMessage

StringFormattedMessage accepts a format string and a list of arguments. It formats the message using java.lang.String#format().

While StringFormattedMessage offers more flexibility compared to ParameterizedMessage, the latter is engineered for performance, e.g., it is garbage-free. You are recommended to use ParameterizedMessage for performance-sensitive setups.

ThreadDumpMessage

If a ThreadDumpMessage is logged, Log4j generates stack traces for all threads. These stack traces will include any held locks.

Structured types

Log4j strives to provide top of the class support for structured logging. It complements structured layouts with message types allowing users to create structured messages effectively resulting in an end-to-end structured logging experience. This section will introduce the predefined structured message types.

What is structured logging?

In almost any modern production deployment, logs are no more written to files read by engineers while troubleshooting, but forwarded to log ingestion systems (Elasticsearch, Google Cloud Logging, etc.) for several observability use cases ranging from logging to metrics. This necessitates the applications to structure their logs in a machine-readable way ready to be delivered to an external system. This act of encoding logs following a certain structure is called structured logging.

MapMessage

MapMessage is a Message implementation that models a Java Map with String-typed keys and values. It is an ideal generic message type for passing structured data.

MapMessage implements MultiformatMessage to facilitate encoding of its content in multiple formats. It supports following formats:

Format Description

XML

format as XML

JSON

format as JSON

JAVA

format as Map#toString() (the default)

JAVA_UNQUOTED

format as Map#toString(), but without quotes

Some appenders handle MapMessages differently when there is no layout:

  • JMS Appender converts to a JMS javax.jms.MapMessage or jakarta.jms.MapMessage

  • JDBC Appender converts to values in an SQL INSERT statement

  • MongoDB NoSQL provider converts to fields in a MongoDB object

JSON Template Layout

JSON Template Layout has a specialized handling for MapMessages to properly encode them as JSON objects.

StructuredDataMessage

StructuredDataMessage formats its content in a way compliant with the Syslog message format described in RFC 5424.

RFC 5424 Layout

StructuredDataMessage is mostly intended to be used in combination with RFC 5424 Layout, which has specialized handling for StructuredDataMessages. By combining two, users can have complete control on how their message is encoded in a way compliant with RFC 5424, while RFC 5424 Layout will make sure the rest of the information attached to the log event is properly injected.

JSON Template Layout

Since StructuredDataMessage extends from MapMessage, which JSON Template Layout has a specialized handling for, StructuredDataMessages will be properly encoded by JSON Template Layout too.

Performance

As explained in Usage, SimpleMessage and ParameterizedMessage instances are created indirectly while interacting with Logger methods; info(), error(), etc. In a modern JVM, the allocation cost difference between these Message instances and plain String objects is marginal. If you observe this cost to be significant enough for your use case, you can enable Garbage-free logging. This will effectively cause Message instances to be recycled and avoid creating pressure on the garbage collector. In such a scenario, if you also have custom message types, consider implementing StringBuilderFormattable and introducing a message recycling mechanism.

Extending

If predefined message types fall short of addressing your needs, you can extend from the Message interface to either create your own message types or make your domain models take control of the message formatting.

Example custom message class
record LoginFailureEvent(String userName, InetSocketAddress remoteAddress)
        implements Message, StringBuilderFormattable { (1)

    @Override
    public void formatTo(StringBuilder buffer) { (2)
        buffer.append("Connection closed by authenticating user ")
                .append(userName())
                .append(" ")
                .append(remoteAddress().getHostName())
                .append(" port ")
                .append(remoteAddress().getPort())
                .append(" [preauth]");
    }

    @Override
    public String getFormattedMessage() { (3)
        StringBuilder buffer = new StringBuilder();
        formatTo(buffer);
        return buffer.toString();
    }

}
1 Extending from both Message and StringBuilderFormattable interfaces
2 Formats the message directly into a StringBuilder
3 getFormattedMessage() reuses formatTo()

Format type

You can extend from MultiformatMessage (and optionally from MultiFormatStringBuilderFormattable) to implement messages that can format themselves in one or more encodings; JSON, XML, etc. Layouts leverage this mechanism to encode a message in a particular format. For instance, when JSON Template Layout figures out that the array returned by getFormats() of a MultiformatMessage contains JSON, it injects the MultiformatMessage#getFormattedMessage({"JSON"}) output as is without quoting it.

Marker interfaces

There are certain Log4j API interfaces that you can optionally extend from in your Message implementations to enable associated features:

LoggerNameAwareMessage

LoggerNameAwareMessage is a marker interface with a setLoggerName(String) method. This method will be called during event construction to pass the associated Logger to the Message.

MultiformatMessage

MultiformatMessage extends from Message to support multiple format types. For example, see MapMessage.java extending from MultiformatMessage to support multiple formats; XML, JSON, etc.

MultiFormatStringBuilderFormattable

StringBuilderFormattable

Many layouts recycle StringBuilders to encode log events without generating garbage, and this effectively results in significant performance benefits. StringBuilderFormattable is the primary interface facilitating the formatting of objects to a StringBuilder.

TimestampMessage

TimestampMessage provides a getTimestamp() method that will be called during log event construction to determine the instant instead of using the current timestamp. Message implementations that want to control the timestamp of the log event they are encapsulated in, they can extend from TimestampMessage.