需求: 将日志按照不同的模块和日志级别输入到不同的日志文件里.
实现方式1:
最初的想法是用 LoggerFactory.getLogger(logName),然后为不同的logName 定义不同的logger指向不同的FileAppender
缺点:由于Logger 的名字改变,不再能根据每个类的名字动态调整日志级别,对错误排查影响较大
实现方式2:
利用Log4j2的Lookup功能动态构建文件名 参考https://logging.apache.org/log4j/2.x/manual/lookups.html
缺点是这个只能在Logger 定义之前指定一个模块:如
static {
System.setProperty("module", "module1");
}
public static final Logger logger = LoggerFactory.getLogger(Module1.class);
没办法在运行时动态切换日志文件
实现方式3:
按利用log4j2 的路由功能 (RoutingAppender)可以根据上下文变量将日志动态路由到的不同的文件
具体配置如下:
<RollingFile name="RollingFilePre" fileName="test-module1.log" filePattern="test-module1.%d{yyyy-MM-dd}.%i.log" append="true">
<DynaJsonLayout compact="true" eventEol="true"/>
<Policies>
<SizeBasedTriggeringPolicy size="1048576"/>
<TimeBasedTriggeringPolicy interval="1" modulate="true"/>
</Policies>
<DefaultRolloverStrategy max="200"/>
</RollingFile>
<RollingFile name="RollingFileRule" fileName="test-module2.log" filePattern="test-module2.%d{yyyy-MM-dd}.%i.log" append="true">
<DynaJsonLayout compact="true" eventEol="true"/>
<Policies>
<SizeBasedTriggeringPolicy size="1048576"/>
<TimeBasedTriggeringPolicy interval="1" modulate="true"/>
</Policies>
<DefaultRolloverStrategy max="200"/>
</RollingFile>
<Routing name="Routing" >
<Routes pattern="${sys:module}">
<Route key="val1" ref="RollingFilePre">
</Route>
<Route key="val2" ref="RollingFileRule">
</Route>
</Routes>
</Routing>
这样可以在程序中随时调用 System.setProperty() 来切换日志文件, 也可以通过其他选项比如ThreadContext property 来动态切换
注意:如果需要程序中动态切换文件,route 和 异步日志 async 会有一定的冲突,切换时可能数据还在内存队列里,这样可能打不到正确的文件里
按日志级别分文件则是另外一种思路,利用ThresholdFilter 将日志同时输出到A,B两个Appender, 再根据日志级别过滤. 比如A只接收Error 级别的日志
B只接收Error以下级别的日志,奇怪的是Log4j2 没有把两者统一起来
<root level="debug">
<appender-ref ref="Console"></appender-ref>
<appender-ref ref="RoutingFileAsync"/>
</root>
<Async name="RoutingFileAsync" bufferSize="300000">
<ThresholdFilter level="ERROR" onMatch="ACCEPT" onMismatch="DENY"/>
<AppenderRef ref="Routing"/>
<ArrayBlockingQueue/>
</Async>
<Console name="Console" target="SYSTEM_OUT">
<ThresholdFilter level="ERROR" onMatch="DENY" onMismatch="ACCEPT"/>
<PatternLayout pattern="%d{HH:mm:ss.SSS} [%t] %-5level %logger{36} - %msg%n"/>
</Console>
LOGSTASH采集:
利用Log4j2 JsonLayout,程序中会有一个工具类将错误日志格式变成JSON的,类似于 {"errorCode":xxx, "errorMessage":xxx}
但是JsonLayout 会把日志报文统一放到message 属性中,这样在日志记录进入Elasticsearch 仍然无法直接被搜索,我们的期望是将
JSON消息报文中的属性展开成多个顶级属性而不是嵌套在message中
一种思路是在日志从KAFKA到Elasticsearch 的时候做一些转换,不确定Logstash 的mutate是否支持
另一种思路是定制一个自己的JsonLayout 重载toSerializable方法来自定义输出的格式和内容, 具体实现步骤如下
1.定义 DynaLogEvent 继承了log4j 原生的 LogEvent, 增加了一个Map 类型的 attributes 属性,如果原生Log4jEvent 中的Message是Json格式则就把解析出的键值对存到
attributes 中
2. 定义了 DynaJsonLayout 通过 Plugin Annotation 注册到Log4j2 中,同时log4j2.xml 中需要声明 <configuration packages="org.apache.logging.log4j.core.layout">
来指定扫描插件的路径 DynaJsonLayout 主要就是重载了 toSerializable 方法,将原生的 LogEvent 转化为 DynaLogEvent 并通过 json ObjectWriter 输出成json 格式
ObjectWriter 使用了定制的 DynaLog4jJsonObjectMapper, 通过工厂类DynaJacksonFactory来构建,DynaJsonLayout 初始化时将 DynaLog4jJsonObjectMapper
注入ObjectWriter 中
3. 实践中发现attributes 并不能被Log4jJsonObjectMapper正确的序列化 (注:Log4j2的Log4jJsonObjectMapper使用了大量jackson的高级技巧来定制LogEvent的序列化,
如Module, Mixin 等,大家有兴趣可以阅读下代码,这里不详细展开了), 所以为DynaLogEvent中的attributes属性添加了JsonIgnore Annotation 避免它被自动序列化,
并定制了DynaLogEventSerializer 来自定义序列化 attributes, DynaLogEventSerializer通过 DynaLog4jJsonModule 注册到 DynaLog4jJsonObjectMapper 中
源代码:https://github.com/wispershadow/myopensources/tree/master/jsonlogger
转载于:https://blog.51cto.com/shadowisper/1974934