概览
各位小伙伴大家好,我又回来更新了,上一篇我们讨论了为什么要使用日志框架,这次我们深入问题的根源,为什么我们需要日志,日志具体如何使用?
大多数开发人员会纠结日志该怎么输出,什么时候输出,输出了会不会有人看等问题,让我们跳出开发人员的局限来考虑这个问题:谁需要日志?日志有几种?日志都需要输出什么?如何输出日志?
谁需要日志?
- 开发者 开发人员在开发过程中需要输出一些变量方便调试,正确的做法是使用日志来输出(使用 System.out来输出,一不小心发布到线上,会被项目经理痛批);其次线上问题很难重放,用户的表述一般都会失真,况且很多用户发现 bug 就删 app 关网页走人了
- 运维人员 整个系统大部分时间都是运维人员来维护,日志可以帮助运维人员来了解系统状态(很多运维系统接入的也是日志),运维人员发现日志有异常信息也可以及时通知开发来排查
- 运营人员 没错,就是运营人员,比如电商的转化率、视频网站的完播率、普通PV数据等都可以通过日志进行统计,随着大数据技术的普及,这部分日志占比也越来越高
- 安全人员 虽然大多数企业不重视安全,但是安全也可以通过日志来进行预警,比如某个用户突然大额转账、再比如数据库突然出现大量无条件分页查库(拖库)等等
日志有几种?
- 调试日志 用于开发人员开发或者线上回溯问题。
- 诊断日志 一般用于运维人员监控系统与安全人员分析预警。
- 埋点日志 一般用于运营决策分析,也有用作微服务调用链路追踪的(运维、调试)。
- 审计日志 与诊断日志类似,诊断日志偏向运维,审计日志偏向安全。
日志都需要输出什么?
注:日志级别会在下面讲解
- 调试日志 DEBUG 或者 TRACE 级别,比如方法调用参数,网络连接具体信息,一般是开发者调试程序使用,线上非特殊情况关闭这些日志 INFO 级别,一般是比较重要却没有风险的信息,如初始化环境、参数,清理环境,定时任务执行,远程调用第一次连接成功 WARN 级别,有可能有风险又不影响系统继续执行的错误,比如系统参数配置不正确,用户请求的参数不正确(要输出具体参数方便排查),或者某些耗性能的场景,比如一次请求执行太久、一条 SQL 执行超过两秒,某些第三方调用失败,不太可能被运行的if分支等 ERROR 级别,用于程序出错打印堆栈信息,不应该用于输出程序问题之外的其他信息,需要注意打印了日志异常(Exception)就不应该抛(throw)了
- 诊断日志 一般输出 INFO 级别,请求响应时间,内存占用等等,线上接入监控系统时打开,建议输出到独立的文件,可以考虑 JSON 格式方便外部工具分析
- 埋点日志 业务按需定制,比如上文提到的转化率可以在用户付款时输出日志,完播率可以在用户播放完成后请求一次后台输出日志,一般可输出 INFO 级别,建议输出到独立的文件,可以考虑JSON格式方便外部工具分析
- 审计日志 大多 WARN 级别或者 INFO 级别,一般是敏感操作即可输出,登陆、转账付款、授权消权、删除等等,建议输出到独立的文件,可以考虑JSON格式方便外部工具分析
一般调试日志由开发者自定义输出,其他三种应该根据实际业务需求来定制。
日志的其他注意点
- 线上日志应尽量谨慎,要思考:这个位置输出日志能帮助排除问题吗?输出的信息与排查问题相关吗?输出的信息足够排除问题吗?做到不少输出必要信息,不多输出无用信息(拖慢系统,淹没有用信息)
- 超级 SessionId 与 RequestId,无论是单体应用还是微服务架构,应该为每个用户每次登陆生成一个超级 SessionId,方便跟踪区分一个用户;RequestId,每次请求生成一个 RequestId,用于跟踪一次请求,微服务也可以用于链路追踪
- 日志要尽量单行输出,一条日志输出一行,否则不方便阅读以及其他第三方系统或者工具分析
- 公司内部应该制定一套通用的日志规范,包括日志的格式,变量名(驼峰、下划线),分隔符(“=”或“:”等),何时输出(比如规定调用第三方前后输出INFO日志),公司的日志规范应该不断优化、调整,找到适合公司业务的最佳规范
OK,理论就聊到这里,接下来让我们回到技术层面。
使用概念
如果要想要学会使用日志框架,先要理解几个简单概念,Logger、Appenders、Layout、日志级别与级别继承(Level Inheritance)
Logger(日志实例)
用于输出日志,调用一次org.slf4j.LoggerFactory#getLogger(java.lang.Class>)或org.slf4j.LoggerFactory#getLogger(java.lang.String)就会产生一个日志实例,相同参数会共用同一个实例。
Appenders
日志输出器,logback 预定义了输出到控制台、文件、Socket 服务器、MySQL、PostgreSQL、Oracle 和其他数据库、JMS 和 UNIX Syslog 系统调用等实现,通过配置文件配置即可使用,当然我们常用的只有控制台和文件两种。
Layout
用于控制日志输出格式,前文所说的”自动输出日志相关信息,如:日期、线程、方法名称等等“就可以用 Layout 来控制,实际使用很简单,写一个 Layout 格式定义表达式(pattern)即可,使用方法类似于Java 的SimpleDateFomat。
日志级别
RFC 5424 (page 11)规定了 8 种日志级别,但是SLF4j 只定义了 5 种日志级别,分别是 ERROR、WARN、INFO、DEBUG、TRACE 这五个级别从高到低,配置级别越高日志输出就越少,如下图
我们看到滑动条上五个点正好对应五个级别,滑动指示器可以左右移动,指示器作为分界点,指示器左侧都可以输出,右侧都不能输出,左右调整指示器就可以调整日志的输出,滑倒右侧就可以全部输出,滑倒左侧就可以减少输出,那么是否能够彻底关闭输出呢?答案是可以的,配置文件中还可以配置为 ALL 与 OFF,分别对应所有(等价于TRACE)与关闭。
级别继承
理解了日志级别,让我们来考虑如下场景:
- 某些重要业务输出 INFO 级别,其他业务输出WARN级别的日志,同时关闭所有库、框架的日志
有需求就会有解决方案,其实很简单,logback 与 log4j 都支持按照日志实例来配置,现在问题解决了,但是新的问题又来了,如果线上所有日志都输出 INFO 级别,难道要一个一个配置吗?这时候就就要请出我们上面所提到的级别继承,如果 Java 一样,logback 与 log4j 中也都是单根继承模型,Java 中是 Object,日志中是 ROOT,如下图:
有了继承机制,我们只需要将 ROOT 调整到 INFO 级别,再按照需求细化调整我们业务对应的 logger 实例级别即可满足绝大多数场景。
Codding 实战
问:把大象装冰箱分几步?分三步:1、引入依赖,2、编码输出日志,3、调整配置文件。前文已经讲过步骤一,如果没有看过的读者请移步公众号查看往期回顾,这里直接进入步骤二。
步骤二
如果项目中使用了Lombok,那么可以直接在类上面加@Slf4j注解既可获得日志实例,否则可以使用static final org.slf4j.Logger logger = LoggerFactory.getLogger(TestLog.class);来获取日志实例
具体日志输出方法如下:
logger.trace("A TRACE Message");logger.debug("A DEBUG Message");logger.info("An INFO Message");logger.warn("A WARN Message");logger.error("An ERROR Message");复制代码
这里有个注意点,尽量使用参数占位而不要手动拼接字符串,如下
String level = "Trace";// 反例logger.trace("A " + level + " Message");// 正确的做法logger.trace("A {} Message", level);复制代码
这样做可以提高效率,如果不输出日志,第一种情况也会拼接字符串造成性能损耗,第二种就不会有此问题(阿里巴巴Java开发手册(华山版)这里表述有问题,占位符效率更高是因为尽量延迟进行字符串处理,如果不需要输出的日志就不处理了,下一篇原理分析会展开),另外我们也不需要if (logger.isTraceEnabled())来进行判断了(性能损耗不高,但是代码好看多了)。
步骤三
配置文件需要区分 logback 与 log4j2,两种框架在配置文件上有差别但很相似,来看具体配置文件。
logback 配置文件位置
logback 支持 xml 与 groovy 脚本两种配置方式,logback 查找配置文件位置规则如下(后续文章会讲如何修改位置)
- logback 尝试在类路径中找到一个名为 logback-test.xml 的文件。
- 如果找不到此类文件,则 logback 会尝试在类路径中找到名为 logback.groovy 的文件。
- 如果找不到这样的文件,它将在类路径中检查文件 logback.xml。
- 如果找不到此类文件,则通过查找文件 META-INFservicesch.qos.logback.classic.spi.Configurator,如果有这个文件且内容是 com.qos.logback.classic.spi.Configurator 实现类的全类名,直接加载这个实现类。
- 如果以上方法均不能成功执行,则 logback 会使用 BasicConfigurator 自动进行自我配置,会将日志输出到控制台。
这段长长的文字其实不用看,我们就把 logback.xml 放入 Classpath 根目录就可以了。。
logback 配置文件编写规则
logback 配置文件主要分为三类,一个或多个 Appender,用于定义输出位置(不同文件位置,或者网络又或者数据库);一个或多个 Logger,用于细化配置不同 logger 的输出级别以及位置;一个 ROOT,是一个特殊的logger,用于配置根 Logger。
我们一起来看下面的配置文件实例
<?xml version="1.0" encoding="UTF-8"?>${CONSOLE_LOG_PATTERN}utf-8${LOG_PATH}/log.log${LOG_PATH}/log-%d{yyyy-MM-dd}.%i.log50MB5005GB true ${FILE_LOG_PATTERN}utf-8复制代码
上面配置文件定义了两个 Appender,一个输出控制台,另一个输出到文件并且自动滚动。需注意的是property标签相当于定义一个变量,可以使用${xxx}进行引用,CONSOLE_LOG_PATTERN 与 FILE_LOG_PATTERN 定义了控制台与文件打印格式,具体编写方式类似于 Java 的SimpleDateFomat就不在此展开了,具体可以参考
- logback:logback.qos.ch/manual/layo…
- log4j:logging.apache.org/log4j/2.x/m…
log4j2 配置文件位置
log4j2 支持 XML、JSON、YAML 或者 properties 格式的配置文件,具体查找方式如下:
- 检查“ log4j.configurationFile”系统属性,如果有,尝试使用与文件扩展名匹配的ConfigurationFactory加载配置。
- 如果未设置系统属性,则属性 ConfigurationFactory 将在类路径中查找 log4j2-test.properties。
- 如果找不到此类文件,则 YAML ConfigurationFactory将在类路径中查找 log4j2-test.yaml或log4j2-test.yml。
- 如果找不到此类文件,则 JSON ConfigurationFactory 将在类路径中查找 log4j2-test.json或log4j2-test.jsn。
- 如果找不到这样的文件,XML ConfigurationFactory 将在类路径中查找 log4j2-test.xml。
- 如果找不到测试文件,则属性 ConfigurationFactory 将在类路径上查找 log4j2.properties。
- 如果找不到属性文件,则 YAML ConfigurationFactory 将在类路径上查找 log4j2.yaml或log4j2.yml。
- 如果无法找到 YAML 文件,则 JSON ConfigurationFactory 将在类路径上查找 log4j2.json或log4j2.jsn。
- 如果无法找到 JSON 文件,则 XML ConfigurationFactory 将尝试在类路径上找到 log4j2.xml。
- 如果找不到配置文件,使用 DefaultConfiguration 自动配置,将日志输出到控制台。
这段更长的文字当然也不用看,我们就把 log4j2.xml 放入 Classpath 根目录就可以了
log4j2 配置文件编写
log4j 也是 Logger 与 Appender 配置项,也有一个ROOT的特殊 Logger,Appender 比logback支持更多输出位置,如kafka、Cassandra、Flume等。
<?xml version="1.0" encoding="UTF-8"?>logs复制代码
可以看得出 log4j2 与 logback 配置文件书写大同小异,甚至同样需要注意additivity="true"时导致的日志重复输出问题,毕竟 log4j1 与 logback 都是 Ceki大神都作品。
总结
得益于 Ceki 大佬的努力,日志使用几乎没有有差异(Logback 与 Log4j2,Google 于 2018年4月开源了流式(fluent)日志框架 Flogger,Slf4j 也将在 2.0 版本支持,而 log4j2 再次落后,不过笔者认为 log4j2 有很多优点,更多内容请关注后续文章)。关于日志如何输出本人也是经验之谈,免不了纰漏,欢迎补充指正,另外每个公司都有不同的应用场景,具体应该遵守公司统一规范。
本篇更多倾向基础使用,接下来的文章将展开对比、原理以及扩展日志框架,敬请各位小伙伴们期待。