在前面我们说过
basic selection rule,这一节我们来讲另一个附加的过滤方法。
Logback filters 可以通过串链方式组成一个复杂过滤规则,类似 linux 系统的 iptables 防火墙。
在Logback-classic中提供两个类型的 filters , 一种是 regular filters , 另一种是 turbo filter。
regular filters 是与appender 绑定的, 而turbo filter是与与logger context(logger 上下文)绑定的,区别就是,turbo filter过滤所有logging request ,而regular filter只过滤某个appender的logging request。
Regular filters
在 logback-classic中,filters 继承
Filter 抽象类,Filter 抽象类有一个 decide()抽象方法,接收一个 ILoggingEvent 对象参数,而在 logback-access 中 则是 AccessEvent 对象。该方法返回一个enum类型 FilterReply。
值可以是
- DENY:如果方法返回DENY(拒绝),则跳出过滤链,而该 logging event 也会被抛弃。
- NRUTRAL:如果返回NRUTRAL(中立),则继续过滤链中的下一个过滤器。
- ACCEPT:如果返回ACCEPT(通过),则跳出过滤链
public abstract FilterReply decide(E event);
下面我们实现一个简单的 Filter
package chapters
.filters
;
import ch .qos .logback .classic .spi . ILoggingEvent ;
import ch .qos .logback .core .filter . Filter ;
import ch .qos .logback .core .spi . FilterReply ;
public class SampleFilter extends Filter < ILoggingEvent > {
@Override
public FilterReply decide ( ILoggingEvent event ) {
if ( event .getMessage ().contains ( "sample" )) {
return FilterReply .ACCEPT ;
} else {
return FilterReply .NEUTRAL ;
}
}
}
import ch .qos .logback .classic .spi . ILoggingEvent ;
import ch .qos .logback .core .filter . Filter ;
import ch .qos .logback .core .spi . FilterReply ;
public class SampleFilter extends Filter < ILoggingEvent > {
@Override
public FilterReply decide ( ILoggingEvent event ) {
if ( event .getMessage ().contains ( "sample" )) {
return FilterReply .ACCEPT ;
} else {
return FilterReply .NEUTRAL ;
}
}
}
然后配置使用自己的Filter
<configuration>
<appender name = "STDOUT" class = "ch.qos.logback.core.ConsoleAppender" >
<filter class="chapters.filters.SampleFilter" />
<encoder>
<pattern>
%-4relative [%thread] %-5level %logger - %msg%n
</pattern>
</encoder>
</appender>
<root>
<appender-ref ref = "STDOUT" />
</root>
</configuration>
<appender name = "STDOUT" class = "ch.qos.logback.core.ConsoleAppender" >
<filter class="chapters.filters.SampleFilter" />
<encoder>
<pattern>
%-4relative [%thread] %-5level %logger - %msg%n
</pattern>
</encoder>
</appender>
<root>
<appender-ref ref = "STDOUT" />
</root>
</configuration>
上面的例子中,我们的filter类继承了Filter抽象类,但实际上,我们都会继承
AbstractMatcherFilter ,AbstractMatcherFilter继承自Filter,并提供了两个属性,OnMatch 和 OnMismatch。
下面我们看一下 Logback-classic中封装的几个 regular Filter
LevelFilter
LevelFilter 的过滤是基于 logging event 的 level ,如果等于配置的level,则过滤器通过,否则拒绝。下面是它的配置
<configuration>
<appender name = "CONSOLE" class = "ch.qos.logback.core.ConsoleAppender" >
<filter class="ch.qos.logback.classic.filter.LevelFilter">
<level>INFO</level>
<onMatch>ACCEPT</onMatch>
<onMismatch>DENY</onMismatch>
</filter>
<encoder>
<pattern>
%-4relative [%thread] %-5level %logger{30} - %msg%n
</pattern>
</encoder>
</appender>
<root level = "DEBUG" >
<appender-ref ref = "CONSOLE" />
</root>
</configuration>
<appender name = "CONSOLE" class = "ch.qos.logback.core.ConsoleAppender" >
<filter class="ch.qos.logback.classic.filter.LevelFilter">
<level>INFO</level>
<onMatch>ACCEPT</onMatch>
<onMismatch>DENY</onMismatch>
</filter>
<encoder>
<pattern>
%-4relative [%thread] %-5level %logger{30} - %msg%n
</pattern>
</encoder>
</appender>
<root level = "DEBUG" >
<appender-ref ref = "CONSOLE" />
</root>
</configuration>
ThresholdFilter
ThresholdFilter 其实跟我们之前说的
basic selection rule 很像,也是基于日志等级门槛过滤的。当logging event的 level 大于等于配置等级,才能通过过滤器。
<configuration>
<appender name = "CONSOLE"
class = "ch.qos.logback.core.ConsoleAppender" >
<!-- deny all events with a level below INFO, that is TRACE and DEBUG -->
<filter class="ch.qos.logback.classic.filter.ThresholdFilter">
<level>INFO</level>
</filter>
<encoder>
<pattern>
%-4relative [%thread] %-5level %logger{30} - %msg%n
</pattern>
</encoder>
</appender>
<root level = "DEBUG" >
<appender-ref ref = "CONSOLE" />
</root>
</configuration>
<appender name = "CONSOLE"
class = "ch.qos.logback.core.ConsoleAppender" >
<!-- deny all events with a level below INFO, that is TRACE and DEBUG -->
<filter class="ch.qos.logback.classic.filter.ThresholdFilter">
<level>INFO</level>
</filter>
<encoder>
<pattern>
%-4relative [%thread] %-5level %logger{30} - %msg%n
</pattern>
</encoder>
</appender>
<root level = "DEBUG" >
<appender-ref ref = "CONSOLE" />
</root>
</configuration>
EvaluatorFilter
EvaluatorFilter 是一个通用的 filter,它包含了一个
EventEvaluator
对象。EventEvaluator对象负责过滤条件的判断,过滤的结果由 onMatch 和 onMismatch这两个属性决定。
注意: EventEvaluator 是一个抽象类。你也可以实现自己的 Evaluator
下面我们看几个常见的EventEvaluator
GEventEvaluator
GEventEvaluator
接收 Groovy 语言的条件表达式作为判断条件。Groovy evaluation expression 是目前最灵活的表达式。GEventEvaluator需要Groovy运行环境。表达式会在运行时,在解释配置文件的时候被编译。
使用maven 添加groovy包到classpath
<dependency>
<groupId>org.codehaus.groovy </groupId>
<artifactId>groovy-all </artifactId>
<version>2.4.0 </version>
</dependency>
<groupId>org.codehaus.groovy </groupId>
<artifactId>groovy-all </artifactId>
<version>2.4.0 </version>
</dependency>
在 evaluation expression 中,你可以通过 event 或者缩写 e ,访问 ILoggingEvent对象。例如你可以通过以下表达式判断 当前logging event的等级是否等于DEBUG :
event.level == DEBUG 或者
e.level == DEBUG ,如果你想通过大于等于,或小于等于这些操作,就需要将 level转换成int值,再判断。例如下面的例子:
<configuration>
<appender name = "STDOUT" class = "ch.qos.logback.core.ConsoleAppender" >
<filter class="ch.qos.logback.core.filter.EvaluatorFilter">
<evaluator class="ch.qos.logback.classic.boolex.GEventEvaluator">
<expression>
e.level.toInt() >= WARN.toInt() && <!-- Stands for && in XML -->
!(e.mdc?.get("req.userAgent") =~ /Googlebot|msnbot|Yahoo/ )
</expression>
</evaluator>
<OnMismatch>DENY</OnMismatch>
<OnMatch>NEUTRAL</OnMatch>
</filter>
<encoder>
<pattern>
%-4relative [%thread] %-5level %logger - %msg%n
</pattern>
</encoder>
</appender>
<root level = "DEBUG" >
<appender-ref ref = "STDOUT" />
</root>
</configuration>
<appender name = "STDOUT" class = "ch.qos.logback.core.ConsoleAppender" >
<filter class="ch.qos.logback.core.filter.EvaluatorFilter">
<evaluator class="ch.qos.logback.classic.boolex.GEventEvaluator">
<expression>
e.level.toInt() >= WARN.toInt() && <!-- Stands for && in XML -->
!(e.mdc?.get("req.userAgent") =~ /Googlebot|msnbot|Yahoo/ )
</expression>
</evaluator>
<OnMismatch>DENY</OnMismatch>
<OnMatch>NEUTRAL</OnMatch>
</filter>
<encoder>
<pattern>
%-4relative [%thread] %-5level %logger - %msg%n
</pattern>
</encoder>
</appender>
<root level = "DEBUG" >
<appender-ref ref = "STDOUT" />
</root>
</configuration>
上面的例子过滤掉 logging event 的level小于 warn 的,并且 mdc中 key为req.userAgent的值不是 Googlebot , msnbot, Yahoo 中的其中一个。
需要注意的是,因为mdc中可能不存在对应的键值对,所以可能为空,上面的例子就使用了安全引用符 "?.” 。
JaninoEventEvaluator
JaninoEventEvaluator 与
GEventEvaluator 恰恰相反,接收的是一个 java 的判断表达式作为判断条件。
JaninoEventEvaluator 依赖于 Janino library
<!-- https://mvnrepository.com/artifact/org.codehaus.janino/janino -->
<dependency>
<groupId>org.codehaus.janino</groupId>
<artifactId>janino</artifactId>
<version>3.0.0</version>
</dependency>
这两个Evaluator相比,GEventEvaluator使用会更加灵活,但
JaninoEventEvaluator判断速度会快得多。
Logback-classic会自动帮我们导出logging event的属性到evaluation 表达式中,所以我们可以直接使用以下属性判断:
Name | Type | Description |
---|---|---|
event | LoggingEvent | 原始的logging event 对象。你可以通过该对象获取以下属性。例如event.getMessage()相当于message |
message | String | logging request 的原始message。例如,当你编码 I.info(“hello {}”, name); 这时, message的值就是 “hello {}” |
formattedMessage | String | 格式化后的message。例如:当你编码 I.info(“hello {}”, name); name=“Alice”,则message的值就是 “hello Alice” |
logger | String | logger的名称 |
loggerContext | LoggerContextVO | logging event 属于的 LoggerContext 对象 |
level | int | logging event 的等级,注意:与GEventEvaluator不同,这里可以直接使用 level > INFO 的方式判断 日志等级,而在GEventEvaluator中需要先转换成int值 |
timeStamp | long | logging event 产生的时间 |
marker | Marker | logging request 的 Marker标签。需要注意:marker可以为空,所以你需要自己判断Marker是否为空,避免空指针异常。 |
mdc | Map | Map类型,包含了该logging event 的MDC值,可以通过:mdc.get(“key”)的方式获取,在logback-classic 0.9.30版本后,mdc永远不为空,但是需要注意,获取的是object对象,按你的编码强制转换类型,例如:((String) mdc.get("k")).contains("val") |
throwable | java.lang.Throwable | logging event的异常信息,如果没有异常关联,则这个值为null。注意,throwable 不支持序列化,所以在远程日志服务器中,该值为Null, 所以需要使用throwableProxy |
throwableProxy | IThrowableProxy | logging event exception 的代理。如果没有异常,则throwableProxy为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>
<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>
上面的例子中,我们使用了默认JaninoEventEvaluator,由于OnMatch 被设置成DENY,所以该过滤器会丢弃所有message包含”billing”的logging event。
当然,因为JaninoEventEvaluator的expression 接收的是一个java 代码块,只要求该代码块返回 boolean值就行。所以我们可以来个复杂的:
<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") && <!-- & encoded as & -->
payee.contains("someSpecialValue") &&
!message.contains("someSecret")) {
return true;
}
return false;
</expression>
</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") && <!-- & encoded as & -->
payee.contains("someSpecialValue") &&
!message.contains("someSecret")) {
return true;
}
return false;
</expression>
</evaluator>
如果你还需要用到正则表达式的话,你还能使用Matcher
Matchers
我们不推荐通过调用String类的matches()方法来匹配规则,因为这样每次都会重新创建一个新Pattern对象,浪费资源。我们推荐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>
<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>
现在我们来说。logback-classic中的第二类过滤器。
TurboFilters
TurboFilter 是一个抽象类,是所有TurboFilter的祖先。与regular filters 一样,他也是串链的逻辑。
其实,Trubo Filter 与 Regular Filter 非常相似,只是有两点主要的不同。
- TurboFilter对象是与 logging context绑定的。因此,它会处理所有的logging request,而不是单独的appender。过滤范围更广。
- 更重要的是,他们在LoggingEvent对象创建之前就已经调用了。因此TurboFilter 对象并不需要 logging event来过滤logging request。因此会有更好的性能表现。
实现自己的TurboFilter
package chapters
.filters
;
import org .slf4j . Marker ;
import org .slf4j . MarkerFactory ;
import ch .qos .logback .classic . Level ;
import ch .qos .logback .classic . Logger ;
import ch .qos .logback .classic .turbo . TurboFilter ;
import ch .qos .logback .core .spi . FilterReply ;
public class SampleTurboFilter extends TurboFilter {
String marker ;
Marker markerToAccept ;
@Override
public FilterReply decide ( Marker marker , Logger logger , Level level ,
String format , Object [] params , Throwable t ) {
if (!isStarted ()) {
return FilterReply .NEUTRAL ;
}
if ((markerToAccept .equals (marker ))) {
return FilterReply .ACCEPT ;
} else {
return FilterReply .NEUTRAL ;
}
}
public String getMarker () {
return marker ;
}
public void setMarker ( String markerStr ) {
this .marker = markerStr ;
}
@Override
public void start () {
if (marker != null && marker .trim ().length () > 0 ) {
markerToAccept = MarkerFactory .getMarker (marker );
super .start ();
}
}
}
import org .slf4j . Marker ;
import org .slf4j . MarkerFactory ;
import ch .qos .logback .classic . Level ;
import ch .qos .logback .classic . Logger ;
import ch .qos .logback .classic .turbo . TurboFilter ;
import ch .qos .logback .core .spi . FilterReply ;
public class SampleTurboFilter extends TurboFilter {
String marker ;
Marker markerToAccept ;
@Override
public FilterReply decide ( Marker marker , Logger logger , Level level ,
String format , Object [] params , Throwable t ) {
if (!isStarted ()) {
return FilterReply .NEUTRAL ;
}
if ((markerToAccept .equals (marker ))) {
return FilterReply .ACCEPT ;
} else {
return FilterReply .NEUTRAL ;
}
}
public String getMarker () {
return marker ;
}
public void setMarker ( String markerStr ) {
this .marker = markerStr ;
}
@Override
public void start () {
if (marker != null && marker .trim ().length () > 0 ) {
markerToAccept = MarkerFactory .getMarker (marker );
super .start ();
}
}
}
配置使用自己的Turbo Filter
<configuration>
<turboFilter class="chapters.filters.SampleTurboFilter">
<Marker>sample</Marker>
</turboFilter>
<appender name = "STDOUT" class = "ch.qos.logback.core.ConsoleAppender" >
<encoder>
<pattern>
%-4relative [%thread] %-5level %logger - %msg%n
</pattern>
</encoder>
</appender>
<root>
<appender-ref ref = "STDOUT" />
</root>
</configuration>
<turboFilter class="chapters.filters.SampleTurboFilter">
<Marker>sample</Marker>
</turboFilter>
<appender name = "STDOUT" class = "ch.qos.logback.core.ConsoleAppender" >
<encoder>
<pattern>
%-4relative [%thread] %-5level %logger - %msg%n
</pattern>
</encoder>
</appender>
<root>
<appender-ref ref = "STDOUT" />
</root>
</configuration>
Logback-classic 封装了几个常用的TurboFilter
MDCFilter : 通过MDC过滤
DynamicThresholdFilter :通过MDC 或 level 过滤
MarkerFilter :通过marker标签过滤
下面看个配置:
<configuration> <turboFilterclass="ch.qos.logback.classic.turbo.MDCFilter"> <MDCKey>username</MDCKey> <Value>sebastien</Value> <OnMatch>ACCEPT</OnMatch> </turboFilter> <turboFilterclass="ch.qos.logback.classic.turbo.MarkerFilter"> <Marker>billing</Marker> <OnMatch>DENY</OnMatch> </turboFilter> <appendername="console"class="ch.qos.logback.core.ConsoleAppender"> <encoder> <pattern>%date [%thread] %-5level %logger - %msg%n</pattern> </encoder> </appender> <rootlevel="INFO"> <appender-refref="console"/> </root> </configuration>
DuplicateMessageFilter
DuplicateMessageFilter
是用来过滤重复日志的。需要注意的是,它的判断方式如下
用例子说明:
logger.debug("Hello "+name0); logger.debug("Hello "+name1);
以上会被认为不重复
logger
.debug
(
"Hello {}."
, name0
);
logger
.debug
(
"Hello {}."
, name1
);
这样则会被认为是重复的。
我们可以通过设置
AllowedRepetitions的值来设置重复的阈值。默认值为5。
为了判断是否重复,
DuplicateMessageFilter
需要维持一个old message的引用在内部缓冲区中。这个缓冲区的大小由 CacheSise 属性决定,默认值是100。
看个例子
<configuration>
<appender name = "console" class = "ch.qos.logback.core.ConsoleAppender" >
<encoder>
<pattern>%date [%thread] %-5level %logger - %msg%n </pattern>
</encoder>
</appender>
<root level = "INFO" >
<appender-ref ref = "console" />
</root>
</configuration>
<turboFilter class="ch.qos.logback.classic.turbo.DuplicateMessageFilter">
<AllowedRepetitions>10</AllowedRepetitions>
<CacheSize>150</CacheSize>
</turboFilter>
<appender name = "console" class = "ch.qos.logback.core.ConsoleAppender" >
<encoder>
<pattern>%date [%thread] %-5level %logger - %msg%n </pattern>
</encoder>
</appender>
<root level = "INFO" >
<appender-ref ref = "console" />
</root>
</configuration>
In logback-access
与其他组件一样,logback-access也提供了与logback-classis 差不多的功能。不过logback-classic的event的类型
AccessEvent
在
logback-access中,只提供了少数的Filter
CountingFilter
通过 CountingFilter , logback-Access 提供数据统计的功能。初始化后,CountingFilter 就会将自己注册成一个MBean,在JMX server 平台上。你可以通过这个MBean访问访问已经统计的数据,例如每分钟,每小时,每天,每星期,每月经过web-server的数据。
看下官网的配置
<configuration>
<statusListener class = "ch.qos.logback.core.status.OnConsoleStatusListener" />
<filter class="ch.qos.logback.access.filter.CountingFilter">
<name>countingFilter</name>
</filter>
<appender name = "STDOUT" class = "ch.qos.logback.core.ConsoleAppender" >
<encoder>
<pattern>%h %l %u %t \"%r\" %s %b </pattern>
</encoder>
</appender>
<appender-ref ref = "STDOUT" />
</configuration>
<statusListener class = "ch.qos.logback.core.status.OnConsoleStatusListener" />
<filter class="ch.qos.logback.access.filter.CountingFilter">
<name>countingFilter</name>
</filter>
<appender name = "STDOUT" class = "ch.qos.logback.core.ConsoleAppender" >
<encoder>
<pattern>%h %l %u %t \"%r\" %s %b </pattern>
</encoder>
</appender>
<appender-ref ref = "STDOUT" />
</configuration>
你可以通过 jconsole 应用查看
EvaluatorFilter(上面已经说过,与logback-classic是同一个,若不指定EvaluatorFilter,则默认使用JaninoEventEvaluator,参考上面
)
logback-access 会自动导出当前AccessEvent对象的属性,你可以在expression中直接使用。详细属性,请查看
AccessEvent
class source code
下面看个例子:这个例子过滤 access event 的状态马不是404的日志
<configuration>
<statusListener class = "ch.qos.logback.core.status.OnConsoleStatusListener" />
<appender name = "STDOUT" class = "ch.qos.logback.core.ConsoleAppender" >
<filter class="ch.qos.logback.core.filter.EvaluatorFilter">
<evaluator>
<expression>event.getStatusCode() == 404</expression>
</evaluator>
<onMismatch>DENY</onMismatch>
</filter>
<encoder><pattern>%h %l %u %t %r %s %b </pattern></encoder>
</appender>
<appender-ref ref = "STDOUT" />
</configuration>
<statusListener class = "ch.qos.logback.core.status.OnConsoleStatusListener" />
<appender name = "STDOUT" class = "ch.qos.logback.core.ConsoleAppender" >
<filter class="ch.qos.logback.core.filter.EvaluatorFilter">
<evaluator>
<expression>event.getStatusCode() == 404</expression>
</evaluator>
<onMismatch>DENY</onMismatch>
</filter>
<encoder><pattern>%h %l %u %t %r %s %b </pattern></encoder>
</appender>
<appender-ref ref = "STDOUT" />
</configuration>
再来一个:这个例子过滤掉状态码不是404并且,uri包含.css的日志
<configuration>
<statusListener class = "ch.qos.logback.core.status.OnConsoleStatusListener" />
<appender name = "STDOUT" class = "ch.qos.logback.core.ConsoleAppender" >
<filter class = "ch.qos.logback.core.filter.EvaluatorFilter" >
<evaluator name = "Eval404" >
<expression>
(event.getStatusCode() == 404)
&& <!-- ampersand characters need to be escaped -->
!(event.getRequestURI().contains(".css"))
</expression>
</evaluator>
<onMismatch>DENY </onMismatch>
</filter>
<encoder><pattern>%h %l %u %t %r %s %b </pattern></encoder>
</appender>
<appender-ref ref = "STDOUT" />
</configuration>
<statusListener class = "ch.qos.logback.core.status.OnConsoleStatusListener" />
<appender name = "STDOUT" class = "ch.qos.logback.core.ConsoleAppender" >
<filter class = "ch.qos.logback.core.filter.EvaluatorFilter" >
<evaluator name = "Eval404" >
<expression>
(event.getStatusCode() == 404)
&& <!-- ampersand characters need to be escaped -->
!(event.getRequestURI().contains(".css"))
</expression>
</evaluator>
<onMismatch>DENY </onMismatch>
</filter>
<encoder><pattern>%h %l %u %t %r %s %b </pattern></encoder>
</appender>
<appender-ref ref = "STDOUT" />
</configuration>