Extending Log4j2

Creating Custom Log4j2 Plugins

As you probably know, the differences between Log4j 1.x and Log4j2 are quite significant, and unless you're doing the most simple usage of Log4j you might have to make quite a few changes to support the version upgrade.

This article will give two simple examples of creating Log4j2 plugins, for those who need to do so for the first time, or those who need to see how they might rewrite existing Log4j1.x plugins. I'll also demonstrate how to write tests for the loggers with the use of a custom built String Appender.

Table Mountain, Cape Town, South Africa. Taken from Lion's Head

Logging Request Ids

Most implementations of API services have some way of tracking the flow of individual requests. The common way to do this is with globally unique identifiers that identify individual requests and are logged with every log message and eventually returned to the customer with the response.

I'll demonstrate how one might provide logging support for a request Id in a system using Log4j2. Of course a possible solution would be to prepend the request Id every time you log something, but that is a seriously unreasonable approach. You could also have a wrapper log method that prepends it for you. But a nicer solution would be to have a new conversion pattern that could be used in PatternLayouts. If we call it %reqId a pattern might look as follows:


              <Pattern>%-5p %d [%reqId] %c: %m%n</Pattern>

Producing logs something like the following:


              INFO  2015-02-20 12:34,567 [abcd-efgh-1234] com.andrew_flower.example.Log4ConverterTest: This is a log!

In the following section you'll see how to add functionality to interpret the new code.

A new Pattern Converter

There are a few different types of Log4j2 plugins. In this case we will be creating a Converter. Converters are used in Patterns such as in the pattern of a PatternLayouts or the filename of a RollingFileAppender. The converter specifies a coverter key/pattern and also defines how that key should be replaced when contained in a pattern string.

We will be creating a converter for PatternLayouts as opposed to for filenames. All Log4j2 plugins must have the @Plugin annotation, but on top of that, each plugin type has additional requirements. I'll list what is needed for a PatternLayout converter:

  1. Must have the @Plugin annotation.
  2. The @Plugin annotation must have a name and specify "Converter" as the category. If this were for filename patterns it would be "FileConverter".
  3. Converters should have the @ConverterKeysannotation and including a list of keys that it should match in a pattern string. You might have a short or long name for the key. For example, the existing ThrowablePatternConverter specifies { "ex", "throwable", "exception" }
  4. A converter for log patterns (like we're creating) must extend LogEventPatternConverter.
  5. It must have a newInstance static method taking a String array and returning an instance of the converter.
  6. It must override the format method. It is this method that must contain the functionality to provide a replacement value for the pattern key.

So now we'll take a look at one way of writing the converter class. Take a look at the code below


          

The example is very straight forward and you can see that the class obeys all the rules we enumerated above. Note that where the constructor calls the superclass it passes two strings. The first is the name of the converter, and the second is a CSS classname. Although we don't use CSS here, I've just used the same name. The format method receives a LogEvent object containing information like the message and level, and a StringBuilder which contains the evaluated log pattern up to the point of encountering this converter key. So all the format method must do is append the appropriate value to insert.

Configuration File

In order to use this file we can create a simple config file that makes use of our converter key.


          

And that's all there is to creating a custom converter! Really simple and easy. I'm sure there many more imaginative things that can be inserted into pattern strings, but I'll leave that up to you guys. Thought I'd be interested to hear what other people have implemented. In the next section I'll run through how we might test this converter.

Testing the Converter

In my opinion code is not finished without tests, so let's complete this example by trying to write some JUnit tests for this. I'll just write two tests but you should obviously think of as many test conditions as you can. Let's write the following two tests:

  1. A unit test to test the format method, that given our ReqIdConverter object, when calling format with a StringBuilder containing a message at the key point, then it will return the StringBuilder with the current requestId appended.
  2. A component test to test that the Log4j2 configuration logs the request Id. So given a log configuration with PatternLayout containing a %reqId, when we call log(), then the log line contains the request Id. For this we'll create a special Appender that allows us to log to a String.

Write the tests

So this is the simple part, let's write the outline for what we want tests to do. The first test formatAppendsRequestId is really easy. We create an instance of our converter and test that it appends the request Id to the StringBuilder that we pass to it. Note that for completeness I've stubbed out the mythical request Id provider. Because our converter doesn't depend on anything from the log message itself, we pass null as the log event parameter. Have a look at the code:


          

The second test logReplacesKeyWithReqId makes use of an imaginary appender type called StringAppender. In Log4j 1.x a similar appender existed, but it's not so easy in Log4j2. We can, however, quite easily create our own StringAppender and we do so in the next section. Note above that we assume there is no log4j2-test.xml defined, so the default configuration is to log ERROR logs only. For the test we therefore call error() on the RootLogger.

A String Appender

Our goal here is to create a means by which we can log to an in-memory String, or String stream. The Log4j2 Manual mentions an OutputStreamAppender that many Appenders actually extend. It is a very powerful class and also depends on a OutputStreamManager to handle the IO. We will just use the bare minimum of these classes to provide the functionality that is needed for our test.

Truly we should be writing a test for this class too, but I will skip that for conciseness' sake.


          

The interesting parts are the methods createStringAppender, addToLogger and getOutput. createStringAppender returns our StringAppender class after setting up a StringOutputStreamManager with a ByteArrayOutputStream that will be used for writing log data. addToLogger will add this StringAppender object to the list of appenders of the provided Logger. Finally getOutput returns the string that was created by the logger. You can lookup PatternLayout.create* methods in the javadoc as well as the superclass constructors for AbstractOutputStreamAppender and OutputStreamManager.

This demonstrates how we might create an in-memory StringAppender to test Loggers in JUnit, or even for other purposes. In production you would want to better check for errors, and probably let it be more customizable, but this serves as an easy example for now.