功能需求
項目里將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(domainId + "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(domainId + "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里使用了占位符。在另起線程掃描配置文件時,該占位符時取不到值的,於是就會報錯。