A Deep Dive into Log4j: Making SyslogAppender Work with PatternLayout
Log4j is a highly popular Java-based logging utility from the Apache Logging Services Project. In this blog post, we will explore what log4j is, why it is essential, its architecture, and how to make SyslogAppender work with PatternLayout.
Though you can configure PatternLayout in SyslogAppender, it doesn't actually work as expected, for example, this StackOverflow question asks the exact question. We will show why it doesn't work and how can we set the layout by adding a customized layout.
What is Log4j?
Log4j is a reliable, flexible, and fast Java-based logging framework. It is used to output log statements from applications to various output targets. It is designed to enable logging at runtime without modifying the application binary.
Why is Logging Essential?
Logging is a critical component of software development, as it:
- Troubleshooting: Helps developers identify and fix issues during development and after deployment.
- Monitoring: Assists in monitoring system behavior and performance.
- Audit: Provides a means of auditing application activity.
- Documentation: Acts as a form of live documentation of system behavior and usage.
Log4j Architecture
The architecture of log4j is built around main components: Loggers, Appenders, Layouts, and Filters, etc.
- Loggers: This is usually the same as package names and the fully qualified Java class names. If you are wondering what is a fully qualified Java class name, it is the Java class name with package names, for example,
org.apache.logging.log4j.Logger
is a fully qualified name. The logger names are tree structure and hierarchical, which meansorg.foo.X
will inherit any settings thatorg.foo
are used if it has no settings. The root of the Loggers is no surprise calledRoot
. - Appenders: This is where you want your log to be saved. A Console Appender will print logs to the standard output. A File Appender will save the logs in a file with the path and name set by you. A Socket Appender will send the log to a specified host and port. It even supports publishing logs to a Kafka topic (Kafka Appender).
- Layouts: This is how you control what the log information would look like. Log4j also supports properties substitution in the pattern strings.
- Filters: This decides if a log event should be appended or not.
There are other important components, you can find more information from the official doc. In the current blog post, knowing the above should be sufficient.
A Hello World Example
Using log4j is very simple. Assume you use Maven for dependency management, you only need 3 simple steps to use Log4j.
First, add log4j in the dependency pom.xml
file:
<dependency>
<groupId>org.apache.logging.log4j</groupId>
<artifactId>log4j-api</artifactId>
<version>2.20.0</version>
</dependency>
<dependency>
<groupId>org.apache.logging.log4j</groupId>
<artifactId>log4j-core</artifactId>
<version>2.20.0</version>
</dependency>
Then, create a log4j2.xml
file under src/resource
folder. A simple example of logging into the console would look like this:
<Configuration>
<Appenders>
<Console name="console">
<PatternLayout pattern="%d [%t] %highlight{%-5level: %msg%n%throwable}"/>
</Console>
</Appenders>
<Loggers>
<Root level="INFO">
<AppenderRef ref="console" />
</Root>
</Loggers>
</Configuration>
Then, you are ready to use the logger in the codebase:
package com.example;
import org.apache.logging.log4j.Logger;
import org.apache.logging.log4j.LogManager;
public class LogExample {
private static final Logger logger = LogManager.getLogger(LogExample.class);
public static void main(String[] args) {
logger.info("this is an info message");
}
}
This should print a log like the one below when running the main method:
2023-10-01 20:00:47,255 [main] INFO : this is an info message
Make SyslogAppend Work with PatternLayout
The official documentation has sections about each important component in the doc. However, sometimes the official doc is not enough during development. For example, when I tried to set the layout for SyslogAppender
, I got no information from the doc and Google. The docs seem to say that SyslogAppender
uses the RFC-5424 layout, however, I have no idea if I can set this layout like what I did with PatternLayout
for ConsoleAppender
above.
This is when reading the source code comes to the rescue. Log4j uses plugin-based implementation. All the important components like layouts and appenders are plugins, which means that we can also write our own plugins if needed.
The SyslogAppender
shows that it is a plugin with name Syslog
,and it extends the SocketAppender
like this:
@Plugin(name = "Syslog", category = Core.CATEGORY_NAME, elementType = Appender.ELEMENT_TYPE, printObject = true)
public class SyslogAppender extends SocketAppender
Instead of using constructors, Log4j uses Builders(See builder pattern) heavily when creating instances of these core components. The builder of SyslogAppender
is shown below:
@Override
public SyslogAppender build() {
final Protocol protocol = getProtocol();
final SslConfiguration sslConfiguration = getSslConfiguration();
final boolean useTlsMessageFormat = sslConfiguration != null || protocol == Protocol.SSL;
final Configuration configuration = getConfiguration();
Layout<? extends Serializable> layout = getLayout();
if (layout == null) {
layout = RFC5424.equalsIgnoreCase(format)
? new Rfc5424Layout.Rfc5424LayoutBuilder()
.setFacility(facility)
.setId(id)
.setEin(enterpriseNumber)
.setIncludeMDC(includeMdc)
.setMdcId(mdcId)
.setMdcPrefix(mdcPrefix)
.setEventPrefix(eventPrefix)
.setIncludeNL(newLine)
.setEscapeNL(escapeNL)
.setAppName(appName)
.setMessageId(msgId)
.setExcludes(excludes)
.setIncludes(includes)
.setRequired(required)
.setExceptionPattern(exceptionPattern)
.setUseTLSMessageFormat(useTlsMessageFormat)
.setLoggerFields(loggerFields)
.setConfig(configuration)
.build() :
// @formatter:off
SyslogLayout.newBuilder()
.setFacility(facility)
.setIncludeNewLine(newLine)
.setEscapeNL(escapeNL)
.setCharset(charsetName)
.build();
// @formatter:on
}
final String name = getName();
if (name == null) {
LOGGER.error("No name provided for SyslogAppender");
return null;
}
final AbstractSocketManager manager = createSocketManager(name, protocol, getHost(), getPort(), getConnectTimeoutMillis(),
sslConfiguration, getReconnectDelayMillis(), getImmediateFail(), layout, Constants.ENCODER_BYTE_BUFFER_SIZE, getSocketOptions());
return new SyslogAppender(name, layout, getFilter(), isIgnoreExceptions(), isImmediateFlush(), manager,
getAdvertise() ? configuration.getAdvertiser() : null, null);
}
In the builder, an Rfc5424Layout
or SyslogLayout
if no layout is provided. Then it creates a SocketManager
. After checking SocketAppender
code I found that SyslogAppender
is just a special case of SocketAppender
that it uses a specific layout.
Since the builder doesn’t override an existing layout, maybe we can set a PatternLayout. But with the layout, the log will no longer show in syslog. This is because the layout doesn’t follow the Syslog protocol (that Rfc5425Layout
and SyslogLayout
follows), it will not be consumed by the syslog daemon.
To further understand this, we can check the implementation of Rfc5424Layout
. The method toSerializable
is used to format a LogEvent
to a log message. The code below shows the layout prepends a few fields that make the string follow the RFC5424 layout.
@Override
public String toSerializable(final LogEvent event) {
final StringBuilder buf = getStringBuilder();
appendPriority(buf, event.getLevel());
appendTimestamp(buf, event.getTimeMillis());
appendSpace(buf);
appendHostName(buf);
appendSpace(buf);
appendAppName(buf);
appendSpace(buf);
appendProcessId(buf);
appendSpace(buf);
appendMessageId(buf, event.getMessage());
appendSpace(buf);
appendStructuredElements(buf, event);
appendMessage(buf, event);
if (useTlsMessageFormat) {
return new TlsSyslogFrame(buf.toString()).toString();
}
return buf.toString();
}
So to make the layout both work with PatternLayout
and follow the RFC5424 layout, we can create a new plugin called Rfc5424PatternLayout
that composites a PatternLayout
instance.
In the methodtoSerializable
we first append the same required fields, then use patternLayout
to serialize the LogEvent
, and then we append the serialized event as message like below:
@Override
public String toSerializable(final LogEvent event) {
// RFC5424 goodies
final StringBuilder buf = getStringBuilder();
appendPriority(buf, event.getLevel());
appendTimestamp(buf, event.getTimeMillis());
appendSpace(buf);
appendHostName(buf);
appendSpace(buf);
appendAppName(buf);
appendSpace(buf);
appendProcessId(buf);
appendSpace(buf);
appendMessageId(buf, event.getMessage());
appendSpace(buf);
appendStructuredElements(buf, event);
// update message that has used pattern layout to set custom pattern first
appendMessage(buf, patternLayout.toSerializable(event);
if (useTlsMessageFormat) {
return new TlsSyslogFrame(buf.toString()).toString();
}
return buf.toString();
}
In this way, we are able to set a pattern layout for Syslog appenders in Log4j. A second thought is that Log4j probably should use composition over inheritance in the first place since SyslogAppender
is just a special case of SocketAppender
.
Summary
In this blog, we went through the basics of the Log4j framework, and we dive deep into the case that sets pattern layout for SyslogAppender
. From this example, we can also find that sometimes the doc is not enough, reading code and understanding the design will help customized and advanced use cases.