项目日志记录规范和标准

 

《项目日志记录规范和标准》(第二版2017年10月)

   第一版(2013年3月)参见这里。

一、说明

 

日志分类如下:

    1. 面向问题排查的日志

    2. 面向提醒或告警的日志

    3. 面向调试和测试的日志

    4. 面向功能的 日志(准确的说,这是数据文件,不是日志)

    5. 面向人阅读的日志

    6. 面向机器解析的日志

本规范 v1.0,主要针对于上面的1、2、3、5点。

对于第2点的告警信息,仅记录日志是不够的,建议配合实时的通知机制或接入监控平台。

对于第6点,这个是未来的一个发展方向,在后续的规划中,可能会借助大数据平台去处理日志。

 

二、日志处理常见问题

1、多次重复记录

    比如在DAO层记录了异常堆栈信息,然后抛出异常,在web层catch了异常之后,又记录了一次异常堆栈信息。

2、滥用日志、记录不够精简、大量无用信息

    大量的内容都记录到日志中,使得要从日志中查找关键信息如同大海捞针。更有甚者,频繁大量打印日志,使得日志文件超过100G、500G甚至撑爆磁盘。

3、缺乏关键标识,很难定位某条信息的位置

    比如,前端web页面报错,告诉开发人员报错了,但开发人员很难去查找当时的操作日志或者异常信息。仅仅根据时间来定位往往是不够的。

4、日志记录对于系统性能、安全性的影响

    关注日志记录是否太过于频繁。日志记录到文件IO或者数据库都是很费CPU和内存资源的事,是否会对系统的性能产生影响。日志是否会被恶意攻击频繁打印日志,直到日志文件塞满主机磁盘。

 

三、日志记录规范 V1.0

 

1、清晰、清楚的日志记录

    所谓“清晰”、“清楚”,包括如下几方面:

1)  不记录很多无用的日志;

2)  日志按重要级别分类,比如ERROR、INFO,能正确反应对当前信息的重视程度;

3)  日志不存在大量重复,特别是异常堆栈的大量重复;

4)  日志描述准确,不能够张冠李戴,日志内容能切实反应当前情况;

5)  日志内容有助于排查问题,有必要记录关键的参数和状态信息;

6)  日志内容有助于定位问题,确保内容的唯一性,不得在不同的地方记录一模一样的日志。

 

违规举例说明(以Java为例):

针对 第 (3) 点,

void aa()  {
    try {
        bb();
    } catch (Exception e) {
        log.error(e); // 重复打印堆栈信息
    }
}

void bb() {
    try {
        // TODO something...
    } catch (Exception e) {
        log.error(e);
        throw new RuntimeException(e); // catch后又抛出
    }
}

针对 第 (4) 点,

public class Demo {
    
    Logger log = LogFactory.getLogger(ItemsDaoImpl.class); // 日志名称不正确

    void service(int amount)  {
        try {
            
            // TODO 计算价格
            
        } catch (Exception e) {
            log.error(e, "查询用户失败"); // 日志描述张冠李戴
        }
    }
}

针对 第 (5) 点,

void service(int userId, int orderId)  {
    try {
        
        // TODO 查询订单
        
    } catch (Exception e) {
        log.error(e); // 没有记录 订单ID,不便于问题排查
    }
}

针对 第 (6) 点,

void aa(int userId, String param)  {
    try {
        // TODO something
    } catch (Exception e) {
        log.error("生成订单失败. userID={}, param={}", userId, param);
    }
}

void bb(int userId, String param) {
    try {
        // TODO something
    } catch (Exception e) {
        // 此处日志和上面aa()中的日志一模一样,如果报错,则分不清是在方法aa()还是bb()中打印的。
        log.error("生成订单失败. userID={}, param={}", userId, param);
    }
}

 

针对上面的规范,相关的解决方案和建议:

1) 异常处理不重复try-catch;

2) 在程序的关键之处(入口处,结束处,容易报错的地方)记录日志;

3) 每一条日志都认真编写描述,说清楚当前的场景,关键信息,记录必要的参数;

4) 定期抽查和改进(项目负责人走查,或者组内开发人员相互走查)。

 

2、接口调用日志记录规范

  外部系统(接口)调用需要按以下要求记录日志:

1、调用开始前打印日志,记录开始时间;

2、调用成功后打印日志,记录完成时间;

3、调用失败时打印详细异常信息和时间;

4、根据需要决定是否打印入参和返回结果,或者只打印其中的关键信息。

 

四、日志级别的标准定义

 

分为5种日志级别,可根据具体情况选择使用:

trace(追踪)、debug(调试)、info(信息)、warn(警告)、error(错误)。

(本规范根据Java语言制定,其他编程语言不需要完全一致,但需要领会其精神)

 

日志级别使用原则:(注意各个级别的区别)

 

1、致命错误(fatal error)处理原则

    发生致命错误,代表 服务器整个 或者 核心功能 已经无法工作了!!

    处理原则如下:

    1)在服务器启动时就应该检查,如果存在致命错误,直接抛异常,让服务器不要启动起来(启动了也无法正常工作,不如不启动)。

    2)如果在服务器启动之后,发生了致命的错误,则记录error级别的错误日志,最好是同时触发相关的修复和告警工作(比如,给开发和维护人员发送告警邮件)。

 

2、error(错误)使用原则

error为功能或者逻辑级别的严重异常,发生error级别的异常,代表功能或者重要逻辑遇到问题、无法正常工作。

 

3、warn(警告)使用原则

warn是警告的意思,它有两方面作用:

一是从程序角度,在某些功能或逻辑不正常时,发生了一些小故障但是没有大的影响。

二是从业务角度,遇到了一些特殊操作或者特殊数据,例如重要数据或配置被修改,或者某些操作需要引起重视。

 

4、info(信息)使用原则

info用于记录一些有用的、关键的信息,一般这些信息出现得不频繁,只是在初始化的地方或者重要操作的地方才记录。

 

5、debug(调试)使用原则

debug用于记录一些调试信息,为了方便查看程序的执行过程和相关数据、了解程序的动态。

 

6、trace(跟踪)使用原则

trace用于记录一些更详细的调试信息,这些信息无需每次调试时都打印出来,只在需要更详细的调试信息时才开启,例如在排查问题时、或者测试时跟踪程序的执行。

 

总结和补充:

    注意,是否需要记录日志,以及日志的级别是根据功能的重要性来决定的,而不是根据程序是否异常来决定的。也就是说,对于程序报错,不一定都记录error级别的日志,在一些不太重要的地方,记录成warn或者debug级别就可以了。

    另外,在系统出现致命错误之后,应该立即采取应对措施,而不仅仅是记error日志。

    trace和debug的区别:

    debug日志相对较正规,它记录下程序执行的关键信息,可以在生产环境下保存下来,用于排查问题,日志输出量要控制在比较小的范围,而trace级别的日志,仅用于开发或者测试时调试程序,用来查看程序详细执行信息,通常只用于开发环境,它在测试环境和生产环境一般是不开启的,只在需要时临时开启来排查问题,对于它的日志量是没有限制的,通常来说trace日志输出比较多。

 

 

项目稳定运行时的日志量(非规范,只是经验,供参考)

1)debug级别的日志量 : info级别的日志量 > 1000 : 1,

也就是说正常情况下,info日志很少,只在部分重要位置会输出 info日志。【言下之意是:要注意不要滥用info日志】

 

2)error日志和warn日志,正常情况下,几乎为0,当出现异常时,error日志和warn日志量 也在可控范围,不会超过debug级别的最大日志量。【言下之意是:要注意不要滥用error日志和warn日志!同时注意发生异常后,确保error日志不会泛滥】

 

3)正常情况下,trace日志至少是debug日志的100倍,

trace级别的日志量 : debug级别的日志量  >  100 : 1,

也就是说trace日志非常多,debug日志相对较少。【言下之意是:要注意区分debug和trace级别】

 

五、Java日志库指导规范

 

原则上,日志 api 规定使用 SLF4J (不得使用System.out或printStackTrace以及其他日志库API),底层日志实现首选 最新版Logback,次选Log4j 2,不建议使用 Log4j 1.x 版本 以及 JDK Logger、Common Logging。

指导原则如下:

 

1. 总是使用 Log Facade,而不是具体 Log Implementation

    使用 Log Facade 可以方便的切换具体的日志实现。而且,如果依赖多个项目,使用了不同的Log Facade,还可以方便的通过 Adapter 转接到同一个实现上。如果依赖项目使用了多个不同的日志实现,就麻烦的多了。

 

2. 只添加一个 Log Implementation依赖

    毫无疑问,项目中应该只使用一个具体的 Log Implementation,建议使用Log4j2 或者 Logback。如果有依赖的项目中,使用的 Log Facade不支持直接使用当前的 Log Implementation,就添加合适的桥接器依赖,例如可以使用slf4j的桥接方式替换掉log4j1和commons-logging。


  org.slf4j
  jcl-over-slf4j


  org.slf4j
  log4j-over-slf4j

同时,对某些传递依赖了其他日志库的组件进行日志库的排除配置,例如:

引入zkclient会传递引入slf4j-log4j12,引入jstorm会引入logback,可以使用maven的exclusion配置来排除依赖:


    com.101tec
    zkclient
    0.6
    
        
            org.slf4j
            slf4j-log4j12
        
    


    com.alibaba.jstorm
    jstorm-core
    2.1.1
    
        
            ch.qos.logback
            logback-classic
        
    

 

3. 具体的日志实现依赖应该设置为optional和使用runtime scope

    在项目中,Log Implementation的依赖强烈建议设置为runtime scope,并且设置为optional。例如项目中使用了 SLF4J 作为 Log Facade,然后想使用 Log4j2 作为 Implementation,那么使用 maven 添加依赖的时候这样设置:


    org.apache.logging.log4j
    log4j-core
    ${log4j.version}
    runtime
    true


    org.apache.logging.log4j
    log4j-slf4j-impl
    ${log4j.version}
    runtime
    true

    设为optional,依赖不会传递,这样如果你是个lib项目,然后别的项目使用了你这个lib,不会被引入不想要的Log Implementation 依赖;

    Scope设置为runtime,是为了防止开发人员在项目中直接使用Log Implementation中的类,而不适用Log Facade中的类。

 

4. 避免为不会输出的log付出代价

例如下面的写法问题:

logger.debug("receive request: " + toJson(request));

logger.debug("receive request: {}", toJson(request));

    logger.debug方法是 仅当日志输出的级别 大于等于DEBUG级别时 才会执行。但是对于程序执行,第一条是直接做了字符串拼接,第二条的方式虽然避免了不必要的字符串拼接,但是toJson()这个函数无论如何都会被调用。所以上面这两种写法,实际上都执行了计算,有性能开销。

    推荐的写法如下:

if (logger.isDebugEnabled()) {  // SLF4J / LOG4J2
    logger.debug("receive request: " + toJson(request)); 
}
logger.debug("receive request: {}", () -> toJson(request)); // JDK 1.8 LOG4J2
logger.debug(() -> "receive request: " + toJson(request)); // JDK 1.8 LOG4J2
logger.debug("start process request, url:{}", url); // SLF4J / LOG4J2

 

5. 生产环境下,为了避免性能问题,日志格式中不要使用行号,函数名等字段

    原因是,为了获取语句所在的函数名或者行号,log库的实现都是获取当前的stack trace,然后分析取出这些信息。而获取stacktrace的代价是很昂贵的。如果有很多的日志输出,就会占用大量的CPU。在没有特殊需要的情况下,建议不要在正式环境的日志中输出这些字段。

 

六、Logback最佳实践和使用指导(参考)

 

参见:Logback最佳实践和使用指导。

 

附录:

1、推荐阅读:“异常处理最佳实践”

    文章中指出:“异常处理 和 日志处理 有密切关系,但是不能混为一谈”。

2、相关文档:“日志脱敏方案”(内部文档,暂不公开,想了解的可以私聊)

3、相关文档:“logback配置使用说明”(内部文档,暂不公开,想了解的可以私聊)

4、推荐阅读:“Log4j2配置及与Logback对比”

5、推荐阅读:“Logback的深度使用经验和最佳实践”

6、推荐阅读:“有赞统一日志平台初探”

 

 

 

 

你可能感兴趣的:(架构设计专栏,日志规范,Java日志规范,日志标准,日志最佳实践)