Log4j 2 Plugins

1. Übersicht

Log4j 2 verwendet Plugins wie Appender und Layouts zum Formatieren und Ausgeben von Protokollen. Diese werden als Core-Plugins bezeichnet, und Log4j 2 bietet uns viele Optionen zur Auswahl.

In einigen Fällen müssen wir jedoch möglicherweise auch das vorhandene Plugin erweitern oder sogar benutzerdefinierte Plugins schreiben.

In diesem Tutorial verwenden wir den Log4j 2-Erweiterungsmechanismus, um benutzerdefinierte Plugins zu implementieren.

2. Erweitern der Log4j 2-Plugins

Plugins in Log4j 2 sind grob in fünf Kategorien unterteilt:

  1. Core Plugins
  2. Konverter
  3. Wichtige Anbieter
  4. Lookups
  5. Typ Konverter

Mit Log4j 2 können wir benutzerdefinierte Plugins in allen oben genannten Kategorien mithilfe eines gemeinsamen Mechanismus implementieren. Darüber hinaus können wir vorhandene Plugins mit demselben Ansatz erweitern.

In Log4j 1.x besteht die einzige Möglichkeit, ein vorhandenes Plugin zu erweitern, darin, seine Implementierungsklasse zu überschreiben. Andererseits erleichtert Log4j 2 das Erweitern vorhandener Plugins durch Annotieren einer Klasse mit @Plugin.

In den folgenden Abschnitten implementieren wir ein benutzerdefiniertes Plugin in einigen dieser Kategorien.

3. Core Plugin

3.1. Implementieren eines benutzerdefinierten Core-Plugins

Schlüsselelemente wie Anhänge, Layouts und Filter werden in Log4j 2 als Kern-Plugins bezeichnet . Obwohl es eine vielfältige Liste solcher Plugins gibt, müssen wir in einigen Fällen möglicherweise ein benutzerdefiniertes Core-Plugin implementieren. Stellen Sie sich beispielsweise einen ListAppender vor , der nur Protokolldatensätze in eine speicherinterne Liste schreibt :

@Plugin(name = "ListAppender", category = Core.CATEGORY_NAME, elementType = Appender.ELEMENT_TYPE) public class ListAppender extends AbstractAppender { private List logList; protected ListAppender(String name, Filter filter) { super(name, filter, null); logList = Collections.synchronizedList(new ArrayList()); } @PluginFactory public static ListAppender createAppender( @PluginAttribute("name") String name, @PluginElement("Filter") final Filter filter) { return new ListAppender(name, filter); } @Override public void append(LogEvent event) { if (event.getLevel().isLessSpecificThan(Level.WARN)) { error("Unable to log less than WARN level."); return; } logList.add(event); } }

Wir haben die Klasse mit @Plugin kommentiert , mit dem wir unser Plugin benennen können . Außerdem werden die Parameter mit @PluginAttribute kommentiert . Die verschachtelten Elemente wie Filter oder Layout werden als @PluginElement übergeben. Jetzt können wir dieses Plugin in der Konfiguration mit demselben Namen referenzieren:


    

3.2. Plugin Builders

The example in the last section is rather simple and only accepts a single parameter name. Generally speaking, core plugins like appenders are much more complex and usually accepts several configurable parameters.

For example, consider an appender that writes logs into Kafka:

To implement such appenders, Log4j 2 provides a plugin builder implementation based on the Builder pattern:

@Plugin(name = "Kafka2", category = Core.CATEGORY_NAME) public class KafkaAppender extends AbstractAppender { public static class Builder implements org.apache.logging.log4j.core.util.Builder { @PluginBuilderAttribute("name") @Required private String name; @PluginBuilderAttribute("ip") private String ipAddress; // ... additional properties // ... getters and setters @Override public KafkaAppender build() { return new KafkaAppender( getName(), getFilter(), getLayout(), true, new KafkaBroker(ipAddress, port, topic, partition)); } } private KafkaBroker broker; private KafkaAppender(String name, Filter filter, Layout layout, boolean ignoreExceptions, KafkaBroker broker) { super(name, filter, layout, ignoreExceptions); this.broker = broker; } @Override public void append(LogEvent event) { connectAndSendToKafka(broker, event); } }

In short, we introduced a Builder class and annotated the parameters with @PluginBuilderAttribute. Because of this, KafkaAppender accepts the Kafka connection parameters from the config shown above.

3.3. Extending an Existing Plugin

We can also extend an existing core plugin in Log4j 2. We can achieve this by giving our plugin the same name as an existing plugin. For example, if we're extending the RollingFileAppender:

@Plugin(name = "RollingFile", category = Core.CATEGORY_NAME, elementType = Appender.ELEMENT_TYPE) public class RollingFileAppender extends AbstractAppender { public RollingFileAppender(String name, Filter filter, Layout layout) { super(name, filter, layout); } @Override public void append(LogEvent event) { } }

Notably, we now have two appenders with the same name. In such a scenario, Log4j 2 will use the appender that is discovered first. We'll see more on plugin discovery in a later section.

Please note that Log4j 2 discourages multiple plugins with the same name. It's better to implement a custom plugin instead and use that in the logging configuration.

4. Converter Plugin

The layout is a powerful plugin in Log4j 2. It allows us to define the output structure for our logs. For instance, we can use JsonLayout for writing the logs in JSON format.

Another such plugin is the PatternLayout. In some cases, an application wants to publish information like thread id, thread name, or timestamp with each log statement. PatternLayout plugin allows us to embed such details through a conversion pattern string in the configuration:

Here, %d is the conversion pattern. Log4j 2 converts this %d pattern through a DatePatternConverter that understands the conversion pattern and replaces it with the formatted date or timestamp.

Now suppose an application running inside a Docker container wants to print the container name with every log statement. To do this, we'll implement a DockerPatterConverter and change the above config to include the conversion string:

@Plugin(name = "DockerPatternConverter", category = PatternConverter.CATEGORY) @ConverterKeys({"docker", "container"}) public class DockerPatternConverter extends LogEventPatternConverter { private DockerPatternConverter(String[] options) { super("Docker", "docker"); } public static DockerPatternConverter newInstance(String[] options) { return new DockerPatternConverter(options); } @Override public void format(LogEvent event, StringBuilder toAppendTo) { toAppendTo.append(dockerContainer()); } private String dockerContainer() { return "container-1"; } }

So we implemented a custom DockerPatternConverter similar to the date pattern. It will replace the conversion pattern with the name of the Docker container.

This plugin is similar to the core plugin we implemented earlier. Notably, there is just one annotation that is different from the last plugin. @ConverterKeys annotation accepts the conversion pattern for this plugin.

As a result, this plugin will convert %docker or %container pattern string into the container name in which the application is running:

5. Lookup Plugin

Lookup plugins are used to add dynamic values in the Log4j 2 configuration file. They allow applications to embed runtime values to some properties in the configuration file. The value is added through a key-based lookup in various sources like a file system, database, etc.

One such plugin is the DateLookupPlugin that allows replacing a date pattern with the current system date of the application:

 %d %p %c{1.} [%t] %m%n 

In this sample configuration file, RollingFileAppender uses a date lookup where the output will be in MM-dd-yyyy format. As a result, Log4j 2 writes logs to an output file with a date suffix.

Similar to other plugins, Log4j 2 provides a lot of sources for lookups. Moreover, it makes it easy to implement custom lookups if a new source is required:

@Plugin(name = "kafka", category = StrLookup.CATEGORY) public class KafkaLookup implements StrLookup { @Override public String lookup(String key) { return getFromKafka(key); } @Override public String lookup(LogEvent event, String key) { return getFromKafka(key); } private String getFromKafka(String topicName) { return "topic1-p1"; } }

So KafkaLookup will resolve the value by querying a Kafka topic. We'll now pass the topic name from the configuration:

 %d %p %c{1.} [%t] %m%n 

We replaced the date lookup in our earlier example with Kafka lookup that will query topic-1.

Since Log4j 2 only calls the default constructor of a lookup plugin, we didn't implement the @PluginFactory as we did in earlier plugins.

6. Plugin Discovery

Finally, let's understand how Log4j 2 discovers the plugins in an application. As we saw in the examples above, we gave each plugin a unique name. This name acts as a key, which Log4j 2 resolves to a plugin class.

There's a specific order in which Log4j 2 performs a lookup to resolve a plugin class:

  1. Serialized plugin listing file in the log4j2-core library. Specifically, a Log4j2Plugins.dat is packaged inside this jar to list the default Log4j 2 plugins
  2. Similar Log4j2Plugins.dat file from the OSGi bundles
  3. A comma-separated package list in the log4j.plugin.packages system property
  4. In programmatic Log4j 2 configuration, we can call PluginManager.addPackages() method to add a list of package names
  5. A comma-separated list of packages can be added in the Log4j 2 configuration file

As a prerequisite, annotation processing must be enabled to allow Log4j 2 to resolve plugin by the name given in the @Plugin annotation.

Since Log4j 2 uses names to look up the plugin, the above order becomes important. For example, if we have two plugins with the same name, Log4j 2 will discover the plugin that is resolved first. Therefore, if we need to extend an existing plugin in Log4j 2, we must package the plugin in a separate jar and place it before the log4j2-core.jar.

7. Conclusion

In this article, we looked at the broad categories of plugins in Log4j 2. We discussed that even though there is an exhaustive list of existing plugins, we may need to implement custom plugins for some use cases.

Later, we looked at the custom implementation of some useful plugins. Furthermore, we saw how Log4j 2 allows us to name these plugins and subsequently use this plugin name in the configuration file. Finally, we discussed how Log4j 2 resolves plugins based on this name.

As always, all examples are available over on GitHub.