# 日志优化方案 - 生产环境快速错误定位

> 文档创建时间：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` | 查看指定接口的请求 |
