Skip to content

Logback 手册 - 第七章:过滤器

🏷️ Logback 手册


来源:https://logback.qos.ch/manual/filters.html
作者:Ceki Gülcü,Sébastien Pennec,Carl Harris
版权所有 © 2000-2022 QOS.ch Sarl

本文档采用 Creative Commons Attribution-​NonCommercial-SéShareAlike 2.5 许可证


Have lots of ideas and throw away the bad ones. You aren't going to have good ideas unless you have lots of ideas and some sort of principle of selection.

拥有许多想法,并且舍弃掉糟糕的想法。除非你有大量的想法和某种选择原则,否则你不会有好点子。

—LINUS PAULING


在前几章中,我们介绍了 logback-classic 的核心——基本选择规则。在本章中,将介绍额外的过滤器方法。

Logback 过滤器基于三值逻辑,可以组合或链式连接起来,形成任意复杂的过滤策略。它们在很大程度上受到 Linux 的 iptables 的启发。

要运行本章中的示例,您需要确保类路径上存在某些 jar 文件。请参考 设置页面 获取详细信息。

在 logback-classic 中

Logback-classic 提供了两种类型的过滤器,常规过滤器和 Turbo 过滤器。

常规过滤器

常规 logback-classic 过滤器扩展了 Filter 抽象类,该类基本上由一个以 ILoggingEvent 实例为参数的 decide() 方法组成。

过滤器按顺序组织为一个有序列表,并且基于三值逻辑。每个过滤器的 decide(ILoggingEvent event) 方法按顺序调用。该方法返回 FilterReply 枚举值之一,即 DENYNEUTRALACCEPT 中的一个。如果 decide() 返回的值是 DENY,则日志事件会立即被丢弃,不再咨询剩余的过滤器。如果返回值是 NEUTRAL,则继续咨询列表中的下一个过滤器。如果没有更多的过滤器需要咨询,则正常处理日志事件。如果返回值是 ACCEPT,则立即处理日志事件,跳过剩余过滤器的调用。

在 logback-classic 中,过滤器可以添加到 Appender 实例中。通过向 appender 添加一个或多个过滤器,您可以根据任意条件对事件进行过滤,例如日志消息的内容、MDC 的内容、时间等日志事件的任何部分。

实现自己的过滤器

创建自己的过滤器很简单。您只需要扩展 Filter 抽象类并实现 decide() 方法即可。

下面是一个示例,展示了 SampleFilter 类。它的 decide 方法对包含字符串 "sample" 的日志事件返回 ACCEPT。对于其他事件,返回值为 NEUTRAL

示例:基本自定义过滤器(logback-examples/src/main/java/chapters/filters/SampleFilter.java

java
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;
    }
  }
}

下面的配置文件将 SampleFilter 添加到了一个 ConsoleAppender

示例:SampleFilter 配置(logback-examples/src/main/resources/chapters/filters/SampleFilterConfig.xml)

借助 Joran,logback 的配置框架,可以轻松地指定属性或子组件给过滤器。在过滤器类中添加相应的 setter 方法后,在名为该属性的 XML 元素内指定属性的值,嵌套在 <filter> 元素中即可。

通常,所需的过滤器逻辑由两个正交部分组成,即匹配 / 不匹配测试和根据匹配 / 不匹配进行响应。例如,对于给定的测试,比如消息等于 "foobar",一个过滤器在匹配时可能会响应 ACCEPT,在不匹配时响应 NEUTRAL;另一个过滤器在匹配时可能会响应 NEUTRAL,在不匹配时响应 DENY

注意到这种正交性,logback 提供了 AbstractMatcherFilter 类,它提供了在匹配和不匹配时指定适当响应的有用骨架,通过两个名为 OnMatchOnMismatch 的属性。logback 中的大多数常规过滤器都是从 AbstractMatcherFilter 派生的。

LevelFilter

LevelFilter 根据精确级别匹配过滤事件。如果事件级别等于配置的级别,则根据 onMatchonMismatch 属性的配置,过滤器将接受或拒绝事件。以下是一个示例配置文件。

ThresholdFilter

ThresholdFilter 过滤低于指定阈值的事件。对于大于或等于阈值的级别的事件,在调用其 decide() 方法时,ThresholdFilter 将会响应 NEUTRAL。然而,级别低于阈值的事件将被拒绝。以下是一个示例配置文件。

EvaluatorFilter

EvaluatorFilter 是一个封装了 EventEvaluator 的通用过滤器。如其名称所示,EventEvaluator 用于评估给定事件是否满足指定的条件。在匹配和不匹配时,托管 EvaluatorFilter 将分别返回由 onMatchonMismatch 属性指定的值。

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

JaninoEventEvaluator

Logback-classic 还提供了另一个称为 JaninoEventEvaluator 的具体 EventEvaluator 实现,它采用一个任意 Java 语言块作为评估条件并返回布尔值。我们将这样的 Java 语言布尔表达式称为“评估表达式”。评估表达式在事件过滤方面具有很大的灵活性。 JaninoEventEvaluator 需要使用 Janino 库. 请参阅 相应部分 的设置文档。

评估表达式会在解释配置文件时即时编译。作为用户,您无需担心实际的管道流程。但是,您有责任确保 Java 语言表达式返回布尔值,即其评估为 truefalse

评估表达式在当前日志事件上进行评估。Logback-classic 自动将日志事件的各个字段导出为可从评估表达式访问的变量。这些导出变量的区分大小写的名称如下所示。

名称类型描述
eventLoggingEvent与日志请求关联的原始日志事件。所有以下变量也可从该事件中获得。例如,event.getMessage() 返回与下一个描述的 message 变量相同的字符串值。
messageString日志请求的原始消息。对于某个记录器 l,当您编写 l.info("Hello {}", name); 其中 name 被赋值为 "Alice" 时,则 "Hello {}" 是消息。
formattedMessageString日志请求中的格式化消息。对于某个记录器 l,当您编写 l.info("Hello {}", name); 其中 name 被赋值为 "Alice" 时,则 "Hello Alice" 是格式化消息。
loggerString记录器的名称。
loggerContextLoggerContextVO与日志事件所属的记录器上下文相关联的受限(值对象)视图。
levelint对应于级别的 int 值。为了帮助创建涉及级别的表达式,默认值 DEBUGINFOWARNERROR 也是可用的。因此,使用 level > INFO 是正确的表达式。
timeStamplong对应于日志事件创建的时间戳。
markerMarker与日志请求关联的 Marker 对象。请注意,Marker 可以为 null,您有责任检查此条件以避免 NullPointerException
mdcMap包含创建日志事件时所有 MDC 值的映射。可以使用以下表达式访问值:mdc.get("myKey")。从 logback-classic 版本 0.9.30 开始,'mdc' 变量永远不会为 null

由于 Janino 不支持泛型,因此 java.util.Map 类型不带参数。它遵循 mdc.get() 返回的类型为 Object 而不是 String。要在返回的值上调用 String 方法,必须将其转换为 String。例如,((String) mdc.get("k")).contains("val")
throwablejava.lang.Throwable如果事件未关联异常,则 "throwable" 变量的值将为 null。不幸的是,"throwable" 不支持序列化。因此,在远程系统上,其值将始终为 null。对于独立于位置的表达式,请使用下面描述的 throwableProxy 变量。
throwableProxyIThrowableProxy与日志事件关联的异常的代理。如果事件未关联异常,则 "throwableProxy" 变量的值将为 null。与 "throwable" 不同,当事件关联异常时,"throwableProxy" 的值即使在远程系统上也将是非空的,即在序列化后也如此。

以下是一个具体的示例。

示例:基本事件评估器用法(logback-examples/src/main/resources/chapters/filters/basicEventEvaluator.xml)

上述配置文件中粗体部分将 EvaluatorFilter 添加到 ConsoleAppender 中。然后,将类型为 JaninoEventEvaluator 的评估器注入到 EvaluatorFilter 中。如果用户在 <evaluator> 元素中未指定 class 属性,Joran 会默认推断评估器的类型为 JaninoEventEvaluator。这是 Joran 在某些情况下隐式推断组件类型的少数几个场景之一。

expression 元素对应于前面讨论的评估表达式。表达式 return message.contains("billing"); 返回一个布尔值。请注意,message 变量是由 JaninoEventEvaluator 自动导出的。

由于将 OnMismatch 属性设置为 NEUTRAL,将 OnMatch 属性设置为 DENY,此评估器过滤器将丢弃所有消息中包含字符串 "billing" 的日志事件。

FilterEvents 应用程序发出了从 0 到 9 编号的 10 个日志记录请求。我们首先运行没有任何过滤器的 FilterEvents 类:

bash
java chapters.filters.FilterEvents src/main/java/chapters/filters/basicConfiguration.xml

所有的请求都会被显示出来,如下所示:

txt
0    [main] INFO  chapters.filters.FilterEvents - logging statement 0
0    [main] INFO  chapters.filters.FilterEvents - logging statement 1
0    [main] INFO  chapters.filters.FilterEvents - logging statement 2
0    [main] DEBUG chapters.filters.FilterEvents - logging statement 3
0    [main] INFO  chapters.filters.FilterEvents - logging statement 4
0    [main] INFO  chapters.filters.FilterEvents - logging statement 5
0    [main] ERROR chapters.filters.FilterEvents - billing statement 6
0    [main] INFO  chapters.filters.FilterEvents - logging statement 7
0    [main] INFO  chapters.filters.FilterEvents - logging statement 8
0    [main] INFO  chapters.filters.FilterEvents - logging statement 9

假设我们想要过滤掉包含字符串 "billing statement" 的消息,上面列出的 basicEventEvaluator.xml 配置文件正好可以实现这个目的。

使用 basicEventEvaluator.xml 运行:

bash
java chapters.filters.FilterEvents src/main/java/chapters/filters/basicEventEvaluator.xml

我们得到以下结果:

txt
0     [main] INFO  chapters.filters.FilterEvents - logging statement 0
0     [main] INFO  chapters.filters.FilterEvents - logging statement 1
0     [main] INFO  chapters.filters.FilterEvents - logging statement 2
0     [main] DEBUG chapters.filters.FilterEvents - logging statement 3
0     [main] INFO  chapters.filters.FilterEvents - logging statement 4
0     [main] INFO  chapters.filters.FilterEvents - logging statement 5
0     [main] INFO  chapters.filters.FilterEvents - logging statement 7
0     [main] INFO  chapters.filters.FilterEvents - logging statement 8
0     [main] INFO  chapters.filters.FilterEvents - logging statement 9

评估表达式可以是 Java 块。例如,以下是一个有效的表达式。

java
<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; <!-- & encoded as &amp; -->
        payee.contains("someSpecialValue") &amp;&amp;
        !message.contains("someSecret")) {
      return true;
    }

    return false;
  </expression>
</evaluator>

匹配器

虽然可以通过调用 String 类中的 matches() 方法进行模式匹配,但这会产生每次过滤器调用时都要编译全新 Pattern 对象的开销。为了消除这种开销,可以预定义一个或多个 Matcher 对象。一旦定义了匹配器,就可以在评估器表达式中通过名称重复引用它。

一个例子可以澄清这个观点:

示例:在事件评估器中定义匹配器(logback-examples/src/main/resources/chapters/filters/evaluatorWithMatcher.xml)

使用 evaluatorWithMatcher.xml 运行:

bash
java chapters.filters.FilterEvents src/main/java/chapters/filters/evaluatorWithMatcher.xml

我们得到的结果是:

txt
260   [main] INFO  chapters.filters.FilterEvents - logging statement 0
264   [main] INFO  chapters.filters.FilterEvents - logging statement 2
264   [main] INFO  chapters.filters.FilterEvents - logging statement 4
266   [main] ERROR chapters.filters.FilterEvents - billing statement 6
266   [main] INFO  chapters.filters.FilterEvents - logging statement 8

如果您需要定义其他匹配器,可以通过添加更多的 <matcher> 元素来实现。

TurboFilters

TurboFilter 对象都继承自 TurboFilter 抽象类。与常规过滤器一样,它们使用三值逻辑来返回对日志事件的评估结果。

总体上,它们的工作方式类似于前面提到的过滤器。但是,Filter 对象和 TurboFilter 对象之间有两个主要区别。

TurboFilter 对象与日志上下文相关联。因此,它们不仅在使用给定的 appender 时被调用,而且在每次发出日志请求时都会被调用。它们的范围比附加到 appender 的过滤器更广。

更重要的是,它们在创建 LoggingEvent 对象之前被调用。TurboFilter 对象不需要实例化一个日志事件来过滤日志请求。因此,TurboFilter 旨在在事件创建之前对日志事件进行高性能过滤。

实现自己的 TurboFilter

要创建自己的 TurboFilter 组件,只需扩展 TurboFilter 抽象类。与之前一样,当实现自定义过滤器对象时,开发自定义 TurboFilter 只需实现 decide() 方法。在下面的示例中,我们创建一个稍微复杂一些的过滤器:

示例:基本的自定义 TurboFilterlogback-examples/src/main/java/chapters/filters/SampleTurboFilter.java

java
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();
    }
  }
}

上面的 TurboFilter 接受包含特定标记的事件。如果找不到该标记,则过滤器将责任传递给链中的下一个过滤器。

为了更灵活,可以在配置文件中指定要测试的标记,因此有了 getter 和 setter 方法。我们还实现了 start() 方法,以检查在配置过程中是否已指定该选项。

下面是一个使用我们新创建的 TurboFilter 的示例配置。

示例:基本自定义 TurboFilter 配置(logback-examples/src/main/resources/chapters/filters/sampleTurboFilterConfig.xml)

Logback classic 提供了几个可立即使用的 TurboFilter 类。MDCFilter 检查 MDC 中是否存在给定值,而 DynamicThresholdFilter 允许基于 MDC 键 / 级别阈值关联进行过滤。另一方面,MarkerFilter 检查与日志请求关联的特定标记是否存在。

下面是一个使用 MDCFilterMarkerFilter 的示例配置。

示例:MDCFilterMarkerFilter 配置(logback-examples/src/main/resources/chapters/filters/turboFilters.xml)

您可以通过执行以下命令来查看此配置:

bash
java chapters.filters.FilterEvents src/main/java/chapters/filters/turboFilters.xml

正如我们之前看到的那样,FilterEvents 应用程序发出了 10 个日志记录请求,编号从 0 到 9。除了请求 3 和 6 之外,所有的请求都是 INFO 级别的,与根记录器分配的级别相同。第 3 个请求以 DEBUG 级别发出,低于有效级别。然而,由于在第 3 个请求之前将 MDC 键 "username" 设置为 "sebastien",并在之后将其删除,MDCFilter 专门接受了该请求(仅仅是这个请求)。第 6 个请求以 ERROR 级别发出,并被标记为 "billing"。因此,它被 MarkerFilter(配置中的第二个 TurboFilter)拒绝。

因此,使用上述配置的 FilterEvents 应用程序的输出是:

txt
2006-12-04 15:17:22,859 [main] INFO  chapters.filters.FilterEvents - logging statement 0
2006-12-04 15:17:22,875 [main] INFO  chapters.filters.FilterEvents - logging statement 1
2006-12-04 15:17:22,875 [main] INFO  chapters.filters.FilterEvents - logging statement 2
2006-12-04 15:17:22,875 [main] DEBUG chapters.filters.FilterEvents - logging statement 3
2006-12-04 15:17:22,875 [main] INFO  chapters.filters.FilterEvents - logging statement 4
2006-12-04 15:17:22,875 [main] INFO  chapters.filters.FilterEvents - logging statement 5
2006-12-04 15:17:22,875 [main] INFO  chapters.filters.FilterEvents - logging statement 7
2006-12-04 15:17:22,875 [main] INFO  chapters.filters.FilterEvents - logging statement 8
2006-12-04 15:17:22,875 [main] INFO  chapters.filters.FilterEvents - logging statement 9

可以看到,第 3 个请求本应该不显示,如果我们只按照总体的 INFO 级别来处理的话,但是它仍然出现了,因为它符合第一个 TurboFilter 的要求并被接受了。

另一方面,第 6 个请求是一个 ERROR 级别的请求,本应该显示。但是它满足了第二个 TurboFilter,其 OnMatch 选项设置为 DENY。因此,第 6 个请求没有显示出来。

DuplicateMessageFilter

DuplicateMessageFilter 需要单独介绍一下。这个过滤器可以检测重复的消息,并在超过一定次数的重复后丢弃重复的消息。

为了检测重复,该过滤器使用消息之间的简单字符串相等性。它无法检测非常相似但只有少数字符不同的消息。例如,如果你写下:

java
logger.debug("Hello "+name0);
logger.debug("Hello "+name1);

假设 name0name1 有不同的值,那么这两个 "Hello" 消息将被认为是不相关的。根据用户需求,未来的版本可能会检查字符串的相似性,消除相似但不完全相同的重复消息。

请注意,在参数化日志记录的情况下,只考虑原始消息。例如,在下面的两个请求中,原始消息即 "Hello {}." 是相同的,因此被认为是重复的。

java
logger.debug("Hello {}.", name0);
logger.debug("Hello {}.", name1);

可以通过 AllowedRepetitions 属性指定允许的重复次数。例如,如果将该属性设置为 1,则相同消息的第 2 次及后续出现将被丢弃。类似地,如果将该属性设置为 2,则相同消息的第 3 次及后续出现将被丢弃。默认情况下,AllowedRepetitions 属性被设置为 5。

为了检测重复,该过滤器需要在内部缓存中保留对旧消息的引用。该缓存的大小由 CacheSize 属性确定。默认情况下,它设置为 100。

示例:DuplicateMessageFilter 的配置(logback-examples/src/main/resources/chapters/filters/duplicateMessage.xml)

因此,使用 duplicateMessage.xml 配置的 FilterEvents 应用程序的输出是:

txt
2008-12-19 15:04:26,156 [main] INFO  chapters.filters.FilterEvents - logging statement 0
2008-12-19 15:04:26,156 [main] INFO  chapters.filters.FilterEvents - logging statement 1
2008-12-19 15:04:26,156 [main] INFO  chapters.filters.FilterEvents - logging statement 2
2008-12-19 15:04:26,156 [main] INFO  chapters.filters.FilterEvents - logging statement 4
2008-12-19 15:04:26,156 [main] INFO  chapters.filters.FilterEvents - logging statement 5
2008-12-19 15:04:26,171 [main] ERROR chapters.filters.FilterEvents - billing statement 6

"logging statement 0" 是消息 "logging statement {}" 的第一次出现。"logging statement 1" 是第一次重复,"logging statement 2" 是第二次重复。有趣的是,级别为 DEBUG 的 "logging statement 3" 是第三次重复,尽管它稍后会被基本选择规则(basic selection rule)丢弃。这可以解释为 Turbo Filter 在其他类型的过滤器之前调用,包括基本选择规则。因此,DuplicateMessageFilter"logging statement 3" 视为重复,而忽视了它将在处理链中进一步被丢弃的事实。"logging statement 4" 是第四次重复,"logging statement 5" 是第五次重复。第 6 条及以后的语句被丢弃,因为默认情况下只允许 5 次重复。

在 logback-access 中

logback-access 提供了大部分 logback-classic 可用的功能。特别地,Filter 对象可用,并且以与 logback-classic 相同的方式工作,但有一个显著区别。logback-access 过滤器不是作用于 LoggingEvent 实例,而是作用于 AccessEvent 实例。目前,logback-access 附带了一些下面描述的有限数量的过滤器。如果您想提出其他过滤器,请联系 logback-dev 邮件列表。

CountingFilter

通过 CountingFilter 类,logback-access 可以提供关于访问 web 服务器的统计数据。在初始化时,CountingFilter 会将自己注册为 MBean 到平台的 JMX 服务器上。然后,您可以通过 jconsole 应用程序查询该 MBean 获取统计数据,例如按分钟、小时、天、周或月的平均值。还可以获取前一周、前一天、前一小时或前一个月的计数,以及总计数。

以下是 logback-access.xml 配置文件声明了一个 CountingFilter

您可以通过 jconsole 应用程序在平台的 JMX 服务器上检查 CountingFilter 维护的各种统计数据。

通过 jconsole 查看 CountingFilter

EvaluatorFilter

EvaluatorFilter 是一个封装 EventEvaluator 的通用过滤器。顾名思义,EventEvaluator 评估给定事件是否满足给定的条件。在匹配和不匹配时,托管的 EvaluatorFilter 将分别返回 onMatchonMismatch 属性指定的值。请注意,EvaluatorFilter 以前已在 logback-classic 的上下文中讨论过(见上文)。本文大部分内容都是对先前讨论的重复。

请注意,EventEvaluator 是一个抽象类。您可以通过继承 EventEvaluator 来实现自己的事件评估逻辑。logback-access 附带了一个具体的实现,名为 JaninoEventEvaluator,它采用任意的 Java 语言布尔表达式作为评估条件。我们将这样的 Java 语言块称为“评估表达式”。评估表达式在事件过滤中提供了很大的灵活性。JaninoEventEvaluator 需要 Janino 库。请参阅设置文档的 相应部分

评估表达式在解析配置文件时动态编译。作为用户,您不需要担心实际的实现细节。但是,您有责任确保 Java 语言表达式返回一个布尔值,即它评估为 truefalse

评估表达式在当前访问事件上进行评估。logback-access 会自动将当前的 AccessEvent 实例导出到变量名为 event 的变量下。您可以使用 event 变量读取与 HTTP 请求以及 HTTP 响应相关的各种数据。请参考 AccessEvent 类源代码 获取确切的列表。

下一个 logback-access 配置文件示例演示了基于 404(未找到) HTTP 响应码的过滤。每个导致 404 错误的请求将打印在控制台上。

示例:访问评估器(logback-examples/src/main/resources/chapters/filters/accessEventEvaluator.xml)

xml
<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 错误的请求,但排除了请求 CSS 文件的请求。

示例 6.10:访问评估器(logback-examples/src/main/resources/chapters/filters/accessEventEvaluator2.xml)

xml
<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)
           &amp;&amp;  <!-- 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>