版权声明:本文为博主原创文章,转载请注明出处:https://twocups.cn/index.php/2023/08/01/53/
一、日志规约的好处
为什么要有日志规约呢?
- 日志作为最典型的基础设施,遍布着所有的应用。它会直接影响我们的应用资源使用情况和应用性能,从而影响用户使用体验。
- 日志和监控告警绑定后,可以达到更细粒度的监控,在主力监控告警前提前知晓线上问题。有好的日志规约,可以避免每次复盘Action中再出现完善监控这一措施。
- 每一次的用户请求对我们来说都是极其珍贵的用户反馈信息。我们可以通过解析日志来分析用户喜好,从而调整我们的战略。而好的日志规约可以让用户信息的解析更加高效。
二、日志规约
阿里巴巴日志规约摘要
以阿里巴巴开发规约中的日志规约为例,我们来讲讲日志规约。其中每一条都很重要,我挑选几条可能大家比较容易忽略的。
阿里日志规约5【强制】
在日志输出时,字符串变量之间的拼接使用占位符的方式。
说明:因为String字符串的拼接会使用StringBuilder的append()方式,有一定的性能损耗。使用占位符仅是替换动作,可以有效提升性能。
正确例子:
logger.debug("Processing trade with id: {} and symbol: {}", id, symbol);
阿里日志规约6【强制】
对于trace/debug/info级别的日志输出,必须进行日志级别的开关判断。
说明:虽然在debug(参数)的方法体内第一行代码isDisabled(Level.DEBUG_INT)为真时(Slf4j的常见实现Log4j和Logback),就直接return,但是参数可能会进行字符串拼接运算。此外,如果debug(getName())这种参数内有getName()方法调用,无谓浪费方法调用的开销。
正确例子:
// 如果判断为真,那么可以输出trace和debug级别的日志 if (logger.isDebugEnabled()) { logger.debug("Current ID is: {} and name is: {}", id, getName()); }
阿里日志规约9【强制】
生产环境禁止直接使用System.out 或System.err 输出日志或使用e.printStackTrace()打印异常堆栈。
说明:标准日志输出与标准错误输出文件每次Jboss重启时才滚动,如果大量输出送往这两个文件,容易造成文件大小超过操作系统大小限制。
阿里日志规约10【强制】
异常信息应该包括两类信息:案发现场信息和异常堆栈信息。如果不处理,那么往上抛。
正确例子:logger.error(“inputParams:{} and errorMessage:{}”, 各类参数或者对象toString(), e.getMessage(), e);
阿里日志规约11【强制】
日志打印时禁止直接用JSON工具将对象转换成String。
说明:如果对象里某些get方法中直接抛出异常,则JSON工具调用其get方法时被迫中断而影响正常业务流程的执行。
正确例子:打印日志时仅打印出业务相关属性值或者调用其对象的toString()方法。
阿里日志规约13【推荐】
谨慎地记录日志。生产环境禁止输出debug日志;有选择地输出info日志;如果使用warn来记录刚上线时的业务行为信息,一定要注意日志输出量的问题,避免把服务器磁盘撑爆,并记得及时删除这些观察日志。
说明:大量地输出无效日志,不利于系统性能提升,也不利于快速定位错误点。记录日志时请思考:这些日志真的有人看吗?看到这条日志你能做什么?能不能给问题排查带来好处?
建议的日志规约
各位在遵循各自公司的公共日志规约的情况下,每个团队还需要有针对各自团队/应用的业务/审计/监控日志规约。我在这里提一些日志规约的建议,希望能给大家一些参考。
1.【强制】日志记录本身不允许失败
日志记录的失败会阻断流程,可能会导致业务逻辑改变。我给一个经典的例子。
// 前置业务逻辑
logger.info("The itemId of the card is {}", card.getItemId());
// 后置业务逻辑
如果card本身为空,那么后续的业务逻辑都不会执行了。
2.【强制】对于trace/debug/info级别的日志输出,必须进行日志级别的开关判断。
一般的trace和debug日志记录方法内部都再进行一次日志级别的判断,但是我们仍然必须要在外部进行日志级别的开关判断。我举一个简单的例子。
logger.debug("The context is {}", JSON.toJSONString(largeData));
如果没有前置的开关判断,那么这个超大对象的JSON序列化就一定会被执行。而大对象的JSON序列化是非常消耗CPU的,而CPU水位的上升又可能会带来一系例的问题,例如RT的上升。
正确例子:
if (logger.isTraceEnable) { logger.trace("This is a {} log", "trace"); } if (logger.isDebugEnable) { logger.debug("This is a {} log", "debug"); } if (logger.isInfoEnable) { logger.info("This is an {} log", "info"); }
3.【强制】日志打印时禁止直接用JSON工具将对象转换成String
我知道,直接记录JSON序列化结果非常方便,打印出来的日志也直接是JSON结构,无论是解析还是直接看都非常便捷。
logger.debug("The data are: {}", JSON.toJSONString(data));
但是,会有性能问题,CPU水位、内存、RT等等都会受到影响。并且,阿里巴巴日志规约第11条:【强制】日志打印时禁止直接用JSON工具将对象转换成String。
我给大家提供两种平替的方法:
- 重写准备序列化对象的toString方法,然后用该对象的toString方法作为日志记录。
- 重写日志记录方法,通过Lambda表达式将JSON序列化变成异步操作。虽然对CPU水位的影响没有优化,但是对RT的影响会降低到最小。
再给大家避两个坑:
- JSON.toJSONString这个方法用起来很爽,但是不是所有的对象都可以进行JSON序列化的。如果对象无法进行JSON序列化,那么就会抛出异常,从而违反第一条日志规约【日志记录本身不允许失败】。所以在使用JSON序列化方法时建议加上SerializerFeature.IgnoreErrorGetter,这个参数的含义是在遇到序列化对象中无法序列化的字段则自动跳过。
String objStr = JSON.toJSONString(obj, SerializerFeature.IgnoreErrorGetter);
- 正常来说在加上SerializerFeature.IgnoreErrorGetter参数后,JSON序列化就不会失败了。但是极少情况下对于某些静态内部类对象,即使加了SerializerFeature.IgnoreErrorGetter参数还是会失败。所以建议在序列化的同时最好要有fallback。
String objStr; try { objStr = JSON.toJSONString(obj, SerializerFeature.IgnoreErrorGetter);} } catch (Exception e) { if (obj != null) { objStr = obj.toString(); } }
4.【强制】拼接日志时使用占位符
拼接字符串会使用StringBuilder的append()方式,会有性能损耗。但使用占位符是替换工作,可以提升性能。
错误例子:
logger.info("The " + nameKey + " is " + nameValue);
正确例子:
logger.info("The {} is {}", nameKey, nameValue);
5.【强制】禁止直接使用Log4J和Logback等日志系统的API
这点在阿里日志规约中也有提到,应该使用SLF4J等日志框架的API,而不要使用具体日志实现的API。
6.【强制】使用异步输出日志,而不使用同步输出日志
同步输出日志会明显影响RT,非常影响用户体验。
7.【强制】打印错误日志时,需要记录全部的错误信息
正常情况下,我们都会在捕获异常的同时,将错误信息通过日志记录下来,方便后续排查问题。但不完整的错误信息的记录,可能会导致我们错失关键信息。
// 不推荐的日志记录方式 logger.error("This solution failed."); logger.error("XXX error: {}", e.getMessage()); // 推荐的日志记录方式 logger.error("XXX error: {}", e);
正常的日志框架的error方法都会专门提供Throwable变量作为入参,所以我们可以放心大胆地直接将异常放入。
8.【强制】生产环境禁止直接使用System.out或System.err输出日志
标准日志输出与标准错误输出文件每次应用容器重启时才滚动,文件可能超过操作系统大小限制。
9.【强制】当出现非预期错误或者程序无法处理的错误,必须打印ERROR日志
方便排查问题的时候能够定位到具体的原因。
10.【强制】禁止打印无意义/重复的日志
有些日志没什么意义,那就把它改成注释,而不是日志。
日志的记录是为了解析和排查问题,所以表达同样信息的日志不要重复打印。
11.【强制】必须限制日志的长度
如果你已经预估到你要打印的日志超级长,那么你可以想想这些日志内容全都是必要的吗?
如果你在设计自己的日志组件,那么你必须要对用户记录的日志长度加以限制,设置合理的截断长度。
12.【强制】重要应用的日志格式应该尽量相同
重要应用的监控更多更细,统一的日志格式无论是在监控告警配置,还是在日志解析上都是极其有利的。
13.【强制】ERROR日志接入监控和告警
大量ERROR日志产生时,就算主力监控没有告警,那也说明线上出现了故障,需要解决。所以强烈建议ERROR日志接入监控和告警。
同时,请谨慎记录ERROR日志。
14.【推荐】尽量避免在循环中打日志
对于RT来说,日志数量的影响是远远大于大对象日志记录的影响的。一般来说,一条日志记录方法会使用500纳秒,而一个大对象可能也就2000纳秒。
就算我们只在一个循环中打印了几条日志,但是这个循环执行了几百次。那么就会对应用RT产生毫秒级别的影响,有可能导致应用超时。对于那些会执行几千次的循环,如果在里面打印日志,那么RT和用户的血压就会一起上升。
15.【推荐】日志内容尽量使用英文,避免乱码
有些时候存在终端编码问题导致中文日志出现乱码,所以还是尽量用英文记录日志。并且阿里巴巴开发规约规定,国际化团队/应用是必须使用英文进行日志记录的。
16.【推荐】谨慎地记录日志
日志记录为了监控/方便问题排查/数据解析,日志不是法外之地。
三、日志规约的其他方面
日志规约是每一位开发者都需要学习的事情,但日志其他方面(例如日志系统的落地、日志的使用)的关注度则会相对低一些。这篇文章我是讲日志规约的,所以我还是会围绕日志规约稍微拓展开一些日志相关的事情,关系不大的就先不提了。
很多复盘Action里面都有一条:完善监控
每个团队肯定都有自己的主力监控,但当主力监控开始告警时,说明问题已经不小了,所以我们需要更加细粒度的监控。监控的级别越细,发现问题的时间就越早。
按照日志规约打印的规范日志往往都是高效的。它体现在ERROR/WARN日志和监控告警的强绑定。一旦总体ERROR/WARN日志达到一定数量,告警就会带着详细的报错原因来找你。这能大大减少排查问题的时间,提升解决线上问题的效率。
日志内容的高效化:错误码
日志内容不是字越多越好的,关键是看日志内容的信息转换效率。“错误码”就是日志内容高效化的最佳手段之一。
当然错误码也是要遵循错误码规约的,不是团队内部自己约定完就好了。阿里巴巴开发规约中对于错误码也有相应的规约,这里就不展开了。
日志规约的应用
日志并不都是简洁的错误码,或详尽的情况描述。当数据量巨大的时候,我们会选择只采集每条用户请求中最精炼的信息。
那我最近的项目举例:由于资源限制,全量采集的情况下我们不能对每一位用户都采集非常详尽的日志。所以这种情况下我们对日志的规约要求就会非常高,日志内容也只采集精炼的必要信息。我们会用隐藏字符作为分隔符,将所有算法日志组装起来,然后上报到消息队列,并且同步落盘离线数据表并执行解析。
大佬牛逼