1. 程式人生 > >Log4j2 - 動態生成Appender

Log4j2 - 動態生成Appender

功能需求

專案裡將User分成了各個區域(domain),這些domain有個標誌domainId,現在要求在列印日誌的時候,不僅將所有User的日誌都列印到日誌檔案logs/CNTCore.log中,還需要另外再列印到對應domain的日誌檔案logs/{domainId}/CNTCore.log

比如User A的domainId是RD2,那麼除了logs/CNTCore.log外,還需要將該User A的日誌額外列印到logs/RD2/CNTCore.log中。

實現思路

將所有User的日誌都列印到日誌檔案logs/CNTCore.log中,這個可以直接使用配置檔案log4j2.xml

來解決,一個簡單的配置如下:

<?xml version="1.0" encoding="UTF-8"?>
<configuration monitorInterval="30">

	<Appenders>
		<Console name="stdout" target="SYSTEM_OUT">
			<PatternLayout pattern="%-5p %m%n" />
			<ThresholdFilter level="debug" onMatch="ACCEPT" onMismatch="DENY" />
		</
Console
>
<RollingFile name="cntCorelog" immediateFlush="true" fileName="logs/CNTCore.log" filePattern="logs/CNTCore.log.%d{yyyy-MM-dd-a}.gz" append="true"> <PatternLayout> <pattern>%d{yyyy-MM-dd HH:mm:ss.SSS}:%p %t %X{TracingMsg} %c - %m%n</pattern> </PatternLayout
>
<Policies> <TimeBasedTriggeringPolicy modulate="true" interval="1" /> </Policies> </RollingFile> </Appenders> <Loggers> <Logger name="com.lewis" level="debug" additivity="true"> <AppenderRef ref="cntCorelog" /> </Logger> <Root level="error"> <AppenderRef ref="stdout" /> </Root> </Loggers> </configuration>

在上邊的配置中,配置了cntCorelog這個appender來生成對應的回滾日誌檔案,具體由com.lewis這個logger來使用該appender進行拼接日誌資訊。

至於另外再列印到對應domain的日誌檔案logs/{domainId}/CNTCore.log,這個可以通過程式碼來動態生成各個domain的appender,並交由com.lewis這個logger來進行拼接日誌。

程式碼的具體實現

專案的Log4j2依賴

<dependency>
	<groupId>org.apache.logging.log4j</groupId>
	<artifactId>log4j-core</artifactId>
	<version>2.11.1</version>
</dependency>

動態生成appender

public static void createDomainAppender(final String domainId){
    final LoggerContext ctx = (LoggerContext) LogManager.getContext(false);
    final org.apache.logging.log4j.core.config.Configuration config = ctx.getConfiguration();
    if (config.getAppender("domainCntCoreLog") != null) {
        return;
    }
    final PatternLayout layout = PatternLayout.newBuilder()
            .withCharset(Charset.forName("UTF-8"))
            .withConfiguration(config)
            .withPattern("%d %t %p %X{TracingMsg} %c - %m%n")
            .build();
    final TriggeringPolicy policy = TimeBasedTriggeringPolicy.newBuilder()
            .withModulate(true)
            .withInterval(1)
            .build();
    final Appender appender = RollingFileAppender.newBuilder()
            .withName("domainCntCoreLog")
            .withImmediateFlush(true)
            .withFileName("logs/" + domainId + "/CNTCore.log")
            .withFilePattern("logs/" + domainId + "/CNTCore.log.%d{yyyy-MM-dd-a}.gz")
            .withLayout(layout)
            .withPolicy(policy)
            .build();
    appender.start();
    config.addAppender(appender);
    final KeyValuePair[] pairs = {KeyValuePair.newBuilder().setKey("domainId").setValue(domainId).build()};
    final Filter filter = ThreadContextMapFilter.createFilter(pairs, null, Result.ACCEPT, Result.DENY);
    config.getLoggerConfig("com.lewis").addAppender(appender, Level.DEBUG, filter);
    ctx.updateLoggers(config);
}

這段程式碼動態生成一個名為omainCntCoreLog的RollingFileAppender,該appender交由com.lewis這個logger來使用,並將日誌資訊輸入到logs/{domainId}/CNTCore.log

該logger在使用omainCntCoreLog這個RollingFileAppender時還設定了一個過濾器ThreadContextMapFilter,這個Filter用來控制logger只能對指定了domainId的進行列印日誌。

ThreadContext是Log4j2用來存放執行緒資訊的,相當於Log4j 1.X中的MDC和NDC,MDC是map,NDC是stack。當每個User登入時,就將該User的domainId存放到ThreadContext中,當退出登入時就將該domainId從ThreadContext中移除。

假如有10個User登入了,一個User對應一個執行緒,每個執行緒都存放了User對應的domainId。在使用者登入時,呼叫上邊的方法來動態生成domain appender;假如有10個domainId,就會生成10個domain appender。

由於這10個domain appender都被add到同一個logger裡了,如果不通過ThreadContextMapFilter來控制,就會造成每個User的日誌資訊都會被輸入到所有domain appender裡去。

在載入配置檔案後拼接domain appender

需要注意的是,必須在讀取配置檔案後才能去動態生成appender或者其他的日誌物件,否則會被原本的配置檔案覆蓋掉。

public static void main(final String[] args) {
    ThreadContext.put("domainId", "RD2");
    final String domainId = "RD2";
    final LoggerContext context1 = (org.apache.logging.log4j.core.LoggerContext) LogManager.getContext(false);
    try {
        context1.setConfigLocation(Loader.getResource("log4j2.xml", null).toURI());
        createDomainAppender(domainId);
    } catch (final Exception e) {
        LogManager.getRootLogger().error("load log4j2 configuration error", e);
        ThreadContext.remove("domainId");
    }

}

上邊的程式碼簡單地動態生成了RD2 domain的appender,需要注意的是,如果啟用了Log4j2的動態載入配置檔案功能,那麼當配置檔案被改動後並被重新載入時,會導致原本動態生成的domain appender無效。

因為重新載入配置檔案會生成新的LoggerContext物件,這時候可能會丟失一部分日誌資訊到對應的domain日誌檔案裡。對於這個暫時沒找到很好的解決方法,目前只能是在每個User登入時去建立domain appender物件,如果已存在就不建立。

對ThreadContextMapFilter的補充

上邊通過程式碼動態生成了RollingFileAppender和ThreadContextMapFilter,下邊記錄下配置檔案裡的寫法:

<RollingFile name="domainCntCoreLog" immediateFlush="true" fileName="logs/RD2/CNTCore.log" filePattern="logs/RD2/CNTCore.log.%d{yyyy-MM-dd-a}.gz" append="true">
   <ThreadContextMapFilter onMatch="ACCEPT"
    onMismatch="DENY">
	   <KeyValuePair key="domainId" value="RD2" />
   </ThreadContextMapFilter>
   <PatternLayout   pattern="%d %t %p %X{TracingMsg} %c - %m%n" />
   <Policies>
      <TimeBasedTriggeringPolicy modulate="true" interval="1" />
   </Policies>
</RollingFile>

從上邊的配置就可以看出來短板了,只能配置死某個domainId的RollingFileAppender以及ThreadContextMapFilter,假如有10個domainId,就要手動配置十個對應的appender和Filter,很是繁瑣。

就算通過佔位符${ctx:domainId}的寫法來避免寫死,也只能生成某個domainId的appender:

<RollingFile name="domainCntCoreLog" immediateFlush="true" fileName="logs/${ctx:domainId}/CNTCore.log" filePattern="logs/${ctx:domainId}/CNTCore.log.%d{yyyy-MM-dd-a}.gz" append="true">
   <ThreadContextMapFilter onMatch="ACCEPT"
    onMismatch="DENY">
	   <KeyValuePair key="domainId" value="${ctx:domainId}" />
   </ThreadContextMapFilter>
   <PatternLayout   pattern="%d %t %p %X{TracingMsg} %c - %m%n" />
   <Policies>
      <TimeBasedTriggeringPolicy modulate="true" interval="1" />
   </Policies>
</RollingFile>

這種方法只能生成一個domain appender,此外如果啟用了動態載入配置檔案的功能,在掃描配置檔案是否改動時,還會報錯,原因是在RollingFileAppender的FileName和filePattern裡使用了佔位符。在另起執行緒掃描配置檔案時,該佔位符時取不到值的,於是就會報錯。

參考連結