1. Overview
1.概述
Log4j 2 uses plugins like Appenders and Layouts to format and output logs. These are known as core plugins, and Log4j 2 provides a lot of options for us to choose from.
Log4j 2使用Appenders和Layouts等插件来格式化和输出日志。这些被称为核心插件,Log4j 2提供了大量的选项供我们选择。
However, in some cases, we may also need to extend the existing plugin or even write custom ones.
然而,在某些情况下,我们可能还需要扩展现有的插件,甚至编写自定义的插件。
In this tutorial, we’ll use the Log4j 2 extension mechanism to implement custom plugins.
在本教程中,我们将使用Log4j 2扩展机制来实现自定义插件。
2. Extending Log4j 2 Plugins
2.扩展Log4j 2插件
Plugins in Log4j 2 are broadly divided into five categories:
Log4j 2中的插件大致分为五类。
- Core Plugins
- Convertors
- Key Providers
- Lookups
- Type Converters
Log4j 2 allows us to implement custom plugins in all the above categories using a common mechanism. Moreover, it also allows us to extend existing plugins with the same approach.
Log4j 2允许我们使用一个共同的机制来实现上述所有类别的自定义插件。此外,它还允许我们用同样的方法扩展现有的插件。
In Log4j 1.x, the only way to extend an existing plugin is to override its implementation class. On the other hand, Log4j 2 makes it easier to extend existing plugins by annotating a class with @Plugin.
在Log4j 1.x中,扩展一个现有插件的唯一方法是覆盖其实现类。另一方面,Log4j 2通过用@Plugin.注释一个类,使扩展现有插件变得更加容易。
In the following sections, we’ll implement a custom plugin in a few of these categories.
在下面的章节中,我们将在其中几个类别中实现一个自定义插件。
3. Core Plugin
3.核心插件
3.1. Implementing a Custom Core Plugin
3.1.实现一个自定义的核心插件
Key elements like Appenders, Layouts, and Filters are known as core plugins in Log4j 2. Although there is a diverse list of such plugins, in some cases, we may need to implement a custom core plugin. For example, consider a ListAppender that only writes log records into an in-memory List:
像Appenders、Layouts和Filter这样的关键元素在Log4j 2中被称为核心插件。虽然有一个多样化的此类插件列表,但在某些情况下,我们可能需要实现一个自定义的核心插件。例如,考虑一个ListAppender,它只将日志记录写入一个内存List中。
@Plugin(name = "ListAppender",
category = Core.CATEGORY_NAME,
elementType = Appender.ELEMENT_TYPE)
public class ListAppender extends AbstractAppender {
private List<LogEvent> 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);
}
}
We have annotated the class with @Plugin that allows us to name our plugin. Also, the parameters are annotated with @PluginAttribute. The nested elements like filter or layout are passed as @PluginElement. Now we can refer this plugin in the configuration using the same name:
我们用@Plugin来注解这个类,它允许我们为我们的插件命名。同时,参数也被注解为@PluginAttribute。像过滤器或布局这样的嵌套元素被作为@PluginElement.传递,现在我们可以在配置中使用相同的名称引用这个插件。
<?xml version="1.0" encoding="UTF-8"?>
<Configuration xmlns:xi="http://www.w3.org/2001/XInclude"
packages="com.baeldung" status="WARN">
<Appenders>
<ListAppender name="ListAppender">
<BurstFilter level="INFO" rate="16" maxBurst="100"/>
</MapAppender>
</Appenders>
<Loggers
<Root level="DEBUG">
<AppenderRef ref="ConsoleAppender" />
<AppenderRef ref="ListAppender" />
</Root>
</Loggers>
</Configuration>
3.2. Plugin Builders
3.2 插件制作者
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.
上一节的例子相当简单,只接受了一个参数name。一般来说,像appenders这样的核心插件要复杂得多,通常接受几个可配置的参数。
For example, consider an appender that writes logs into Kafka:
例如,考虑一个将日志写进Kafka的appender:。
<Kafka2 name="KafkaLogger" ip ="127.0.0.1" port="9010" topic="log" partition="p-1">
<PatternLayout pattern="%pid%style{%message}{red}%n" />
</Kafka2>
To implement such appenders, Log4j 2 provides a plugin builder implementation based on the Builder pattern:
为了实现这种附加器,Log4j 2提供了一个基于Builder模式的插件构建器实现。
@Plugin(name = "Kafka2", category = Core.CATEGORY_NAME)
public class KafkaAppender extends AbstractAppender {
public static class Builder implements org.apache.logging.log4j.core.util.Builder<KafkaAppender> {
@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<? extends Serializable> 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.
简而言之,我们引入了一个Builder类,并用@PluginBuilderAttribute>来注释参数。因为这样,KafkaAppender接受了来自上图所示配置的Kafka连接参数。
3.3. Extending an Existing Plugin
3.3.扩展一个现有的插件
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:
我们也可以在Log4j 2中扩展一个现有的核心插件。我们可以通过赋予我们的插件与现有插件相同的名称来实现这一点。例如,如果我们要扩展RollingFileAppender:
@Plugin(name = "RollingFile", category = Core.CATEGORY_NAME, elementType = Appender.ELEMENT_TYPE)
public class RollingFileAppender extends AbstractAppender {
public RollingFileAppender(String name, Filter filter, Layout<? extends Serializable> 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.
值得注意的是,我们现在有两个具有相同名称的应用程序。在这种情况下,Log4j 2将使用首先被发现的appender。我们将在后面的章节中看到更多关于插件发现的内容。
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.
请注意,Log4j 2不鼓励使用同名的多个插件。最好是实现一个自定义插件,并在日志配置中使用该插件。
4. Converter Plugin
4.转换器插件
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.
布局是Log4j 2中一个强大的插件。它允许我们为我们的日志定义输出结构。例如,我们可以使用JsonLayout来编写JSON格式的日志。
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:
另一个这样的插件是 PatternLayout。在某些情况下,应用程序希望在每个日志语句中发布线程ID、线程名称或时间戳等信息。PatternLayout插件允许我们通过配置中的转换模式字符串嵌入此类细节。
<Configuration status="debug" name="baeldung" packages="">
<Appenders>
<Console name="stdout" target="SYSTEM_OUT">
<PatternLayout pattern="%d{yyyy-MM-dd HH:mm:ss} %p %m%n"/>
</Console>
</Appenders>
</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.
这里,%d是转换模式。Log4j 2通过一个DatePatternConverter来转换这个%d模式,它理解转换模式并将其替换为格式化的日期或时间戳。
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:
现在,假设一个在Docker容器内运行的应用程序想要在每条日志语句中打印容器名称。要做到这一点,我们将实现一个DockerPatternConverter,并改变上述配置以包括转换字符串。
@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.
所以我们实现了一个类似于日期模式的自定义DockerPatternConverter。它将用Docker容器的名称替换转换模式。
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.
这个插件与我们之前实现的核心插件类似。值得注意的是,只有一个注解与上一个插件不同。@ConverterKeys注解接受这个插件的转换模式。
As a result, this plugin will convert %docker or %container pattern string into the container name in which the application is running:
因此,这个插件将把%docker或%container模式字符串转换成应用程序运行的容器名称。
<?xml version="1.0" encoding="UTF-8"?>
<Configuration xmlns:xi="http://www.w3.org/2001/XInclude" packages="com.baeldung" status="WARN">
<Appenders>
<xi:include href="log4j2-includes/console-appender_pattern-layout_colored.xml" />
<Console name="DockerConsoleLogger" target="SYSTEM_OUT">
<PatternLayout pattern="%pid %docker %container" />
</Console>
</Appenders>
<Loggers>
<Logger name="com.baeldung.logging.log4j2.plugins" level="INFO">
<AppenderRef ref="DockerConsoleLogger" />
</Logger>
</Loggers>
</Configuration>
5. Lookup Plugin
5.查询插件
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.
Lookup插件用于在Log4j 2配置文件中添加动态值。它们允许应用程序将运行时的值嵌入到配置文件的一些属性中。该值是通过在文件系统、数据库等各种来源的基于键的查找来添加的。
One such plugin is the DateLookupPlugin that allows replacing a date pattern with the current system date of the application:
其中一个插件是DateLookupPlugin,它允许用应用程序的当前系统日期替换一个日期模式。
<RollingFile name="Rolling-File" fileName="${filename}"
filePattern="target/rolling1/test1-$${date:MM-dd-yyyy}.%i.log.gz">
<PatternLayout>
<pattern>%d %p %c{1.} [%t] %m%n</pattern>
</PatternLayout>
<SizeBasedTriggeringPolicy size="500" />
</RollingFile>
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.
在这个样本配置文件中,RollingFileAppender使用了一个date查询,输出将是MM-dd-yyyy格式。因此,Log4j 2将日志写到一个带有日期后缀的输出文件。
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:
与其他插件类似,Log4j 2为查找提供了很多来源。此外,如果需要一个新的来源,它可以很容易地实现自定义查找。
@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:
所以KafkaLookup将通过查询一个Kafka主题来解决这个值。我们现在将从配置中传递话题名称。
<RollingFile name="Rolling-File" fileName="${filename}"
filePattern="target/rolling1/test1-$${kafka:topic-1}.%i.log.gz">
<PatternLayout>
<pattern>%d %p %c{1.} [%t] %m%n</pattern>
</PatternLayout>
<SizeBasedTriggeringPolicy size="500" />
</RollingFile>
We replaced the date lookup in our earlier example with Kafka lookup that will query topic-1.
我们用Kafka查询代替了前面例子中的date查询,将查询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.
由于Log4j 2只调用查找插件的默认构造函数,我们没有像早期插件那样实现@PluginFactory。
6. Plugin Discovery
6.发现插件
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.
最后,让我们了解一下Log4j 2是如何发现一个应用程序中的插件的。正如我们在上面的例子中看到的,我们给每个插件一个唯一的名字。这个名字就像一个密钥,Log4j 2将其解析为一个插件类。
There’s a specific order in which Log4j 2 performs a lookup to resolve a plugin class:
Log4j 2执行查询以解决一个插件类,有一个特定的顺序。
- 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
- Similar Log4j2Plugins.dat file from the OSGi bundles
- A comma-separated package list in the log4j.plugin.packages system property
- In programmatic Log4j 2 configuration, we can call PluginManager.addPackages() method to add a list of package names
- 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.
作为前提条件,必须启用注解处理,以允许Log4j 2通过@Plugin注解中给出的名称来解析插件。
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.
由于Log4j 2使用名称来查找插件,上述顺序变得很重要。例如,如果我们有两个名字相同的插件,Log4j 2将首先发现被解析的插件。因此,如果我们需要在Log4j 2中扩展一个现有的插件,我们必须将该插件打包在一个单独的jar中,并将其放在log4j2-core.jar之前。
7. Conclusion
7.结语
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.
在这篇文章中,我们研究了Log4j 2中的插件的大体类别。我们讨论了即使有一个详尽的现有插件列表,我们也可能需要为某些用例实现自定义插件。
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.
后来,我们看了一些有用插件的自定义实现。此外,我们看到Log4j 2如何允许我们为这些插件命名,并随后在配置文件中使用这个插件名称。最后,我们讨论了Log4j 2如何根据这个名字来解析插件。
As always, all examples are available over on GitHub.
一如既往,所有的例子都可以在GitHub上找到。