Architecture

Log4j Core is the reference implementation of Log4j API and composed of several components. In this section we will try to explain major pillars its architecture stands on. An overview these major classes can be depicted as follows:

An overview of major classes and their relation
@startuml

class LoggerContext {
  Configuration config
  Logger[] loggers
  Logger getLogger(String name)
}

note left of LoggerContext {
  Anchor for the logging system
}

LoggerContext --> "0..*" Logger

package "Configuration" as c {

    class Configuration {
      Appender[] appenders
      Filter filter
      LoggerConfig[] loggerConfigs
      LoggerConfig getLoggerConfig(String name)
      StrSubstitutor substitutor
    }

    note left of Configuration
      Encapsulates components compiled
      from a user-provided configuration
      file (e.g., `log4j2.xml`)
    end note

    Configuration --> Filter

    Configuration --> "0..*" Appender

    Configuration --> "0..*" LoggerConfig

    Configuration --> StrSubstitutor

    class Appender {
      AbstractManager manager
      Layout layout
      Filter filter
      void append(LogEvent)
    }

    Appender --> Layout

    Appender --> Filter

    class Layout {
      byte[] encode(LogEvent)
    }

    class Filter {
      Result filter(LogEvent)
    }

    note right of Filter
      Note that a `Filter` can
      be provided at 4 levels:
      1. `Configuration`
      2. `LoggerConfig`
      3. `AppenderControl`
      4. `Appender`
    end note

    class LoggerConfig {
      AppenderControl[] appenderControls
      Level level
      Filter filter
      void log(LogEvent)
    }

    LoggerConfig -[#green,thickness=6]-> "0..*" AppenderControl

    LoggerConfig --> Filter

    class AppenderControl {
      Appender appender
      Filter filter
      void append(LogEvent)
    }

    note right of AppenderControl
      Decorates an `Appender`
      with a `Filter`
    end note

    AppenderControl -[#green,thickness=6]-> Appender

    AppenderControl --> Filter

    class StrSubstitutor {
      Interpolator interpolator
      String replace(String input)
    }

    note right of StrSubstitutor
      Responsible for
      property substitution
      (e.g., `${env:USER}`)
    end note

    StrSubstitutor --> Interpolator

    class Interpolator {
      StrLookup[] lookups
      String lookup(String input)
    }

    Interpolator --> "0..*" StrLookup

    class StrLookup {
      String lookup(String input)
    }
}

LoggerContext --> Configuration

class Logger {
  void log(Level level, Message message)
}

note right of Logger
  The main API entry point
  users interact with
end note

Logger -[#green,thickness=6]-> LoggerConfig : delegates `log()`

class AbstractManager {
}

Appender -[#green,thickness=6]-> AbstractManager

@enduml

At a really high level,

  • A LoggerContext, the composition anchor, gets created in combination with a Configuration. Both can be created either directly (i.e., programmatically) or indirectly at first interaction with Log4j.

  • LoggerContext creates Loggers that users interact with for logging purposes.

  • Appender delivers a LogEvent to a target (file, socket, database, etc.) and typically uses a Layout to encode log events and an AbstractManager to handle the lifecycle of the target resource.

  • LoggerConfig encapsulates configuration for a Logger, as AppenderControl and AppenderRef for Appenders.

  • Configuration is equipped with StrSubstitutor et al. to allow property substitution in String-typed values.

  • A typical log() call triggers a chain of invocations through classes Logger, LoggerConfig, AppenderControl, Appender, and AbstractManager in order – this is depicted using green arrows in An overview of major classes and their relation.

Following sections examine this interplay in detail.

LoggerContext

The LoggerContext acts as the anchor point for the logging system. It is associated with an active Configuration and is primarily responsible for instantiating Loggers.

LoggerContext and other directly related classes
@startuml

class LoggerContext #line.bold {
  Configuration config
  Logger[] loggers
  Logger getLogger(String name)
}

LoggerContext --> Configuration

LoggerContext --> "0..*" Logger

class Configuration {
  Appender[] appenders
  Filter filter
  LoggerConfig[] loggerConfigs
  LoggerConfig getLoggerConfig(String name)
  StrSubstitutor substitutor
}

class Logger {
  void log(Level level, Message message)
}

@enduml

In most cases, applications have a single global LoggerContext. Though in certain cases (e.g., Java EE applications), Log4j can be configured to accommodate multiple LoggerContexts. Refer to Log Separation for details.

Configuration

Every LoggerContext is associated with an active Configuration. It models the configuration of all appenders, layouts, filters, loggers, and contains the reference to StrSubstitutor et al..

Configuration and other directly related classes
@startuml

class LoggerContext {
  Configuration config
  Logger[] loggers
  Logger getLogger(String name)
}

LoggerContext --> Configuration

class Configuration #line.bold {
  Appender[] appenders
  Filter filter
  LoggerConfig[] loggerConfigs
  LoggerConfig getLoggerConfig(String name)
  StrSubstitutor substitutor
}

Configuration --> "0..*" Filter

Configuration --> "0..*" Appender

Configuration --> "0..*" LoggerConfig

Configuration --> StrSubstitutor

class Appender {
  Layout layout
  void append(LogEvent)
}

class Filter {
  Result filter(LogEvent)
}

class LoggerConfig {
  AppenderRef[] appenderRefs
  AppenderControl[] appenderControls
  Level level
  Filter filter
  void log(LogEvent)
}

class StrSubstitutor {
  Interpolator interpolator
  String replace(String input)
}
@enduml

Configuration of Log4j Core is typically done at application initialization. The preferred way is by reading a configuration file, but it can also be done programmatically. This is further discussed in Configuration.

Reconfiguration reliability

The main motivation for the existing architecture is the reliability to configuration changes. When a reconfiguration event occurs, two Configuration instances are active at the same time. Threads that already started processing a log event will either:

  • continue logging to the old configuration, if execution already reached the LoggerConfig class,

  • or switch to the new configuration.

The service that manages the reconfiguration process is called ReliabilityStrategy and it decides:

  • when should Loggers switch to the new configuration,

  • when should the old configuration be stopped.

Overview of the reconfiguration process
@startuml
left to right direction

package LoggerContext {
    object Logger

    package "New Configuration" as c2 {
        object "LoggerConfig" as lc2
        object "AppenderControl" as ac2
        object "Appender" as app2
    }

    package "Old Configuration" as c1 {
        object "LoggerConfig" as lc1
        object "AppenderControl" as ac1
        object "Appender" as app1
    }
}

object AbstractManager

Logger ..> lc1
lc1 --> ac1
ac1 --> app1
app1 --> AbstractManager

Logger --> lc2
lc2 --> ac2
ac2 --> app2
app2 --> AbstractManager
@enduml

Logger

Loggers are the primary user entry point for logging. They are created by calling one of the getLogger() methods of LogManager – this is further documented in Log4j API. The Logger itself performs no direct actions. It simply has a name and is associated with a LoggerConfig.

Logger and other directly related classes
@startuml

class LoggerContext {
  Configuration config
  Logger[] loggers
  Logger getLogger(String name)
}

LoggerContext --> "0..*" Logger

class LoggerConfig {
  AppenderRef[] appenderRefs
  AppenderControl[] appenderControls
  Level level
  Filter filter
  void log(LogEvent)
}

class Logger #line.bold {
  void log(Level level, Message message)
}

Logger -[#green,thickness=6]-> LoggerConfig : delegates `log()`

@enduml

The hierarchy between LoggerConfigs, implies the very same hierarchy between Loggers too. You can use LogManager.getRootLogger() to get the root logger. Note that Log4j API has no assumptions on a Logger hierarchy – this is a feature implemented by Log4j Core.

When the Configuration is modified, Loggers may become associated with a different LoggerConfig, thus causing their behavior to be modified. Refer to configuring Loggers for further information.

LoggerConfig

LoggerConfig binds Logger definitions to their associated components (appenders, filters, etc.) as declared in the active Configuration. The details of mapping a Configuration to LoggerConfigs is explained here. Loggers effectively interact with appenders, filters, etc. through corresponding LoggerConfigs. A LoggerConfig essentially contains

  • A reference to its parent (except if it is the root logger)

  • A level denoting the severity of messages that are accepted (defaults to ERROR)

  • Filters that must allow the LogEvent to pass before it will be passed to any Appenders

  • References to Appenders that should be used to process the event

LoggerConfig and other directly related classes
@startuml

class Configuration {
  Appender[] appenders
  Filter filter
  LoggerConfig[] loggerConfigs
  LoggerConfig getLoggerConfig(String name)
  StrSubstitutor substitutor
}

Configuration --> "0..*" LoggerConfig

class Filter {
  Result filter(LogEvent)
}

class LoggerConfig #line.bold {
  AppenderRef[] appenderRefs
  AppenderControl[] appenderControls
  Level level
  Filter filter
  void log(LogEvent)
}

LoggerConfig --> "0..*" AppenderRef

LoggerConfig -[#green,thickness=6]-> "0..*" AppenderControl

LoggerConfig --> Filter

class AppenderRef {
  String appenderName
  Level level
  Filter filter
}

class AppenderControl {
  Appender appender
  Filter filter
  void append(LogEvent)
}

class Logger {
  void log(Level level, Message message)
}

Logger -[#green,thickness=6]-> LoggerConfig : delegates `log()`

@enduml

Logger hierarchy

Log4j Core has a hierarchical model of LoggerConfigs, and hence Loggers. A LoggerConfig called child is said to be parented by parent, if parent has the longest prefix match on name. This match is case-sensitive and performed after tokenizing the name by splitting it from . (dot) characters. For a positive name match, tokens must match exhaustively. See Example hierarchy of loggers named X, X.Y, X.Y.Z, and X.YZ for an example.

Example hierarchy of loggers named X, X.Y, X.Y.Z, and X.YZ
@startmindmap
* root
** X
*** X.Y
**** X.Y.Z
*** X.YZ
@endmindmap

If a LoggerConfig is not provided an explicit level, it will be inherited from its parent. Similarly, if a user programmatically requests a Logger with a name that doesn’t have a directly corresponding LoggerConfig configuration entry with its name, the LoggerConfig of the parent will be used.

Click for examples on LoggerConfig hierarchy

Below we demonstrate the LoggerConfig hierarchy by means of level inheritance. That is, we will examine the effective level of a Logger in various LoggerConfig settings.

Table 1. Only the root logger is configured with a level, and it is DEBUG
Logger name Assigned LoggerConfig name Configured level Effective level

root

root

DEBUG

DEBUG

X

root

DEBUG

X.Y

root

DEBUG

X.Y.Z

root

DEBUG

Table 2. All loggers are configured with a level
Logger name Assigned LoggerConfig Configured level Effective level

root

root

DEBUG

DEBUG

X

X

ERROR

ERROR

X.Y

X.Y

INFO

INFO

X.Y.Z

X.Y.Z

WARN

WARN

Table 3. All loggers are configured with a level, except the logger X.Y
Logger name Assigned LoggerConfig Configured level Effective level

root

root

DEBUG

DEBUG

X

X

ERROR

ERROR

X.Y

X

ERROR

X.Y.Z

X.Y.Z

WARN

WARN

Table 4. All loggers are configured with a level, except loggers X.Y and X.Y.Z
Logger name Assigned LoggerConfig Configured level Effective level

root

root

DEBUG

DEBUG

X

X

ERROR

ERROR

X.Y

X

ERROR

X.Y.Z

X

ERROR

Table 5. All loggers are configured with a level, except the logger X.YZ
Logger name Assigned LoggerConfig Configured level Effective level

root

root

DEBUG

DEBUG

X

X

ERROR

ERROR

X.Y

X.Y

INFO

INFO

X.YZ

X

ERROR

For further information on log levels and using them for filtering purposes in a configuration, see Levels.

Filter

In addition to the level-based filtering facilitated by LoggerConfig, Log4j provides Filters to evaluate the parameters of a logging call (i.e., context-wide filter) or a log event, and decide if it should be processed further in the pipeline.

Filter and other directly related classes
@startuml

class Configuration {
  Appender[] appenders
  Filter filter
  LoggerConfig[] loggerConfigs
  LoggerConfig getLoggerConfig(String name)
  StrSubstitutor substitutor
}

Configuration --> Filter

Configuration --> "0..*" LoggerConfig

class Filter #line.bold {
  Result filter(LogEvent)
}

class LoggerConfig {
  AppenderRef[] appenderRefs
  AppenderControl[] appenderControls
  Level level
  Filter filter
  void log(LogEvent)
}

LoggerConfig --> "0..*" AppenderRef

LoggerConfig -[#green,thickness=6]-> "0..*" AppenderControl

LoggerConfig --> Filter

class AppenderRef {
  String appenderName
  Level level
  Filter filter
}

class AppenderControl {
  Filter filter
}

AppenderRef --> Filter

AppenderControl --> Filter

@enduml

Refer to Filters for further information.

Appender

Appenders are responsible for delivering a LogEvent to a certain target; console, file, database, etc. While doing so, they typically use Layouts to encode the log event. See Appenders for the complete guide.

Appender and other directly related classes
@startuml

class Configuration {
  Appender[] appenders
  Filter filter
  LoggerConfig[] loggerConfigs
  LoggerConfig getLoggerConfig(String name)
  StrSubstitutor substitutor
}

Configuration --> "0..*" Filter

Configuration --> "0..*" Appender

Configuration --> "0..*" LoggerConfig

class Appender #line.bold {
  Layout layout
  void append(LogEvent)
}

Appender -[#green,thickness=6]-> Layout

class Layout {
  byte[] encode(LogEvent)
}

class Filter {
  Result filter(LogEvent)
}

class LoggerConfig {
  AppenderRef[] appenderRefs
  AppenderControl[] appenderControls
  Level level
  Filter filter
  void log(LogEvent)
}

LoggerConfig --> "0..*" AppenderRef

LoggerConfig -[#green,thickness=6]-> "0..*" AppenderControl

LoggerConfig --> Filter

class AppenderRef {
  String appenderName
  Level level
  Filter filter
}

AppenderRef --> Filter

class AppenderControl {
  Appender appender
  Filter filter
  void append(LogEvent)
}

AppenderControl -[#green,thickness=6]-> Appender

AppenderControl --> Filter

@enduml

An Appender can be added to a Logger by calling the addLoggerAppender() method of the current Configuration. If a LoggerConfig matching the name of the Logger does not exist, one will be created, and the Appender will be attached to it, and then all Loggers will be notified to update their LoggerConfig references.

Appender additivity

Each enabled logging request for a given logger will be forwarded to all the appenders in the corresponding Logger's LoggerConfig, as well as to the Appenders of the LoggerConfig's parents. In other words, Appenders are inherited additively from the LoggerConfig hierarchy. For example, if a console appender is added to the root logger, then all enabled logging requests will at least print on the console. If in addition a file appender is added to a LoggerConfig, say LC, then enabled logging requests for LC and LC's children will print in a file and on the console. It is possible to override this default behavior so that appender accumulation is no longer additive by setting additivity attribute to false on the Logger declaration in the configuration file.

The output of a log statement of Logger L will go to all the appenders in the LoggerConfig associated with L and the ancestors of that LoggerConfig. However, if an ancestor of the LoggerConfig associated with Logger L, say P, has the additivity flag set to false, then L's output will be directed to all the appenders in L's LoggerConfig and it’s ancestors up to and including P but not the appenders in any of the ancestors of P.

Click for an example on appender additivity
Example hierarchy of logger configurations to demonstrate appender additivity
@startmindmap
* root
** A
*** A.B1 (additivity=false)
**** A.B1.C
***** A.B1.C.D
*** A.B2.C
**** A.B2.C.D (additivity=false)
@endmindmap

In Example hierarchy of logger configurations to demonstrate appender additivity, the effective appenders for each logger configuration are as follows:

Table 6. Effective appenders of logger configurations in Example hierarchy of logger configurations to demonstrate appender additivity

Appender

Logger configuration

A

A.B1

A.B1.C

A.B1.C.D

A.B2.C

A.B2.C.D

root

A

A.B1

-

-

-

A.B1.C

-

-

-

-

A.B1.C.D

-

-

-

-

-

A.B2.C

-

-

-

-

A.B2.C.D

-

-

-

-

-

AbstractManager

To multiplex the access to external resources (files, network connections, etc.), most appenders are split into an AbstractManager that handles the low-level access to the external resource and an Appender that transforms log events into a format that the manager can handle.

Managers that share the same resource are shared between appenders regardless of the Configuration or LoggerContext of the appenders. For example file appenderss with the same fileName attribute all share the same FileManager.

Due to the manager-sharing feature of many Log4j appenders, it is not possible to configure multiple appenders for the same resource that only differ in the way the underlying resource is configured.

For example, it is not possible to have two file appenders (even in different logger contexts) that use the same file, but a different value of the append option. Since during a reconfiguration event multiple instances of the same appender exists, it is also not possible to toggle the value of the append option through reconfiguration.

Layout

An Appender uses a layout to encode a LogEvent into a form that meets the needs of whatever will be consuming the log event.

Layout and other directly related classes
@startuml

class Appender {
  Layout layout
  void append(LogEvent)
}

Appender -[#green,thickness=6]-> Layout

class Layout #line.bold {
  byte[] encode(LogEvent)
}

@enduml

Refer to Layouts for details.

StrSubstitutor et al.

StrSubstitutor is a String interpolation tool that can be used in both configurations and components (e.g., appenders, layouts). It accepts an Interpolator to determine if a key maps to a certain value. Interpolator is essentially a facade delegating to multiple StrLookup (aka. lookup) implementations.

StrSubstitutor et al. and other directly related classes
@startuml

class Configuration {
  Appender[] appenders
  Filter[] filters
  LoggerConfig[] loggerConfigs
  LoggerConfig getLoggerConfig(String name)
  StrSubstitutor substitutor
}

Configuration --> StrSubstitutor

class StrSubstitutor #line.bold {
  Interpolator interpolator
  String replace(String input)
}

StrSubstitutor --> Interpolator

class Interpolator {
  StrLookup[] lookups
  String lookup(String input)
}

Interpolator --> "0..*" StrLookup

class StrLookup {
  String lookup(String input)
}

@enduml

See how property substitution works and the predefined lookups for further information.