简介

logback 过滤器基于三元逻辑,允许它们组装或者链接在一起组成一个任意复杂的过滤策略。它们在很大程度上受到 Linux iptables 的启发。

在 logback-classic 中,有两种类型的过滤器,regular 过滤器以及 turbo 过滤器。

Regular

reqular 过滤器继承自 Filter 这个抽象类。本质上它由一个单一的 decide() 方法组成,接收一个 ILoggingEvent 实例作为参数。

过滤器通过一个有序列表进行管理,并且基于三元逻辑。每个过滤器的 decide(ILoggingEvent event) 被依次调用。这个方法返回 FilterReply 枚举值中的一个, DENY, NEUTRAL 或者 ACCEPT。如果 decide() 方法返回 DENY,那么日志事件会被丢弃掉,并且不会考虑后续的过滤器。如果返回的值是 NEUTRAL,那么才会考虑后续的过滤器。如果没有其它的过滤器了,那么日志事件会被正常处理。如果返回值是 ACCEPT,那么会跳过剩下的过滤器而直接被处理。

在 logback-classic 中,过滤器可以被直接添加到 Appender 实例上。通过将一个或者多个过滤器添加到 appender 上,你可以通过任意标准来过滤日志事件。例如,日志消息的内容,MDC 的内容,时间,或者日志事件的其它部分。

自定义过滤器

创建一个自己的过滤器非常的简单。只需要继承 Filter 并且实现 decide() 方法就可以了。

自定一个过滤器,把日志内容包含“different”的不打印

/**
 * @author blog.unclezs.com
 * @date 2020/12/3 1:05 上午
 */
public class CustomRegularFilter extends Filter<ILoggingEvent> {

  @Override
  public FilterReply decide(ILoggingEvent event) {
    return event.getMessage().contains("different")?FilterReply.DENY:FilterReply.ACCEPT;
  }
}

/**
 * @author blog.unclezs.com
 * @since 2020/12/04 17:26
 */
public class FilterSample {
  public static void main(String[] args) {
    LoggerHelper.reconfigure("logback-filter.xml");
    Logger logger = LoggerFactory.getLogger(FilterSample.class);
    logger.info("different log");
    logger.info("same log");
  }
}
<configuration debug="false" scan="false" scanPeriod="1 second" packagingData="false">
    <property name="pattern"
              value="%red(%d{yyyy-MM-dd HH:mm:ss}) %green([%thread]) %highlight(%-5level) %boldMagenta(%logger) - %cyan(%msg%n)"/>
    <appender name="console" class="ch.qos.logback.core.ConsoleAppender">
        <filter class="com.unclezs.samples.log.slf4j.logback.filter.CustomRegularFilter"/>
        <encoder class="ch.qos.logback.classic.encoder.PatternLayoutEncoder">
            <pattern>${pattern}</pattern>
            <!--在头部打印出pattern-->
            <outputPatternAsHeader>true</outputPatternAsHeader>
        </encoder>
        <!--立即刷新到流-->
        <immediateFlush>true</immediateFlush>
    </appender>
    <logger name="com.unclezs.samples.log.slf4j.logback" level="info" additivity="false">
        <appender-ref ref="console"/>
    </logger>
    <root level="off">
    </root>
</configuration>

LevelFilter

LevelFilter 基于级别来过滤日志事件。如果事件的级别与配置的级别相等,过滤器会根据配置的 onMatch 与 onMismatch 属性,接受或者拒绝事件。如下是一个简单的示例:

<filter class="ch.qos.logback.classic.filter.LevelFilter">
      <level>INFO</level>
      <onMatch>ACCEPT</onMatch>
      <onMismatch>DENY</onMismatch>
</filter>

ThresholdFilter

ThresholdFilter 基于给定的临界值来过滤事件。如果事件的级别等于或高于给定的临界值,当调用 decide() 时,ThresholdFilter 将会返回 NEUTRAL。但是事件的级别低于临界值将会被拒绝。下面是一个简单的例子:

<filter class="ch.qos.logback.classic.filter.ThresholdFilter">
  <level>INFO</level>
</filter>

EvaluatorFilter

EvaluatorFilter 是一个通用的过滤器,它封装了一个 EventEvaluator。顾名思义,EventEvaluator 根据给定的标准来评估给定的事件是否符合标准。在 match 和 mismatch 的情况下,EvaluatorFilter 将会返回 onMatch 或 onMismatch 指定的值。

注意 EventEvaluator 是一个抽象类。你可以通过继承 EventEvaluator 来实现自己事件评估逻辑。

GEventEvaluator是EventEvaluator一个实现类,通过Groogy脚本判断是否需要过滤。

JaninoEventEvaluator

logback-classic 附带的另外一个 EventEvaluator 的具体实现名为 JaninoEventEvaluator,它接受任意返回布尔值的 Java 代码块作为评判标准。我们把这种 Java 布尔表达式称为 “评估表达式”。评估表达式在事件过滤中可以更加的灵活。JaninoEventEvaluator 需要 Janino 类库。请参见相关章节进行设置。跟 JaninoEventEvaluator 相比,GEventEvaluator 使用 Groovy 语言,使用起来非常方便。但是 JaninoEventEvaluator 将使用运行更快的等效表达式。

评估表达式在解析配置文件期间被动态编译。作为用户,不需要考虑实际的情况。但是,你需要确保你的 Java 表达式是有效的,保证它的评估结果为 true 或 false。

评估表达式对当前日志事件进行评估。logback-classic 自动导出日志事件的各种字段作为变量,为了可以从评估表达式访问。这些导出的变量是大小写敏感的,如下表所示:

名字 类型 描述
event LoggingEvent 日志请求的原始日志事件。下面所有的变量都来自这个日志事件。例如,event.getMessage() 返回的字符串跟下面的 message 变量返回的字符串一样。
message String 日志请求的原始信息。例如,对于 logger I,当你写的是 I.info(“Hello {}“, name); 时,name 的值被指定为 “Alice”,消息就为 “Hello {}“。
formattedMessage String 日志请求中格式化后的消息。例如,对于 logger I,当你写的是 I.info(“Hello {}“, name); 时,name 的值被指定为 “Alice”,格式化后的消息就为 “Hello Alice”。
logger String logger 的名字
loggerContext LoggerContextVO 日志事件属于 logger 上下文中哪个受限的视图 (值对象)
level int 事件级别对应的 int 值。用来创建包含级别的表达式。默认值是 DEBUG,INFO,WARN 以及 ERROR 也是有效的。所以 level > INFO 是有效的表达式。
timeStamp long 日志事件创建的时间
marker Marker 与日志请求相关的 Marker 对象。注意,marker 可能会为 null,因此你需要对这种情况进行检查,进而避免 NullPointerException。
mdc Map 创建日志事件时包含的所有的 MDC 值的一个映射。可以通过 mdc.get(“myKey”) 来获取 MDC 中对应的值。在 0.9.30 版本的 logback-classic,mdc 变量永远不会为 null。java.util.Map 类型是非参数化的,因为 Janino 不支持泛型。因此,mdc.get() 返回值的类型是 Object 而不是 String。但是可以将返回值强制转换为 String。例如, ((String) mdc.get(“k”)).contains(“val”)。
throwable java.lang.Throwable 如果日志事件没有相关的异常,那么变量 “throwable” 的值为 null。”throwable” 不可以被序列化。所以在远程服务器上,这个值永远为 null。想要使用与位置无关的表达式,可以使用下面的 throwableProxy。
throwableProxy IThrowableProxy 日志事件的异常代理。如果日志事件没有相关的异常,那么 throwableProxy 的值为 null。与 “throwable” 相反,即使在远程服务器上序列化之后,日志事件相关的异常也不会为 null。
<configuration>

  <appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
    <filter class="ch.qos.logback.core.filter.EvaluatorFilter">      
      <evaluator> <!-- defaults to type ch.qos.logback.classic.boolex.JaninoEventEvaluator -->
        <expression>return message.contains("billing");</expression>
      </evaluator>
      <OnMismatch>NEUTRAL</OnMismatch>
      <OnMatch>DENY</OnMatch>
    </filter>
    <encoder>
      <pattern>
        %-4relative [%thread] %-5level %logger - %msg%n
      </pattern>
    </encoder>
  </appender>

  <root level="INFO">
    <appender-ref ref="STDOUT" />
  </root>
</configuration>

上面的配置将 EvaluatorFilter 添加到 ConsoleAppender。一个类型为 JaninoEventEvaluator 的 evaluator 之后被注入到 EvaluatorFilter 中。<evaluator 在缺少 class 属性的情况下,Joran 会指定 evaluator 的默认类型为 JaninoEventEvaluator。这是少数几个需要 Joran 默认指定类型的组件。

expression 元素对应刚才讨论过的评估表达式。表达式 return message.contains(“billing”); 返回一个布尔值。message 变量会被 JaninoEventEvaluator 自动导出。

甚至可以是更复杂的表达式

<evaluator>
  <expression>
    if(logger.startsWith("org.apache.http"))
      return true;
    if(mdc == null || mdc.get("entity") == null)
      return false;
    String payee = (String) mdc.get("entity");
    if(logger.equals("org.apache.http.wire") &amp;&amp;
        payee.contains("someSpecialValue") &amp;&amp;
        !message.contains("someSecret")) {
      return true;
    }
    return false;
  </expression>
</evaluator>

Matchers

虽然可以通过调用 String 类的 matches() 方法来进行模式匹配,但是每次调用 filter 都需要耗费时间重新编译一个新的 Pattern 对象。为了消除这种影响,你可以预先定义一个或者多个 Matcher 对象。一旦定义了一个 matcher,就可以在评估表达式中重复使用了。


<configuration debug="true">
  <appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
    <filter class="ch.qos.logback.core.filter.EvaluatorFilter">
      <evaluator>        
        <matcher>
          <Name>odd</Name>
          <!-- filter out odd numbered statements -->
          <regex>statement [13579]</regex>
        </matcher>
        <expression>odd.matches(formattedMessage)</expression>
      </evaluator>
      <OnMismatch>NEUTRAL</OnMismatch>
      <OnMatch>DENY</OnMatch>
    </filter>
    <encoder>
      <pattern>%-4relative [%thread] %-5level %logger - %msg%n</pattern>
    </encoder>
  </appender>

  <root level="DEBUG">
    <appender-ref ref="STDOUT" />
  </root>
</configuration>

TurboFilters

TurboFilter 对象都继承 TurboFilter 抽象类。对于 regular 过滤器,它们使用三元逻辑来返回对日志事件的评估。

总之,它们跟之前提到的过滤工作原理差不多。主要的不同点在于 Filter 与 TurboFilter 对象。

TurboFilter 对象被绑定刚在 logger 上下文中。因此,在使用给定的 appender 以及每次发出的日志请求都会调用 TurboFilter 对象。因此,turbo 过滤器可以为日志事件提供高性能的过滤,即使是在事件被创建之前。

实现自己的 TurboFilter

想要创建自己的 TurboFilter 组件,只需要继承 TurboFilter 这个抽象类就可以了。跟之前的一样,想要实现定制的过滤器对象,开发自定义的 TurboFilter,只需要实现 decide() 方法就可以了。下一个例子,我们会创建一个稍微复杂一点的过滤器:

/**
 * @author blog.unclezs.com
 * @date 2020/12/4 10:32 下午
 */
public class CustomTurboFilter extends TurboFilter {
    @Override
    public FilterReply decide(Marker marker, Logger logger, Level level, String format, Object[] params, Throwable t) {
        if (t == null) {
            return FilterReply.DENY;
        }
        return FilterReply.ACCEPT;
    }
}
<configuration debug="false" scan="false" scanPeriod="1 second" packagingData="false">
    <property name="pattern"
              value="%red(%d{yyyy-MM-dd HH:mm:ss}) %green([%thread]) %highlight(%-5level) %boldMagenta(%logger) - %cyan(%msg%n)"/>
    <turboFilter class="com.unclezs.samples.log.slf4j.logback.filter.CustomTurboFilter"/>
    <appender name="console" class="ch.qos.logback.core.ConsoleAppender">
        <filter class="com.unclezs.samples.log.slf4j.logback.filter.CustomRegularFilter"/>
        <encoder class="ch.qos.logback.classic.encoder.PatternLayoutEncoder">
            <pattern>${pattern}</pattern>
            <!--在头部打印出pattern-->
            <outputPatternAsHeader>true</outputPatternAsHeader>
        </encoder>
        <!--立即刷新到流-->
        <immediateFlush>true</immediateFlush>
    </appender>
    <logger name="com.unclezs.samples.log.slf4j.logback" level="info" additivity="false">
        <appender-ref ref="console"/>
    </logger>
    <root level="off">
    </root>
</configuration>

其他

loback-classic 附带了几个 TurboFilter 类可以开箱即用。MDCFilter 用来检查给定的值在 MDC 中是否存在。DynamicThresholdFilter 根据 MDC key/level 相关的阀值来进行过滤。MarkerFilter 用来检查日志请求中指定的 marker 是否存在。

  • DuplicateMessageFilter
  • MarkerFilter
  • MDCFilter
  • DynamicThresholdFilter

评论

博客
分类
标签
归档
关于