Lookups 提供了一种在任意位置向Log4j配置添加值的方法。 它们是实现StrLookup接口的特定类型的插件。

在之前我们也稍微知道了Log4j中的属性替换的用法。使用${name},使用特定的前后缀标记某个字段表示将要被替换。

我们先看一下 StrSubstitutor这个类的注释内容:

In addition to this usage pattern there are some static convenience methods that cover the most common use cases. These methods can be used without the need of manually creating an instance. However if multiple replace operations are to be performed, creating and reusing an instance of this class will be more efficient.

Variable replacement works in a recursive way. Thus, if a variable value contains a variable then that variable will also be replaced. Cyclic replacements are detected and will cause an exception to be thrown.

以上可知变量的替换是以递归的方式进行,入过变量包含变量,则该变量也会变替换,如果检测到循环替换那么会报异常!但是这里我不是很懂如何让替换进入一个死循环【mark】

在配置初始化的时候,Log4j会替换这些属性,但是我们可能会遇到一种情况,我的替换的字段是一个变量,例如会随着时间变化或者随用户的变化而变化,那么我们就希望能够在运行过程中进行替换插值,所以需要让他能够保持住${name}的格式,度过初始化后在运行时再进行替换。

To achieve this effect there are two possibilities: Either set a different prefix and suffix for variables which do not conflict with the result text you want to produce. The other possibility is to use the escape character, by default '$'. If this character is placed before a variable reference, this reference is ignored and won't be replaced. For example:

注释里给出了两种方式:1.为变量设置不同的前缀和后缀,这些变量与要生成的结果文本不冲突;2.是使用转义字符,默认情况下为'$'。 如果将此字符放在变量引用之前,则该引用将被忽略并且不会被替换。

例如:

<?xml version="1.0" encoding="UTF-8"?>
<Configuration status="debug" name="RoutingTest" packages="org.apache.logging.log4j.test">
  <Properties>
    <Property name="filename">target/rolling1/rollingtest-$${sd:type}.log</Property>
  </Properties>
  <ThresholdFilter level="debug"/>
 
  <Appenders>
    <Console name="STDOUT">
      <PatternLayout pattern="%m%n"/>
      <ThresholdFilter level="debug"/>
    </Console>
    <Routing name="Routing">
      <Routes pattern="$${sd:type}">
        <Route>
          <RollingFile name="Rolling-${sd:type}" fileName="${filename}"
                       filePattern="target/rolling1/test1-${sd:type}.%i.log.gz">
            <PatternLayout>
              <pattern>%d %p %c{1.} [%t] %m%n</pattern>
            </PatternLayout>
            <SizeBasedTriggeringPolicy size="500" />
          </RollingFile>
        </Route>
        <Route ref="STDOUT" key="Audit"/>
      </Routes>
    </Routing>
  </Appenders>
 
  <Loggers>
    <Logger name="EventLogger" level="info" additivity="false">
      <AppenderRef ref="Routing"/>
    </Logger>
 
    <Root level="error">
      <AppenderRef ref="STDOUT"/>
    </Root>
  </Loggers>
 
</Configuration>

因此,我们会在Log4j的配置文件中看到一些像 $${sd:type},也会看到${sd:type},首次处理配置文件时,仅删除第一个 $字符,在运行中进行第二次替换,其他的会在首次处理直接替换成相应的值。但是并非所有元素都支持运行时解析变量。 支持组件将在其文档中明确指出。例如RollingFileAppender`RollingRandomAccessFileAppenderfilePatternPatternLayoutpatternRoutepattern

另外要注意的有的替换项在处理xml配置文件的时候是不会被解析的,例如RollingFile appender,因为只有当整个RollingFile元素经由Route被匹配到并且执行的时候才会触发解析。那你可能疑惑为什么RollingFile的${filename}为啥外面的定义需要$$,其实二者是可以等效的。

如果是$当解析外面的properties内容时直接替换,如果是$$先被去掉一个$,然后在解析RollingFile Appender 时进行递归替换。

常用的Lookup

Log4j本身已经为我们定义一些常用的Lookup,打开Interpolator类我们可以看到这个插值器里定义了一个Map,下面是Map的部分Lookup内容以及引用的字段

strLookupMap.put("log4j", new Log4jLookup());
strLookupMap.put("sys", new SystemPropertiesLookup());
strLookupMap.put("env", new EnvironmentLookup());
strLookupMap.put("main", MainMapLookup.MAIN_SINGLETON);
strLookupMap.put("marker", new MarkerLookup());
strLookupMap.put("java", new JavaLookup());
strLookupMap.put("lower", new LowerLookup());
strLookupMap.put("upper", new UpperLookup());
strLookupMap.put("date", new DateLookup());
strLookupMap.put("ctx", new ContextMapLookup());

Context Map Lookup

ContextMapLookup允许应用程序将数据存储在Log4j ThreadContext Map中,然后在Log4j配置中检索值。 在下面的示例中,应用程序将使用键“ loginId”将当前用户的登录ID存储在ThreadContext Map中。 在初始配置过程中,第一个“ $”将被删除。 PatternLayout支持使用Lookups进行插值,然后将解析每个事件的变量。这将等同于使用%X {loginId}

  <PatternLayout>
    <pattern>%d %p %c{1.} [%t] $${ctx:loginId} %m%n</pattern>
  </PatternLayout>

Date Lookup

DateLookup与其他lookup有些不同,因为它用于指定对SimpleDateFormat有效的日期格式字符串。类似%d{MM-dd-yyyy}。如下如果是使用%d{MM-dd-yyyy},时间会是配置轮转的时间点也就是0点整,但是使用date lookup时间会是实际发生轮转的时间。

<RollingFile name="Rolling-${map:type}" 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>

Java Lookup

JavaLookup允许使用java:前缀以方便的预格式化字符串检索Java环境信息。

<File name="Application" fileName="application.log">
  <PatternLayout header="${java:runtime} - ${java:vm} - ${java:os}">
    <Pattern>%d %m%n</Pattern>
  </PatternLayout>
</File>

System Properties Lookup

由于使用系统属性在应用程序内部和外部定义值是很常见的,因此很自然应该可以通过lookup来访问它们。 由于系统属性通常是在应用程序外部定义的,因此很常见的情况是:

<Appenders>
  <File name="ApplicationLog" fileName="${sys:logPath}/app.log"/>
</Appenders>

此lookup还支持默认值语法

<Appenders>
  <File name="ApplicationLog" fileName="${sys:logPath:-/var/logs}/app.log"/>
</Appenders>

小结

除了这些之外当然还有其他一些Lookup,甚至你还可以自定义Lookup。下面我们将举个例子来创造一个自定义的Lookup。

实战

我们在logback的系列中的实战中提到想要实现一个按线程打印的日志配置,这里我们同样以这个为例子实战。

在官方文档的FAQ中,官方给了一个方案,使用RoutingAppenderHow do I dynamically write to separate log files?

示例:

<?xml version="1.0" encoding="UTF-8"?>
<Configuration status="OFF">
    <Appenders>
        <Routing name="Routing">
            <Routes pattern="$${ctx:ROUTINGKEY}">
                <!-- 如果 ThreadContext 的 ROUTINGKEY key的值为special 将会走到这个路由中-->
                <Route key="special">
                    <RollingFile name="Rolling-${ctx:ROUTINGKEY}" fileName="logs/special-${ctx:ROUTINGKEY}.log"
                                 filePattern="./logs/${date:yyyy-MM}/${ctx:ROUTINGKEY}-special-%d{yyyy-MM-dd}-%i.log.gz">
                        <PatternLayout>
                            <Pattern>%d{ISO8601} [%t] %p %c{3} - %m%n</Pattern>
                        </PatternLayout>
                        <Policies>
                            <TimeBasedTriggeringPolicy interval="6" modulate="true"/>
                            <SizeBasedTriggeringPolicy size="10 MB"/>
                        </Policies>
                    </RollingFile>
                </Route>
                <!-- ThreadContext 的 ROUTINGKEY key不存在 ,将会在replace过程中返回原本的值,即 pattern的值 $${ctx:ROUTINGKEY}-->
                <Route key="$${ctx:ROUTINGKEY}">
                    <RollingFile name="Rolling-default" fileName="logs/default.log"
                                 filePattern="./logs/${date:yyyy-MM}/default-%d{yyyy-MM-dd}-%i.log.gz">
                        <PatternLayout>
                            <pattern>%d{ISO8601} [%t] %p %c{3} - %m%n</pattern>
                        </PatternLayout>
                        <Policies>
                            <TimeBasedTriggeringPolicy interval="6" modulate="true"/>
                            <SizeBasedTriggeringPolicy size="10 MB"/>
                        </Policies>
                    </RollingFile>
                </Route>
                <!-- 默认路由,有且只能有一个,这个路由用于输出不同的线程日志 -->
                <Route>
                    <RollingFile name="Rolling-${ctx:ROUTINGKEY}" fileName="logs/other-${ctx:ROUTINGKEY}.log"
                                 filePattern="./logs/${date:yyyy-MM}/${ctx:ROUTINGKEY}-other-%d{yyyy-MM-dd}-%i.log.gz">
                        <PatternLayout>
                            <pattern>%d{ISO8601} [%t] %p %c{3} - %m%n</pattern>
                        </PatternLayout>
                        <Policies>
                            <TimeBasedTriggeringPolicy interval="6" modulate="true"/>
                            <SizeBasedTriggeringPolicy size="10 MB"/>
                        </Policies>
                    </RollingFile>
                </Route>
            </Routes>
        </Routing>
    </Appenders>
    <Loggers>
        <Root level="DEBUG" includeLocation="true">
            <AppenderRef ref="Routing"/>
        </Root>
    </Loggers>
</Configuration>

测试代码:

new Thread(() -> {
            ThreadContext.put("ROUTINGKEY", Thread.currentThread().getName());
            log.info("info");
            log.debug("debug");
            log.error("error");
            ThreadContext.remove("ROUTINGKEY");
        }).start();
        new Thread(() -> {
            ThreadContext.put("ROUTINGKEY", Thread.currentThread().getName());
            log.info("info");
            log.debug("debug");
            log.error("error");
            ThreadContext.remove("ROUTINGKEY");
        }).start();

        log.info("info");
        log.debug("debug");
        log.error("error");

输出3个文件:default.log,other-Thread-1.log,other-Thread-2.log

简单解读一下这个配置文件,RoutingAppender通过评估LogEvents,然后将其路由到下级Appender。 目标Appender可以是先前配置的Appender,可以通过其名称引用,也可以根据需要动态创建Appender。 应该在路由引用的任何Appender之后对其进行配置,以使其能够正常关闭。

您还可以使用脚本配置RoutingAppender:可以在启动附加程序时以及为日志事件选择路由时运行脚本。

这种方案虽然能达到目的,很显然会出现很多的冗余代码去赋值,清理结果。所以就有了方案2:

首先自定义一个Lookup,即一个替换属性集合

import org.apache.logging.log4j.core.LogEvent;
import org.apache.logging.log4j.core.config.plugins.Plugin;
import org.apache.logging.log4j.core.lookup.StrLookup;

/**
 * @author chenly
 * @create 2020-04-30 23:11
 */
@Plugin(name = "thread", category = StrLookup.CATEGORY)
public class ThreadLookup implements StrLookup {
    @Override
    public String lookup(String key) {
        return Thread.currentThread()
                .getName();
    }

    @Override
    public String lookup(LogEvent event, String key) {
        return event.getThreadName() == null ? this.lookup(key) : event.getThreadName();
    }
}

配置文件:

<?xml version="1.0" encoding="UTF-8"?>
<Configuration status="OFF">
    <Appenders>
        <Routing name="Routing">
            <Routes pattern="$${thread:threadName}">
                <Route>
                    <RollingFile name="logFile-${thread:threadName}"
                                 fileName="logs/concurrent-${thread:threadName}.log"
                                 filePattern="logs/concurrent-${thread:threadName}-%d{MM-dd-yyyy}-%i.log">
                        <PatternLayout pattern="%d %-5p [%t] %C{2} - %m%n"/>
                        <Policies>
                            <SizeBasedTriggeringPolicy size="50 MB"/>
                        </Policies>
                        <DefaultRolloverStrategy max="100"/>
                    </RollingFile>
                </Route>
            </Routes>
        </Routing>
        <Async name="async" bufferSize="1000" includeLocation="true">
            <AppenderRef ref="Routing"/>
        </Async>
    </Appenders>
    <Loggers>
        <Root level="info" includeLocation="true">
            <AppenderRef ref="async"/>
        </Root>
    </Loggers>
</Configuration>

测试代码:


        new Thread(() -> {
            log.info("info");
            log.debug("debug");

            log.error("error");
        }).start();
        new Thread(() -> {
            log.info("info");
            log.debug("debug");
            log.error("error");
        }).start();

输出:concurrent-Thread-1.log,concurrent-Thread-2.log两个文件。

这种方式简洁又优雅!

另:Plugin的name必须是小写,不能定义成包含大写

见:https://stackoverflow.com/questions/31342950/log4j2-lookup-plugin-strlookup-to-resolve-threadname-for-routing-rollinglogf

参考:

http://codepub.cn/2016/12/18/Log4j2-to-achieve-different-levels-of-different-threads-log-output-to-a-different-file/