从入门到精通的实战指南
目录导读
- 为什么异常日志需要“上下文”
- 常见错误:只记录错误信息而不记录场景
- 详细上下文日志的三个核心要素
- 实战代码示例:在Java和Python中实现
- 日志框架配置技巧(Logback / Log4j2)
- 问答:开发者最常犯的6个错误
- 如何自动化上下文日志注入(AOP + MDC)
- 从“能运行”到“可诊断”的飞跃
为什么异常日志需要“上下文”
你有没有遇到过这样的场景:线上系统报错,但你看到的日志只有一行:
java.lang.NullPointerException at com.example.service.UserService.getUser(UserService.java:45)
然后呢?哪个用户触发了?请求参数是什么?当时数据库状态如何?什么都不清楚,你只能拿着这行日志去猜,去翻代码,甚至去问“刚才是谁操作了系统”——这就是典型的“无上下文日志”灾难。

上下文日志,就是在记录异常时,同时记录下导致该异常发生的所有相关环境信息,
- 当前请求的URL、HTTP方法、请求参数(但不包含密码等敏感信息)
- 当前登录用户ID、Session ID、客户端IP
- 调用链路中的唯一追踪ID(Trace ID)
- 关键业务数据(如订单号、商品ID、数据库查询条件)
- 运行时环境信息(线程名、JVM内存快照、组件版本)
只有带上这些信息,你才能在发生异常时,像看监控回放一样定位问题,而不是靠猜。
常见错误:只记录错误信息而不记录场景
很多团队在写日志时只记了异常信息,
try {
// 业务逻辑
} catch (Exception e) {
log.error("处理订单失败", e);
}
这条日志的问题:
- 没有记录是哪个订单
- 没有记录是哪个用户操作的
- 没有记录请求参数
- 没有记录当时的系统负载和缓存状态
如果这条日志出现在凌晨2点,运维人员根本无法复现,正确的做法应该是:
try {
log.info("开始处理订单, orderId={}, userId={}", orderId, userId);
// 业务逻辑
} catch (Exception e) {
log.error("处理订单失败, orderId={}, userId={}, requestBody={}",
orderId, userId, requestBody, e);
}
亮点:在catch块中,把关键业务标识和请求体一起打印出来,并附上异常堆栈。
详细上下文日志的三个核心要素
要写出高质量的上下文日志,你需要在代码中做到这三点:
- 唯一标识符:每一次请求或每一个任务,都应该有一个全局唯一的ID(例如UUID或雪花ID),把这个ID注入到日志中,就能把所有相关日志串联起来。
- 业务关键数据:只记录能帮助你定位问题的数据,而不是全部,订单号、用户ID、商品SKU,而不是用户密码或信用卡号。
- 环境快照:记录当前线程名、数据库连接池状态、缓存命中率等,这些信息在排查偶发性能问题时特别有用。
实战代码示例:在Java和Python中实现
Java示例(使用SLF4J + Logback)
@Slf4j
public class OrderService {
public void processOrder(String orderId, String userId, String requestBody) {
// 你可以使用MDC(Mapped Diagnostic Context)来关联上下文
MDC.put("traceId", UUID.randomUUID().toString());
MDC.put("userId", userId);
MDC.put("orderId", orderId);
try {
log.info("开始处理订单请求, requestBody={}", requestBody);
// ... 业务逻辑 ...
} catch (Exception e) {
log.error("订单处理异常, orderId={}, userId={}", orderId, userId, e);
} finally {
MDC.clear(); // 一定要清除MDC,避免线程池复用导致混乱
}
}
}
Python示例(使用logging模块)
import logging
import uuid
logger = logging.getLogger(__name__)
def process_order(order_id, user_id, request_body):
trace_id = str(uuid.uuid4())
logger.info(f"开始处理订单, trace_id={trace_id}, order_id={order_id}, user_id={user_id}")
try:
# 业务逻辑
pass
except Exception as e:
logger.error(f"订单处理异常, trace_id={trace_id}, order_id={order_id}, user_id={user_id}, request_body={request_body}", exc_info=True)
日志框架配置技巧(Logback / Log4j2)
光在代码中打印还远远不够,你还需要在日志配置中格式化输出,让上下文信息一目了然。
Logback配置示例(logback-spring.xml)
<appender name="FILE" class="ch.qos.logback.core.rolling.RollingFileAppender">
<encoder>
<pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{36} [%X{traceId}] [%X{userId}] - %msg%n</pattern>
</encoder>
</appender>
这个配置会把MDC中的traceId和userId自动注入到每一行日志中。
Log4j2配置示例(log4j2.xml)
<PatternLayout pattern="%d{ISO8601} [%t] %-5level %c{1} [%MDC{traceId}] [%MDC{userId}] - %msg%n"/>
核心思路:每一个日志条目都自动携带traceId和userId,这样你只需一次搜索就能找到所有相关日志。
问答:开发者最常犯的6个错误
问题1:为什么我记录了MDC,但日志里没有显示?
答:可能是因为你忘记了在finally块中调用MDC.clear(),如果使用线程池(如Tomcat线程池),不清除MDC会导致上下文信息被下一个请求误用。
问题2:应该记录哪些参数?有哪些敏感信息不能记录?
答:记录业务标识(orderId、userId)、请求路径、查询参数。绝对不要记录:密码、身份证号、银行卡号、Token或Session ID,可以在记录前用MaskUtils.mask(sensitiveData)脱敏。
问题3:日志太多怎么办?会不会影响性能?
答:采用异步日志(Logback的AsyncAppender)和采样日志(只对1%的请求记录详细上下文),不要在循环中反复打印日志,尽量合并成一次打印。
问题4:分布式系统中如何传递Trace ID?
答:通过HTTP Header(如X-Request-Id)在服务间传递,在Spring Cloud中可以用Spring Cloud Sleuth或Micrometer Tracing自动完成。
问题5:日志文件怎么检索上下文?
答:使用结构化日志,例如JSON格式,这样你可以在ELK、Splunk或Sumo Logic中直接用字段名搜索,orderId:"ORD12345"。
问题6:异常日志要不要记录完整堆栈?
答:必须记录,否则你无法知道异常发生的确切代码位置,使用log.error(..., e)而不是log.error(e.getMessage()),前者会打印堆栈,后者只打印一句话。
如何自动化上下文日志注入(AOP + MDC)
手动在每个方法里写MDC.put是体力活,推荐使用AOP(面向切面编程)来自动注入。
Spring AOP示例
@Aspect
@Component
public class LogContextAspect {
@Before("execution(* com.example.controller.*.*(..))")
public void injectContext(JoinPoint joinPoint) {
// 从请求中提取userId、traceId等
ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
if (attributes != null) {
HttpServletRequest request = attributes.getRequest();
MDC.put("traceId", request.getHeader("X-Request-Id"));
MDC.put("userId", request.getAttribute("userId"));
}
}
@After("execution(* com.example.controller.*.*(..))")
public void clearContext() {
MDC.clear();
}
}
这样,你的所有Controller方法都会自动携带上下文日志,不需要每个方法重复写。
从“能运行”到“可诊断”的飞跃
记录详细的上下文日志,本质上是在为你的代码建立事件档案,当异常发生时,这份档案就是你破解问题的唯一线索。
记住三个黄金原则:
- 每次异常都必须包含业务标识(orderId、userId、traceId)
- 不记录敏感信息,但记录关键数据
- 可视化日志(结构化输出、结合APM工具)
你的目标不是“出了错再查日志”,而是“看一眼日志就能立刻定位问题”,当你的日志体系达到这个水平,恭喜你,你已经从“写代码的”升级为“系统的真正守护者”了。
现在就检查一下你的项目:在所有的catch块中,是不是都包含了完整的上下文信息?如果没有,马上动手改——这可能是你本月做的性价比最高的一次优化。