JAVA 项目中日志的正确使用姿势

发布时间 2023-10-15 17:23:13作者: ayiZzzz

什么是日志

日志:记录程序的运行轨迹,方便查找关键信息,也方便快速定位解决问题。

日志的作用

1、排查和定位错误的手段
日志的作用就是在测试、生产环境没有 Debug 调试工具时开发和测试人员定位问题的手段。日志打得好,就能根据日志的轨迹快速定位并解决线上问题,反之,日志输出不好,不仅无法辅助定位问题反而可能会影响到程序的运行性能和稳定性
2、为程序提供可修复的手段
例如 Mysql binlog,可以用户我们做缓存一致性,redo log 重做日志、中继日志用户做主从备份
3、问题监控、状态监控、安全审计

常见日志框架

Logging
这是 Java 自带的日志工具类,在 JDK 1.5 开始就已经有了,在 java.util.logging 包下
commons-logging
commons-logging 是日志的门面接口,它也是Apache 最早提供的日志门面接口,用户可以根据喜好选择不同的日志实现框架,而不必改动日志定义,这就是日志门面的好处,符合面对接口抽象编程。
Slf4j
slf4j,英文全称为“Simple Logging Facade for Java”,为java提供的简单日志Facade。Facade门面,更底层一点说就是接口。它允许用户以自己的喜好,在工程中通过slf4j接入不同的日志系统。
重点:这也是我们的工程项目使用的最多的一种日志框架,他基于门面设计模式,为我们提供了一种优雅切换日志框架的手段 (参考 Spring Boot 自动装配原理)

常见的日志级别

error:错误日志,指比较严重的错误,对正常业务有影响,需要运维配置监控的;
warn:警告日志,一般的错误,对业务影响不大,但是需要开发关注(网络连接超时、系统资源预警);
info:信息日志,记录排查问题的关键信息,如调用时间、出参入参等等;
debug:用于开发 DEBUG 的,关键逻辑里面的运行时数据;
trace:最详细的信息,一般这些信息只记录到日志文件中。

logback 日志配置文件

注意事项
1、注意配置打印日志格式需要包含信息:时间、线程ID、traceId (链路追踪 ID)、spandID (链路跨度 ID)等相关信息

            <pattern>%black(控制台-) %green userId-[%X{userId}] %red(%d{yyyy-MM-dd HH:mm:ss}) %highlight(%-5level) [%X{traceId}/%X{spanId}]
                %green[%thread] %boldMagenta(%logger{10})
                [%file:%line] %cyan(%msg%n)
            </pattern>

2、需要对不同业务或者不同模块的日志进行分类处理
3、测试和开发环境需要关闭控制台日志打印

    <!-- 控制台输出日志级别 -->
    <root level="info">
        <springProfile name="dev">
            <appender-ref ref="STDOUT"/>
        </springProfile>
    </root>

4、更改日志存储方式为异步存储模式

    <appender name="ASYNC_FILING" class="ch.qos.logback.classic.AsyncAppender">
        <!-- 设置为不丢失日志,默认如果队列的80%已满,则会丢弃TRACT、DEBUG、INFO级别的日志 -->
        <discardingThreshold>0</discardingThreshold>
        <!-- 更改默认的队列的深度,默认值为256 -->
        <queueSize>1000</queueSize>
        <appender-ref ref="syslog"/>
    </appender>

5、在使用日志框架时最好使用 SL4J 这种门面日志框架

日志的正确使用姿势

1、明确日志的作用:排查 BUG、分析问题;在这个的前提下我们应该避免打印无效的日志信息
错误示例

if(user.isVip()){
  
  log.info("该用户是会员,Id:{}",user,getUserId());
  
  //冗余,可以跟前面的日志合并一起
  log.info("开始处理会员逻辑,id:{}",user,getUserId());
  
  //会员逻辑
}

正确示例

if(user.isVip()){
  
  log.info("该用户是会员,Id:{}",user,getUserId());
  //会员逻辑
}

2、低日志级别的打印,需要使用开关判断例如 Debug、Trace
错误使用姿势

logger.debug("Processing trade with id:" + id + " and symbol:" + symbol);  

正确使用姿势

if (log.isDebugEnabled()){
   
    log.debug("userId is:{}", user.getId());
}

低日志级别使用时,进行开关的判断,这样可以避免无效的日志记录从而造成的额外开销
3、日志拼接使用占位符的形式
错误姿势

log.debug("Processing trade with id: " + id + " and symbol: " + symbol);  

使用这种方式会造成额外的字符串拼接产生的额外开销,如果拼接的是一个对象还需要调用他的 toString()方法
正确姿势

log.info("Processing trade with id: {} and symbol : {} ", id, symbol); 

4、静止在线上环境开启 Debug 日志 (很重要哦)
因为一般系统的 debug 日志会很多,并且各种框架中也大量使用 debug 的日志,线上开启 debug 不久可能会打满磁盘,影响业务系统的正常运行。
5、在 throw 异常之后,在他前面不需要在打印日志信息
错误姿势

log.error("IO exception", e);
throw new MyException(e);

正确姿势

throw new MyException(e);

过滤敏感日志信息

日志记录中的敏感信息:手机号、身份证号、邮箱号、真实姓名、银行卡号等
基于之定义日志组件,对日志进行脱敏操
开源项目:https://github.com/houbb/sensitive/tree/master/sensitive-test

统一日志处理

传统的日志打印方式

 private static final Logger log = LoggerFactory.getLogger(IdCardDesensitizationSerializer.class);

或者使用 lombok 为我们提供的 @SL4J 注解都存在几个问题
1、不能对日志进行统一化模板配置
2、如果后续需要对日志进行收集和处理,那么更改范围不可控

为了解决上面所说的两个问题,我们这里可以基于美团的日志处理文章 https://tech.meituan.com/2021/09/16/operational-logbook.html
1、对日志进行统一的处理基于 AOP + SL4J 的方式去对日志与业务进行解耦
2、采用这种方式也可以对日志进行统一化的处理