赞
踩
前言
组内许多服务既有同步接口也有异步脚本,接口和脚本的日志都打印在同一个日志文件中,日志繁杂给排查问题带来不少的阻碍。
为了解决这个问题,同事提了个按照接口分类日志文件的技术需求,也就是一个同步接口对应一个日志文件,从而将日志区分开。
目前组内所有服务都是使用 logback 日志框架,笔者对这个需求产生了一定的兴趣,查找资料了解到了 logback 日志过滤器,因此有了本文,读者有兴趣的话也可以去官方文档:
https://logback.qos.ch/manual/filters.html
logback 提供两种类型的过滤器, 一种是常规过滤器(regular filters) ,另一种是全局过滤器(turbo filter)。常规过滤器与 appender 绑定, 全局过滤器与 logger context 绑定,二者的区别就是全局过滤器过滤所有 logging request ,而常规过滤器只过滤某个 appender 的 logging request
在 logback-classic 中常规过滤器继承
ch.qos.logback.core.filter.Filter 抽象类,该抽象类的 decide()抽象方法接收一个 ILoggingEvent 参数,返回一个FilterReply枚举值。枚举值标明了过滤器对当前日志事件的过滤情况,具体处理如下表
LevelFilter 的过滤是基于日志事件的级别,如果日志级别等于配置的 level,则过滤器通过,否则拒绝,其代码实现如下。另外ThresholdFilter也是基于日志等级门槛过滤的,只不过其逻辑是当日志级别大于等于配置等级才能通过过滤器,此处不再赘述
需注意源码中涉及的成员变量 this.level、this.onMatch以及this.onMismatch都是在过滤器初始化时根据配置的值自动注入的,代码中只要提供其 set 方法即可
- public class LevelFilter extends AbstractMatcherFilter<ILoggingEvent> {
- Level level;
-
- public LevelFilter() {
- }
-
- public FilterReply decide(ILoggingEvent event) {
- if (!this.isStarted()) {
- return FilterReply.NEUTRAL;
- } else {
- return event.getLevel().equals(this.level) ? this.onMatch : this.onMismatch;
- }
- }
-
- public void setLevel(Level level) {
- this.level = level;
- }
-
- public void start() {
- if (this.level != null) {
- super.start();
- }
-
- }
- }
以下为LevelFilter 配置示例,该配置需要关注的点如下:
- <configuration>
- <appender name="FILE_ALL" class="ch.qos.logback.core.rolling.RollingFileAppender">
- <filter class="ch.qos.logback.classic.filter.LevelFilter">
- <level>INFO</level>
- <onMatch>ACCEPT</onMatch>
- <onMismatch>DENY</onMismatch>
- </filter>
- <rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
- <maxHistory>90</maxHistory>
- <fileNamePattern>${BASE_PATH_ERROR}/%d{yyyy-MM-dd}.%i.log.gz</fileNamePattern>
- <timeBasedFileNamingAndTriggeringPolicy class="ch.qos.logback.core.rolling.SizeAndTimeBasedFNATP">
- <maxFileSize>200MB</maxFileSize>
- </timeBasedFileNamingAndTriggeringPolicy>
- </rollingPolicy>
- <encoder>
- <pattern> %d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{30} - %msg%n</pattern>
- </encoder>
- </appender>
-
- <root level="INFO">
- <appender-ref ref="FILE_ALL" />
- </root>
-
- </configuration>
EvaluatorFilter 是一个抽象类,它包含了一个 EventEvaluator 对象,这个对象是日志评估过滤器实现的关键。它会封装配置在< expression >标签中的过滤逻辑,在处理日志事件时负责过滤条件的判断
EventEvaluator主要有两个实现类,分别是 GEventEvaluator 和 JaninoEventEvaluator。GEventEvaluator 接收 Groovy 语言的条件表达式作为判断条件,JaninoEventEvaluator 接收一个 java 的判断表达式作为判断条件,logback 默认使用的是 JaninoEventEvaluator,其依赖于 Janino 相关库
使用 JaninoEventEvaluator评估器时 logback 会自动导出日志事件对象的属性到 evaluation 表达式中,所以可以直接使用以下属性
JaninoEventEvaluator 的使用配置如下所示,< expression >标签包裹的表达式即为日志过滤的判断逻辑。根据其源代码实现来看,如果未检测到表达式中的 return 字符串将直接在表达式首尾添上 return 和 分号
- <configuration>
- <appender name="FILE_ALL" class="ch.qos.logback.core.rolling.RollingFileAppender">
- <filter class="ch.qos.logback.core.filter.EvaluatorFilter">
- <evaluator> <!-- defaults to type ch.qos.logback.classic.boolex.JaninoEventEvaluator -->
- <expression>return message.contains("nathan");</expression>
- </evaluator>
- <OnMismatch>NEUTRAL</OnMismatch>
- <OnMatch>ACCEPT</OnMatch>
- </filter>
- <rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
- <maxHistory>90</maxHistory>
- <fileNamePattern>${BASE_PATH_ERROR}/%d{yyyy-MM-dd}.%i.log.gz</fileNamePattern>
- <timeBasedFileNamingAndTriggeringPolicy class="ch.qos.logback.core.rolling.SizeAndTimeBasedFNATP">
- <maxFileSize>200MB</maxFileSize>
- </timeBasedFileNamingAndTriggeringPolicy>
- </rollingPolicy>
- <encoder>
- <pattern> %d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{30} - %msg%n</pattern>
- </encoder>
- </appender>
-
- <root level="INFO">
- <appender-ref ref="FILE_ALL" />
- </root>
-
- </configuration>
在 logback-classic 中全局过滤器都继承自抽象类
ch.qos.logback.classic.turbo.TurboFilter ,其实 Trubo Filter 与 Regular Filter 只有两点主要的不同:
logback 提供了一些常用的 TurboFilter:
以下为全局过滤器使用的配置示例
- <configuration>
- <turboFilter class="ch.qos.logback.classic.turbo.DynamicThresholdFilter">
- <Key>username</Key>
- <DefaultThreshold>DEBUG</DefaultThreshold>
- <MDCValueLevelPair>
- <value>nathan</value>
- <level>DEBUG</level>
- </MDCValueLevelPair>
- </turboFilter>
-
- <turboFilter class="ch.qos.logback.classic.turbo.MDCFilter">
- <MDCKey>username</MDCKey>
- <Value>nathan</Value>
- <OnMatch>ACCEPT</OnMatch>
- </turboFilter>
-
- <turboFilter class="ch.qos.logback.classic.turbo.MarkerFilter">
- <Marker>nathan</Marker>
- <OnMatch>DENY</OnMatch>
- </turboFilter>
-
- <appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
- <encoder>
- <pattern>%date [%thread] %-5level %logger - %msg%n</pattern>
- </encoder>
- </appender>
-
- <root level="INFO">
- <appender-ref ref="STDOUT"/>
- </root>
-
- </configuration>
按照接口的名称分类日志,将接口方法执行期间的所有日志输出到指定到接口文件中,其需要解决的主要问题有两个:
对于问题1,首先想到的就是对特定的日志打上特定的标记,可以参考文章 Slf4j 中的 MDC ,借助 MDC 把接口名称以一个 key 存储下来,则可据此区分不同的接口的日志
对于问题2,本文介绍的 logback 日志过滤器便起到了作用。我们可以为每个接口重新配置一个 appender,其关键点如下:
- <configuration>
- <appender name="FILE_METHOD" class="ch.qos.logback.core.rolling.RollingFileAppender">
- <filter class="ch.qos.logback.core.filter.EvaluatorFilter">
- <evaluator> <!-- defaults to type ch.qos.logback.classic.boolex.JaninoEventEvaluator -->
- <expression>return ((String)mdc.get("key")).equals("method");</expression>
- </evaluator>
- <OnMismatch>NEUTRAL</OnMismatch>
- <OnMatch>ACCEPT</OnMatch>
- </filter>
- <rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
- <maxHistory>90</maxHistory>
- <fileNamePattern>${BASE_PATH_ERROR}/%d{yyyy-MM-dd}.%i.log.method.gz</fileNamePattern>
- <timeBasedFileNamingAndTriggeringPolicy class="ch.qos.logback.core.rolling.SizeAndTimeBasedFNATP">
- <maxFileSize>200MB</maxFileSize>
- </timeBasedFileNamingAndTriggeringPolicy>
- </rollingPolicy>
- <encoder>
- <pattern> %d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{30} - %msg%n</pattern>
- </encoder>
- </appender>
-
- <root level="INFO">
- <appender-ref ref="FILE_METHOD" />
- </root>
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。