在过去的两年里,一直在做日志类工具,期间对日志该如何打印思考不少,下面就结合自己的理解与实践尝试着讲讲这个话题。
首先,我们来看下日志的用途及意义,大致包括:
为了更好地发挥日志的价值及意义,我们还需要考虑日志打印的性能及影响,实现对日志更快地打印和更好地管理。
接下来讲的所有规则、建议及方法的总结都会围绕上面四个维度展开:
运维与监控、运营与决策、安全审计、性能与影响
从不同的视角出发,我们往往会有一些不同的认识。不同的人,关注的维度也不一样。
下面我们一起来看看关于日志打印方面一些主流的规则或建议或方法(PS:其中涉及的一些具体事例,语言为java)。
【严格遵守团队或者公司关于日志级别之间的约定,不要凭感觉定级别,尤其ERROR级别的日志,不要随意打印。另外需要思考下,具体打印什么会比较有利于后期的运维监控】
从运维与监控的角度,能否根据日志级别就判断出问题的等级,能否使用相关工具(如:zabbix、splunk、ELK等)配置日志关键字告警,对自己特别关注的业务能否做出单独的监控,问题定位时,利用日志相关工具能否根据相关的关键字快速找到问题,对于事务类型的,能否根据某个事务ID将一个事务相关的日志串在一起查看等等,这些都是我们需要考虑的问题。
关于日志级别,比较常见的有:TRACE, DEBUG, INFO, WARN, ERROR, FATAL。不过,在实际中用的比较多的还是DEBUG, INFO, WARN, ERROR这几个,可以采用这4种类别去定义不同的事件。团队或者公司可以针对不同的级别定义相关的规则,哪些操作场景中,需要打印日志,分别打印什么级别的日志,要有一个明确的约定。
我见过生产环境每天一直在打印ERROR日志,服务却没什么问题的情况。有些团队根本不重视日志打印,生产环境中服务日志随意打印。这样是很不利于运维与监控的,如果日志中不打印方便定位的有效日志,服务出问题后就很难通过日志来快速定位问题。如果日志中打印了太多不影响服务的ERROR日志,那么通过日志将没法进行有效监控。比如常见的日志关键字监控场景对于这种日志就没有任何监控意义。
对于ERROR和WARN级别,如果拿不准时,我觉得可以参考StackOverFlow上的一个说法: “如果你希望系统管理者深夜从床上爬起来处理的问题,就可以记录为ERROR级别日志,如果不是,那就设为WARN级别”。
关于日志级别的约定,我觉得可以参考:
日志级别 | 说明 |
---|---|
DEBUG | 主要记录调试信息,跟踪函数的进入或退出等,记录当前调用的函数名、参数、内部变量值、返回值等信息,方便开发人员调试一些较复杂的、比较容易出问题的地方。其他等级不方便记录的信息,可以通过DEBUG来记录。 |
INFO | 主要保留系统正常工作时的一些关键运行指标,一些状态信息如数据库容量等,一些用户业务流程中的核心处理记录,方便问题回溯时上下文场景的复现,某些子系统的初始化,某些请求的成功执行等。INFO级别的日志也要适量,不宜太多。 |
WARN | 业务处理时,触发了异常流程,但是不影响整个系统的运行。系统状态或者用户输入和预期不一样,对于一些级别上还达不到ERROR级别的,可以考虑WARN级别。 |
ERROR | 高级别错误,系统已经出现了严重的问题,已经影响了用户的正常访问,无法自动恢复到正常状态,需要马上进行人工介入和处理的。 |
对于每一条日志的打印,需要思考其合理的日志等级,尤其是ERROR级别的日志,一定要想清楚是否是合理的。
另外,非ERROR级别的日志中,尽量不要出现类似error、fault、fail、exception等敏感词汇,合理的日志等级选择,才能发挥日志关键字监控的作用。并且,日志最终一般都会采集到相关的日志平台进行集中检索和统计分析,合理的日志,才能更好地发挥出日志检索的意义,帮助快速定位到问题。
对于日志级别的思考,还可以参考阿里JAVA规范中如下两条:
【推荐】谨慎地记录日志。生产环境禁止输出debug日志;有选择地输出info日志;如果使用warn来记录刚上线时的业务行为信息,一定要注意日志输出量的问题,避免把服务器磁盘撑爆,并记得及时删除这些观察日志。
说明:大量地输出无效日志,不利于系统性能提升,也不利于快速定位错误点。记录日志时请思考:这些日志真的有人看吗?看到这条日志你能做什么?能不能给问题排查带来好处?
【参考】可以使用warn日志级别来记录用户输入参数错误的情况,避免用户投诉时,无所适从。注意日志输出的级别,error级别只记录系统逻辑出错、异常、或者重要的错误信息。如非必要,请不要在此场景打出error级别。
【请将用于问题定位、业务分析和安全审计的日志分开打印,用于业务分析的日志最好使用json格式,并根据不同的业务场景定义便于业务统计分析的字段】
关于文件如何分开打印以及日志的json输出,在java中,如果使用的logback,可以类似这样操作:
<appender name="myAppender" class="ch.qos.logback.core.rolling.RollingFileAppender">
<rollingPolicy class="ch.qos.logback.core.rolling.SizeAndTimeBasedRollingPolicy">
<FileNamePattern>${LOG_PATH}_%d{yyyyMMddHH}_.logFileNamePattern>
<maxHistory>${mainMaxHistory}maxHistory>
rollingPolicy>
<encoder>
<pattern>{“date”: “%d{yyyy-MM-dd HH:mm:ss:SS}”, “msg”: %msg}%npattern>
encoder>
appender>
<logger name="myLog" level="INFO" additivity="false">
<appender-ref ref=" myAppender " />
logger>
private static final Logger logger = LoggerFactory.getLogger(“myLog”);
说明:
首先为一类日志创建一个appender,并取一个自己的name,然后中间的FileNamePattern可以定义不同的文件名,之后这类日志就会打印到定义好的文件中去了。
对于json格式的输出,可以简单的利用pattern模式构建json格式的日志,一般日志都需要一个日期,所以第一个key对应date,接下来是msg字段,msg中放入构建好的json格式的string类型的字符串即可。
最后,在需要用到这种方式打印的地方,引用前面定义好的logger,getLogger中填入定义好的名字myLog即可。
【日志规范制定者或者开发人员需要对日志工具有少量的了解】
因为线上的服务日志很多时候会分散到多台机器中,同时生产环境的机器大部分人是没有权限登录的,基本都是通过日志工具来进行查询、分析与监控的,所以什么样的日志在自己能使用上的日志工具中可以比较好的实现上述能力,是需要了解一些的,这样才能更大程度的发挥日志的价值。
比较常见的日志工具(如:Splunk, SumoLogic, ELK, 日志易等)大致上来说,有下面三大功能:
其实通过日志工具的能力,可以实现很多有用的查询、统计分析和监控能力。怎么更好的打印日志,更好的利用日志工具的能力,是需要实践去丰富和完善的。
【从日志打印的性能以及对系统可能产生的影响角度,重新审视日志框架的选择,日志打印方式的选择,日志打印可能存在的异常情况等】
我们一般会使用日志框架进行日志打印,不同的日志框架在日志打印的性能上是有差异的。另外,使用不同的日志打印方式,在性能上也是存在差异的。还有,在大数据量或者一些异常发生的场景中,有可能造成日志的疯狂打印,这种情况我们是需要有一些处理手段来规避的。否则可能出现,生产环境中疯狂抛出exception日志,对服务所在机器以及所使用的日志采集工具造成不同程度的影响。
下面看一些使用实例(使用语言为java)
对于日志框架的选择,可以参考阿里的日志规约:
【强制】应用中不可直接使用日志系统(Log4j、Logback)中的API,而应依赖使用日志框架SLF4J中的API,使用门面模式的日志框架,有利于维护和各个类的日志处理方式统一。
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
private static final Logger logger = LoggerFactory.getLogger(Abc.class);
补充:
对于日志打印方式的选择,可以参考唯品会以及阿里的日志规约:
【推荐】对不确定会否输出的日志,采用占位符或条件判断
logger.debug("Processing trade with id: {} symbol : {} ", id, symbol); //条件判断
或者
if (logger.isDebugEnabled()) {
logger.debug("Processing trade with id: " + id + " symbol: " + symbol);
} //占位符
【推荐】对确定输出,而且频繁输出的日志,采用直接拼装字符串的方式
如果这是一条WARN,ERROR级别的日志,或者确定输出的INFO级别的业务日志,直接字符串拼接,比使用占位符替换,更加高效
logger.warn("Processing trade with id: " + id + " symbol: " + symbol);
【强制】避免重复打印日志,浪费磁盘空间,务必在log4j.xml中设置additivity=false。
【强制】异常信息应该包括两类信息:案发现场信息和异常堆栈信息。如果不处理,那么往上抛。
logger.error(各类参数或者对象toString + "_" + e.getMessage(), e);
【推荐】尽量使用异步日志
低延时的应用,使用异步输出的形式(以AsyncAppender串接真正的Appender),可减少IO造成的停顿。
补充:
<appender name="myFile" class="org.apache.log4j.DailyRollingFileAppender">
<param name="File" value="log4jTest.log" />
<param name="Append" value="true" />
<param name="DatePattern" value="'.'yyyy-MM-dd'.log'" />
<layout class="org.apache.log4j.PatternLayout">
<param name="ConversionPattern" value="[%t] - %m%n" />
layout>
appender>
<appender name="async_file" class="org.apache.log4j.AsyncAppender">
<param name="BufferSize" value="32" />
<appender-ref ref="myFile" />
appender>
对于日志中存在的异常情况可能有:
这块的内容后面再补上 ?