当前位置: 首页 > article >正文

日志打印规范

1. 日志的用途

不管是使用何种编程语言,日志输出几乎无处不在,总结起来,日志大概有以下几种用途:

  • 问题跟踪:通过日志可以追踪程序的一些bug,也在可以在安装配置时,可以及时发现问题
  • 状态监控:通过实时分析日志,可以监控系统的运行状态,做到早发现问题,早处理问题
  • 安全审计:审计主要体现在安全方面上,通过日志进行分析,可以发现是否存在非授权的操作
  • 日志级别:为日志设置不同的级别,例如:DEBUG、INFO、WARNING、ERROR和CRITICAL。这样,在不同情况下,可以方便地过滤和查看相关地日志
  • 日志内容:日志应当包含足够的上下文信息,便于诊断问题、例如,记录请求参数、用户ID、操作类型、时间戳等。同时,也要注意保护敏感信息,避免泄露
  • 结构化日志:使用结构化的日志格式(JSON),以便于后期日志分析和处理
  • 日志轮转与归档:根据日志地大小、事件或其他条件,定期轮转和归档日志文件,避免单个日志文件过大,影响系统性能
     

日志记录原则

  • 保持日志简介明了,避免冗余信息;
  • 适当地使用日志级别,以便快速定位问题;
  • 遵循团队或项目地日志规范,保持一致性。

日志记录时机

级别

说明

TRACE

用于记录程序的详细运行信息,例如方法的入参、出参等

DEBUG用于开发阶段,记录详细地程序运行信息,有助于发现潜在问题
INFO记录程序运行的关键节点信息,如程序启动、关闭、配置文件加载、关键操作等
WARNING记录一些非致命性问题,如临时性错误、资源不足等,可能需要关注但不会导致 程序中断
ERROR记录程序中的错误,如异常、错误的输入参数等,可能会导致程序无法正常运行

日志输出

环境

日志打开级别

开发

INFO或DEBUG

测试

INFO或DEBUG

线上INFO

2. 日志打印规范

2.1 选择恰当的日志级别

日常开发中常见日志级别有:trace、debug、info、warn、error(级别依次增大)

ERROR

影响到程序和当前请求正常运行的异常情况,比如像:

  • 打开配置文件失败;
  • 所有第三方对接的异常(包括第三方返回错误码);
  • 所有影响功能使用的异常,包括:SQLException和除了业务异常之外的所有异常(RuntimeException和Exception)。

伪代码示例

 try {

          // 业务代码

      } catch (Exception e) {

          log.error("获取用户[{}]的用户信息时出错", userName, e);

      }
WARN

不应该出现但是不影响程序和当前请求正常运行的异常情况,比如:

  1. 有容错机制的时候出现的错误情况;
  2. 找不到配置文件,但是系统能自动创建配置文件;
  3. 即将接近临界值的时候,例如:缓存池占用达到警告线。

伪代码示例

 if (判断条件) {

          log.warn("缓存达到最大值警告[{}]", max);

               return;

           }

           //业务代码
INFO

系统的运行信息

  1. Service方法中对于系统/业务状态的变更;
  2. 主要逻辑中的分步骤。

外部接口部分

  1. 客户端请求参数(REST/WS);
  2. 调用第三方时的调用参数和调用结果。

伪代码示例

// 反例

public List listByBaseType(Integer baseTypeId) {



    log.info("开始查询基地");

    BaseExample ex=new BaseExample();

    BaseExample.Criteria ctr = ex.createCriteria();

    ctr.andIsDeleteEqualTo(IsDelete.USE.getValue());

    Optionals.doIfPresent(baseTypeId, ctr::andBaseTypeIdEqualTo);

    log.info("查询基地结束");

    return baseRepository.selectByExample(ex);

}



// 正例

public List handleHook(Integer baseTypeId) {



        String endpointSecret = endPointSecret;

        InputStream inputStream = request.getInputStream();

        byte[] bytes = IOUtils.toByteArray(inputStream);

        String payload = new String(bytes, StandardCharsets.UTF_8);

        String sigHeader = request.getHeader("Stripe-Signature");

        log.info("Stripe Webhook start, sigHeader[{}]", sigHeader);

        // 业务代码

}
DEBUG
  1. 可以填写所有想知道的相关信息(不代表可以随便写,debug信息要有意义,最好有相关参数);
  2. 生产环境需要关闭DEBUG信息;
  3. 如果在生产情况下需要开启DEBUG,需要使用开关进行管理,不能一直开启。
if (logger.isDebugEnabled()) {

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

}
TRACE

特别详细的系统运行完成信息,业务代码中,不要使用(除非有特殊用意,否则请使用DEBUG级别替代即可)。

伪代码示例

@Override

@Transactional

public void createUserAndBindMobile(@NotBlank String mobile, @NotNull User user) throws CreateConflictException{

    boolean debug = log.isDebugEnabled();

    log.debug("开始创建用户并绑定手机号. args[mobile=[{}],user=[{}]]", mobile, LogObjects.toString(user));



    try {

        user.setCreateTime(new Date());

        user.setUpdateTime(new Date());

        userRepository.insertSelective(user);

        log.debug("创建用户信息成功. insertedUser=[{}]",LogObjects.toString(user));

        UserMobileRelationship relationship = new UserMobileRelationship();

        relationship.setMobile(mobile);

        relationship.setOpenId(user.getOpenId());

        relationship.setCreateTime(new Date());

        relationship.setUpdateTime(new Date());

        userMobileRelationshipRepository.insertOnDuplicateKey(relationship);

        log.debug("绑定手机成功. relationship=[{}]",LogObjects.toString(relationship));

    }catch(DuplicateKeyException e){

        log.info("创建用户并绑定手机号失败,已存在相同的用户. openId=[{}],mobile=[{}]",user.getOpenId(),mobile);

        throw new CreateConflictException("创建用户发生冲突, openid=[%s]",user.getOpenId());

    }

}

2.2 日志要打印方法的入参和出参

例如在Controller层,请求入参、响应出参和响应异常,一般需要打印日志,出问题时,方便追踪代码逻辑运行的路线,建议这里使用日志切面统一日志打印。

其他层级的方法入参和出参,如有必要,可以打印整个出参和入参的数据,繁殖可以打印有效的关键日志,方便问题定位即可。

2.3 日志格式

能够清晰展示时间、线程、级别、Logger 名称和消息的格式,便于快速定位问题。

%d{yyyy-MM-dd HH:mm:ss} [%thread] %-5level %logger{36} - %msg%n

2.4 在多个if-else等条件时,每个分支首行尽量打印日志

可以在进入分支前打印日志,后续可以快速定位到进入了哪个分支,方便排查问题。

String requestNo = "RN7195458555001";

String channel = "weixin";

log.info("请求流水号[{}]支付处理,渠道为:{}", requestNo, channel);

if (Objects.equals("zhifubao", channel)) {

    // TODO

} else if (Objects.equals("yinlian", channel)) {

    // TODO

} else if (Objects.equals("weixin", channel)) {

    // TODO

} else {

    // TODO

}
2.5 日志级别比较低时,进行日志开关判断

对于trace、debug级别的日志打印,需要进行开关判断

if (log.isTraceEnabled()) {

    log.trace("trace log detail......");

}

if (log.isDebugEnabled()) {

    log.debug("debug log detail......");           

}
2.6 使用日志框架SLF4J中的API

SLF4J是门面模式的日志框架,需要更换日志框架实现时,在不改动代码的情况下,可以方便切换到不同的日志实现框架。

import org.slf4j.Logger;

import org.slf4j.LoggerFactory;

  

private static final Logger logger = LoggerFactory.getLogger(Demo.class);

  

// 或者使用Lombok  @Slf4j
2.7 建议使用参数占位{},而不是用+拼接

字符串使用“+”进行拼接操作,会有一定的性能损耗,虽然高版本的jdk对字符串拼接进行了性能优化,但不建议使用。

使用大括号{}进行占位符的替换,相比字符串拼接,性能上更高,日志代码也更加优雅。

log.info("Payment processing,requestNo={},channel={}", requestNo, channel);
2.8 不要使用e.printStackTrace()和System.out.println()
//打印在堆栈信息中,如果异常过来,会导致堆栈内存不足,出现运行极慢的现象,最后出现OOM,这是一种非常糟糕的现象。

try{

  // TODO 业务代码处理

}catch(Exception e){

 //System.out.println("xxx业务处理异常,异常信息:" + e);

  e.printStackTrace();

}





//日志会记录在日志文件中,占用的是磁盘内存,一般不会出现运行极慢的现象,如果磁盘占用内存比较高时,需要对日志进行备份处理,然后清理日志。

try {

    // TODO 业务代码处理

} catch (Exception e) {

    log.error("xxx业务处理异常 requestNo={}", requestNo, e);

}
2.9 异常日志不要只打一半,要输出全部错误信息
 
// 反例 e.getMessage()不会记录详细的堆栈异常信息,只会记录错误的基本描述信息,不利于排查问题。

log.error("xxx业务处理异常");

log.error("xxx业务处理异常", e.getMessage());



// 正例

log.error("xxx业务处理异常 requestNo={}", requestNo, e);

2.10 禁止在线上环境开启debug

一般业务系统的debug日志较多,引入的第三方框架dubug日志也较多,随着业务交易的增多,容易占用磁盘空间,最后可能会影响正常业务系统的运行,所以生产环境禁止开启dubug。

2.11 不要既打印了异常日志,又抛出异常
try {

    // TODO 业务代码处理

} catch (Exception e) {

    log.error("xxx业务处理异常", e);

    // BusinessException:自定义业务处理异常类

    throw new BusinessException("xxx业务处理异常", e);

}

此处会打印两次异常日志:
第一次是log.error(“XXX业务处理异常”,e);
第二次是throw new BusinessException(“XXX业务处理异常”,e);
可以根据具体情况来选择,例如此处就应该抛异常处理,那么就可以仅使用throw new BusinessException(“XXX业务处理异常”,e)即可。

2.12 避免重复打印日志

如果一次日志可以表达清楚,,则使用一次打印即可,避免日志信息冗余。

// 反例

log.info("创建用户信息 userId={}", userId);

log.info("创建用户信息 userName={}", userName);



// 正例

log.info("创建用户信息 userId={},userName={}", userId, userName);
2.13 日志文件分离

根据不同的日志级别,打印在不同的日志文件中,例如deug、info、error日志级别的日志分别创建一个日志文件debug.log、info.log、warn.log、error.log。

// 反例

log.info("创建用户信息 userId={}", userId);

log.info("创建用户信息 userName={}", userName);



// 正例

log.info("创建用户信息 userId={},userName={}", userId, userName);
2.14 核心功能模块,建议打印较完整地日志

业务系统中,核心功能地代码,尽可能打印日志完整,核心代码执行频率极高,出问题时,根据日志信息能够快速定位。

2.15 INFO日志和WARN日志

INFO级别的日志,每个小模块都必须有,关键的流程,都至少有INFO级别的日志。

守护线程清理资源等必须有WARN级别的日志。

2.16 异常信息要求

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

logger.error(各类参数或者对象toString + "_" + e.getMessage(), e);

2.17 禁止使用JSON工具打印

日志打印时禁止直接用 JSON 工具将对象转换成 String,如果你的类需要在log里面打印,请重写toString方法,不然不知道怎么找问题,只会打印对象名,不会知道具体的对象内容。

// 反例

log.info("付款成功逻辑处理开始[{}]", JSON.toJSONString(hookInfo));


http://www.kler.cn/a/412374.html

相关文章:

  • C 语言函数递归探秘:从基础概念到复杂问题求解的进阶之路
  • 利用 OSHI获取机器的硬件信息
  • EXTI配置流程 含中断延时消抖点亮小灯
  • 【GAMES101笔记速查——Lecture 19 Cameras,Lenses and Light Fields】
  • 24.11.26 Mybatis2
  • 【通俗理解】隐变量的变分分布探索——从公式到应用
  • AVL、B树和B+树
  • 学习笔记039——SpringBoot整合Redis
  • width设置100vh但出现横向滚动条的问题
  • 速度革命:esbuild如何改变前端构建游戏 (1)
  • 多模态大模型打造沉浸式社交体验,Soul App创始人张璐团队海外首秀GITEX GLOBAL
  • 使用OpenCV实现图像拼接
  • 【C++第三方库】Muduo库结合ProtoBuf库搭建服务端和客户端的过程和源码
  • 【JavaEE初阶 — 网络编程】Socket 套接字 & UDP数据报套接字编程
  • Linux 虚拟机下安装RedisJSON
  • 【Pytorch框架】无中生有,从0到1使用Dataset类处理MNIST数据集
  • 多线程1:基础概念、接口介绍、锁
  • 通俗理解人工智能、机器学习和深度学习的关系
  • 【carla生成车辆时遇到的问题】carla显示的坐标和carlaworld中提取的坐标y值相反
  • 前后端中Json数据的简单处理
  • Javaweb 前端 HTML css 案例 总结
  • 开发一个基于MACOS M1/2芯片的Android 12的模拟器
  • 基于STM32的智能风扇控制系统
  • digit_eye开发记录(2): Python读取MNIST数据集
  • 渗透测试笔记—window基础
  • 蓝桥杯每日真题 - 第24天