日志实践---日志该如何打印?

快速跳转

    • 写在前面的话
    • 运维与监控
    • 运营与决策
    • 综合
    • 性能与影响
    • 安全审计

写在前面的话

在过去的两年里,一直在做日志类工具,期间对日志该如何打印思考不少,下面就结合自己的理解与实践尝试着讲讲这个话题。
首先,我们来看下日志的用途及意义,大致包括:

  1. 通过日志检索、关键字及业务指标监控,实现更快的问题发现及定位;
  2. 通过对日志数据的统计分析或深度挖掘,帮助更好的运营及决策;
  3. 从安全审计角度,有助于实现网络安全及审计工作。

为了更好地发挥日志的价值及意义,我们还需要考虑日志打印的性能及影响,实现对日志更快地打印和更好地管理。

接下来讲的所有规则、建议及方法的总结都会围绕上面四个维度展开:
运维与监控、运营与决策、安全审计、性能与影响

从不同的视角出发,我们往往会有一些不同的认识。不同的人,关注的维度也不一样。

  • 从开发者的角度:更多的会关注我的日志如何打印,性能会比较好、对服务影响最小、对后面程序的拓展性最好、有助于定位问题。
  • 从运维者的角度:更多的会关注打印出的日志能否暴露服务的问题,能否快速定位现网问题,能否实现有效的监控及告警,最怕一大堆无用日志淹没了有用日志,日志打印不合规,没法进行有效监控。
  • 从运营者或者领导层的角度:关注的是业务相关的日志,通过这些日志能否分析出对市场和产品决策有帮助的信息,能否真实反映出一些有价值的用户行为数据,是否打印出了产品运营所需要的信息,能否产生商业价值。
  • 从安全审计的角度:更多的会关注打印的日志是否符合公司及国家相关的规定,能否实现安全审计的需求,能否有效防止安全事故的发生。
  • 从日志平台创建者的角度:希望满足各方面人员的需求,最大程度地实现日志的价值,体现日志平台的意义。

下面我们一起来看看关于日志打印方面一些主流的规则或建议或方法(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格式,并根据不同的业务场景定义便于业务统计分析的字段】

  1. 从日志滚动策略和保存策略角度:用于问题定位、业务分析和安全审计的日志量以及需要保留的时长都是不一样的,所以它们的滚动策略和保存策略也应该是不一样的,最好分不同的文件打印,使用不同的管理策略。
  2. 从日志格式的角度:用于问题定位的日志需要可读性比较好,方便阅读,用普通的文本格式是比较好的。业务日志主要是用于统计分析用的,最终一般会以表格或者图形的方式呈现出现,需要方便日志工具或者其他的统计分析工具进行数据的分析,所以建议采用json格式输出。因为json格式的数据比较灵活,并且每个字段的意义都可以直接从数据中获取,另外json格式的数据,对工具的要求也是最低的,可以降低分析数据的成本。
  3. 从业务的角度:不同的业务场景下,需要关注的字段是不一样的,在哪些业务场景下具体应该记录哪些字段,一般都需要业务方根据自己的需求制定。可以从运营推广的角度、决策者的角度、如何提升产品体验的角度、安全的角度、业务指标监控的角度等等方面去思考,然后根据实际需要制定出一个比较合适的业务场景字段定义。一般来说,在产品的不断成长与优化的过程中,这些字段应该也是在不断变化与完善的。

关于文件如何分开打印以及日志的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, 日志易等)大致上来说,有下面三大功能:

  1. 【日志查询定位能力】这块能力一般是通过搜索引擎来实现的,如开源的elasticsearch。可以实现关键字匹配搜索,模糊搜索,组合搜索,条件搜索等。可以先通过关键字或者其他过滤条件找到自己感兴趣的那条日志,然后通过上下文查看事件发生的前后信息。
  2. 【日志统计分析能力】这块主要是通过基于搜索引擎的统计分析命令来实现的,可以统计出一些比较关注的业务指标,如实时的在线人数、按时间统计各个商品的销售情况等等。如何做好日志的统计分析,一般是和业务紧密联系在一起的,需要业务方先考虑好自己关注的业务点,然后通过日志工具的统计分析能力呈现出来,可以选择表格或图形的方式。一般业务日志采用json格式会比较好,同时打印的一条日志最好是最小单位层次的(比如:一个产品下有8个子类,想统计8个子类的销售情况,那么一条业务日志中应该只包含一个子类的业务数据,上层需要的聚合统计交给日志工具去实现,而不是将8个子类封装成一个json打印出来),这样子灵活性就比较好了,同时对工具来说也会比较好实现,否则它就需要先拆分,然后再聚合。
  3. 【日志监控能力】这块主要分为关键字监控和业务指标监控两大类。关键字监控通过检索特定关键字的发生来达到监控的目的,前提条件是日志中有合适的关键字,比如ERROR。但是如果日志打印不规范,一直在打印ERROR,业务却没什么影响的情况下,那日志关键字监控就没法做。另外,对于特别关注的点,也可以通过特定的关键字实现监控。业务指标的监控,主要通过某个或某些指标值超过一个阈值时,实现告警。这块其实是用到了日志统计分析+告警的能力,所以对于需要做业务指标告警的日志最好也是json输出,不然还需要多出一个字段解析的过程。

其实通过日志工具的能力,可以实现很多有用的查询、统计分析和监控能力。怎么更好的打印日志,更好的利用日志工具的能力,是需要实践去丰富和完善的。

性能与影响

【从日志打印的性能以及对系统可能产生的影响角度,重新审视日志框架的选择,日志打印方式的选择,日志打印可能存在的异常情况等】

我们一般会使用日志框架进行日志打印,不同的日志框架在日志打印的性能上是有差异的。另外,使用不同的日志打印方式,在性能上也是存在差异的。还有,在大数据量或者一些异常发生的场景中,有可能造成日志的疯狂打印,这种情况我们是需要有一些处理手段来规避的。否则可能出现,生产环境中疯狂抛出exception日志,对服务所在机器以及所使用的日志采集工具造成不同程度的影响。

下面看一些使用实例(使用语言为java)

对于日志框架的选择,可以参考阿里的日志规约:

【强制】应用中不可直接使用日志系统(Log4j、Logback)中的API,而应依赖使用日志框架SLF4J中的API,使用门面模式的日志框架,有利于维护和各个类的日志处理方式统一。

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
private static final Logger logger = LoggerFactory.getLogger(Abc.class);

补充:

  • 在代码中只依赖slf4j,修改实现类一般只需要修改pom文件中的依赖包还有对应的配置文件(如xml配置)即可,这个大家一般也是这么用的。不过,这里只提到了log4j和logback,忽略了log4j2,有些对性能要求比较高的场景,可以考虑具体的实现采用log4j2。一些测试数据显示,log4j2的性能要优于log4j和logback。在使用上,三者的配置差别不大。
  • 从复用原则上来讲,很多时候我们日志打印的初始化会放在基类中,这时上面的Abc.class需要改成this.getClass(),private需要改成protected.

对于日志打印方式的选择,可以参考唯品会以及阿里的日志规约:

【推荐】对不确定会否输出的日志,采用占位符或条件判断

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造成的停顿。

补充:

  • 对于不确定是否输出的日志,比如生产环境中debug级别的日志,只有特定的时候才会开启,这样子如果采用直接拼装字符串的方式,那么即使debug日志没有配置开启,也会进行字符串的拼接,这个是比较耗性能的,尤其是大数据场景下。
  • 对于additivity,它的作用在于 children-logger是否使用
    rootLogger配置的appender进行输出。默认属性为true,表示子Logger会重复打印父Logger的日志,出现一条日志打印两次的情况。
  • 对于AsyncAppender异步日志,下面给出一个使用方式供参考:
<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>
  • 还有,对于线上日志应该避免使用System.out、System.err、e.printStackTrace()等。

对于日志中存在的异常情况可能有:

  • 在一个无限循环体中,如果需要先请求一个接口,然后再进行下面的操作,这时如果接口请求异常,那么这个程序将循环抛出exception。这时应该针对这种情况加上一些处理逻辑,比如尝试几次失败后,隔一段时间再尝试。
  • 对于数据量非常大的组件(比如每秒处理上万条数据),在对数据处理的过程中,如果数据处理环节出现异常,并且出现的问题基本都是一样的,如果每条数据的处理都进行异常捕获,那么这个异常日志将会非常庞大,这时可以采用采样打印的方式打印异常日志,类似下面这种方式:
    if(++exCount % 50 == 1){
     logger.warn(“xxxx caused by: ” + ex.getMessage(), ex);
    }
  • 如果使用了nohup进行程序启动时,最好将标准输出和标准错误都重定向到/dev/null中去,否则nohup默认的重定向文件nohup.out会越来越大,因为我们一般会使用logback等配置文件来配置日志的滚动和保存策略,但是很有可能会忽略nohup.out文件的滚动和保存策略,并且nohup打印的日志也是重复的。可以采用类似下面的方式进行规避:
    nohup xxxx > /dev/null 2>&1 &

安全审计

这块的内容后面再补上 ?

你可能感兴趣的:(日志规范)