参考:Java日志记录最佳实践
log4j、Logging、commons-logging、slf4j、logback,开发的同学对这几个日志相关的技术不陌生吧,为什么有这么多日志技术,它们都是什么区别和联系呢?且看下文分解:
这是 Java 自带的日志工具类,在 JDK 1.5 开始就已经有了,在 java.util.logging 包下。通常情况下,这个基本没什么人用了,了解一下就行。
commons-logging 是日志的门面接口,它也是Apache 最早提供的日志门面接口,用户可以根据喜好选择不同的日志实现框架,而不必改动日志定义,这就是日志门面的好处,符合面对接口抽象编程。现在已经不太流行了,了解一下就行。
slf4j,英文全称为“Simple Logging Facade for Java”,为java提供的简单日志Facade。Facade门面,更底层一点说就是接口。它允许用户以自己的喜好,在工程中通过slf4j接入不同的日志系统。
因此slf4j入口就是众多接口的集合,它不负责具体的日志实现,只在编译时负责寻找合适的日志系统进行绑定。具体有哪些接口,全部都定义在slf4j-api中。查看slf4j-api源码就可以发现,里面除了public final class LoggerFactory类之外,都是接口定义。因此slf4j-api本质就是一个接口定义。
注意:log4j 在 2015.08.05 这一天被 Apache 宣布停止维护了,用户需要切换到 Log4j2上面去。 Log4j 是 Apache 的一个开源日志框架,也是市场占有率最多的一个框架。
Log4j 2 Apache Log4j 2是apache开发的一款Log4j的升级产品。
Log4j2与Log4j1发生了很大的变化,log4j2不兼容log4j1。
Logback 是 Slf4j 的原生实现框架,同样也是出自 Log4j 一个人之手,但拥有比 log4j 更多的优点、特性和更做强的性能,现在基本都用来代替 log4j 成为主流。
Logback相对于log4j拥有更快的执行速度。基于我们先前在log4j上的工作,logback 重写了内部的实现,在某些特定的场景上面,甚至可以比之前的速度快上10倍。在保证logback的组件更加快速的同时,同时所需的内存更加少。
选项太多了的后果就是选择困难症,我的看法是没有最好的,只有最合适的:
在看线上日志的时候,我们可曾陷入到日志泥潭?该出现的日志没有,无用的日志一大堆,或者需要的信息分散在各个角落,特别是遇到紧急的在线bug时,有效的日志被大量无意义的日志信息淹没,焦急且无奈地浪费大量精力查询日志。那什么是记录日志的合适时机呢?
日志变量往往不变,最好定义成 final static,变量名用大写。
private static final Logger log = LoggerFactory.getLogger({SimpleClassName}.getClass());
通常一个类只有一个 log 对象,如果有父类可以将 log 定义在父类中。
日志变量类型定义为门面接口(如 slf4j 的 Logger),实现类可以是 Log4j、Logback 等日志实现框架,不要把实现类定义为变量类型,否则日志切换不方便,也不符合抽象编程思想。
使用参数化形式 {} 占位, [] 进行参数隔离
log.debug("Save order with order no:[{}], and order amount:[{}]");
这种可读性好,这样一看就知道[]里面是输出的动态参数,{}用来占位类似绑定变量,而且只有真正准备打印的时候才会处理参数,方便定位问题。
如果日志框架不支持参数化形式,且日志输出时不支持该日志级别时会导致对象冗余创建,浪费内存,此时就需要使用 isXXEnabled
判断,如:
if(log.isDebugEnabled()){
// 如果日志不支持参数化形式,debug又没开启,那字符串拼接就是无用的代码拼接,影响系统性能
log.debug("Save order with order no:" + orderNo + ", and order amount:" + orderAmount);
}
至少 debug 级别是需要开启判断的,线上日志级别至少应该是 info 以上的。
日志输出主要在文件中,应包括以下内容:
11:44:44.827 WARN [93ef3E0120160803114444] [main] [ClassPathXmlApplicationContext] Exception encountered during context initialization - cancelling refresh attempt
作为日志产生的日期和时间,这个数据非常重要,一般精确到毫秒。由于线上一般配置为按天滚动日志文件,日期标识在文件名上,所以可以不放在这个时间中,使用 HH:mm:ss.SSS 格式即可。非要加上也未尝不可,格式推荐:yyyy-MM-dd HH:mm:ss.SSS。
主要使用如下的四个级别:
问题定位:
log.error(“[接口名或操作名] [Some Error Msg] happens. [Probably Because]. [Probably need to do] [params] .”);
log.error(“[接口名或操作名] [Some Error Msg] happens. [Probably Because]. [please contact xxx@xxx] [params] .”);
在分布式应用中,用户的一个请求会调用若干个服务完成,这些服务可能还是嵌套调用的,因此完成一个请求的日志并不在一个应用的日志文件,而是分散在不同服务器上不同应用节点的日志文件中。该标识是为了串联一个请求在整个系统中的调用日志。
调用链标识格式:
调用链标识作为可选项,无该数据时只输出 [] 即可。
输出该日志的线程名称,一般在一个应用中一个同步请求由同一线程完成,输出线程名称可以在各个请求产生的日志中进行分类,便于分清当前请求上下文的日志。
日志记录器名称一般使用类名,日志文件中可以输出简单的类名即可,看实际情况是否需要使用包名和行号等信息。主要用于看到日志后到哪个类中去找这个日志输出,便于定位问题所在。
// 不推荐
log.debug( "Load student(id={}), name: {}" , id , student.getName() );
// 推荐
log.debug( "Load student(id={}), student: {}" , id , student );
对于一些一定需要进行拼接字符串,或者需要耗费时间、浪费内存才能产生的日志内容作为日志输出时,应使用 log.isXxxxxEnable() 进行判断后再进行拼接处理,比如:
if (log.isDebugEnable()) {
StringBuilder builder = new StringBuilder();
for (Student student : students) {
builder.append("student: ").append(student);
}
builder.append("value: ").append(JSON.toJSONString(object));
log.debug( "debug log example, detail: {}" , builder );
}
异常堆栈一般会出现在 ERROR 或者 WARN 级别的日志中,异常堆栈含有方法调用链的系统,以及异常产生的根源。异常堆栈的日志属于上一行日志的,在日志收集时需要将其划至上一行中。
日志文件放置于固定的目录中,按照一定的模板进行命名,推荐的日志文件名称:
当前正在写入的日志文件名:<应用名>[-<功能名>].log
已经滚入历史的日志文件名:<应用名>[-<功能名>].log.<yyyy-MM-dd>
根据不同的环境配置不同的日志输出方式:
logback 日志工具可以在日志文件滚动后将前一文件进行压缩,以减少磁盘空间占用,若使用 logback 对于日志量庞大的应用建议开启该功能。
具体的配置示例,由于篇幅较长,单独开一篇介绍。详情可移步:日志使用项目实战
Interface interface = new InterfaceImpl();
日志框架里面也是如此,上面也说了,日志有门面接口,有具体实现的实现框架,所以大家不要面向实现编程。
log.error(msg)
和log.error(msg,e.getMessage())
这样的日志输出方法会丢失掉最重要的StackTrace信息。
void foo(){ try{ //do somehing }catch(Exception e){ log.error(e.getMessage());//错误示范 log.erroe("Bad Things",e.getMessage());//错误示范 log.error("Bad Things",e);//正确演示 } } System.out.println()
和System.error.println()
语句。因为这个只会打印到控制台,而不会记录到日志文件中,不方便管理日志。此外,标准输出不会显示类名和行号信息,一旦代码中大量出现标准输出的代码,且日志中打印有标准输出的内容,很难定位日志内容和日志打印的位置,根本无法排查问题,想删除无用日志输出也改不动,这个是笔者在重构古董代码的时候亲自踩过的一个坑。
void foo(){ try{ //do somehing }catch(Exception e){ Syste.out.println(e.getMessage());//错误 System.error.println(e.getMessage());//错误 log.error("Bad Things",e);//正确 } } 来看一下它的源码:
public void printStackTrace() {
printStackTrace(System.err);
}
它其实也是利用 System.err
输出到了Tomcat控制台。
<debug、info、warn>
。一是因为项目本身 debug 日志太多,二是各种框架中也大量使用 debug 的日志,线上开启 debug 不久就会打满磁盘,影响业务系统的正常运行。
for(int i=0; i<2000; i++){
log.info("XX");
}
最好的办法是在循环中记录要点,在循环外面总结打印出来。
@Override
@Transactional
public void createUserAndBindMobile(@NotBlank String mobile, @NotNull User user) throws CreateConflictException{
boolean debug = log.isDebugEnabled();
if(debug){
log.debug("开始创建用户并绑定手机号. args[mobile=[{}],user=[{}]]", mobile, LogObjects.toString(user));
}
try {
user.setCreateTime(new Date());
user.setUpdateTime(new Date());
userRepository.insertSelective(user);
if(debug){
log.debug("创建用户信息成功. insertedUser=[{}]",LogObjects.toString(user));
}
UserMobileRelationship relationship = new UserMobileRelationship();
relationship.setMobile(mobile);
relationship.setOpenId(user.getOpenId());
relationship.setCreateTime(new Date());
relationship.setUpdateTime(new Date());
userMobileRelationshipRepository.insertOnDuplicateKey(relationship);
if(debug){
log.debug("绑定手机成功. relationship=[{}]",LogObjects.toString(relationship));
}
log.info("创建用户并绑定手机号. userId=[{}],openId=[{}],mobile=[{}]",user.getId(),user.getOpenId(),mobile);
}catch(DuplicateKeyException e){
log.info("创建用户并绑定手机号失败,已存在相同的用户. openId=[{}],mobile=[{}]",user.getOpenId(),mobile);
throw new CreateConflictException("创建用户发生冲突, openid=[%s]",user.getOpenId());
}
}
切记不要 log 密码及个人信息相关的内容!为了便于进行问题定位,以下是涉及敏感信息日志输出时最为宽松(明文显示的数据只能更少,不能更多)的要求:
类型 | 要求 | 示例 | 说明 |
---|---|---|---|
密码 | 不输出 | ****** | 登录密码、支付密码等各种类型的密码 |
信用卡 CVV2 | 不输出 | *** | |
信用卡有效期 | 不输出 | **** | |
验证码 | 不输出 | ****** | 图形验证码、短信验证码、邮件验证码等 |
密钥、盐 | 不输出 | ****** | 用于加解密算法的密钥,消息摘要的盐,以及数字签名及签名验证算法所使用的公私钥对等 |
会话 ID设备指纹 (ID)指纹 token密文数据 | 前 5 后 5 | *** | 主要有以下类型:1. 应用的会话标识,比如:Web、APP、H5 等用于识别会话状态信息的标识2. APP 标识设备的设备指纹或者设备 ID3. APP 用于指纹验证的 token4. 密文数据指的是加密后的数据被掩码的字符无论多少位都输出 3 个 * |
银行卡卡号 | 前 6 后 4 | 622666**0831 | 银行卡卡号最多 19 位数字 |
手机号 | 前 3 后 4 | 137****9574 | 定长 11 位数字 |
身份证号 | 前 1 后 1 | 3**X | 定长 18 位数字 |
姓名 | 隐姓 | *世仁 | 将姓氏隐藏 |
IP 地址 | 前 1 后 1 | 10...27 | 隐藏 IP 地址的第 2、第 3 段 |
邮箱地址 | 前 1 后 1 | w**3@gmail.com | 仅对 @ 之前的邮箱名称进行掩码,掩码部分不管多少位均输出 *** |
地址 | 隐号码 | 上海市西藏北路 *** 号 *** 楼 *** 室 |
上述仅列取出部分数据的显示要求,其他的显示原则为通过掩码后的数据无法得知原始数据。
实现了如上掩码的工具类,参考:https://github.com/frankiegao123/mask-utils