第六章:Layouts

什么是 layout?

layout 是 logback 的组件,负责将日志事件转换为字符串。Layout 接口中的 format() 方法接受一个表示日志事件的对象 (任何类型) 并返回一个字符串。Layout 接口的概要如下:

public interface Layout<E> extends ContextAware, LifeCycle {

  String doLayout(E event);
  String getFileHeader();
  String getPresentationHeader();
  String getFileFooter();
  String getPresentationFooter();
  String getContentType();
}

这个接口相对简单,但是它可以满足大部分的格式化需求。

Logback-classic

logback-classic 仅仅用来处理 ch.qos.logback.classic.spi.ILoggingEvent 类型的日志事件。我们将在这个部分说明这个事实。

定制 Layout

让我们为 logback-classic 模块实现一个简单但是实用的功能,打印应用启动所耗费的时间,日志事件的级别,被综括号包裹的调用者线程,logger 名,破折号后面跟日志信息,以及新起一行。

类似下面的输出:

10489 DEBUG [main] com.marsupial.Pouch - Hello world.

下面是一种可能的实现:

Example: MySampleLayout.java

package chapters.layouts;

import ch.qos.logback.classic.spi.ILoggingEvent;
import ch.qos.logback.core.LayoutBase;

public class MySampleLayout extends LayoutBase<ILoggingEvent> {

  public String doLayout(ILoggingEvent event) {
    StringBuffer sbuf = new StringBuffer(128);
    sbuf.append(event.getTimeStamp() - event.getLoggingContextVO.getBirthTime());
    sbuf.append(" ");
    sbuf.append(event.getLevel());
    sbuf.append(" [");
    sbuf.append(event.getThreadName());
    sbuf.append("] ");
    sbuf.append(event.getLoggerName();
    sbuf.append(" - ");
    sbuf.append(event.getFormattedMessage());
    sbuf.append(CoreConstants.LINE_SEP);
    return sbuf.toString();
  }
}

MySampleLayout 继承自 LayoutBase。这个类管理所有 layout 实例的状态信息,例如:layout 是否启动或者停止,头部,尾部以及内容类型数据。它让开发者通过自己 Layout 集中在日志具体的格式化上。LayoutBase 类是通用的。在它的类声明上,MySampleLayout 继承 LayoutBase<ILoggingEvent>

在上面这个例子中,doLayout 方法忽略了日志事件中任何可能的异常。在实际应用中,你可能需要打印异常信息。

配置自定义的 layout

配置自定义的 layout 跟其它的组件一样的配置。根据之前提到的,FileAppender 及其子类期望一个 encoder。为了去满足这个需求,我们将一个包裹了我们自己定义的 MySampleLayoutLayoutWrappingEncoder 的实例传递给 FileAppender。下面是配置示例:

Example: sampleLayoutConfig.xml

<configuration>

  <appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
    <encoder class="ch.qos.logback.core.encoder.LayoutWrappingEncoder">
      <layout class="chapters.layouts.MySampleLayout" />
    </encoder>
  </appender>

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

chapters.layouts.SampleLogging 这个简单的应用通过第一个参数接收配置文件,然后打印了一个 debug 信息,接着打印了 error 信息。

logback-examples 文件夹下通过以下命令来运行:

java chapters.layouts.SampleLogging src/main/java/chapters/layouts/sampleLayoutConfig.xml

将会输出:

0 DEBUG [main] chapters.layouts.SampleLogging - Everything's going well
0 ERROR [main] chapters.layouts.SampleLogging - maybe not quite...

这种足够简单。读者应该会发现,在 MySampleLayout2.java 中,我们自定义的 layout 做了一点点的修改。正如本手册一直提到的,为 layout 或者其它 logback 的组件添加一个属性,跟为这个属性添加一个 set 方法一样简单。

MySampleLayout2 类包含了两个属性。第一个是可以将一个前缀添加到输出的日志中。第二个属性可以用来选择是否展示发送日志请求的线程名。

下面是 MySampleLayout2 类:

package chapters.layouts;

import ch.qos.logback.classic.spi.ILoggingEvent;
import ch.qos.logback.core.LayoutBase;

public class MySampleLayout2 extends LayoutBase<ILoggingEvent> {

  String prefix = null;
  boolean printThreadName = true;

  public void setPrefix(String prefix) {
    this.prefix = prefix;
  }

  public void setPrintThreadName(boolean printThreadName) {
    this.printThreadName = printThreadName;
  }

  public String doLayout(ILoggingEvent event) {
    StringBuffer sbuf = new StringBuffer(128);
    if (prefix != null) {
      sbuf.append(prefix + ": ");
    }
    sbuf.append(event.getTimeStamp() - event.getLoggerContextVO().getBirthTime());
    sbuf.append(" ");
    sbuf.append(event.getLevel());
    if (printThreadName) {
      sbuf.append(" [");
      sbuf.append(event.getThreadName());
      sbuf.append("] ");
    } else {
      sbuf.append(" ");
    }
    sbuf.append(event.getLoggerName());
    sbuf.append(" - ");
    sbuf.append(event.getFormattedMessage());
    sbuf.append(LINE_SEP);
    return sbuf.toString();
  }
}

添加相应的 set 方法就可以开启属性的配置。PrintThreadName 属性是 boolean 而不是 String 类型。关于配置 logback 的详细信息请参见第三章:logback 的配置第十一章将会提供更详细的内容。下面是关于 MySampleLayout2 的相关配置:

<configuration>

  <appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
    <encoder class="ch.qos.logback.core.encoder.LayoutWrappingEncoder">
      <layout class="chapters.layouts.MySampleLayout2"> 
        <prefix>MyPrefix</prefix>
        <printThreadName>false</printThreadName>
      </layout>
    </encoder>
  </appender>

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

PatternLayout

logback 配备了一个更加灵活的 layout 叫做 PatternLayout。跟所有的 layout 一样,PatternLayout 接收一个日志事件并返回一个字符串。但是,可以通过调整 PatternLayout 的转换模式来进行定制。

PatternLayout 中的转换模式与 C 语言中 printf() 方法中的转换模式密切相关。转换模式由字面量与格式控制表达式也叫转换说明符组成。你可以在转换模式中自由的插入字面量。每一个转换说明符由一个百分号开始 '%',后面跟随可选的格式修改器,以及用综括号括起来的转换字符与可选的参数。转换字符需要转换的字段。如:logger 的名字,日志级别,日期以及线程名。格式修改器控制字段的宽度,间距以及左右对齐。

正如我们已经在其它地方提到过的,FileAppender 及其子类需要一个 encoder。因为,当将 FileAppender 及其子类与 PatternLayout 结合使用时,PatternLayout 必须用 encoder 包裹起来。鉴于 FileAppender/PatternLayout 结合使用很常见,因此 logback 单独设计了一个名叫 PatternLayoutEncoder 的 encoder,包裹了一个 PatternLayout,因此它可以被当作一个 encoder。下面是通过代码配置 ConsoleAppenderPatternLayoutEncoder 使用的例子:

Example: PatternSample.java

package chapters.layouts;

import org.slf4j.LoggerFactory;

import ch.qos.logback.classic.Logger;
import ch.qos.logback.classic.LoggerContext;
import ch.qos.logback.classic.encoder.PatternLayoutEncoder;
import ch.qos.logback.classic.spi.ILoggingEvent;
import ch.qos.logback.core.ConsoleAppender;

public class PatternSample {

  static public void main(String[] args) throws Exception {
    Logger rootLogger = (Logger)LoggerFactory.getLogger(Logger.ROOT_LOGGER_NAME);
    LoggerContext loggerContext = rootLogger.getLoggerContext();
    // we are not interested in auto-configuration
    loggerContext.reset();

    PatternLayoutEncoder encoder = new PatternLayoutEncoder();
    encoder.setContext(loggerContext);
    encoder.setPattern("%-5level [%thread]: %message%n");
    encoder.start();

    ConsoleAppender<ILoggingEvent> appender = new ConsoleAppender<ILoggingEvent>();
    appender.setContext(loggerContext);
    appender.setEncoder(encoder); 
    appender.start();

    rootLogger.addAppender(appender);

    rootLogger.debug("Message 1"); 
    rootLogger.warn("Message 2");
  } 
}

在上面这个例子中,转换模式被设置为 "%-5level [%thread]: %message%n ",关于 logback 中简短的转换字符将会很快给出。运行 PatternSample

java java chapters.layouts.PatternSample

将会输出如下信息:

DEBUG [main]: Message 1 
WARN  [main]: Message 2

在转换模式 "%-5level [%thread]: %message%n" 中,字面量与转换说明符之间没有明显的分隔符。当对转换模式进行解析的时候,PatternLayout 有能力对字面量 (空格符,方括号,冒号) 和 转换说明符进行区分。在上面的例子中,转换说明符 %-5level 表示日志事件的级别的字符应该向左对齐,保持五个字符的宽度。具体的转换格式将会在下面介绍。

PatternLayout 中,括号用于对转换模式进行分组。'(' 与 ')' 有特殊的含义,因此如果想用作字面量,需要进行特殊的转义。圆括号的特殊含义将在下面 进行详细的介绍。

之前提到过,特定的转换模式可以通过花括号指定可选的参数。一个简单的可选转换模式可以是 %logger{10}。在这里 "logger" 就是转换字符,10 就是可选参数。可选参将在下面详细介绍。

转换字符与它们的可选参数在下面的表格中进行详细叙述。当多个转换字符在同一个单元格中被列出来,它们被当作别名来考虑。

转换字符

效果

c{length} lo{length} logger{length}

输出 logger 的名字作为日志事件的来源。转换字符接收一个作为它的第一个也是为一个参数。转换器的简写算法将会缩短 logger 的名字,但是通过不会丢失重要的信息。设置 length 的值为 0 是一个例外。它将会导致转换字符返回 logger 名字中最右边的点右边的字符。下面的表格提供了一个示例: logger 名字最右边的部分永远不会被简写,即使它的长度比 length 的值要大。其它的部分可能会被缩短为一个字符,但是永不会被移除。

C{length} class{length}

输出发出日志请求的类的全限定名称。 跟 %logger% 转换符一样,它也可以接收一个整型的可选参数去缩短类名。0 表示特殊含义,在打印类名时将不会输出包的前缀名。默认表示打印类的全限定名。 生成调用者类的信息并不是特别快。因此,应该避免使用,除非执行速度不是问题。

contextName cn

输出日志事件附加到的 logger 上下文的名字。

d{pattern} date{pattern} d{pattern, timezone} date{pattern, timezone}

用于输出日志事件的日期。日期转换符允许接收一个字符串作为参数。字符串的语法与 SimpleDateFormat 中的格式完全兼容。 你可以指定 "ISO8601" 来表示将日期格式为 ISO8601 类型。如果没有指定日期格式,那么 %date 转换字符默认为 ISO860 类型。 这里有一个例子。它假设当前时间为 2006.10.20 星期五,作者刚刚吃完饭准备写这篇文档。 第二个参数用于指定时区。例如, '%date{HH:mm:ss.SSS, Australia/Perth}' 将会打印世界上最孤立的城市,澳大利亚佩斯所在时区的日期。如果没有指定时区参数,则默认使用 Java 平台所在主机的时区。如果指定的时区不能识别或者拼写错误,则 [TimeZone.getTimeZone(String)](http://docs.oracle.com/javase/6/docs/api/java/util/TimeZone.html#getTimeZone(java.lang.String)) 方法会指定时区为 GMT。 常见错误: 对于 HH:mm:ss,SSS 模式,逗号会被解析为分隔符,所以最终会被解析为 HH:mm:ssSSS 会被当作时区。如果你想在日期模式中使用逗号,那么你可以这样使用,%date{"HH:mm:ss,SSS"} 用双引号将日期模式包裹起来。

F / file

输出发出日志请求的 Java 源文件名。 由于生成文件的信息不是特别快,因此,应该避免使用,除非速度不是问题。

caller{depth} caller{depthStart..depthEnd} caller{depth, evaluator-1, ... evaluator-n} caller{depthStart..depthEnd, evaluator-1, ... evaluator-n}

输出生成日志的调用者所在的位置信息。 位置信息依赖 JVM 的实现,但是通常由调用方法的全限定名以及调用者的来源组成。以及由圆括号括起来的文件名与行号。 caller 转换符还可以接收一个整形的参数,用来配置展示信息的深度。 例如,%caller{2} 会展示如下的信息: %caller{3} 会展示如下信息: caller 转换符还可以接收一个范围用来展示深度在这个范围内的信息。 例如,%caller{1..2} 会展示如下信息: 转换字符还可以接收一个 evaluator,在计算调用者数据之前通过指定的标准对日志事件进行测验。例如,%caller{3, CALLER_DISPLAY_EVAL} 会在 CALLER_DISPLAY_EVAL 返回一个肯定的答案,才会显示三行堆栈信息。 将在下面详细叙述 evaluator。

L / line

输出发出日志请求所在的行号。 生成行号不是特别快。因此,不建议使用,除非生成速度不是问题。

m / msg / message

输出与日志事件相关联的,由应用程序提供的日志信息。

M / method

输出发出日志请求的方法名。 生成方法名不是特别快,因此,应该避免使用,除非生成速度不是问题。

n

输出平台所依赖的行分割字符。 转换字符提供了像 "\n" 或 "\r\n" 一样的转换效果。因此指定行分隔符它是首选的指定方式。

p / le / level

输出日志事件的级别。

r / relative

输出应用程序启动到创建日志事件所花费的毫秒数

t / thread

输出生成日志事件的线程名。

X{key:-defaultVal} mdc{key:-defaultVal}

输出生成日志事件的线程的 MDC (mapped diagnostic context)。 如果 MDC 转换字符后面跟着用花括号括起来的 kye,例 %MDC{userid},那么 'userid' 所对应 MDC 的值将会输出。如果该值为 null,那么通过 :- 指定的默认值 将会输出。如果没有指定默认值,那么将会输出空字符串。 如果没有指定的 key,那么 MDC 的整个内容将会以 "key1=val1, key2=val2" 的格式输出。 查详情请见 第八章

ex{depth} exception{depth} throwable{depth} ex{depth, evaluator-1, ..., evaluator-n} exception{depth, evaluator-1, ..., evaluator-n} throwable{depth, evaluator-1, ..., evaluator-n}

输出日志事件相关的堆栈信息,默认情况下会输出全部的堆栈信息。 throwable 转换词可以接收如下的参数: 下面是一些示例: 在输出前,转换字符还可以使用给定的标准再次检验日志事件。例如,使用 %ex{full, EX_DISPLAY_EVAL},只有 EX_DISPLAY_EVAL 返回一个否定的答案,才会输出全部的堆栈信息。evaluator 在接下来的文档中将会进一步叙述。 如果你没有指定 %throwable 或者其它跟 throwable 相关的转换字符,那么 PatternLayout 会在最后一个转换字符加上这个。因为堆栈信息非常的重要。如果你不想展示堆栈信息,那么可以使用 %nopex (作者原文为 $nopex) 可以替代 %throwable。详情见 %nopex。

xEx{depth} xException{depth} xThrowable{depth} xEx{depth, evaluator-1, ..., evaluator-n} xException{depth, evaluator-1, ..., evaluator-n} xThrowable{depth, evaluator-1, ..., evaluator-n}

跟 %throwable 类似,只不过多了类的包信息。 在每个堆栈信息的末尾,多了包含 jar 文件的字符串,后面再加上具体的实现版本。这项创造性的技术是来自 James Strachan 的建议。如果该信息不确定,那么类的包信息前面会有一个波浪号 (~)。 下面是一个例子: logback 努力的去确保类的包信息正确的展示,即使是在复杂的类加载层次中。但是,一个不能保证信息的绝对正确,那么在这些数据的前面将会多一个波浪符 (~)。因此,从理论上来说,打印的类的包信息跟真实的类的包信息是有区别的。在上面的例子中,类 Wombat 的包信息前面有一个波浪符,在实际的情况中,它真实包可能为 [wombat.jar:1.7]。 但是请注意潜在的性能损耗,计算包信息默认是禁止的。当启用了计算包信息,那么 PatternLayout 将会自动认为在字符串模式的末尾 %xThrowable 替代了 %throwable。 根据用户的反馈,Netbeans 会阻止包信息的打印。

nopex nopexception

这个转换字符不会输出任何数据,因此,它可以用来有效忽略异常信息。 %nopex 转换字符允许用户重写 PatternLayout 内部的安全机制,该机制将会在没有指定其它处理异常的转换字符时,默认添加 %xThrowable。

marker

输出与日志请求相关的标签。 一旦标签包含子标签,那么转换器将会根据下面的格式展示父标签与子标签。 parentName [child1, child2]

property{key}

输出属性 key 所对应的值。相关定义参见 定义变量 以及作用域。如果 key 在 logger context 中没有找到,那么将会去系统属性中找。 key 没有默认值,如果缺失,则会展示 " Property_HAS_NO_KEY" 的错误信息。

在子模式 'p' 产生的字符中,将所有出现正则表达式 'r' 的地方替换为 't'。例如,"%replace(%msg){'\s', ''}" 将会移除事件消息中所有空格。 模式 'p' 可以是任意复杂的甚至由多个转换字符组成。例如,"%replace(%logger %msg){'.', '/'}" 将会替换 logger 以及消息中所有的点为斜杆。

rEx{depth} rootException{depth} rEx{depth, evaluator-1, ..., evaluator-n} rootException{depth, evaluator-1, ..., evaluator-n}

输出与日志事件相关的堆栈信息,根异常将会首先输出,而是标准的"根异常最后输出"。下面是一个输出例子: %rootException 跟 %xException 类似,也允许一些可选的参数,包括深度以及 evaluator。它也会输出包信息。简单来说,%rootException 跟 %xException 非常的类似,仅仅是异常输出的顺序完全相反。 %rootException 的作者 Tomasz Nurkiewicz 在他的博客说明了他所作的贡献 "Logging exceptions root cause first"

% 有特殊的含义

在给定的转换模式上下文中,% 有特殊的含义。如果作为字面量,需要进行转义。例如,"%d %p \% %m%n"。

转换字符对字面量的限制

在大多数的情况下,字面量包括空格或者其它的分隔符,所以它们不会与转换字符混淆。例如,"%level [%thread] - %message%n" 包含字面量字符 " [" 与 "] - "。但是,如果一个转换字符后面紧跟着一个字面量,那么 logback 的模式解析器将会错误的认为这个字面量也是转换字符的一部分。例如,"%date%nHello" 将会被解析成两个转换字符 %date 与 %nHello,但是 %nHello 不是一个转换字符,所以 logback 将会输出 %PARSER_ERROR[nHello]。如果你想要区分 %n 跟 Hello,可以通过给 %n 传递一个空参数。例如,"%date%n{}Hello" 将会被解析为 %date %n 再紧跟着一个字符串 "Hello"。

格式修改器

默认情况下,相关信息按照原样输出。但是,在格式修改器的帮助下,可以对每个数据字段进行对齐,以及更改最大最小宽度。

可选的格式修改器放在百分号跟转换字符之间。

第一个可选的格式修改器是左对齐标志,也就是减号 (-) 字符。接下来的是最小字段宽度修改器,它是一个十进制常量,表示输出至少多少个字符。如果字段包含很少的数据,它会选择填充左边或者右边,直到满足最小宽度。默认是填充左边 (右对齐),但是你可以通过左对齐标志来对右边进行填充。填充字符为空格。如果字段的数据大于最小字段的宽度,会自动扩容去容纳所有的数据。字段的数据永远不会被截断。

这个行为可以通过使用最大字段宽度修改器来改变,它通过一个点后面跟着一个十进制常量来指定。如果字段的数据长度大于最大字段的宽度,那么会从数据字段的开头移除多余的字符。举个🌰,如果最大字段的宽度是 8,数据长度是十个字符的长度,那么开头的两个字符将会被丢弃。这个行为跟 C 语言中 printf 函数从后面开始截断的行为相违背。

如果想从后面开始截断,可以在点后面增加一个减号。如果是这样的话,最大字段宽度是 8,数据长度是十个字符的长度,那么最后两个字符将会被丢弃。

下面是各种格式修改器的例子:

格式修改器

左对齐

最小宽度

最大宽度

备注

%20logger

false

20

none

如果 logger 的名字小于 20 个字符的长度,那么会在左边填充空格

%-20logger

true

20

none

如果 logger 的名字小于 20 个字符的长度,那么会在右边填充空格

%.30logger

NA

none

30

如果 logger 的名字大于 30 个字符的长度,那么从前面开始截断

%20.30logger

false

20

30

如果 logger 的名字大于 20 个字符的长度,那么会从左边填充空格。但是如果 logger 的名字大于 30 字符,将会从前面开始截断

%-20.30logger

true

20

30

如果 logger 的名字小于 20 个字符的长度,那么从右边开始填充空格。但是如果 logger 的名字大于 30 个字符,将会从前面开始截断

%.-30logger

NA

none

30

如果 logger 的名字大于 30 个字符的长度,那么从后面开始截断

下面的表格列出了格式修改器截断的例子。但是请注意综括号 "[]" 不是输出结果的一部分,它只是用来区分输出的长度。

格式修改器

logger 的名字

结果

[%20.20logger]

main.Name

[           main.Name]

[%-20.20logger]

main.Name

[main.Name           ]

[%10.10logger]

main.foo.foo.bar.Name

[o.bar.Name]

[%10.-10logger]

main.foo.foo.bar.Name

[main.foo.f]

只输出日志等级的一个字符

除了可以输出 TRACE, DEBUG, WARN, INFO 或者 ERROR 来表示日志等级之外,还是输出T, D, W, I 与 E 来进行表示。你可以自定义转换器 或者利用刚才讨论的格式修改器来缩短日志级别为一个字符。这个转换说明符可能为 "%.-1level"。

转换字符的选项

一个转换字符后面可以跟一个选项。它们通过综括号来声明。我们之前已经看到了一些可能的选项。例如之前的 MDC 转换说明符 %mdc{someKey}

一个转换说明符可能有多个可选项。一个转换说明符可以充分利用我们即将介绍到的 evaluator,可以添加多个 evaluator 的名字到可选列表。如下:

<pattern>%-4relative [%thread] %-5level - %msg%n \
  %caller{2, DISP_CALLER_EVAL, OTHER_EVAL_NAME, THIRD_EVAL_NAME}</pattern>

如果这些选项中包含了一些特殊字符,例如花括号,空格,逗号。你可以使用单引号或者双引号来包裹它们。例如:

<pattern>%-5level - %replace(%msg){'\d{14,16}', 'XXXX'}%n</pattern>

我们传递 \d{16}XXXXreplace 转换字符。它将消息中 14,15 或者 16 位的数字替换为 XXXX,用来混淆信用卡号码。在正则表达式中,"\d" 表示一个数字的简写。"{14,16}" 会被解析成 "{14,16}",也就是说前一个项将会被重复至少 14 次,至多 16 次。

特殊的圆括号

在 logback 里,模式字符串中的圆括号被看作为分组标记。因此,它能够对子模式进行分组,并且直接对子模式进行格式化。在 0.9.27 版本,logback 开始支持综合转换字符,例如 %replace 可以对子模式进行转换。

例如一下模式:

%-30(%d{HH:mm:ss.SSS} [%thread]) %-5level %logger{32} - %msg%n

将会对子模式 "%d{HH:mm:ss.SSS} [%thread]" 进行分组输出,为了在少于 30 个字符时进行右填充。

如果没有进行分组将会输出:

13:09:30 [main] DEBUG c.q.logback.demo.ContextListener - Classload hashcode is 13995234
13:09:30 [main] DEBUG c.q.logback.demo.ContextListener - Initializing for ServletContext
13:09:30 [main] DEBUG c.q.logback.demo.ContextListener - Trying platform Mbean server
13:09:30 [pool-1-thread-1] INFO  ch.qos.logback.demo.LoggingTask - Howdydy-diddly-ho - 0
13:09:38 [btpool0-7] INFO c.q.l.demo.lottery.LotteryAction - Number: 50 was tried.
13:09:40 [btpool0-7] INFO c.q.l.d.prime.NumberCruncherImpl - Beginning to factor.
13:09:40 [btpool0-7] DEBUG c.q.l.d.prime.NumberCruncherImpl - Trying 2 as a factor.
13:09:40 [btpool0-7] INFO c.q.l.d.prime.NumberCruncherImpl - Found factor 2

如果对 "%-30()" 进行分组将会输出:

13:09:30 [main]            DEBUG c.q.logback.demo.ContextListener - Classload hashcode is 13995234
13:09:30 [main]            DEBUG c.q.logback.demo.ContextListener - Initializing for ServletContext
13:09:30 [main]            DEBUG c.q.logback.demo.ContextListener - Trying platform Mbean server
13:09:30 [pool-1-thread-1] INFO  ch.qos.logback.demo.LoggingTask - Howdydy-diddly-ho - 0
13:09:38 [btpool0-7]       INFO  c.q.l.demo.lottery.LotteryAction - Number: 50 was tried.
13:09:40 [btpool0-7]       INFO  c.q.l.d.prime.NumberCruncherImpl - Beginning to factor.
13:09:40 [btpool0-7]       DEBUG c.q.l.d.prime.NumberCruncherImpl - Trying 2 as a factor.
13:09:40 [btpool0-7]       INFO  c.q.l.d.prime.NumberCruncherImpl - Found factor 2

后者的格式更加容易阅读。

如果你想将圆括号当作字面量输出,那么你需要对每个圆括号用反斜杠进行转义。就像 (%d{HH:mm:ss.SSS} [%thread]) 一样。

着色

如上所述的圆括号分组,允许对子模式进行着色。在 1.0.5 版本,PatternLayout 可以识别 "%black","%red","%green","%yellow","%blue","%magenta","%cyan", "%white", "%gray", "%boldRed","%boldGreen", "%boldYellow", "%boldBlue", "%boldMagenta""%boldCyan", "%boldWhite" 以及 "%highlight" 作为转换字符。这些转换字符都还可以包含一个子模式。任何被颜色转换字符包裹的子模式都会通过指定的颜色输出。

下面是关于着色的配置文件。

<configuration debug="true">
    <appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
<!--               在 Windows 平台下,设置 withJansi = true 来开启 ANSI 颜色代码需要 Jansi 类库 -->
<!--               需要在 classpath 引入 org.fusesource.jansi:jansi:1.8 包 -->
<!--               在基于 Unix 操作系统,像 Linux 以及 Mac OS X 系统默认支持 ANSI 颜色代码 -->
        <withJansi>true</withJansi>
        <encoder>
            <pattern>[%thread] %highlight(%-5level) %cyan(%logger{15}) - %msg %n</pattern>
        </encoder>
    </appender>

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

下面是相关的输出:

[main] WARN  c.l.TrivialMain - a warning message 0
[main] DEBUG c.l.TrivialMain - hello world number1
[main] DEBUG c.l.TrivialMain - hello world number2
[main] INFO  c.l.TrivialMain - hello world number3
[main] DEBUG c.l.TrivialMain - hello world number4
[main] WARN  c.l.TrivialMain - a warning message 5
[main] ERROR c.l.TrivialMain - Finish off with fireworks

其实是有颜色的,但是 md 不支持直接对字体颜色进行操作,而我懒得去折腾 HTML

只需要几行代码就可以创建一个着色转换字符。在自定义转换说明符部分,我们将讨论怎样在配置文件中注册一个转换字符。

Evaluators

像之前提到的,当一个转换字符需要基于一个或者多个 EventEvaluator 对象动态表现时,EventEvaluator 对象根据规则可以决定给定的日志事件是否匹配。

让我们来回顾一下包含 EventEvaluator 的例子。下一个配置文件输出日志事件到控制台,显示日期,线程,日志级别,消息,以及调用者数据。获取日志事件调用者的信息成本比较高,只有当日志请求来源特定的 logger,或者消息包含特定的字符串时,我们才会这样做。换句话说,在调用者信息是多余的情况下,我们不应该去影响应用的性能。

Evaluator 与 评价表达式 (evaluation expressions) 都会在第七章 详细介绍。如果你想利用 evaluator 去做一些有意思的事情,你必须看一下对这个的详细介绍。下面的例子基于 JaninoEventEvaluator,所以需要 Janino 类库。查看相关文档进行设置。

Example: callerEvaluatorConfig.xml

<configuration>
  <evaluator name="DISP_CALLER_EVAL">
    <expression>logger.contains("chapters.layouts") &amp;&amp; \
      message.contains("who calls thee")</expression>
  </evaluator>

  <appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender"> 
    <encoder>
      <pattern>
        %-4relative [%thread] %-5level - %msg%n%caller{2, DISP_CALLER_EVAL}
      </pattern>
    </encoder>
  </appender>

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

上面的评价表达式用来匹配从名为 "chapters.layouts" logger 发出,并且消息中包含字符串 "who calls thee" 的日志事件。由于 XML 的编码规则,& 符号需要被转义为 &amp;

下面的类利用了配置文件中所提到的特性。

Example: CallerEvaluatorExample.java

package chapters.layouts;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import ch.qos.logback.classic.LoggerContext;
import ch.qos.logback.classic.joran.JoranConfigurator;
import ch.qos.logback.core.joran.spi.JoranException;
import ch.qos.logback.core.util.StatusPrinter;

public class CallerEvaluatorExample {

  public static void main(String[] args)  {
    Logger logger = LoggerFactory.getLogger(CallerEvaluatorExample.class);
    LoggerContext lc = (LoggerContext) LoggerFactory.getILoggerFactory();

    try {
      JoranConfigurator configurator = new JoranConfigurator();
      configurator.setContext(lc);
      configurator.doConfigure(args[0]);
    } catch (JoranException je) {
      // StatusPrinter will handle this
    }
    StatusPrinter.printInCaseOfErrorsOrWarnings(lc);

    for (int i = 0; i < 5; i++) {
      if (i == 3) {
        logger.debug("who calls thee?");
      } else {
        logger.debug("I know me " + i);
      }
    }
  }
}

上面的应用没有什么特别的地方。发出五条日志请求,第三条的的请求信息为 "who calls thee?"。

通过命令:

java chapters.layouts.CallerEvaluatorExample src/main/java/chapters/layouts/callerEvaluatorConfig.xml

将会输出:

0    [main] DEBUG - I know me 0 
0    [main] DEBUG - I know me 1 
0    [main] DEBUG - I know me 2 
0    [main] DEBUG - who calls thee? 
Caller+0   at chapters.layouts.CallerEvaluatorExample.main(CallerEvaluatorExample.java:28)
0    [main] DEBUG - I know me 4

当发出日志请求时,会评价相应的日志事件。仅仅只有第三个日志事件会匹配到评价规则,所以它的调用者信息会被展示出来。对于其它的日志事件,由于没有匹配到评价规则,调用者信息不会被打印。

可以通过更改表达式来应对真实的应用场景。举个🌰,你可以结合 logger 名与日志级别,日志级别在 WARN 以上的日志请求被当作一个敏感的部分,在金融业务模块中,我们可以这样做来获取调用者的信息。

重要:评价表达式true 时,通过 caller 转换字符,可以输出调用者的信息。

考虑这么一种情况,当日志请求中包含异常信息时,它们的堆栈信息也会输出。但是,对于某些特定的异常信息,可能需要禁止输出堆栈信息。

下面的代码创建了三条日志请求,每一条都包含一个异常信息。第二条的异常信息跟其它的不一样,它包含 "do not display this" 字符串,并且它的异常信息类型为 chapters.layouts.TestException。现在让我们来阻止第二条日志的打印。

Example: ExceptionEvaluatorExample.java

package chapters.layouts;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import ch.qos.logback.classic.LoggerContext;
import ch.qos.logback.classic.joran.JoranConfigurator;
import ch.qos.logback.core.joran.spi.JoranException;
import ch.qos.logback.core.util.StatusPrinter;

public class ExceptionEvaluatorExample {

  public static void main(String[] args) {
    Logger logger = LoggerFactory.getLogger(ExceptionEvaluatorExample.class);
    LoggerContext lc = (LoggerContext) LoggerFactory.getILoggerFactory();

    try {
      JoranConfigurator configurator = new JoranConfigurator();
      configurator.setContext(lc);
      lc.reset();
      configurator.doConfigure(args[0]);
    } catch (JoranException je) {
       // StatusPrinter will handle this
    }
    StatusPrinter.printInCaseOfErrorsOrWarnings(lc);

    for (int i = 0; i < 3; i++) {
      if (i == 1) {
        logger.debug("logging statement " + i, new TestException(
            "do not display this"));
      } else {
        logger.debug("logging statement " + i, new Exception("display"));
      }
    }
  }
}

下面的配置文件通过评价表达式来匹配包含 chapters.layouts.TextException 类型的日志事件,也就是我们之前说要禁止的异常类型。

Example: exceptionEvaluatorConfig.xml

<configuration>
  <!-- evaluator 需要在 appender 前面定义 -->
  <evaluator name="DISPLAY_EX_EVAL">
    <expression>throwable != null &amp;&amp; throwable instanceof  \
      chapters.layouts.TestException</expression>
  </evaluator>

  <appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
    <encoder>
      <pattern>%msg%n%xEx{full, DISPLAY_EX_EVAL}</pattern>
    </encoder>
  </appender>

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

作者原文里面是 %ex,应该是笔误

通过这个配置文件,每当日志请求中包含一个 chapters.layouts.TestException 时,堆栈信息不会被输出。

通过如下命令启动:

java chapters.layouts.ExceptionEvaluatorExample src/main/java/chapters/layouts/exceptionEvaluatorConfig.xml

将会输出:

logging statement 0
java.lang.Exception: display
    at chapters.layouts.ExceptionEvaluatorExample.main(ExceptionEvaluatorExample.java:16)
logging statement 1
logging statement 2
java.lang.Exception: display
    at chapters.layouts.ExceptionEvaluatorExample.main(ExceptionEvaluatorExample.java:16)

作者原文还输出了 jar 包的信息,是因为打包后通过命令行执行的 (I think 😂)

第二条日志没有堆栈信息,因为我们禁止 TextException 类型的堆栈信息。每条堆栈信息的最后用综括号包裹起来的是具体的包信息

注意:%ex 转换说明符中的评价表达式为 false 时,堆栈信息才会输出。

自定义转换说明符

我们可以在 PatternLayout 中使用内置的转换字符。我们也可以使用自己新建的转换字符。

新建一个自定义的转换字符需要两步。

第一步

首先,你必须继承 ClassicConverter 类。ClassicConverter 对象负责从 ILoggingEvent 实例中抽取信息并输出字符串。例如,%logger 对应的转换器 LoggerConverter,可以从 ILoggingEvent 从抽取 logger 的名字,返回一个字符串。它可以缩写 logger 的名字。

下面是一个自定义的转换器,返回从创建开始经过的时间,单位为纳秒。

Example: MySampleConverter

public class MySampleConverter extends ClassicConverter {

  long start = System.nanoTime();

  @Override
  public String convert(ILoggingEvent event) {
    long nowInNanos = System.nanoTime();
    return Long.toString(nowInNanos-start);
  }
}

这个实现非常简单。MySampleConverter 继承了 ClassicConverter 并实现了 convert 方法,返回从创建开始经过多少纳秒。

第二步

第二步,我们必须让 logback 知道这个新建的 Converter。所以我们需要在配置文件中进行声明,如下:

Example: mySampleConverterConfig.xml

<configuration>

  <conversionRule conversionWord="nanos" 
                  converterClass="chapters.layouts.MySampleConverter" />

  <appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
    <encoder>
      <pattern>%-6nanos [%thread] - %msg%n</pattern>
    </encoder>
  </appender>

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

执行命令如下:

java chapters.layouts.SampleLogging src/main/java/chapters/layouts/mySampleConverterConfig.xml

输出信息如下:

26113953 [main] - Everything's going well
26672034 [main] - maybe not quite...

可以看一下其它 Converter 的实现,例如 MDCConverter ,去定制更加复杂的功能,如可选处理。想创建自己的颜色主题,可以看一下 HighlightingCompositeConverter

HTMLLayout

HTMLLayout (包含在 logback-classic 中) 以 HTML 格式生成日志。HTMLLayout 通过 HTML 表格输出日志,每一行对应一条日志事件。

下面是 HTMLLayout 通过默认的 CSS 样式生成的。

表格的列是通过转换模式指定的。关于转换模式的文档请查看 PatternLayout。所以,你可以完全控制表格的内容以及格式。你可以选择并且展示任何跟 PatternLayout 组合的转换器。

一个值得注意的问题是使用 PatternLayout 中的 HTMLLayout 时,不要使用空格或者其它的字面量来分隔转换说明符。转换模式中的每个说明符都会被当做一个单独的列。同样的转换模式中的每个文本块也会被当作一个单独的列,这会占用屏幕的空间。

下面的 HTMLLayout 相关的配置:

Example: htmlLayoutConfig1.xml

<configuration debug="true">
  <appender name="FILE" class="ch.qos.logback.core.FileAppender">
    <encoder class="ch.qos.logback.core.encoder.LayoutWrappingEncoder">
      <layout class="ch.qos.logback.classic.html.HTMLLayout">
        <pattern>%relative%thread%mdc%level%logger%msg</pattern>
      </layout>
    </encoder>
    <file>test.html</file>
  </appender>

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

TrivialMain 包含一些消息以及一个结束异常。执行以下命令:

java chapters.layouts.TrivialMain src/main/java/chapters/layouts/htmlLayoutConfig1.xml

将会当前文件夹创建一个 test.html 文件。test.html 文件的内容与下面类似:

堆栈信息

如果你使用 %ex 转换字符去展示堆栈信息,那么将会创建一个列来展示堆栈信息。在大多数的情况下,列会为空,那么就会浪费屏幕的空间。而且,在单独的列打印堆栈信息,输出的结果阅读起来有难度。但是,%ex 转换字符不是唯一一个用来展示堆栈信息的。

原文第一个 %ex 为 %em

一个更好的解决办法是通过实现 IThrowableRenderer 接口。实现的接口可以分配给 HTMLLayout 来管理相关的异常数据。默认情况下,会给每个 HTMLLayout 实例分配一个 DefaultThrowableRenderer。它将异常的堆栈信息写入到表格新的一行,并且非常易读,就跟上面展示的表格一样。

如果在某些情况下,你仍然想要使用 %ex,那么你可以在配置文件中指定 NOPThrowableRenderer 来禁止在单独一行展示堆栈信息。我们不理解为什么你要这样做,但是你开心就好。

CSS

HTMLLayout 创建的 HTML 是通过 CSS 来控制样式的。在缺少指定命令的情况下,HTMLLayout 会使用内部默认的样式。但是,你可以告诉 HTMLLayout 去使用外部的 CSS 文件。通过在 <layout> 元素内置 <cssBuilder> 元素可以做到。如下所示:

<layout class="ch.qos.logback.classic.html.HTMLLayout">
  <pattern>%relative...%msg</pattern>
  <cssBuilder class="ch.qos.logback.classic.html.UrlCssBuilder">
    <!-- css 文件的路径 -->
    <url>http://...</url>
  </cssBuilder> 
</layout>

HTMLLayout 通常与 SMTPAppender 配合使用,所以邮件可以被格式化成 HTML。

Log4j XMLLayout

XMLLayout (logback-classic 的一部分) 生成一个 log4j.dtd 格式的文件,用来与类似 Chainsaw 以及 Vigilog 这样的工具进行交互操作,这些工具可以处理由 log4j XMLLayout 生成的文件。

跟 log4j 1.2.15 版本的 XMLLayout 一样,logback-classic 中的 XMLLayout 接收两个 boolean 属性:locationInfoproperties。设置 locationInfo 的值为 true,可以在每个事件中开启包含位置信息 (调用者的数据)。设置 properties 为 true,可以开启包含 MDC 信息。默认情况下,两个属性都设置为 false。

下面是一个示例:

Example: log4jXMLLayout.xml

<configuration>
  <appender name="FILE" class="ch.qos.logback.core.FileAppender">
    <file>test.xml</file>
    <encoder class="ch.qos.logback.core.encoder.LayoutWrappingEncoder">
      <layout class="ch.qos.logback.classic.log4j.XMLLayout">
        <locationInfo>true</locationInfo>
      </layout>
    </encoder> 
  </appender> 

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

Logback access

大多数 logback-access 的 layout 仅仅只是 logback-classic 的 layout 的改编。logback-classic 与 logback-access 模块定位不同的需求,但是都提供了类似的功能。

写你自己的 layout

在 logback-access 中写一个定制的 Layout 与在 logback-classic 的 Layout 中几乎一致。

PatternLayout

配置 logback-access 中的 PatternLayout,与在 logback-classic 中配置相同。但是它添加了一些额外的转换说明符来适应 HTTP 请求以及 HTTP 响应中特定信息位的记录。

下表是 logback-access 中 PatternLayout 的相关转换说明符。

转换字符

效果

a / remoteIP

远程 IP 地址

A / localIP

本地 IP 地址

b / B / bytesSent

响应内容的长度

h / clientHost

远程 host

H / protocol

请求协议

l

远程日志名,在 logback-access 中,转换器总是返回 "-"

reqParameter{paramName}

响应参数。 这个转换字符在花括号中接受一个参数,在请求中寻找相应的参数。 %reqParameter{input_data} 展示相应的参数。

i{header} / header{header}

请求头。 %header{Referer} 显示请求的来源。 如果没有指定选项,将会展示所有可用的请求头

m / requestMethod

请求方法

r / requestURL

请求 URL

s / statusCode

请求状态码

D / elapsedTime

请求所耗费的时间,单位为毫秒

T / elapsedSeconds

请求所耗费的时间,单位为秒

t / date

输出日志事件的日期。日期说明符需要用花括号指定。日期格式来源 java.text.SimpleDateFormatISO8601 也是一个有效的值。 例如,%t{HH:mm:ss,SSS} 或者 %t{dd MMM yyyy ;HH:mm:ss,SSS}。如果没有指定日期格式字符,那么会默认指定为 %t{dd/MMM/yyyy:HH:mm:ss Z}

u / user

远程用户

q / queryString

请求查询字符串,前缀为 '?'

U / requestURI

请求 URI

S / sessionID

Session ID.

v / server

服务器名

I / threadName

处理该条请求的线程

localPort

本地端口

reqAttribute{attributeName}

请求的属性。 %reqAttribute{SOME_ATTRIBUTE} 展示相应的属性。

reqCookie{cookie}

请求 cookie。 %cookie{COOKIE_NAME} 展示相应的 cookie。

responseHeader{header}

响应头。 %header{Referer} 展示响应的来源。

requestContent

展示请求的内容,即请求的 InputStream。它与 TeeFilter 结合使用。一个使用 TeeHttpServletRequest 替代 HttpServletRequest 的 javax.servlet.Filter。前者可以多次访问请求的 InputStream 而不会丢失内容。

fullRequest

请求的数据。包括所有的请求头以及请求内容。

responseContent

展示响应的内容,也就是响应的 InputStream。 它与 TeeFilter 结合使用。一个使用 TeeHttpServletResponse 替代 HttpServletResponsejavax.servlet.Filter。前者可以多次访问响应 (原文为请求) 的 InputStream 而不会丢失内容。

fullResponse

获取响应所有可用的数据,包括所有的响应头以及响应内容。

logback-access 的 PatternLayout 能够识别三个关键字,有点类似快捷键。

关键字

相等的转换模式

common or CLF

%h %l %u [%t] "%r" %s %b

combined

%h %l %u [%t] "%r" %s %b "%i{Referer}" "%i{User-Agent}"

关键字 common 对应 '%h %l %u [%t] "%r" %s %b',分别展示客户端主机,远程日志名,用户,日期,请求 URL,状态码,以及响应内容的长度。

关键字 combined 对应 '%h %l %u [%t] "%r" %s %b "%i{Referer}" "%i{User-Agent}"'。跟 common 有点类似,但是它还会再显示两个请求头,referer 以及 user-agent。

HTMLLayout

logback-access 中的 HTMLLayout 与 logback-classic 中的 HTMLLayout 有点类似。

默认情况下,它会创建一个包含如下数据的表格:

  • 请求 IP (Remote IP)

  • 日期 (Date)

  • 请求 URL (Request URL)

  • 状态码 (Status code)

  • 内容长度 (Content Length)

下面是 logback-access 中的 HTMLLayout 输出的一个例子:

还有比真实的例子更好的例子吗?我们自己的 log4j.properties 用于 logback 翻译器,充分的利用了 logback-access 在线演示 RollingFileAppenderHTMLLayout 的输出。

每一个新的用户请求 翻译器 这个网站,一个新的条目就会添加到访问日志,你可以通过这个链接 查看。

Last updated