日志规范:怎么写才不算写废话

日志这事,写多了烦人,写少了出事。线上出问题排查半天,发现日志要么没打,要么打了一堆没用的。

今天聊点实际的,总结一下怎么写好日志。

日志级别用对了吗

java 复制代码
// ERROR - 程序出错了,需要人工处理
logger.error("数据库连接失败,第{}次重试", retryCount, ex);

// WARN - 有问题,但程序还能跑
logger.warn("当前库存为0,用户:{}", userId);

// INFO - 正常业务流程的关键节点
logger.info("用户下单成功,订单号:{}", orderId);

// DEBUG - 开发调试用,生产环境默认不输出
logger.debug("收到消息,topic:{}, partition:{}", topic, partition);

级别越高越重要。线上出问题,先看 ERROR,再看 WARN。

什么时候该打日志

1. 系统启动和停止

java 复制代码
// 应用启动
logger.info("=== 应用启动,端口:{} ===", port);
logger.info("数据库连接池初始化完成,最大连接:{}", maxPoolSize);

// 应用关闭
logger.info("=== 收到关闭信号,开始优雅停机 ===");

2. 业务关键节点

java 复制代码
// 好的例子 - 记录关键流程
logger.info("用户登录成功,用户ID:{},来源:{}", userId, source);
logger.info("订单支付成功,订单号:{},金额:{}", orderId, amount);
logger.info("异步任务开始执行,任务ID:{},类型:{}", taskId, taskType);

3. 外部调用

java 复制代码
// 调用外部接口
logger.info("调用支付接口,订单号:{},金额:{}", orderId, amount);
long start = System.currentTimeMillis();
try {
    PaymentResponse response = paymentClient.pay(request);
    logger.info("支付接口调用成功,耗时:{}ms", System.currentTimeMillis() - start);
} catch (Exception e) {
    logger.error("支付接口调用失败,订单号:{},耗时:{}ms,错误:{}",
        orderId, System.currentTimeMillis() - start, e.getMessage());
}

4. 异常捕获

java 复制代码
// 不好的写法
try {
    doSomething();
} catch (Exception e) {
    log.error("出错了");  // 打了等于没打
}

// 好的写法
try {
    doSomething();
} catch (BusinessException e) {
    logger.error("业务异常,业务编码:{},错误信息:{}", e.getCode(), e.getMessage());
} catch (Exception e) {
    logger.error("系统异常,操作:{},用户ID:{},异常类型:{}",
        operation, userId, e.getClass().getName(), e);
}

什么时候不该打日志

1. 循环内的日志

java 复制代码
// 不好的写法
for (OrderItem item : orderItems) {
    logger.info("处理订单项:{}", item.getId());  // 1000个订单就是1000条日志
}

2. 不要打印敏感信息

java 复制代码
// 不好的写法
logger.info("用户登录,用户名:{},密码:{}", username, password);  // 密码都打出来了

// 好的写法
logger.info("用户登录,用户名:{},来源:{}", username, source);

3. 不要打印无意义的调试信息

java 复制代码
// 不好的写法
logger.info("进入方法A");
logger.info("开始查询数据库");
logger.info("查询结束");

// 这些日志没有任何价值,线上全是噪音

日志内容要规范

1. 带上关键上下文

java 复制代码
// 不好的写法
logger.error("处理失败");

// 好的写法
logger.error("订单处理失败,订单号:{},用户ID:{},原因:{}", orderId, userId, reason);

2. 统一日志格式

复制代码
# 推荐格式
[时间] [级别] [线程] [类名] [traceId] - 消息

# 例子
2024-01-15 10:23:45.123 INFO [http-nio-8080-exec-1] [OrderService] [abc123] 用户下单成功,订单号:ORDER001

3. 使用占位符而不是字符串拼接

java 复制代码
// 不好的写法
logger.info("用户" + userId + "下单成功,金额" + amount);

// 好的写法
logger.info("用户{}下单成功,金额{}", userId, amount);

// 原因:字符串拼接即使不打印也会执行,浪费性能

traceId 打通全链路

微服务架构下,一个请求经过多个服务,排查问题需要 traceId 串联:

java 复制代码
// 拦截器:请求进来时生成 traceId
public class TraceInterceptor implements HandlerInterceptor {
    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) {
        String traceId = request.getHeader("X-Trace-Id");
        if (traceId == null) {
            traceId = UUID.randomUUID().toString().replace("-", "");
        }
        TraceContext.setTraceId(traceId);
        MDC.put("traceId", traceId);
        return true;
    }
}

// 配置文件
logback.xml
<pattern>[%d{yyyy-MM-dd HH:mm:ss.SSS}] [%level] [%thread] [%logger{36}] [%X{traceId}] - %msg%n</pattern>

// 日志输出
2024-01-15 10:23:45.123 INFO [http-nio-8080-exec-1] [OrderService] [abc123] 用户下单成功
2024-01-15 10:23:45.125 INFO [http-nio-8080-exec-1] [PaymentService] [abc123] 开始调用支付

几个实用技巧

1. 参数太长的处理

java 复制代码
// 订单备注可能有几百个字符
String remark = order.getRemark();
logger.info("用户下单成功,订单号:{},备注:{}",
    orderId, remark != null && remark.length() > 50 ? remark.substring(0, 50) + "..." : remark);

2. 集合内容的打印

java 复制代码
List<Long> ids = Arrays.asList(1L, 2L, 3L);
// 不要直接打印集合,可能很大
logger.info("订单IDs:{}", ids);  // 集合太大会撑爆日志

// 打印长度和前几个
logger.info("订单IDs数量:{},前10个:{}",
    ids.size(), ids.stream().limit(10).collect(Collectors.toList()));

3. 异步日志防丢失

java 复制代码
// 重要业务日志用异步方式避免阻塞
logger.info("关键业务操作,用户:{},操作:{},结果:{}", userId, operation, result);

// logback 配置异步 appender
<appender name="ASYNC_INFO" class="ch.qos.logback.classic.AsyncAppender">
    <appender-ref ref="INFO_FILE"/>
    <queueSize>512</queueSize>
    <discardingThreshold>0</discardingThreshold>
</appender>

日志归档和清理

xml 复制代码
<!-- logback 配置日志归档 -->
<appender name="FILE" class="ch.qos.logback.core.rolling.RollingFileAppender">
    <file>logs/app.log</file>
    <rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
        <fileNamePattern>logs/app.%d{yyyy-MM-dd}.log.gz</fileNamePattern>
        <maxHistory>30</maxHistory>
        <totalSizeCap>10GB</totalSizeCap>
    </rollingPolicy>
</appender>

总结

场景 建议
系统启动/停止 INFO,记录关键配置
业务关键节点 INFO,带齐上下文参数
外部调用 INFO 开始,WARN/ERROR 结果
异常捕获 ERROR,记录堆栈和关键参数
循环内 不要打,或打摘要
敏感信息 不要打,脱敏处理

好的日志:能帮你在凌晨3点快速定位问题,而不是让你在几千行日志里猜。

相关推荐
Binarydog_Lee2 小时前
Rust 核心机制:所有权、借用与生命周期
开发语言·rust
XMYX-02 小时前
17 - Go 通道 Channel 底层原理 + 实战详解
开发语言·golang
CQU_JIAKE2 小时前
4.17[Q]
java·linux·服务器
Hello--_--World3 小时前
ES13:类私有属性和方法、顶层 await、at() 方法、Object.hasOwnProperty()、类静态块 相关知识点
开发语言·javascript·es13
Hugh-Yu-1301233 小时前
二元一次方程组求解器c++代码
开发语言·c++·算法
weixin_520649873 小时前
C#进阶-特性全知识点总结
开发语言·c#
楼田莉子3 小时前
同步/异步日志系统:日志落地模块\日志器模块\异步日志模块
linux·服务器·c++·学习·设计模式
文祐3 小时前
C++类之虚函数表及其内存布局
开发语言·c++
亦暖筑序3 小时前
Spring AI Alibaba 报错合集:我踩过的那些坑
java·后端