项目日志记录规范和标准

《项目日志记录规范和标准》(第二版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。

<dependency>
  <groupId>org.slf4j</groupId>
  <artifactId>jcl-over-slf4j</artifactId>
</dependency>
<dependency>
  <groupId>org.slf4j</groupId>
  <artifactId>log4j-over-slf4j</artifactId>
</dependency>

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

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

<dependency>
    <groupId>com.101tec</groupId>
    <artifactId>zkclient</artifactId>
    <version>0.6</version>
    <exclusions>
        <exclusion>
            <groupId>org.slf4j</groupId>
            <artifactId>slf4j-log4j12</artifactId>
        </exclusion>
    </exclusions>
</dependency>
<dependency>
    <groupId>com.alibaba.jstorm</groupId>
    <artifactId>jstorm-core</artifactId>
    <version>2.1.1</version>
    <exclusions>
        <exclusion>
            <groupId>ch.qos.logback</groupId>
            <artifactId>logback-classic</artifactId>
        </exclusion>
    </exclusions>
</dependency>


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

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

<dependency>
    <groupId>org.apache.logging.log4j</groupId>
    <artifactId>log4j-core</artifactId>
    <version>${log4j.version}</version>
    <scope>runtime</scope>
    <optional>true</optional>
</dependency>
<dependency>
    <groupId>org.apache.logging.log4j</groupId>
    <artifactId>log4j-slf4j-impl</artifactId>
    <version>${log4j.version}</version>
    <scope>runtime</scope>
    <optional>true</optional>
</dependency>

    设为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、推荐阅读:“有赞统一日志平台初探”


© 2009-2020 Zollty.com 版权所有。渝ICP备20008982号