Fork me on GitHub

Java 日志工具介绍

容易被忽略的数据生产源

目前,随着大数据方向的推进,越来越多的数据被应用于数据分析和挖掘,而其中一大部分就是项目中的 日志数据。而 Java 项目中有很多的日志输出包,不同项目使用不同的日志工具会造成数据结构的不一致,这样就为 数据分析 增添了一定的麻烦,今天记录下对各中日志工具的说明。

日志组件历史


JUL

这个是 java.util.logging 的缩写,也就是 Java 本身 JDK 自带的日志工具,但是通常它的功能有限,因此,项目中的日志输出都是采用特有的日志工具进行记录。而日志工具中得到广泛使用的就是 log4j.

Log4j

Java 界里有许多实现日志功能的工具,最早得到广泛使用的是 log4j, 许多应用程序的日志部分都交给了 log4j, 不过作为组件开发者,他们希望自己的组件不要紧紧依赖某一个工具,毕竟在同一个时候还有很多其他很多日志工具,假如一个应用程序用到了两个组件,恰好两个组件使用不同的日志工具,那么应用程序就会有两份日志输出了。

JCL

为了解决这个问题,Apache Commons Logging ( 之前叫 Jakarta Commons Logging, 所以缩写为 JCL )粉墨登场,JCL 只提供 log 接口,具体的实现则在运行时 动态寻找。这样一来组件开发者只需要针对 JCL 接口开发,而调用组件的应用程序则可以在运行时搭配自己喜好的日志实践工具。

所以即使到现在你仍会看到很多程序应用 JCL + log4j 这种搭配,不过当程序规模越来越庞大时,JCL动态绑定 并不是总能成功,具体原因大家可以 Google 一下,这里就不再赘述了。解决方法之一就是在程序部署时 静态绑定 指定的日志工具,这就是 SLF4J 产生的原因。

SLF4j

JCL 一样,SLF4J 也是只提供 log 接口,具体的实现是在打包应用程序时所放入的绑定器( 名字为 slf4j-XXX-version.jar )来决定,XXX 可以是 log4j12, jdk14, jcl, nop 等,他们实现了跟具体日志工具( 比如 log4j )的绑定及代理工作。举个例子:如果一个程序希望用 log4j 日志工具,那么程序只需针对 slf4j-api 接口编程,然后在打包时再放入 slf4j-log4j12-version.jarlog4j.jar 就可以了。

现在还有一个问题,假如你正在开发应用程序所调用的组件当中已经使用了 JCL 的,还有一些组建可能直接调用了 java.util.logging,这时你需要一个桥接器( 名字为 XXX-over-slf4j.jar )把他们的日志输出重定向到 SLF4J, 所谓的桥接器就是一个假的日志实现工具,比如当你把 jcl-over-slf4j.jar 放到 CLASS_PATH 时,即使某个组件原本是通过 JCL 输出日志的,现在却会被 jcl-over-slf4j “骗到” SLF4J 里,然后 SLF4J 又会根据绑定器把日志交给具体的日志实现工具。过程如下。

这时,你可能会发现一个有趣的问题,假如在 CLASS_PATH 里同时放置 log4j-over-slf4j.jarslf4j-log4j12-version.jar 会发生什么情况呢?没错,日志会被踢来踢去,最终进入死循环。

日志搭配组合


日志工具那么多,有门面也有具体实现,那到底如何进行搭配呢?这里主要给出目前最流行的两种搭配:

JCL + Log4j 搭配

这种方式是采用 JCL 作为日志门面抽象接口,具体日志输出使用 Log4j. 具体用到的 Jar 包和资源文件如下:

1
2
3
1. commons-logging-1.1.jar		// JCL 日志门面
2. log4j-1.2.15.jar // Log4j 具体日志输出
3. log4j.properties // Log4j 的日志配置文件

对于 Log4j.properties 如何配置,下面会讲到,这里给出常用日志定义代码:

1
2
3
4
5
6
7
8
9
10
11
12
// 注意导入的包
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
public class A {
private static Log logger = LogFactory.getLog(A.class);
private static void main(String[] args){
logger.debug("This is debug");
logger.info("This is info");
logger.warn("This is warn");
logger.eror("This is error");
}
}

SLF4j + Log4j 搭配

这种方式采用 SLF4j 作为日志门面抽象接口,具体日志输出仍然使用 Log4j. 具体用到的 Jar 包和资源文件如下:

1
2
3
4
1. slf4j-api-1.5.11.jar
2. slf4j-log4j12-1.5.11.jar // slf4j 桥接给 log4j
3. log4j-1.2.15.jar
4. log4j.properties

我们可以看到除了各自的 API jar 包 还有一个 slf4j-log4j12-1.5.11.jar, 这个就是输出流重定向的意思,将 slf4j 接口输出转到具体的 log4j 实现。而假如你目前项目中已经用 JUL 实现日志输出了,你想用此种配置方式怎么办?那就再加一个 jar 包:

1
jul-to-slf4j-1.7.25.jar

或者你已经使用了 JCL 日志门面接口,那如何转,只要加下面的 jar 包:

1
jcl-over-slf4j-1.7.25.jar

从中我们可以看出,slf4j 接口使用还是很广泛的,不管是入口还是出口都有各种对应的 jar 包可供使用的,那它可定制化以及适应性是非常广泛的,因此我推荐大家以后尽量使用 SLF4j 这个日志门面作为通用日志输出接口。

使用 SLF4j 的日志代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
// 注意导入的包和上面的 JCL 不一样的,不要混淆了
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
public class A {
private static Logger logger= LoggerFactory.getLogger(A.class);
private static void main(String[] args){
logger.debug("This is debug");
logger.info("This is info");
logger.warn("This is warn");
logger.eror("This is error: {}","error stack string");
// error 的输出中 {} 是占位符,可以有多个,对应的后面字符串参数也应该多个
}
}

这里我们发现一个不同点,就是 SLF4j 可以用 {} 作为占位符,进行日志字符串的拼接操作,那这个有什么好处呢?这里也说明下:

首先看不用占位符是怎么使用多字符串拼接的:

1
logger.debug("This is debug: " + "debug stack string");

如上所示,完成了一个 debug 日志输出,很多人都是这样实现的,但是大家知道,我们上线的应用不能将 debug 日志输出的,因为 debug 只能在开发调试阶段使用。因此,我们需要配置我们的日志工具,使其只能输出 info, warn, error 的日志信息。那么,logger.debug 这句话内部就会自动判断是否要进行输出,当在内部判断后确实不需要输出!

但是,我们发现一个问题,就是参数字符串拼接都是要先执行的,也就是不管你内部要不要输出,字符串都是要先拼接好才能进入 logger 内部判断的。如果日志记录很少有加的字符串还没多少性能问题,但如果有很多字符串拼接操作,并且拼接很多个字符串,那么会白白地浪费这些字符串拼接过程的性能。因此,正确的做法是:

1
2
3
if (logger.isDebugEnabled()){
logger.debug("This is debug: " + "debug stack string");
}

但是没次输出都要先进行判断是不是太过于重复了,因此,带占位符的字符串拼接操作就诞生了:

1
logger.debug("This is debug: {}" , "debug stack string");

这种方式多个字符串当做参数传入,不会先进行拼接再传入,而是在内部判断后再进行拼接操作,因此这也是 SLF4j 日志工具的一大优势。那下面就主要讲下 SLF4j 的配置参数。

SLF4j 配置


SLF4j 由于其适配广泛,通用性强,因此很多开源项目中都是使用它作为自己的日志记录接口,就如 Hadoop 系列生态。我们在开发 Hadoop 生态应用的时候,常常会在调试的时候打印出:

1
2
3
log4j:WARN No appenders could be found for logger (org.apache.hadoop.util.Shell).  
log4j:WARN Please initialize the log4j system properly.  
log4j:WARN See http://logging.apache.org/log4j/1.2/faq.html#noconfig for more info.

这个就是因为没有配置 log4j.properties 所导致的。那通常的解决方法就是在项目路径里新建一个 log4j.properties, 然后填下面信息就可以了:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
log4j.rootLogger=INFO, stdout, logfile

# 标准输出流:输出到终端
log4j.appender.stdout=org.apache.log4j.ConsoleAppender
log4j.appender.stdout.Encoding=UTF-8
log4j.appender.stdout.Threshold=DEBUG
log4j.appender.stdout.ImmediateFlush=true
log4j.appender.stdout.layout = org.apache.log4j.PatternLayout
log4j.appender.stdout.layout.ConversionPattern=[%p] %-d{yyyy-MM-dd HH:mm:ss} [%c] %m%n

# 文件输出流:输出到日志文件
log4j.appender.logfile=org.apache.log4j.FileAppender
log4j.appender.logfile.Threshold=INFO
log4j.appender.logfile.Encoding=UTF-8
log4j.appender.logfile.File=${user.dir}/logs/mylog.log
log4j.appender.logfile.Append=true
log4j.appender.logfile.layout=org.apache.log4j.PatternLayout
log4j.appender.logfile.layout.ConversionPattern=[%p] %-d{yyyy-MM-dd HH:mm:ss} [%c] %m%n

那这些配置信息到底是什么意思?下面详细讲下。

配置 RootLogger

首先看第 1 行,也就是 log4j.rootLogger 的配置,其语法为:

1
log4j.rootLogger = [ level ] , appenderName1, appenderName2, ...

(1). level : 是日志记录的优先级,分为 OFF、FATAL、ERROR、WARN、INFO、DEBUG、ALL 或者您定义的级别。Log4j 建议只使用四个级别,优先级从高到低分别是 ERROR、WARN、INFO、DEBUG. 如果设置为 INFO 则,ERROR, WARN, INFO 都会输出,而 DEBUG 不会输出。

(2). appenderName : 是日志输出的目的地,名字是 自定义,也可以写多个;上面的配置 stdout 就是一个 appenderName 名字,当然你也可以叫其他名字的。当然你这里定义了这个名字,那么下面就要配置这个名字对应的输出地的相关信息,那下面配置的信息就得和这里设置的一致。下面会讲到。

配置信息输出目的地

上面代码的第 4-712-16 行都是配置日志输出目的地的,目的地有多种类型:

(1). org.apache.log4j.ConsoleAppender(控制台)

1
2
3
Threshold=WARN			// 指定日志消息的输出最低层次。
ImmediateFlush=true // 默认值是true,意谓着所有的消息都会被立即输出。
Target=System.err // 默认情况下是:System.out,指定输出控制台

(2). org.apache.log4j.FileAppender(文件)

1
2
3
4
Threshold=WARN	// 指定日志消息的输出最低层次。
ImmediateFlush=true // 默认值是true,意谓着所有的消息都会被立即输出。
File=mylog.txt // 指定消息输出到mylog.txt文件。
Append=false // 默认值是true,即将消息增加到指定文件中,false指将消息覆盖指定的文件内容。

(3). org.apache.log4j.DailyRollingFileAppender(每天产生一个日志文件)

1
2
3
4
5
6
7
8
9
10
11
Threshold=WARN 		// 指定日志消息的输出最低层次。
ImmediateFlush=true // 默认值是true,意谓着所有的消息都会被立即输出。
File=mylog.txt // 指定消息输出到mylog.txt文件。
Append=false // 默认值是true,即将消息增加到指定文件中,false指将消息覆盖指定的文件内容。
DatePattern=''.''yyyy-ww //每周滚动一次文件,即每周产生一个新的文件。当然也可以指定按月、周、天、时和分。即对应的格式如下:
1)''.''yyyy-MM: 每月
2)''.''yyyy-ww: 每周
3)''.''yyyy-MM-dd: 每天
4)''.''yyyy-MM-dd-a: 每天两次
5)''.''yyyy-MM-dd-HH: 每小时
6)''.''yyyy-MM-dd-HH-mm: 每分钟

(4). org.apache.log4j.RollingFileAppender(文件大小到达指定尺寸的时候产生一个新的文件)

1
2
3
4
5
6
Threshold=WARN		// 指定日志消息的输出最低层次。
ImmediateFlush=true // 默认值是true,意谓着所有的消息都会被立即输出。
File=mylog.txt // 指定消息输出到mylog.txt文件。
Append=false // 默认值是true,即将消息增加到指定文件中,false指将消息覆盖指定的文件内容。
MaxFileSize=100KB // 后缀可以是KB, MB 或者是 GB. 在日志文件到达该大小时,将会自动滚动,即将原来的内容移到mylog.log.1文件。
MaxBackupIndex=2 // 指定可以产生的滚动文件的最大数。

(5). org.apache.log4j.WriterAppender(将日志信息以流格式发送到任意指定的地方)

这个用得相对较少,这里就不介绍了。

日志信息的输出格式

上面代码的第 8-917-18 行都是配置日志输出格式的,也有多种类型:

(1). org.apache.log4j.HTMLLayout(以HTML表格形式布局)

1
2
LocationInfo=true	// 默认值是 false, 输出 java 文件名称和行号
Title=my app file // 默认值是 Log4J Log Messages.

(2). org.apache.log4j.PatternLayout(可以灵活地指定布局模式)

1
2
ConversionPattern=%m%n		// 指定怎样格式化指定的消息。
ConversionPattern=%-4r %-5p %d{yyyy-MM-dd HH:mm:ssS} %c %m%n // 这个模式下面会详细介绍

(3). org.apache.log4j.SimpleLayout(包含日志信息的级别和信息字符串)

1
LocationInfo=true:默认值是false,输出java文件和行号

(4). org.apache.log4j.TTCCLayout(包含日志产生的时间、线程、类别等等信息)

对于 PatternLayout 模式下:

ConversionPattern=%-4r %-5p %d{yyyy-MM-dd HH:mm:ssS} %c %m%n

这里需要说明的就是日志信息格式中几个符号所代表的含义:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
-x号: x信息输出时左对齐;
%p: 输出日志信息优先级,即 DEBUG,INFO,WARN,ERROR,FATAL,
%d: 输出日志时间点的日期或时间,默认格式为 ISO8601,也可以在其后指定格式,比如:%d{yyy MMM dd HH:mm:ss,SSS},输出类似:2002年10月18日 22:10:28,921
%r: 输出自应用启动到输出该log信息耗费的毫秒数
%c: 输出日志信息所属的类目,通常就是所在类的全名
%t: 输出产生该日志事件的线程名
%l: 输出日志事件的发生位置,相当于%C.%M(%F:%L)的组合,包括类目名、发生的线程,以及在代码中的行数。举例:Testlog4.main(TestLog4.java:10)
%x: 输出和当前线程相关联的NDC(嵌套诊断环境),尤其用到像java servlets这样的多客户多线程的应用中。
%%: 输出一个"%"字符
%F: 输出日志消息产生时所在的文件名称
%L: 输出代码中的行号
%m: 输出代码中指定的消息,产生的日志具体信息
%n: 输出一个回车换行符,Windows平台为"\r\n",Unix平台为"\n"输出日志信息换行
可以在%与模式字符之间加上修饰符来控制其最小宽度、最大宽度、和文本的对齐方式。如:
1)%20c:指定输出category的名称,最小的宽度是20,如果category的名称小于20的话,默认的情况下右对齐。
2)%-20c:指定输出category的名称,最小的宽度是20,如果category的名称小于20的话,"-"号指定左对齐。
3)%.30c:指定输出category的名称,最大的宽度是30,如果category的名称大于30的话,就会将左边多出的字符截掉,但小于30的话也不会有空格。
4)%20.30c:如果category的名称小于20就补空格,并且右对齐,如果其名称长于30字符,就从左边交远销出的字符截掉

下面给出一个完整的配置说明:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
# 这里里配置了DEBUG等级,则可显示DEBUG以上的所有信息;
# 配置的输出地有两个apconsole,apfile, 与下面的 后面的log4j.appender.apconsole 最后一个单词相同
log4j.rootLogger = DEBUG, apconsole, apfile

# ========================= apconsole ===========================

# apconsole 目的地配置为:ConsoleAppender(控制台)
log4j.appender.apconsole = org.apache.log4j.ConsoleAppender
# apconsole 输出日志编码 UTF-8
log4j.appender.apconsole.Encoding=UTF-8
# apconsole 的 Threshold: 指定日志消息的输出最低层次为 DEBUG
log4j.appender.apconsole.Threshold=DEBUG
# apconsole 的 ImmediateFlush=true,默认值是true,意谓着所有的消息都会被立即输出
log4j.appender.apconsole.ImmediateFlush=true
# apconsole 的 Target=System.out:默认情况就是是:System.out, 指定输出控制台
# log4j.appender.apconsole.Target=System.out
# apconsole 的信息输出格式为:PatternLayout(可以灵活地指定布局模式)
log4j.appender.apconsole.layout = org.apache.log4j.PatternLayout
# apconsole 的输出格式 ConversionPattern 此处输出为:日志优先级 日志时间 日志所属类全名 日志具体信息 回车
log4j.appender.apconsole.layout.ConversionPattern=[%p] %-d{yyyy-MM-dd HH:mm:ss} [%c] %m%n

# ========================== apfile =============================

# apfile 目的地配置为:DailyRollingFileAppender(每天生成一个文件)
log4j.appender.apfile = org.apache.log4j.DailyRollingFileAppender
# apfile 输出日志文件格式为 UTF-8
log4j.appender.apfile.Encoding=UTF-8
# apfile 输出日志文件地址为 tomcat 主目录的 logs 下的 log.txt
log4j.appender.apfile.File=${catalina.home}/logs/log.txt
# apfile 将消息增加到指定文件中, false 则是覆盖原文件内容
log4j.appender.apfile.Append=true
# apfile 每天产生一个日志文件, 名称为 年月日
log4j.appender.apfile.DatePattern='.'yyyyMMdd
# apfile 采用灵活指定输出布局模式
log4j.appender.apfile.layout = org.apache.log4j.PatternLayout
# apfile 使用 ConversionPattern 此处输出为:日志优先级 日志时间 日志所属类全名 日志具体信息 回车
log4j.appender.apfile.layout.ConversionPattern=[%p] %-d{yyyy-MM-dd HH:mm:ss} [%c] %m%n

参考

苟且一下