Java日志规范

前言

  写好程序的日志可以帮助我们大大减轻后期维护压力,开发人员应在一开始就养成良好的日志撰写习惯

  日志可以帮我们解决以下问题:
    ①、程序是不是按预期执行
    ②、程序哪里出现了BUG
    ③、用户在系统上干了什么
    ④、问题是谁造成的,是依赖的业务系统还是自身系统

 

一、日志框架选型

  logback、log4j2

    logback:推荐,Springboot默认的日志框架
    log4j2:版本大于2.15.0

 

二、日志规约

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

    日志框架(SLF4J、JCL--Jakarta Commons Logging)的使用方式(推荐使用 SLF4J)

// 使用 SLF4J: 
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 private static final Logger logger = LoggerFactory.getLogger(Test.class); 
 
 // 使用 JCL:
 import org.apache.commons.logging.Log;
 import org.apache.commons.logging.LogFactory; 
 private static final Log log = LogFactory.getLog(Test.class); 

 

  【强制】所有日志文件至少保存15天,因为有些异常具备以“周”为频次发生的特点。对于 当天日志,以“info.log/error.log”来保存,保存在/home/work/logs/应用名 目录下,过往日志 格式为: {logname}.log.{保存日期},日期格式:yyyy-MM-dd

    示例: 以 aap 应用为例,日志保存在/home/work/logs/aap/info.log,历史日志名称为 info.log.2022-05-20 

  【强制】在日志输出时,字符串变量之间的拼接使用占位符的方式

    说明:因为 String 字符串的拼接会使用 StringBuilder 的 append()方式,有一定的性能损耗。使用占位符仅 是替换动作,可以有效提升性能

    示例:logger.debug("Processing trade with id: {} and symbol: {}", id, symbol); 

  【强制】对于 trace/debug/info 级别的日志输出,必须进行日志级别的开关判断

    说明:虽然在 debug(参数)的方法体内第一行代码 isDisabled(Level.DEBUG_INT)为真时(Slf4j 的常见实现Log4j 和 Logback),就直接 return,但是参数可能会进行字符串拼接运算。此外,如果 debug(getName())这种参数内有 getName()方法调用,无谓浪费方法调用的开销

// 如果判断为真,那么可以输出 trace 和 debug 级别的日志 
 if (logger.isDebugEnabled()) {
 logger.debug("Current ID is: {} and name is: {}", id, getName()); 
 } 

  【强制】避免重复打印日志,浪费磁盘空间,务必在日志配置文件中设置 additivity=false

<logger name="com.taobao.dubbo.config" additivity="false"> 

  【强制】生产环境禁止直接使用 System.out 或 System.err 输出日志或使用 e.printStackTrace()打印异常堆栈

    说明:标准日志输出与标准错误输出文件每次 Jboss 重启时才滚动,如果大量输出送往这两个文件,容易 造成文件大小超过操作系统大小限制

  【强制】异常信息应该包括两类信息:案发现场信息异常堆栈信息。如果不处理,那么通过 关键字 throws 往上抛出

logger.error("inputParams:{} and errorMessage:{}", 各类参数或者对象 toString(), e.getMessage(), e);

  【强制】日志打印时禁止直接用 JSON 工具将对象转换成 String  

    说明:如果对象里某些 get 方法被覆写,存在抛出异常的情况,则可能会因为打印日志而影响正常业务流 程的执行

    正例:打印日志时仅打印出业务相关属性值或者调用其对象的 toString()方法

  【强制】我们在写日志的时候,需要注意输出适当的内容。首先,尽量使用业务相关的描述

    说明:我们的程序是实现某种业务的,那么就最好能描述清楚这个时候走到了业务过程的哪一步

    其次,避免在日志中输出一些敏感信息,例如用户名和密码。以及,要保持编码的一致。如果不能保证就尽量使用英文而不是中文。这样当我们拿到日志之后就不会因为看到一堆乱码而不知所云了

  【强制】控制日志输出的长度在一定范围内,比如请求参数,避免打印大量的业务日志

    如日志layout中 msg配置为 %.-2048msg,输出日志长度最长为2048字节,多余的将从后面截断不打印

    

三、打日志的正确方式

  1、日志需要打印哪些信息

    开发人员只需关注日志内容和异常堆栈即可,其余信息在日志配置文件中会自动输出

    ①、日志时间:yyyy-MM-dd HH:mm:ss.SSS

    ②、日志级别:DEBUG、INFO、WARN、ERROR

    ③、记录器名称(类名)

      日志的记录器名称一般是声明日志记录器实例的类名,通过记录器名称可以快速定位到日志输出的类是哪个

    ④、方法名,打印日志的方法

    ⑤、产生行数

      即产生日志的所在类的源代码行号  

    ⑥、日志内容

      开发人员输出业务相关的日志

    ⑦、tracing 标识

      通过 AOP 切面,日志框架 MDC 等技术结合,在日志上添加一些链路追踪的扩展元素,将会很大程度方便通过日志进行程序请求调用的链路追踪,这在分布式系统中尤其重要

    ⑧、异常堆栈

      堆栈异常信息有助于程序异常的排查定位,ERROR级别必须打印异常堆栈,其他日志级别不必打印

 

  2、什么时候应该打印日志

    ①、当你遇到问题时,只能通过debug功能来确定问题,你应该考虑打日志,良好的系统,是可以通过日志进行问题定位的

    ②、当你使用if...else或者swith分支时,要在分支的首行打印日志,用来确定进入了哪个分支

    ③、经常以功能为核心进行开发,应该在提交代码前,可以确定通过日志可以看到整个流程

 

  3、日志基本格式

    ①、必须使用参数化信息的方式:logger.debug("Processing trade with id:[{}] and symbol : [{}] ", id, symbol);

      不要进行字符串拼接,那样会产生很多String对象,占用空间,影响性能

    ②、使用[] 进行参数变量隔离

       logger.debug("Processing trade with id:[{}] and symbol : [{}] ", id, symbol);这样的格式写法,可读性更好,对于排查问题更有帮助

 

四、日志追踪

  1、为什么要进行日志追踪?

    日志追踪可以根据traceId追踪到一个请求链路中的所有日志信息,便于排查问题

  2、如何进行日志追踪?

   单体服务:

    通过 AOP 切面,日志框架 MDC 等技术结合,在日志中打印traceId,而后根据traceId可检索整个请求链路的日志

    1)AOP切面结合MDC方式

      ①、日志配置文件中配置traceId占位符

 <pattern>%date{yyyy-MM-dd HH:mm:ss.SSS}|%t|%-5level|%X{traceId}|%C{0}#%M:%L|%msg%n</pattern>

      ②、AOP切入要增强traceId的方法

      ③、在增强类中往MDC中添加traceId字段,并赋值,并在finally块移除MDC中traceId字段

@Aspect
@Component
public class LogAspect {

    private static final Logger LOGGER = LoggerFactory.getLogger(LogAspect.class);

    @Pointcut(value = "@annotation(org.springframework.web.bind.annotation.RequestMapping)||@annotation(org.springframework.web.bind.annotation.PostMapping)||@annotation(org.springframework.web.bind.annotation.GetMapping)")
    private void webPointcut() {
        // doNothing
    }

    /**
     * 为所有的HTTP请求添加traceId
     *
     * @param joinPoint
     * @throws Throwable
     */
    @Around(value = "webPointcut()")
    public Object around(ProceedingJoinPoint joinPoint) throws IllegalAccessException, InstantiationException {
        // 方法执行前加上线程号,并将线程号放到线程本地变量中
        String uuid = StringUtil.getUUID();
        MDC.put("traceId", uuid);
        //...此处省略其他业务处理代码
        // 执行切点方法
        Object result = joinPoint.proceed();
        } finally {
            // 方法执行结束移除线程号,并移除线程本地变量,防止内存泄漏
            MDC.remove("traceId");
        }
        return result;
    }
}

    2)直接使用MDC方式

      ①、日志配置文件中配置traceId占位符,同上

      ②、在方法执行业务逻辑前往MDC中添加traceId字段,并赋值

      ③、在方法返回前移除MDC中的traceId字段

MDC.put("traceId", uuid);
try {
    // 业务逻辑处理
} finally {
    MDC.remove("traceId");
}

   

   分布式服务: TODO

 

 

五、推荐日志配置

  •   ERROR级别单独打印在error.log中,INFO及以下级别日志打印在info.log中
  •   日志文件按天滚动输出
  •   日志异步打印,提高系统性能

 

  logback.xml:

<?xml version="1.0" encoding="UTF-8"?>
<!--默认每隔一分钟扫描此配置文件的修改并重新加载-->
<configuration>
    <!--定义日志文件的存储地址 勿在LogBack的配置中使用相对路径-->
    <property name="LOG_HOME" value="/home/work/log/${artifactId}"/>
    <!--输出日志到文件中-->
    <appender name="FILE" class="ch.qos.logback.core.rolling.RollingFileAppender">
        <file>${LOG_HOME}/info.log</file>
        <!--不输出ERROR级别的日志-->
        <filter class="ch.qos.logback.classic.filter.LevelFilter">
            <level>ERROR</level>
            <onMatch>DENY</onMatch>
            <onMismatch>ACCEPT</onMismatch>
        </filter>
        <!--根据日期滚动输出日志策略-->
        <rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
            <fileNamePattern>${LOG_HOME}/info.log.%d{yyyy-MM-dd}</fileNamePattern>
            <!--保存的存档日志文件的数量-->
            <maxHistory>15</maxHistory>
        </rollingPolicy>
        <encoder>
            <pattern>%date{yyyy-MM-dd HH:mm:ss.SSS}|%t|%-5level|%X{traceId}|%C{0}#%M:%L|%.-2048msg%n</pattern>
        </encoder>
    </appender>
    <!--错误日志输出文件-->
    <appender name="ERROR_FILE" class="ch.qos.logback.core.rolling.RollingFileAppender">
        <file>${LOG_HOME}/error.log</file>
        <!--只输出ERROR级别的日志-->
        <filter class="ch.qos.logback.classic.filter.LevelFilter">
            <level>ERROR</level>
            <onMatch>ACCEPT</onMatch>
            <onMismatch>DENY</onMismatch>
        </filter>
        <!--根据日期滚动输出日志策略-->
        <rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
            <fileNamePattern>${LOG_HOME}/error.log.%d{yyyy-MM-dd}</fileNamePattern>
            <!--保存的存档日志文件的数量-->
            <maxHistory>15</maxHistory>
        </rollingPolicy>
        <encoder>
            <pattern>%date{yyyy-MM-dd HH:mm:ss.SSS}|%t|%-5level|%X{traceId}|%C{0}#%M:%L|%msg%n</pattern>
        </encoder>
    </appender>
    <!--异步打印日志,任务放在阻塞队列中,如果队列达到80%,将会丢弃TRACE,DEBUG,INFO级别的日志任务,对性能要求不是太高的话不用启用-->
    <appender name="ASYNC" class="ch.qos.logback.classic.AsyncAppender">
        <!--队列的深度,该值会影响性能,默认256-->
        <queueSize>512</queueSize>
        <!--设为0表示队列达到80%,也不丢弃任务-->
        <discardingThreshold>0</discardingThreshold>
        <!--日志上下文关闭后,AsyncAppender继续执行写任务的时间,单位毫秒-->
        <maxFlushTime>1000</maxFlushTime>
        <!--队列满了直接丢弃要写的消息-->
        <neverBlock>true</neverBlock>
        <!--是否包含调用方的信息,false则无法打印类名方法名行号等-->
        <includeCallerData>true</includeCallerData>
        <!--One and only one appender may be attached to AsyncAppender,添加多个的话后面的会被忽略-->
        <appender-ref ref="FILE"/>
    </appender>
    <appender name="ERROR_ASYNC" class="ch.qos.logback.classic.AsyncAppender">
        <queueSize>256</queueSize>
        <!--设为0表示队列达到80%,也不丢弃任务-->
        <discardingThreshold>0</discardingThreshold>
        <!--日志上下文关闭后,AsyncAppender继续执行写任务的时间,单位毫秒-->
        <maxFlushTime>1000</maxFlushTime>
        <!--队列满了直接丢弃要写的消息,不阻塞写入队列-->
        <neverBlock>true</neverBlock>
        <!--是否包含调用方的信息,false则无法打印类名方法名行号等-->
        <includeCallerData>true</includeCallerData>
        <!--One and only one appender may be attached to AsyncAppender,添加多个的话后面的会被忽略-->
        <appender-ref ref="ERROR_FILE"/>
    </appender>

    <!--指定一些依赖包的日志输出级别,所有的logger会继承root,为了避免日志重复打印,需指定additivity="false",将不会继承root的append-ref-->
<!--    <logger name="com.xiaomi.mitv.outgoing" level="ERROR" additivity="false">
        <appender-ref ref="STDOUT"/>
        &lt;!&ndash;<appender-ref ref="ERROR_FILE"/>&ndash;&gt;
    </logger>-->

    <root level="INFO">
        <!--使用异步打印日志-->
        <appender-ref ref="ASYNC"/>
        <appender-ref ref="ERROR_ASYNC"/>
    </root>
</configuration>

  
   

 

END.

 

posted @ 2022-05-06 20:05  杨岂  阅读(2718)  评论(0编辑  收藏  举报