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:
@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 aConfiguration
. Both can be created either directly (i.e., programmatically) or indirectly at first interaction with Log4j. -
LoggerContext
createsLogger
s that users interact with for logging purposes. -
Appender
delivers aLogEvent
to a target (file, socket, database, etc.) and typically uses aLayout
to encode log events and anAbstractManager
to handle the lifecycle of the target resource. -
LoggerConfig
encapsulates configuration for aLogger
, asAppenderControl
andAppenderRef
forAppender
s. -
Configuration
is equipped withStrSubstitutor
et al. to allow property substitution inString
-typed values. -
A typical
log()
call triggers a chain of invocations through classesLogger
,LoggerConfig
,AppenderControl
,Appender
, andAbstractManager
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 Logger
s.
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 LoggerContext
s.
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
Logger
s switch to the new configuration, -
when should the old configuration be stopped.
@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
Logger
s 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 LoggerConfig
s, implies the very same hierarchy between Logger
s 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, Logger
s may become associated with a different LoggerConfig
, thus causing their behavior to be modified.
Refer to configuring Logger
s 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 LoggerConfig
s is explained here.
Logger
s effectively interact with appenders, filters, etc. through corresponding LoggerConfig
s.
A LoggerConfig
essentially contains
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 LoggerConfig
s, and hence Logger
s.
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.
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.
Logger name | Assigned LoggerConfig name |
Configured level | Effective level |
---|---|---|---|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
Logger name | Assigned LoggerConfig |
Configured level | Effective level |
---|---|---|---|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
Logger name | Assigned LoggerConfig |
Configured level | Effective level |
---|---|---|---|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
Logger name | Assigned LoggerConfig |
Configured level | Effective level |
---|---|---|---|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
Logger name | Assigned LoggerConfig |
Configured level | Effective level |
---|---|---|---|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
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 Filter
s 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
Appender
s are responsible for delivering a LogEvent
to a certain target; console, file, database, etc.
While doing so, they typically use Layout
s 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 Logger
s 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 Appender
s of the LoggerConfig
's parents.
In other words, Appender
s 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
@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:
Appender |
Logger configuration |
|||||
---|---|---|---|---|---|---|
|
|
|
|
|
|
|
|
✅ |
✅ |
✅ |
✅ |
✅ |
❌ |
|
✅ |
❌ |
❌ |
❌ |
✅ |
❌ |
|
- |
✅ |
✅ |
✅ |
- |
- |
|
- |
- |
✅ |
✅ |
- |
- |
|
- |
- |
- |
✅ |
- |
- |
|
- |
- |
- |
- |
✅ |
❌ |
|
- |
- |
- |
- |
- |
✅ |
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 |
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.