浅谈Log之Log4j

一个成熟的项目,日志系统是必不可少的。当前流行的框架有 log4j , logback ,它们都是基于 slf4j 日志规范/接口 开发。本文就来介绍一下 Log4j 。

认识 Log4J

日志架构

Log4j API 采用分层架构,每一层有不同的对象,完成不同的任务。这种分层架构让设计变得灵活,且易于日后扩展。

log4j-framework

核心对象★

涉及一览 说明
Logger 负责获取日志信息,并存储于一个分层的命名空间之中。
Layout 负责格式化日志信息的对象,在发布日志信息之前,它为 appender 对象提供支持。
Appender 负责将日志信息输出到不同目的地,比如数据库、文件、控制台、Unix Syslog 等。

Logger

该对象位于分层架构中的最上层,负责获取日志信息,并存储于一个分层的命名空间之中。

对象获取

Logger 类不允许初始化一个新的实例,但提供了三个静态方法用来获取 Logger 对象:

实际上是从 LogManager 的Logger工厂中 获取 / 设置 和参数同名的Logger对象。

1
2
3
4
5
6
/** 获得根日志对象*/
public static Logger getRootLogger();
/** 通过calzz的名字(getName())获取对应的日志对象,底层和 getLogger(String name)是相同的 */
public static Logger getLogger(Class clazz);
/** 通过字符串获取对应的日志对象 */
public static Logger getLogger(String name);

日志对象继承 & 日志对象名

可以通过Logger对象来触发打印日志。

Logger具有 父子/继承关系 ,且所有日志对象都是RootLogger的子对象。子logger会继承父logger的appender等,触发打印后会从子类向上依次调用appender。

1
2
3
4
5
# 设置两个日志对象,日志对象名为com.ibatis和com.ibatis.common.jdbc.SimpleDataSource
# 其中com.ibatis是com.ibatis.common.jdbc.SimpleDataSource的父logger
# 且com.ibatis包下所有类获得的日志对象都是com.ibatis日志对象的子logger
log4j.logger.com.ibatis=DEBUG
log4j.logger.com.ibatis.common.jdbc.SimpleDataSource=INFO

Logger对象的继承关系是通过 日志对象名 来管理的。

除了以上在获取Logger对象的同时设置日志对象名外,还可以在 log4j.properties 设置并细化配置

1
2
3
4
# 自定义一个日志对象,日志对象名为securityLog
# 从上面的解释可能会认为日志对象securityLog是作用于securityLog包下的类
# 但一般也可以用Logger.getLogger("securityLog")来获取日志对象
log4j.logger.securityLog = INFO,log_security,database

日志信息级别

一旦获取一个有名字的 logger 实例,就可以使用如下方法用于打印日志信息。

注意 :日志信息不一定会被输出,其会被日志对象(及其父logger)的最低日志级别 过滤

并且还需要对应的appender,才能输出到指定位置。

优先级 方法和描述
1 public void fatal(Object message) 使用 Level.FATAL 级别打印日志。
2 public void error(Object message) 使用 Level.ERROR 级别打印日志。
3 public void warn(Object message) 使用 Level.WARN 级别打印日志。
4 public void info(Object message) 使用 Level.INFO 级别打印日志。
5 public void debug(Object message) 使用 Level.DEBUG 级别打印日志。
6 public void trace(Object message) 使用 Level.TRACE 级别打印日志。

最低日志级别

可以设置Logger对象打印日志信息的最低级别,只有大于或等于该级别的信息才会被输出

例如以下配置中:com.ibatis.common.jdbc.SimpleDataSource 日志对象会无法输出日志,而 com.ibatis.Demo 日志对象可以使用根日志对象的appender(CONSOLE)将日志信息输出到控制台。

1
2
3
4
5
6
7
8
9
10
# 设置根日志对象最低日志级别为INFO,并设置一个appender(CONSOLE)
log4j.rootLogger = INFO,CONSOLE

log4j.logger.com.ibatis=INFO
log4j.logger.com.ibatis.common.jdbc.SimpleDataSource=DEBUG

# print the message to console(ENABLE)
log4j.appender.CONSOLE = org.apache.log4j.ConsoleAppender
log4j.appender.CONSOLE.layout = org.apache.log4j.PatternLayout
log4j.appender.CONSOLE.layout.ConversionPattern = %p %d{yyyy-MM-dd HH:mm:ss} [%c] - %m%n
1
2
//最低日志级别也可以在java中设置
log.setLevel(Level.WARN);

Layout

负责格式化日志信息的对象,在发布日志信息之前,它为 appender 对象提供支持。

所有 Layout 对象从 Appender 对象那里接收一个 LoggingEvent 对象,然后从 LoggingEvent 对象那里获取信息,并使用恰当的 ObjectRenderer 对象获取该信息的字符串形式。

位于继承关系顶层的是抽象类 org.apache.Log4j.Layout,这是所有 Log4j API 中 Layout 类的基类。

在应用中我们从不直接使用该类,而是使用它的子类,如下所示:

其中最常用的是 PatternLayout , 本wiki主要主要针对其进行讲解。

Appender

该对象位于分层架构中的较低一层,负责将日志信息输出到不同目的地,比如数据库、文件、控制台、Unix Syslog 等。

支持对象☆

这些是框架的可选对象,它们支持核心对象做一些额外的任务,同样在框架中发挥着重要作用。

Level

Level 对象定义了日志信息的粒度和优先级。

org.apache.Log4j.Level 类定义了日志级别,您可通过继承 Level 类定制自己的级别。

级别(优先级:desc) 描述
OFF 最高级别,用于关闭日志。
FATAL 指明非常严重的错误事件,可能会导致应用终止执行。
ERROR 指明错误事件,但应用可能还能继续运行。
WARN 指明潜在的有害状况。
INFO 指明描述信息,从粗粒度上描述了应用运行过程。
DEBUG 指明细致的事件信息,对调试应用最有用。
TRACE 比 DEBUG 级别的粒度更细。
ALL 所有级别,包括定制级别。

Filter

Filter 对象用来分析日志信息,进而决定该条日志是否被记录。

一个 Appender 对象可对应多个 Filter 对象,当日志信息传给 Appender 对象时,与其关联的所有 Filter 对象需要判断是否将日志信息发布到目的地。

ObjectRenderer

ObjectRenderer 对象负责为传入日志框架的不同对象提供字符串形式的表示,Layout 对象使用该对象来准备最终的日志信息。

LogManager

LogManager 对象管理日志框架,它负责从系统级的配置文件或类中读取初始配置参数。

使用 Log4j

单纯的使用只需要 slf4j-api , log4j ; 一些项目需要 slf4j-log4j12 (如: MyBatis )。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
<!-- log (slf4j, log4j, slf4j-log4j整合)-->
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-api</artifactId>
<version>1.7.7</version>
</dependency>
<dependency>
<groupId>log4j</groupId>
<artifactId>log4j</artifactId>
<version>1.2.17</version>
</dependency>
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-log4j12</artifactId>
<version>1.7.25</version>
</dependency>

log4j.properties

默认情况下, LogManager 会在 classpath 中寻找 log4j.properties 文件。

以下是较为详细的配置示例,模式转换字符请参考:附录:PatternLayout

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
# 设置根日志,最低日志级别为INFO,并添加了2个appender(CONSOLE,log_error)确定输出详情
log4j.rootLogger = INFO,CONSOLE,log_error

# 自定义securityLog日志对象,最低日志级别为INFO,并添加了2个appender(log_security,database)确定输出详情
log4j.logger.securityLog = INFO,log_security,database

log4j.logger.com.ibatis=DEBUG
log4j.logger.com.ibatis.common.jdbc.SimpleDataSource=INFO
log4j.logger.com.ibatis.common.jdbc.ScriptRunner=INFO
log4j.logger.com.ibatis.sqlmap.engine.impl.SqlMapClientDelegate=DEBUG
log4j.logger.java.sql.Connection=DEBUG
log4j.logger.java.sql.Statement=INFO
log4j.logger.java.sql.PreparedStatement=DEBUG,stdout

# 使用控制台输出日志
log4j.appender.CONSOLE = org.apache.log4j.ConsoleAppender
log4j.appender.CONSOLE.layout = org.apache.log4j.PatternLayout
log4j.appender.CONSOLE.layout.ConversionPattern = %p %d{yyyy-MM-dd HH:mm:ss} [%c] - %m%n
# 使用文件输出日志
log4j.appender.log_security = org.apache.log4j.DailyRollingFileAppender
log4j.appender.log_security.file = D:/PHIMP/logs/log_security
log4j.appender.log_security.DatePattern = '_'yyyyMMdd'.log'
log4j.appender.log_security.layout = org.apache.log4j.PatternLayout
log4j.appender.log_security.layout.ConversionPattern = %p %d{yyyy-MM-dd HH:mm:ss} [%c] - %m%n
# 使用文件输出日志,最低级别为error
log4j.appender.log_error = org.apache.log4j.DailyRollingFileAppender
log4j.appender.log_error.Threshold = error
log4j.appender.log_error.file = D:/PHIMP/logs/log_error
log4j.appender.log_error.DatePattern = '_'yyyyMMdd'.log'
log4j.appender.log_error.layout = org.apache.log4j.PatternLayout
log4j.appender.log_error.layout.ConversionPattern = %p %d{yyyy-MM-dd HH:mm:ss} [%c] - %m%n
# 使用数据库输出日志,最低级别为info
# 推荐继承org.apache.Log4j.jdbc.JDBCAppender(org.log.LogJdbcAppender)直接获得数据库相关信息
log4j.appender.database = org.log.LogJdbcAppender
log4j.appender.database.Threshold = info
log4j.appender.database.layout = org.apache.log4j.PatternLayout
log4j.appender.database.sql = INSERT INTO %X{tableName} (%X{fields}) VALUES (%X{values})

*.java

获取日志对象,设置日志信息。

控制台 / 文件 输出日志

控制台输出: [INFO] 2017-01-17 17:01:17 [org.Demo] - 用户**访问了***

1
2
3
4
5
6
7
8
9
10
package org.Demo;
import org.apache.log4j.Logger;
public class Demo{
// 获取与类同名的日志对象
private Logger logger = Logger.getLogger(this.getClass());

public void log(){
logger.info("用户**访问了***");
}
}

数据库 输出日志

JDBCAppender.java

继承org.apache.Log4j.jdbc.JDBCAppender直接获得数据库相关信息,不需要单独配置

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
38
39
40
41
42
43
44
45
46
47
48
49
public class LogJdbcAppender extends JDBCAppender {
protected final Logger logger = Logger.getLogger(getClass());

public LogJdbcAppender() {
super();
}

@Override
protected Connection getConnection() {
try {
connection =((SqlMapClient)ServiceLocator.getBean("sqlMapClient")).getDataSource().getConnection();
return connection;
} catch (Exception e) {
e.printStackTrace();
}

return null;
}

@Override
protected void closeConnection(Connection con) {
try {
if (con != null && !con.isClosed()) {
con.close();
con = null;
}
} catch (Exception e) {
e.printStackTrace();
}
}

@Override
public void flushBuffer() {
removes.ensureCapacity(buffer.size());
for (Iterator i = buffer.iterator(); i.hasNext();) {
try {
LoggingEvent logEvent = (LoggingEvent)i.next();
String sql = getLogStatement(logEvent);
removes.add(logEvent);
execute(sql);
}
catch (SQLException e) {
errorHandler.error("Failed to excute sql", e, ErrorCode.FLUSH_FAILURE);
}
}
buffer.removeAll(removes)
removes.clear();
}
}

调用

控制台/文件输出: [INFO] 2017-01-17 17:01:17 [org.Demo] - 用户**访问了***

数据库表 VISIT_LOG 插入了 values 记录。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
package org.Demo;
public class Demo{
// 获取securityLog日志对象
private Logger logger = Logger.getLogger("securityLog");

public void log(){
// 字段
String fields = "ID, USER_ID, USER_NAME, RESOURCES_ID, VISIT_TIME";
StringBuffer values = new StringBuffer();
//...设置values值

MDC.put("tableName", "VISIT_LOG");
MDC.put("fields", fields);
MDC.put("values", values.toString());
logger.info("用户**访问了***");
}
}

附录

PatternLayout

PatternLayout 是一个简单的 Layout 对象,提供了如下属性,该属性可通过配置文件更改:

序号 属性 & 描述
1 conversionPattern设置转换模式,默认为 %r [%t] %p %c %x - %m%n。

模式转换字符

下面的表格解释了上面模式中用到的字符,以及所有定制模式时能用到的字符:

转换字符 含义
c 输出日志对象名,一般为发起记录日志请求类的全名/自设置的日志对象名。比如对于类 org.apache.xyz.SomeClass ,模式 %C{1} 会输出 SomeClass 。也有可能是自定义日志对象名,比如 log4j.logger.securityLog = INFOsecurityLog
d 输出记录日志的日期,比如 %d{HH:mm:ss,SSS}%d{dd MMM yyyy HH:mm:ss,SSS}
F 在记录日志时,使用它输出文件名。
l 输出生成日志的调用者的地域信息。
L 输出发起日志请求的行号。
m 输出和日志事件关联的,由应用提供的信息。
M 输出发起日志请求的方法名。
n 输出平台相关的换行符。
p 输出日志事件的优先级。
r 输出从构建布局到生成日志事件所花费的时间,以毫秒为单位。
t 输出生成日志事件的线程名。
x 输出和生成日志事件线程相关的 NDC (嵌套诊断上下文)。
X 该字符后跟 MDC 键,比如 X{clientIP} 会输出保存在 MDC 中键 clientIP 对应的值。
% 百分号, %% 会输出一个 %

格式修饰符

缺省情况下,信息保持原样输出。但是借助格式修饰符的帮助,就可调整最小列宽、最大列宽以及对齐。

下面的表格涵盖了各种修饰符:

格式修饰符 左对齐 最小宽度 最大宽度 注释
%20c 20 如果列名少于 20 个字符,左边使用空格补齐。
%-20c 20 如果列名少于 20 个字符,右边使用空格补齐。
%.30c 不适用 30 如果列名长于 30 个字符,从开头剪除。
%20.30c 20 30 如果列名少于 20 个字符,左边使用空格补齐,如果列名长于 30 个字符,从开头剪除。
%-20.30c 20 30 如果列名少于 20 个字符,右边使用空格补齐,如果列名长于 30 个字符,从开头剪除。