Appearance
日志规范
1. 背景
通常,Java 程序员在开发项目时都是依赖 Eclipse/IDEA 等集成开发工具的 Debug 调试功能来跟踪解决 Bug,但项目发布到了测试、生产环境并不能这么做。所以,日志的作用就是在测试、生产环境没有 Debug 调试工具时开发和测试人员定位问题的手段。
日志打得好,就能根据日志的轨迹快速定位并解决线上问题,反之,日志输出不好,不仅无法辅助定位问题反而可能会影响到程序的运行性能和稳定性。
2. 目的
- 问题追踪:辅助排查和定位线上问题,优化程序运行性能;
- 状态监控:通过日志分析,可以监控系统的运行状态;
- 安全审计:审计主要体现在安全上,可以发现非授权的操作;
- 业务指导:通过统计和分析相关的业务、用户行为日志,对业务进行预测指导。
4. 记录日志的时机
在看线上日志的时候,我们可曾陷入到日志泥潭?该出现的日志没有,无用的日志一大堆,或者需要的信息分散在各个角落,特别是遇到紧急的在线 bug 时,有效的日志被大量无意义的日志信息淹没,焦急且无奈地浪费大量精力查询日志。那什么是记录日志的合适时机呢?
总结几个需要写日志的点:
- 编程语言提示异常
如今各类主流的编程语言都包括异常机制,业务相关的流行框架有完整的异常模块。这类捕获的异常是系统告知开发人员需要加以关注的,是质量非常高的报错。应当适当记录日志,根据实际结合业务的情况使用 warn
或者 error
级别。
- 业务流程预期不符
除开平台以及编程语言异常之外,项目代码中结果与期望不符时也是日志场景之一,简单来说所有流程分支都可以加入考虑。取决于开发人员判断能否容忍情形发生。常见的合适场景包括外部参数不正确,数据处理问题导致返回码不在合理范围内等等。
- 系统核心角色,组件关键动作
系统中核心角色触发的业务动作是需要多加关注的,是衡量系统正常运行的重要指标,建议记录 INFO
级别日志,比如电商系统用户从登录到下单的整个流程;微服务各服务节点交互;核心数据表增删改;核心组件运行等等,如果日志频度高或者打印量特别大,可以提炼关键点 INFO
记录,其余酌情考虑 DEBUG
级别。
- 系统初始化
系统或者服务的启动参数。核心模块或者组件初始化过程中往往依赖一些关键配置,根据参数不同会提供不一样的服务。务必在这里记录 INFO
日志,打印出参数以及启动完成态服务表述。
5. 日志规范
5.1 日志变量定义
日志变量往往不变,最好定义成 final static
,变量名用大写。
java
private static final Logger log = LoggerFactory.getLogger(SimpleClassName.getClass());
通常一个类只有一个 log
对象,如果有父类可以将 log
定义在父类中。
推荐引入 lombok 的依赖,在类的头部加上 @Slf4j
的注解,之后便可以在程序的任意位置使用 log
变量打印日志信息了,使用起来更加简洁一点,在重构代码尤其是修改类名的时候无需改动原有代码。
5.2 参数占位格式
- 使用参数化形式
{}
占位,[]
进行参数隔离
java
log.debug("Save order with order no:[{}], and order amount:[{}]");
log.debug("Save order with order no:[{}], and order amount:[{}]");
这种可读性好,这样一看就知道 []
里面是输出的动态参数,{}
用来占位类似绑定变量,而且只有真正准备打印的时候才会处理参数,方便定位问题。
- 如果日志框架不支持参数化形式,且日志输出时不支持该日志级别时会导致对象冗余创建,浪费内存,此时就需要使用
isXXEnabled
判断,如:
java
if (log.isDebugEnabled()) {
// 如果日志不支持参数化形式,debug又没开启,那字符串拼接就是无用的代码拼接,影响系统性能
log.debug("Save order with order no:"+orderNo +", and order amount:"+orderAmount);
}
至少 debug
级别是需要开启判断的,线上日志级别至少应该是 info
以上的。
这里推荐大家用 SLF4J
的门面接口,可以用参数化形式输出日志,debug
级别也不必用 if
判断,简化代码。
5.3 日志内容
- 禁用
System.out.println
和System.err.println
; - 输出日志的对象,应在其类中实现快速的
toString()
方法,以便于在日志输出时仅输出这个对象类名和hashCode()
- 预防空指针:不要在日志中调用对象的方法获取值,除非确保该对象肯定不为
null
,否则很有可能会因为日志的问题而导致应用产生空指针异常。
java
// 不推荐
log.debug( "Load student(id={}), name: {}",id, student.getName() );
// 推荐
log.debug( "Load student(id={}), student: {}",id, student);
- 避免输出拼接日志,对于一些一定需要进行拼接字符串,或者需要耗费时间、浪费内存才能产生的日志内容作为日志输出时,应使用
log.isXxxxxEnable()
进行判断后再进行拼接处理,比如:
java
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);
}
5.4 exception 日志
- 输出
Exceptions
的全部Throwable
信息;因为log.error(msg)
和log.error(msg,e.getMessage())
这样的日志输出方法会丢失掉最重要的StackTrace
信息。
java
void foo() {
try {
//do somehing
} catch (Exception e) {
//错误示范
log.error(e.getMessage());
//错误示范
log.erroe("Bad Things", e.getMessage());
//正确演示
log.error("Bad Things", e);
}
}
- 不允许记录日志后又抛出异常。如捕获异常后又抛出了自定义业务异常,此时无需记录错误日志,由最终捕获方进行异常处理。不能又抛出异常,又打印错误日志,不然会造成重复输出日志。
java
void foo() throws LogException {
try {
//do somehing
} catch (Exception e) {
//正确
log.error("Bad Things", e);
throw new LogException("Bad Things", e);
}
}
5.5 禁止项
- 禁止
System.out.println()
和System.error.println()
语句; - 禁止出现
printStackTrace()
; - 禁止大循环中打印日志 例如:
java
for (int i = 0; i < 2000; i++) {
log.info("XX");
}
- 禁止在线上环境开启
debug
级别日志输出。