原文标题:Java日志通关(五) - 最佳实践
原文作者:阿里云开发者
冷月清谈:
要点二:避免打印分隔线
- 使用关键字标记日志,方便快速过滤。
要点三:避免因写日志而抛错
- 判空后再调用方法,避免 NPE。
要点四:使用 Fastjson 参数
- 使用 IgnoreErrorGetter 参数忽略 getter 中的异常。
- 使用 IgnoreNonFieldGetter 参数忽略没有实体字段对应的 getter 方法。
要点五:不要遗漏异常堆栈
- 将异常参数不占用字符模板,保证异常堆栈输出完整。
要点六:限制日志输出长度
- 使用格式修饰符限制日志文本和堆栈层级。
要点七:将堆栈合并为一行
- 使用 Logback 配置将堆栈合并为一行。
要点八:不建议使用 %method 和 %line
- 获取堆栈轨迹消耗资源,建议使用其他方式输出方法名和行号。
要点九:不要将日志输出至控制台
- 控制台输出的日志无人关注,浪费资源。
要点十:抛弃 LogUtil
- Slf4j 和 Logback 已提供所需功能,不应再使用 LogUtil。
要点十一:熟读日志规约
- 参考《阿里巴巴Java开发手册》中的日志规约。
要点十二:注意 Key-Value 分隔符
- 使用冒号或等号分隔 Key-Value,根据输出格式而定。
怜星夜思:
2、为什么建议将堆栈合并为一行输出?
3、在 Logback 中,如何只打印前 50 层堆栈,并将其转换为一行输出?
原文内容
阿里妹导读
一、总是使用接口层
<dependency> <groupId>ch.qos.logback</groupId> <artifactId>logback-core</artifactId> <version>${logback.version}</version> <scope>runtime</scope> <optional>true</optional> </dependency>
简单解释一下:
-
<scope>runtime</scope>:runtime 的包编译时会被忽略(认为运行环境已经有对应包了);
-
<optional>true</optional>:依赖不会传递,Maven 不会自动安装此包;
二、不要打印分隔线
三、避免因写日志而抛错
Object result = rpcResource.call();
// 如果 result 为 null 会抛 NPE
log.info(“result.userId={}”, result.getUserId());
这个问题老生常谈,这里不展开说了。
四、两个 Fastjson 参数
4.1 IgnoreErrorGetter
public class Foo { private Long userId; @Deprecated private Long accountId;
// getter 有异常抛出
public Long getAccountId() {
throw new RuntimeException(“请使用 userId”);
}
}
// 这样打印日志,就不会被 getter 抛出的异常阻断了
log.info(“foo={}”, JSON.toJSONString(foo, SerializerFeature.IgnoreErrorGetter));
4.2 IgnoreNonFieldGetter
@Data public class Result<T> { private boolean success; private T data;
public boolean isError() {
return !success;
}
}
// 这样打印日志,就不会有 “error”:false 了
log.info(“result={}”, JSON.toJSONString(result, SerializerFeature.IgnoreNonFieldGetter));
这个参数对于打印 Result 包装类非常有帮助。如果打印出 "error":false,那当你希望使用 error 关键字查询错误时,就会匹配到很多包含 error 却并非错误的无效数据。
五、不要遗漏异常堆栈
Exception e = new RuntimeException("blahblahblah"); log.error("exception={}", e); // 此时 IDEA 会给出警告:参数比占位符少
此时因为 e 与对应的{}位置匹配,Slf4j 会尝试将异常转为字符串拼到日志模板中,最终这句相当于:
log.error("exception={}", e.toString());
// 用 e.getMessage() 拼到日志信息后,同时有独立的 e 用于打印堆栈
log.error("exception={}", e.getMessage(), e);
最终会输出:
exception=blahblahblah 换行后会有堆栈信息
六、限制日志输出长度
6.1 限制日志文本最大长度
有时候一个 POJO 非常大,当我们通过 :
log.info("result={}", JSON.toJSONString(result))
%.-2000message
6.2 限制堆栈的层级
七、将堆栈合并为一行
%replace(%exception){'[\r\n\t]+', ' '}%nopex
简单说明一下:
-
%replace(p){r, t}:将给到的 p,使用正则 r 进行匹配,命中的替换为 t,所以上边就是,将 %exception 中的 [\r\n\t](即换行、回车、Tab)替换为 (四个空格);
-
%nopex:如果不加,Logback 会自动在日志最后追加 %exception,导致异常堆栈打两遍(一遍我们自己转为一行的,一遍带原始换行的);
%.-10000replace(%exception{50}){'[\r\n\t]+', ' '}%nopex
即:
-
只打印前 50 层堆栈;
-
转为一行后,再限制最大长度为 10000,超过的部分丢弃尾部字符;
八、不建议使用 %method 和 %line
log.info("queryUserInfo, request={}, result={}", request, result);
九、不要将日志输出至 Console
-
机器上线后,没有人会盯着控制台看,所以输出至控制台毫无意义,还浪费机器资源;
-
本地 Debug 时,要么直接加断点,要么会翻日志文件,也基本不会检查控制台输出;
-
通过 main 函数跑测试代码时,一般直接用 System.out.println,不涉及日志系统;
十、无用的 LogUtil
-
实现日志内容拼接,请见第三篇【3.1 info 方法】一节;
-
实现日志参数默认转 JSON;
-
日志超过最大长度截断,请见【6.1 限制日志文本最大长度】一节;
-
将异常堆栈合并在同一行输出,请见【七、将堆栈合并为一行】一节;
-
通过动态开关控制是否打印某些日志;
-
日志中追加 traceId,请见第三篇【五、MDC】、第四篇【五、MDC 中的 traceId】两节;
十一、熟读《日志规约》
十二、一个小细节
@Data public class Foo { private String bar; }
Foo foo = new Foo();
foo.setBar(“baz”);// 方案一(注意第一个参数里的冒号)
log.info(“foo:{}”, foo);
// 输出 foo:Foo{bar=baz}
// 方案二(注意第一个参数里的等号)
log.info(“foo={}”, JSON.toJSONString(foo));
// 输出 foo={“bar”:“baz”}
看出两者的区别了吗?
[1]https://logback.qos.ch/manual/layouts.html#Evaluators
[2]https://github.com/alibaba/p3c