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 high level,
-
A
LoggerContext, the composition anchor, gets created in combination with aConfiguration. Both can be created directly (i.e., programmatically) or indirectly at first interaction with Log4j. -
LoggerContextcreatesLoggers that users interact with for logging purposes. -
Appenderdelivers aLogEventto a target (file, socket, database, etc.) and typically uses aLayoutto encode log events and anAbstractManagerto handle the lifecycle of the target resource. -
LoggerConfigencapsulates configuration for aLogger,asAppenderControlandAppenderRefforAppenders. -
Configurationis equipped withStrSubstitutoret al. to allow property substitution inString-typed values. -
A typical
log()call triggers a chain of invocations through classesLogger,LoggerConfig,AppenderControl,Appender, andAbstractManagerin order – this is depicted using green arrows in An overview of major classes and their relation.
The 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
LoggerConfigclass, -
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.
@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
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.
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 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
@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.