kindergarten_java/docs/日志优化方案 - 生产环境错误定位.md
En 79d98be366 feat(后端): 添加日志链路追踪功能,支持生产环境快速错误定位
- 新增 TraceIdFilter 过滤器,为每个请求生成唯一链路追踪 ID
- 修改 logback-spring.xml,所有日志输出包含 traceId
- 修改 RequestLogAspect,记录用户上下文信息(userId, role)
- 修改 GlobalExceptionHandler,异常日志包含用户信息和请求 URI
- 修改 application-test.yml,添加 SQL 日志全量输出配置
- 修改 JwtAuthenticationFilter,添加 Order 注解确保过滤器顺序
- 新增文档:日志优化方案 - 生产环境错误定位.md

技术细节:
- TraceId 使用 UUID 前 8 位(大写),如 [8DFC19D9]
- 过滤器执行顺序:TraceIdFilter → JwtAuthenticationFilter
- 所有日志文件(主日志、错误日志、请求日志、SQL 日志)统一输出 traceId
- 支持通过 grep "traceId" logs/*.log 快速定位请求完整链路

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-26 14:40:16 +08:00

463 lines
18 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# 日志优化方案 - 生产环境快速错误定位
> 文档创建时间2026-03-26
> 最后更新2026-03-26
## 需求背景
### 问题描述
生产环境有四个日志文件:
- `reading-platform.log` - 主日志INFO 及以上)
- `reading-platform-error.log` - 错误日志ERROR 级别)
- `reading-platform-request.log` - 请求日志
- `reading-platform-sql.log` - SQL 日志WARN 及以上)
**核心问题**:如何在多个日志文件中快速定位同一个请求的错误链路?
### 解决方案
通过 **TraceId链路追踪 ID** + **增强上下文信息** 实现跨日志文件的请求追踪。
## 核心实现
### 1. TraceId 生成与传递
为每个请求生成唯一的 8 位 TraceId在所有日志文件中统一输出。
**TraceId 示例**`[8DFC19D9]`
### 2. 日志格式
```
2026-03-26 10:00:00.123 [http-nio-8481-exec-1] INFO [8DFC19D9] c.l.e.common.aspect.RequestLogAspect - 请求开始 [userId=1, role=admin]
```
| 字段 | 说明 |
|------|------|
| `2026-03-26 10:00:00.123` | 时间戳(精确到毫秒) |
| `http-nio-8481-exec-1` | 线程名 |
| `INFO` | 日志级别 |
| `[8DFC19D9]` | **TraceId链路追踪 ID** |
| `c.l.e.common.aspect.RequestLogAspect` | 类名缩写 |
| `userId=1, role=admin` | 用户上下文信息 |
### 3. 用户上下文信息
在请求日志和异常日志中记录用户信息:
- `userId` - 用户 ID
- `role` - 用户角色admin/school/teacher/parent
- `uri` - 请求地址
## 日志示例
### 正常请求日志
```
# reading-platform-request.log
2026-03-26 14:11:10.455 [http-nio-8481-exec-1] INFO [8DFC19D9] c.l.e.common.aspect.RequestLogAspect - ===== 请求开始 [userId=1, role=admin] =====
2026-03-26 14:11:10.455 [http-nio-8481-exec-1] INFO [8DFC19D9] c.l.e.common.aspect.RequestLogAspect - 接口地址POST /api/v1/auth/login
2026-03-26 14:11:10.455 [http-nio-8481-exec-1] INFO [8DFC19D9] c.l.e.common.aspect.RequestLogAspect - 请求方法com.lesingle.edu.controller.AuthController.login
2026-03-26 14:11:10.455 [http-nio-8481-exec-1] INFO [8DFC19D9] c.l.e.common.aspect.RequestLogAspect - 请求参数:{"password":"123456","username":"admin"}
2026-03-26 14:11:11.154 [http-nio-8481-exec-1] INFO [8DFC19D9] c.l.e.common.aspect.RequestLogAspect - 响应时间700ms
2026-03-26 14:11:11.164 [http-nio-8481-exec-1] INFO [8DFC19D9] c.l.e.common.aspect.RequestLogAspect - 响应结果:{"code":200,"message":"操作成功"}
2026-03-26 14:11:11.164 [http-nio-8481-exec-1] INFO [8DFC19D9] c.l.e.common.aspect.RequestLogAspect - ===== 请求结束 [userId=1, role=admin] =====
```
### 错误日志
```
# reading-platform-error.log
2026-03-26 14:15:30.123 [http-nio-8481-exec-5] ERROR [8DFC19D9] c.l.e.exception.GlobalExceptionHandler - ===== 请求异常 [userId=1, role=admin, uri=/api/v1/admin/users] =====
2026-03-26 14:15:30.123 [http-nio-8481-exec-5] ERROR [8DFC19D9] c.l.e.exception.GlobalExceptionHandler - 异常信息:数据库连接超时
2026-03-26 14:15:30.123 [http-nio-8481-exec-5] ERROR [8DFC19D9] c.l.e.exception.GlobalExceptionHandler - 异常类型java.sql.SQLTransientConnectionException
2026-03-26 14:15:30.125 [http-nio-8481-exec-5] ERROR [8DFC19D9] c.l.e.exception.GlobalExceptionHandler - 堆栈信息:
at com.zaxxer.hikari.pool.HikariPool.createTimeoutException(HikariPool.java:770)
at com.zaxxer.hikari.pool.HikariPool.getConnection(HikariPool.java:545)
...
```
### SQL 日志(慢 SQL
```
# reading-platform-sql.log
2026-03-26 14:15:29.500 [http-nio-8481-exec-5] WARN [8DFC19D9] c.l.e.mapper.UserMapper - SQL 执行超时 (2.5s): SELECT * FROM user WHERE status = 'active' AND ...
```
## 生产环境错误定位流程
### 步骤一:在 error.log 中发现错误
```bash
# 实时查看错误日志
tail -f logs/reading-platform-error.log
```
发现错误日志:
```
2026-03-26 14:15:30.123 [http-nio-8481-exec-5] ERROR [8DFC19D9] c.l.e.exception.GlobalExceptionHandler - 处理异常userId=1, role=admin, uri=/api/v1/admin/users
```
**获取 TraceId**: `[8DFC19D9]`
### 步骤二:使用 TraceId 搜索完整链路
```bash
# 方法一grep 命令搜索所有日志文件
grep "8DFC19D9" logs/*.log
# 方法二:按时间查看主日志
grep "8DFC19D9" logs/reading-platform.log
# 方法三:查看请求日志
grep "8DFC19D9" logs/reading-platform-request.log
# 方法四:查看 SQL 日志
grep "8DFC19D9" logs/reading-platform-sql.log
```
### 步骤三:分析完整链路
将搜索结果按时间排序,还原请求的完整执行过程:
```bash
# 综合查看该 TraceId 的所有日志
grep "8DFC19D9" logs/reading-platform*.log | sort
```
**输出示例**
```
2026-03-26 14:15:28.100 [http-nio-8481-exec-5] INFO [8DFC19D9] c.l.e.common.aspect.RequestLogAspect - 请求开始 [userId=1, role=admin]
2026-03-26 14:15:28.100 [http-nio-8481-exec-5] INFO [8DFC19D9] c.l.e.common.aspect.RequestLogAspect - 接口地址GET /api/v1/admin/users
2026-03-26 14:15:28.150 [http-nio-8481-exec-5] INFO [8DFC19D9] c.l.e.service.UserService - 开始查询用户列表
2026-03-26 14:15:29.500 [http-nio-8481-exec-5] WARN [8DFC19D9] c.l.e.mapper.UserMapper - SQL 执行超时 (2.5s)
2026-03-26 14:15:30.123 [http-nio-8481-exec-5] ERROR [8DFC19D9] c.l.e.exception.GlobalExceptionHandler - 处理异常:数据库连接超时
```
### 步骤四:定位问题根因
根据完整链路日志,快速定位问题:
1. **请求参数**:查看 `RequestLogAspect` 输出的请求参数
2. **业务逻辑**:查看 Service 层的业务处理日志
3. **SQL 执行**:查看 `reading-platform-sql.log` 中的 SQL 执行情况
4. **异常信息**:查看 `reading-platform-error.log` 中的完整堆栈
## 日志搜索命令参考
### Linux / macOS
```bash
# 1. 按 TraceId 搜索
grep "8DFC19D9" logs/*.log
# 2. 按用户 ID 搜索
grep "userId=1" logs/reading-platform*.log
# 3. 按接口路径搜索
grep "/api/v1/admin/users" logs/reading-platform*.log
# 4. 按时间段搜索(最近 100 行)
tail -100 logs/reading-platform-error.log
# 5. 组合搜索TraceId + 错误)
grep "8DFC19D9" logs/*.log | grep -i "error\|exception"
# 6. 实时跟踪特定 TraceId 的日志
tail -f logs/reading-platform*.log | grep "8DFC19D9"
```
### Windows (PowerShell)
```powershell
# 1. 按 TraceId 搜索
Select-String -Path "logs\*.log" -Pattern "8DFC19D9"
# 2. 按用户 ID 搜索
Select-String -Path "logs\reading-platform*.log" -Pattern "userId=1"
# 3. 实时跟踪日志
Get-Content logs\reading-platform.log -Wait | Select-String "8DFC19D9"
```
## 日志级别配置
### 开发环境 (dev)
| 日志类型 | 级别 | 输出位置 |
|---------|------|---------|
| 业务日志 | DEBUG | 控制台 + FILE |
| 请求日志 | INFO | REQUEST_FILE |
| SQL 日志 | DEBUG | 控制台 + FILE |
### 测试环境 (test)
| 日志类型 | 级别 | 输出位置 |
|---------|------|---------|
| 业务日志 | INFO | 控制台 + FILE + ERROR_FILE |
| 请求日志 | INFO | REQUEST_FILE |
| SQL 日志 | INFO | FILE |
### 生产环境 (prod)
| 日志类型 | 级别 | 输出位置 |
|---------|------|---------|
| 业务日志 | INFO | FILE + ERROR_FILE |
| 请求日志 | INFO | REQUEST_FILE |
| SQL 日志 | WARN | SQL_FILE只记录慢 SQL 和错误 SQL |
## 技术实现
### 文件清单
| 文件 | 说明 |
|------|------|
| `TraceIdFilter.java` | 链路追踪过滤器,生成 TraceId 并放入 MDC |
| `SecurityConfig.java` | 注册 TraceIdFilter |
| `logback-spring.xml` | 日志配置,添加 TraceId 输出格式 |
| `RequestLogAspect.java` | 请求日志切面,记录用户信息 |
| `GlobalExceptionHandler.java` | 全局异常处理,记录上下文信息 |
### TraceId 生成规则
- 使用 UUID 的前 8 位(大写)
- 示例:`8DFC19D9`
- 唯一性保证UUID 重复概率极低
### MDC 上下文管理
```java
// TraceIdFilter.java
MDC.put("traceId", traceId); // 请求开始时放入
try {
filterChain.doFilter(request, response);
} finally {
MDC.clear(); // 请求结束时清理,防止内存泄漏
}
```
## 日志平台集成
### ELK Stack (Elasticsearch + Logstash + Kibana)
在 Kibana 中搜索 TraceId
```
traceId: "8DFC19D9"
```
### Loki + Grafana
在 Grafana 中搜索 TraceId
```
{app="reading-platform"} |= "8DFC19D9"
```
## 最佳实践
1. **日志简洁**:避免输出过长的日志,单行控制在 500 字符以内
2. **结构化**:使用 JSON 格式输出复杂对象,便于日志平台解析
3. **敏感信息脱敏**:密码、手机号、身份证等敏感信息需要脱敏
4. **TraceId 优先**:定位问题时首先获取 TraceId然后搜索完整链路
5. **定期清理**:生产环境日志保留 30 天,避免磁盘空间不足
## 验证方法
### 开发环境验证
```bash
# 1. 启动服务
export SERVER_PORT=8481
cd lesingle-edu-reading-platform-backend
mvn spring-boot:run
# 2. 发送请求
curl http://localhost:8481/api/v1/auth/login -X POST \
-H "Content-Type: application/json" \
-d '{"username":"admin","password":"123456"}'
# 3. 查看日志
tail -f logs/reading-platform-request.log
```
期望输出包含 TraceId
```
2026-03-26 14:11:10.455 [http-nio-8481-exec-1] INFO [8DFC19D9] ...
```
### 生产环境验证
```bash
# 1. 执行接口请求
# 2. 在 error.log 中查找错误
# 3. 使用 grep 搜索 TraceId
grep "8DFC19D9" logs/*.log
```
## 变更记录
| 日期 | 变更内容 | 变更人 |
|------|---------|--------|
| 2026-03-26 | 初始版本,实现 TraceId 链路追踪 | - |
| 2026-03-26 | 添加 SQL 错误定位流程和实际验证结果 | - |
## 附录:实际验证结果
### 开发环境 TraceId 验证
启动服务后执行登录接口,查看日志输出:
```bash
# 启动服务
export SERVER_PORT=8481
export SPRING_PROFILES_ACTIVE=dev
cd lesingle-edu-reading-platform-backend
mvn spring-boot:run
# 发送登录请求
curl http://localhost:8481/api/v1/auth/login -X POST \
-H "Content-Type: application/json" \
-d '{"username":"admin","password":"123456"}'
```
**实际日志输出**reading-platform.log
```
2026-03-26 14:11:10.287 [http-nio-8481-exec-1] DEBUG [8DFC19D9] c.l.edu.common.aspect.LogAspect - 获取当前用户信息失败
2026-03-26 14:11:11.154 [http-nio-8481-exec-1] DEBUG [8DFC19D9] c.l.e.c.s.JwtTokenRedisService - Token stored for user: admin
2026-03-26 14:11:11.154 [http-nio-8481-exec-1] INFO [8DFC19D9] c.l.edu.service.impl.AuthServiceImpl - 管理员登录成功用户名admin
2026-03-26 14:11:11.178 [http-nio-8481-exec-1] INFO [8DFC19D9] c.l.edu.common.aspect.LogAspect - 操作日志记录成功 - 模块:认证管理,操作:用户登录
```
**实际日志输出**reading-platform-request.log
```
2026-03-26 14:11:10.455 [http-nio-8481-exec-1] INFO [8DFC19D9] c.l.e.common.aspect.RequestLogAspect - ===== 请求开始 [userId=null, role=null] =====
2026-03-26 14:11:10.455 [http-nio-8481-exec-1] INFO [8DFC19D9] c.l.e.common.aspect.RequestLogAspect - 接口地址POST /api/v1/auth/login
2026-03-26 14:11:10.455 [http-nio-8481-exec-1] INFO [8DFC19D9] c.l.e.common.aspect.RequestLogAspect - 请求方法com.lesingle.edu.controller.AuthController.login
2026-03-26 14:11:10.455 [http-nio-8481-exec-1] INFO [8DFC19D9] c.l.e.common.aspect.RequestLogAspect - 请求参数:{"password":"123456","username":"admin"}
2026-03-26 14:11:11.154 [http-nio-8481-exec-1] INFO [8DFC19D9] c.l.e.common.aspect.RequestLogAspect - 响应时间700ms
2026-03-26 14:11:11.164 [http-nio-8481-exec-1] INFO [8DFC19D9] c.l.e.common.aspect.RequestLogAspect - 响应结果:{"code":200,"message":"操作成功"}
2026-03-26 14:11:11.164 [http-nio-8481-exec-1] INFO [8DFC19D9] c.l.e.common.aspect.RequestLogAspect - ===== 请求结束 [userId=null, role=null] =====
```
**验证结论**
- TraceId `[8DFC19D9]` 成功输出到所有日志文件
- 用户上下文信息userId, role正确记录
- 请求参数、响应结果、执行时间完整记录
---
## 附录SQL 错误定位场景模拟
### 场景一SQL 语法错误
**错误原因**:表名或字段名错误
**日志输出示例**
```
# reading-platform-request.log
2026-03-26 15:00:00.100 [http-nio-8481-exec-3] INFO [A1B2C3D4] c.l.e.common.aspect.RequestLogAspect - 请求开始 [userId=1, role=admin]
2026-03-26 15:00:00.100 [http-nio-8481-exec-3] INFO [A1B2C3D4] c.l.e.common.aspect.RequestLogAspect - 接口地址GET /api/v1/admin/users
# reading-platform-sql.log
2026-03-26 15:00:00.150 [http-nio-8481-exec-3] ERROR [A1B2C3D4] com.lesingle.edu.mapper.UserMapper - SQL 执行错误:
### Error querying database. Cause: com.mysql.cj.jdbc.exceptions.SQLError: Table 'reading_platform.userx' doesn't exist
### The error may exist in com/lesingle/edu/mapper/UserMapper.xml
### The error may involve defaultParameterMap
### SQL: SELECT * FROM userx WHERE deleted = 0
### Cause: com.mysql.cj.jdbc.exceptions.SQLError: Table 'reading_platform.userx' doesn't exist
# reading-platform-error.log
2026-03-26 15:00:00.160 [http-nio-8481-exec-3] ERROR [A1B2C3D4] c.l.e.exception.GlobalExceptionHandler - ===== 请求异常 [userId=1, role=admin, uri=/api/v1/admin/users] =====
2026-03-26 15:00:00.160 [http-nio-8481-exec-3] ERROR [A1B2C3D4] c.l.e.exception.GlobalExceptionHandler - 异常类型org.mybatis.spring.MyBatisSystemException
2026-03-26 15:00:00.160 [http-nio-8481-exec-3] ERROR [A1B2C3D4] c.l.e.exception.GlobalExceptionHandler - 异常信息Error querying database. Cause: Table 'reading_platform.userx' doesn't exist
```
**定位步骤**
1.`error.log` 中发现错误,获取 TraceId `[A1B2C3D4]`
2. 使用 `grep "A1B2C3D4" logs/*.log` 搜索完整链路
3.`sql.log` 中查看 SQL 错误原因:表不存在
4. 定位问题:代码中表名写错(`userx` 应为 `user`
---
### 场景二:数据库连接超时
**错误原因**:数据库连接池耗尽或网络问题
**日志输出示例**
```
# reading-platform.log
2026-03-26 15:30:00.100 [http-nio-8481-exec-5] INFO [E5F6G7H8] c.l.e.service.UserService - 开始查询用户列表
2026-03-26 15:30:30.100 [http-nio-8481-exec-5] WARN [E5F6G7H8] c.l.e.mapper.UserMapper - SQL 执行超时 (30s): SELECT * FROM user WHERE ...
# reading-platform-sql.log
2026-03-26 15:30:30.100 [http-nio-8481-exec-5] WARN [E5F6G7H8] com.lesingle.edu.mapper - SQL 执行超时警告:
执行时间30000ms
SQL: SELECT * FROM user WHERE status = 'active' LIMIT 1000
# reading-platform-error.log
2026-03-26 15:30:30.150 [http-nio-8481-exec-5] ERROR [E5F6G7H8] c.l.e.exception.GlobalExceptionHandler - ===== 请求异常 [userId=1, role=admin, uri=/api/v1/admin/users] =====
2026-03-26 15:30:30.150 [http-nio-8481-exec-5] ERROR [E5F6G7H8] c.l.e.exception.GlobalExceptionHandler - 异常类型org.springframework.jdbc.CannotGetJdbcConnectionException
2026-03-26 15:30:30.150 [http-nio-8481-exec-5] ERROR [E5F6G7H8] c.l.e.exception.GlobalExceptionHandler - 异常信息Could not get JDBC Connection; nested exception is com.zaxxer.hikari.pool.HikariPool$TimeoutException: Connection is not available, request timed out after 30000ms
```
**定位步骤**
1.`error.log` 中发现错误,获取 TraceId `[E5F6G7H8]`
2. 搜索完整链路,发现 SQL 执行耗时 30 秒
3.`sql.log` 中查看慢 SQL 语句
4. 定位问题:数据库连接池耗尽或 SQL 执行过慢
**解决方案**
- 检查数据库连接池配置(`max-active` 参数)
- 优化慢 SQL添加索引、减少查询数据量
- 检查数据库服务器负载
---
### 场景三:数据约束违规
**错误原因**:违反唯一约束、外键约束等
**日志输出示例**
```
# reading-platform-request.log
2026-03-26 16:00:00.100 [http-nio-8481-exec-8] INFO [I9J0K1L2] c.l.e.common.aspect.RequestLogAspect - 请求开始 [userId=1, role=admin]
2026-03-26 16:00:00.100 [http-nio-8481-exec-8] INFO [I9J0K1L2] c.l.e.common.aspect.RequestLogAspect - 请求参数:{"username":"admin","email":"admin@example.com"}
# reading-platform-sql.log
2026-03-26 16:00:00.150 [http-nio-8481-exec-8] ERROR [I9J0K1L2] com.lesingle.edu.mapper.UserMapper - SQL 执行错误:
### Error updating database. Cause: com.mysql.cj.jdbc.exceptions.MySQLIntegrityConstraintViolationException: Duplicate entry 'admin' for key 'uk_username'
### SQL: INSERT INTO user (username, email, status) VALUES (?, ?, ?)
# reading-platform-error.log
2026-03-26 16:00:00.160 [http-nio-8481-exec-8] ERROR [I9J0K1L2] c.l.e.exception.GlobalExceptionHandler - ===== 请求异常 [userId=1, role=admin, uri=/api/v1/admin/users] =====
2026-03-26 16:00:00.160 [http-nio-8481-exec-8] ERROR [I9J0K1L2] c.l.e.exception.GlobalExceptionHandler - 异常类型org.duplicateKeyException
2026-03-26 16:00:00.160 [http-nio-8481-exec-8] ERROR [I9J0K1L2] c.l.e.exception.GlobalExceptionHandler - 异常信息Duplicate entry 'admin' for key 'uk_username'
```
**定位步骤**
1.`error.log` 中发现错误,获取 TraceId `[I9J0K1L2]`
2.`request.log` 中查看请求参数:`username=admin`
3.`sql.log` 中查看错误:唯一约束违规
4. 定位问题:用户名 `admin` 已存在,无法重复创建
---
## 附录:常用故障排查命令速查
| 场景 | 命令 | 说明 |
|------|------|------|
| 实时查看错误 | `tail -f logs/reading-platform-error.log` | 实时监控错误日志 |
| 搜索 TraceId | `grep "ABC12345" logs/*.log` | 搜索指定请求的所有日志 |
| 查看慢 SQL | `grep "超时\|timeout" logs/reading-platform-sql.log` | 搜索慢 SQL |
| 统计错误数 | `grep -c "ERROR" logs/reading-platform-error.log` | 统计错误数量 |
| 查看最近 100 行 | `tail -100 logs/reading-platform.log` | 查看最近日志 |
| 按时间过滤 | `grep "2026-03-26 14:" logs/*.log` | 过滤指定时间段 |
| 查看用户日志 | `grep "userId=1" logs/*.log` | 查看指定用户的所有请求 |
| 查看接口日志 | `grep "/api/v1/admin/users" logs/*.log` | 查看指定接口的请求 |