How to Write a Custom Appender in Log4j2
Ever wanted to test whether a log statement is triggered? Or whether the format is the way you want? In this post, we’re going to create a custom log appender so we can be sure logging is behaving the way we expect.
/* package declaration, imports... */
@Plugin(name = "CustomListAppender",
category = Core.CATEGORY_NAME,
elementType = Appender.ELEMENT_TYPE,
printObject = true)
public final class CustomListAppender extends AbstractAppender {
// for storing the log events
private List<LogEvent> events = new ArrayList<>();
protected CustomListAppender(
String name,
Filter filter,
Layout<? extends Serializable> layout,
boolean ignoreExceptions) {
super(name, filter, layout, ignoreExceptions);
}
@Override
public void append(LogEvent event) {
if (event instanceof MutableLogEvent) {
events.add(((MutableLogEvent) event).createMemento());
} else {
events.add(event);
}
}
public List<LogEvent> getEvents() {
return events;
}
@PluginFactory
public static CustomListAppender createAppender(
@PluginAttribute("name") String name,
@PluginElement("Layout") Layout<? extends Serializable> layout,
@PluginElement("Filter") Filter filter) {
if (name == null) {
LOGGER.error("No name provided for TestLoggerAppender");
return null;
}
if (layout == null) layout = PatternLayout.createDefaultLayout();
return new CustomListAppender(name, filter, layout, true);
}
}
Our CustomListAppender
extends AbstractAppender
, because that
implements a lot of the methods from the Appender
interface for us
that we would otherwise have to implement ourselves.
The @Plugin
annotation identifies this class a plugin that should be
picked up by the PluginManager
:
- The
name
attribute defines the name of the appender that can be used in the configuration. - The
category
attribute should be"Core"
, because “Core plugins are those that are directly represented by an element in a configuration file, such as an Appender, Layout, Logger or Filter” (source). And we are creating an appender. - The
elementType
attribute defines which type of element in the Core category this plugin should be. In our case,"appender"
. - The
printObject
attribute defines whether our custom plugin class defines a usefultoString()
method. We do, because theAbstractAppender
class we’re extending is taking care of that for us.
We implement the Appender#append(LogEvent)
method to add each event to
our events
list. If the LogEvent
happens to be mutable, we must take
care to create an immutable copy of the event, otherwise subsequent log
events will overwrite it (we will get a list of, say, three log events
that are all referencing the same object). We also add a simple getter
method to retrieve all log events.
For the PluginManager
to create our custom plugin, it needs a way to
instantiate it. log4j2 uses a factory method for that, indicated by the
annotation @PluginFactory
. An appender contains attributes, such as a
name, and other elements, such as layouts and filters. To allow for
these, we use the corresponding annotations @PluginAttribute
to
indicate that a parameter represents an attribute, and @PluginElement
to indicate that a parameter represents an element.
To log errors that might occur during this setup, we can make use of the
StatusLogger
. This logger is available as LOGGER
, and is defined in
one of the parents of our custom plugin, AbstractLifeCycle
. (The level
of log messages that should be visible can be adjusted in the
<Configuration status="warn" ...>
element.)
Configuration
Configuration:
packages: com.relentlesscoding.logging.plugins
status: warn
appenders:
Console:
name: STDOUT
CustomListAppender:
name: MyVeryOwnListAppender
Loggers:
logger:
-
name: com.relentlesscoding.logging
level: info
AppenderRef:
ref: MyVeryOwnListAppender
Root:
level: error
AppenderRef:
ref: STDOUT
The packages
attribute on the Configuration
element indicates the
package that should be scanned by the PluginManager
for custom plugins
during initialization.
How to use our custom list appender?
private CustomListAppender appender;
@Before
public void setupLogging() {
LoggerContext context = LoggerContext.getContext(false);
Configuration configuration = context.getConfiguration();
appender = configuration.getAppender("MyVeryOwnListAppender");
appender.getEvents().clear();
}
When we run tests now, we are able to see all logged events by calling
appender.getEvents()
. Before each test, we take care to clear the list
of the previous log statements.