目录
(一)pattern关键字介绍
(二)源码分析
(一)pattern关键字介绍
%d或%date:表示日期,可配置格式化%d{yyyy-MM-dd HH:mm:ss}
%r或%relative:也是日期,不过显示的是相对时间,是相对于日志框架加载开始的时间,单位是ms。
%level或%le或%p:表示日志级别
%t或%thread:表示线程名
%lo或%logger或%c(小写c):表示logger的名称,即 LoggerFactory.getLogger传入的名称,如果传入的是class则为类全限定名。
%m或%msg或%message:表示日志消息
%C(大写C)或%class:日志写入调用所在类名
%M或%method:日志写入调用所在的方法
%L或者%line:日志写入调用所在的行数
%F或者%file:日志写入调用所在的文件
%X或%mdc:mdc写入的信息,记得mdc设置后,需要在finally中删除,避免内存泄漏。
%ex或%exception或%throwable:异常信息
%rEx或%rootException:带根因的异常信息
%xEx或%xException或%xThrowable:显示继承的异常信息
显示的异常信息量:%rootException > %xException > %exception;如果pattern中没有指定异常显示,那么默认会在末尾添加异常显示,根据上下文中的packagingDataEnabled设置,如果为true,加的是%xException,false为%exception(默认为false); %nopex或%nopexception:表示不打印异常信息。%ex在有异常的情况下回自动添加,而如果配置了这个那么就不会输出异常,所以和%ex是互斥的
%cn或%contextName:日志上下文的名称
%caller:表示日志详细的调用链
%maker:显示日志的标记,marker主要用于日志的分类和过滤。打印日志时可以调带Marker参数的方法,实现Marker的传递。
%property: 显示配置,会先查日志上下文的配置,如果没有从系统配置中获取,如:%property{java.version}就是打印java版本
%n: 表示换行
以下面格式的pattern,我们打印一个带异常的日志,看看效果,也好对应:
%d{yyyy-MM-dd HH:mm:ss} -%r [%thread] %-5level %logger{50} %class.%method-%line %F [%X{logTrackId}][%cn] [%caller] [%marker] %property{java.version} - %msg%n
使用下面代码进行比较全的日志打印,这样和日志对照起来,对关键字就有了更具体的理解:
public void doNothing() {String logTrackId = UUID.randomUUID().toString();MDC.put("logTrackId", logTrackId);try {RuntimeException causeException = new RuntimeException("this is a cause exception");Marker marker = MarkerFactory.getMarker("basicMarker");ImpossibleLogger.warn(marker, "some exception happen:", new RuntimeException("this is encosing exception", causeException));} finally {MDC.remove("logTrackId");}
}
(二)源码分析
日志是如何根据pattern进行转化的呢?
下面我们看一下源码:
首先是<pattern>标签对应的PatternLayout类:这里定义了很多默认的转换器类:
可以看到关键字d和date都匹配的DateConverter。所以我们在pattern中定义的每个关键字都会有一个对应的转换器,既然是转换器那么一定会有转换的方法:convert,可以看一下DateConverter的convert方法,看一下它是如何转换的:
很简单只是把ILoggingEvent中获取时间戳,然后使用cachingDateFormatter进行转换,cachingDateFormatter实际上只是包装了一个SimpleDateFormat对象,因为其是线程不安全的,所以使用cachingDateFormatter将其包了一层,调用format时使用synchronized进行同步保证线程安全。
对于一个日志的格式化转换就是循环调用每个转换器然后将转换的信息添加到字符串中
那么转换器又是怎么加载到PatternLayout中的呢?可以看PatternLayoutBase.start方法,这个方法在Context初始化创建Pattern后会被调用。
核心的步骤有3个:
(1)创建一个解析器Parser,然后调用parse对pattern字符串进行解析,解析出的结果是一个Node层级对象,里面包含了%d、%thread等都被解析成对应的Node
(2)getEffectiveConverterMap获取有效的Converter集合,它主要把默认的转换器和以及context中PATTERN_RULE_REGISTRY存储自定义的转换器获取过来。
所以我们实际上可以通过往context中的PATTERN_RULE_REGISTRY这个key对应的map中写入自己的转换器类实现。key是关键字,值是自定义的转换器类名,而实现方式实际上是在配置文件中configuration节点下定义conversionRule标签,这个标签会被解析成converter,可参见ConversionRuleAction这个类
但要注意的是,xml的解析是有顺序的,所以conversionRule自定义的转换器需要在使用此转换器对应关键字的pattern之前,否则pattern解析时从PATTERN_RULE_REGISTRY获取不到对应的转换器。
(3)调用p.compile:这里就是转换器实际实例化创建的地方
第一步解析的Node有3中类型,分别是Node.LITERAL(字面量)、Node.COMPOSITE_KEYWORD(组合关键字)、Node.SIMPLE_KEYWORD(简单关键字)。字面量就是对应我们的pattern中的一些常量,如空格、-等会直接输出到日志中,不做转换。而组合转换器是比较复杂的转换器,会有子转换器的嵌套。我们常用的大部分都是SIMPLE_KEYWORD。
主要到DynamicConverter都会调用setFormattingInfo以及setOptionList,这些两个可能会被转换器用到。
如我们经常在%d后会加日期格式化yyyy-MM-dd HH:mm:ss,这个就会放到OptionList中,通过,分隔,如日期还可以设置时区%d{yyyy-MM-dd HH:mm:ss,GMT-8:00},这是OptionList中就有两个值
而%-5level中的-5则会设置到FormattingInfo,FormattingInfo主要设置关键字转换的字符串的最小长度和最大长度,用.分隔,如:%-5,7level,这里5就是最小值,7是最大值,表示日志级别最小显示长度为5,最大长度为7。最小值和最大值都可以带符号,最小值的正负号表示字符串长度不够时补齐的方向,-号是右边补齐,所以level如果是warn,那么不够5个字符串,那么右边会补一个空格。而最大值正负号则字符串超出,需要截取掉超出最大值的字符串,表示截取的方向,-号是截取左边的。