Files
YG_FT/docs/生产级日志系统方案.md
2026-08-19 17:26:49 +08:00

489 lines
15 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.
好的,这是一份可以直接放在项目根目录的 `日志规范要求.md`,涵盖**格式标准、分类分级、内容规范、链路追踪、性能安全、运维告警**六大模块,每条规范都配有正反例,你的团队照着这个写代码就行。
---
# 生产级日志规范要求
> 版本v2.0 | 适用于所有后端服务Python/Java/Go/Node.js
---
## 一、核心原则
| 原则 | 说明 |
|------|------|
| **结构化** | 所有日志必须输出为 JSON 格式,便于自动化采集和分析 |
| **可追踪** | 每个请求链路必须有唯一的 `traceId`,贯穿全流程 |
| **有上下文** | 每条日志必须包含足够的业务信息,能独立理解发生了什么 |
| **高性能** | 异步打印,禁止在业务主流程中同步写磁盘 |
| **安全合规** | 敏感信息自动脱敏禁止打印密码、token、身份证号等 |
| **可告警** | ERROR 日志必须触发实时告警,且有明确的错误分类 |
---
## 二、日志分类
生产环境必须按用途分流存储,**禁止所有日志混写在同一文件**
| 分类 | 文件名示例 | 用途 | 保留周期 |
|------|-----------|------|----------|
| **业务日志** | `app-biz.log` | 记录核心业务流程(订单、支付、登录、任务状态变更等) | 7天热存 + 30天冷存 |
| **系统日志** | `app-sys.log` | 记录框架、中间件、连接池、GC、线程池状态 | 7天 |
| **访问日志** | `app-access.log` | 记录所有 HTTP/RPC 请求的入参、出参、耗时 | 15天用于审计 |
| **错误日志** | `app-error.log` | **仅记录 ERROR 级别**,含完整堆栈 | 30天用于复盘 |
**配置要点**
- 业务日志和错误日志必须独立文件,便于快速定位异常
- 框架类日志(如 `httpx``urllib3`)归入系统日志,且生产环境设为 WARN 级别
---
## 三、日志格式标准
### 3.1 统一 JSON 格式
所有日志必须输出为以下 JSON 结构,**字段名不得随意变更**
```json
{
"@timestamp": "2026-08-19T10:30:45.123+08:00",
"level": "INFO",
"logger": "com.order.service.OrderService",
"traceId": "abc-123-def-456",
"spanId": "span-001",
"userId": "U10086",
"message": "订单状态更新成功",
"fields": {
"orderId": "ORD-20260819-001",
"fromStatus": "PENDING",
"toStatus": "PAID",
"costMs": 23,
"retryCount": 0
},
"file": "OrderService.java:156",
"thread": "http-nio-8080-exec-8",
"host": "pod-order-7x9k2",
"app": "order-service",
"env": "prod"
}
```
### 3.2 字段说明
| 字段 | 类型 | 必填 | 说明 |
|------|------|------|------|
| `@timestamp` | string | ✅ | ISO8601 格式,带时区(如 `+08:00` |
| `level` | string | ✅ | DEBUG / INFO / WARNING / ERROR |
| `logger` | string | ✅ | 日志记录器名称,通常为类名 |
| `traceId` | string | ✅ | 全局唯一追踪ID从入口生成全链路透传 |
| `spanId` | string | 推荐 | 当前节点ID用于区分调用链中的不同服务 |
| `userId` | string | 业务必填 | 操作用户标识,未登录可为空 |
| `message` | string | ✅ | 人类可读的日志摘要,简洁明了 |
| `fields` | object | ✅ | 结构化业务字段,所有动态数据放入此处 |
| `file` | string | 推荐 | 代码文件名和行号 |
| `thread` | string | 推荐 | 线程名 |
| `host` | string | 推荐 | 主机名或 Pod 名称 |
| `app` | string | ✅ | 应用名称 |
| `env` | string | ✅ | dev / test / staging / prod |
| `error` | object | ERROR时必填 | 包含 `type``message``stack_trace` |
### 3.3 ERROR 日志额外字段
`level = ERROR` 时,必须包含:
```json
{
"error": {
"type": "ConnectionTimeoutError",
"message": "连接下游服务超时",
"stack_trace": "完整堆栈信息...",
"root_cause": "socket timeout after 3000ms"
}
}
```
---
## 四、日志内容规范
### 4.1 日志级别使用标准
| 级别 | 使用场景 | 示例 |
|------|----------|------|
| **DEBUG** | 开发调试信息,生产环境**默认关闭** | 变量值、中间计算结果 |
| **INFO** | 关键业务流程节点、状态变更、外部调用结果 | 订单创建成功、支付回调收到、任务状态变更 |
| **WARNING** | 可恢复的异常、降级处理、重试、资源使用超阈值 | 重试第3次成功、缓存穿透、磁盘使用率>80% |
| **ERROR** | 业务失败、系统异常、需要人工介入的错误 | 支付失败、数据库连接断开、第三方接口返回500 |
### 4.2 INFO 级别日志内容要求
每条 INFO 日志必须回答 **5W1H**
```
Who谁操作 + What做了什么 + When何时 + Where哪个服务/节点) + Why上下文 + How结果如何
```
**✅ 正例:**
```python
logger.info(
"任务日志拉取成功",
extra={
"userId": "U10086",
"fields": {
"jobId": "ft_a016cd8885cd",
"tailLines": 5000,
"logSize": "2.3MB",
"costMs": 42,
"source": "frontend"
}
}
)
```
**❌ 反例(禁止):**
```python
logger.info("get logs success")
logger.info(f"job {job_id} status is {status}") # 禁止字符串拼接
```
### 4.3 WARNING/ERROR 日志内容要求
**必须包含三要素**
1. 发生了什么what
2. 为什么发生why—— 异常类型/错误码
3. 业务上下文context—— 哪个业务对象失败了
**✅ 正例:**
```python
logger.warning(
"计算轮询检测到任务失败",
extra={
"fields": {
"jobId": "ft_a016cd8885cd",
"failureReason": "GPU资源不足",
"errorCode": "RESOURCE_INSUFFICIENT",
"retryCount": 3,
"lastRetryTime": "2026-08-19T08:38:25.495+08:00"
}
}
)
```
**❌ 反例(禁止):**
```python
logger.warning("compute polling reported failures") # 没有任何上下文
logger.error(f"error: {e}") # 只打了异常信息没有业务ID
```
### 4.4 禁止打印的内容
| 类别 | 说明 |
|------|------|
| 密码/密钥 | 任何形式的 `password``secret``token``api_key` |
| 个人隐私 | 身份证号、手机号(需脱敏)、银行卡号 |
| 超大对象 | 超过 1KB 的 JSON/列表/文本内容 |
| 循环日志 | 禁止在 for/while 循环内打印 INFO 及以上级别 |
| 异常堆栈重复 | 同一异常在一个请求中只打印一次完整堆栈 |
---
## 五、链路追踪TraceId
### 5.1 基本原则
- **入口生成**:网关/前端/定时任务入口生成全局唯一的 `traceId`32位UUID
- **全链路透传**:通过 HTTP Header`X-Trace-Id`、RPC Meta、消息队列 Property 向下游传递
- **日志自动注入**:所有日志输出自动追加 `traceId`,代码中无需手动传入
- **跨线程传递**:使用 `MDC``ContextVars` 实现跨线程/协程的透传
### 5.2 实现要求
```python
# Python 示例:使用 logging 的 Filter 自动注入 traceId
class TraceIdFilter(logging.Filter):
def filter(self, record):
record.traceId = get_current_trace_id() or "N/A"
return True
# 所有日志自动带上 traceId
logger.info("订单创建成功") # 自动注入 traceId代码无需传参
```
**❌ 绝对禁止**`traceId` 字段值为 `"-"``null`
---
## 六、性能与安全
### 6.1 性能要求
| 配置项 | 要求 |
|--------|------|
| **异步打印** | 必须使用异步 Appender禁止同步刷盘阻塞业务线程 |
| **单文件大小** | ≤ 1GB达到阈值自动滚动 |
| **滚动策略** | 按大小滚动(如 1GB或按天滚动 |
| **采样率** | 核心业务 100%,非核心(如健康检查、非关键查询)≤ 10% |
| **禁止打印循环** | 循环体内不得打印 INFO 及以上日志 |
| **大对象截断** | 超过 1KB 的内容自动截断前500字符 + 后500字符 |
### 6.2 安全要求
| 要求 | 说明 |
|------|------|
| **敏感字段自动脱敏** | 对 `mobile``idCard``password``token` 等字段自动掩码 |
| **脱敏规则** | 手机号:`138****5678`;身份证:`110***********1234` |
| **日志查询权限** | 生产日志平台必须有 RBAC 权限控制,禁止随意导出 |
| **审计追踪** | 谁在什么时候查询了哪些日志,必须记录审计日志 |
### 6.3 脱敏实现示例
```python
# 脱敏工具函数
def mask_sensitive(data: dict) -> dict:
sensitive_keys = {"password", "token", "api_key", "mobile", "id_card"}
for key in sensitive_keys:
if key in data:
value = str(data[key])
if len(value) >= 11: # 手机号
data[key] = value[:3] + "****" + value[-4:]
elif len(value) >= 18: # 身份证
data[key] = value[:3] + "***********" + value[-4:]
return data
```
---
## 七、运维与告警
### 7.1 日志采集架构
```
应用日志(本地文件)
Filebeat轻量采集器
Kafka削峰填谷保证不丢
Logstash解析、过滤、脱敏
Elasticsearch索引存储
Kibana / Grafana查询展示
```
**关键要求**
- 禁止应用直接写入 ES必须经过 Kafka 缓冲
- Filebeat 采集失败时必须有本地持久化和重试机制
### 7.2 告警规则
| 条件 | 动作 | 优先级 |
|------|------|--------|
| 同一服务 5 分钟内出现 ≥ 3 次 ERROR | 钉钉/企微告警 + 电话P0级 | 最高 |
| 同一服务 10 分钟内 ERROR 率 > 5% | 钉钉告警P1级 | 高 |
| 磁盘使用率 > 80% | 钉钉告警P2级 | 中 |
| 单个 ERROR 堆栈重复出现 ≥ 10 次/分钟 | 聚合为一条告警,避免轰炸 | - |
### 7.3 错误聚合策略
- 相同 `error.type` + 相同 `logger` + 相同堆栈前3行 → 视为同一类错误
- 同一类错误 5 分钟内只发 **1 条告警**(防告警轰炸)
- 告警内容必须包含:`app``env``error.type`、首次发生时间、最近发生时间、累计次数
---
## 八、日志查询与使用规范
| 场景 | 查询方式 | 时效要求 |
|------|----------|----------|
| 日常运维 | Kibana 按 `traceId``userId` 检索 | 实时 |
| 异常排查 | 按 `app` + `level:ERROR` + 时间范围 | 实时 |
| 业务审计 | 按 `userId` + `logger:xxx` + 时间范围 | 30分钟内 |
| 性能分析 | 按 `costMs` 排序,找出慢请求 | 实时 |
| 安全审计 | 查询所有访问日志,按 IP/用户筛选 | 按需 |
---
## 九、检查清单Code Review 必查)
| 检查项 | 通过标准 |
|--------|----------|
| ☐ JSON 格式 | 所有日志输出均为 JSON字段名符合规范 |
| ☐ traceId | 所有日志都有 `traceId`,且不为 `"-"` |
| ☐ userId | 涉及用户操作的日志都有 `userId` |
| ☐ 业务上下文 | INFO 日志包含 `fields`有订单ID/任务ID等 |
| ☐ ERROR 日志 | 包含 `error.type` + `stack_trace` + 业务ID |
| ☐ 敏感信息 | 无密码/手机号明文,有脱敏处理 |
| ☐ 异步打印 | 使用异步 Appender |
| ☐ 日志级别 | 框架类日志 ≥ WARN业务日志分级合理 |
| ☐ 循环内日志 | 无循环内的 INFO 日志 |
| ☐ 日志分流 | 业务/系统/错误日志分文件存储 |
---
## 十、附:完整日志示例
### 示例一业务成功流程INFO
```json
{
"@timestamp": "2026-08-19T10:30:45.123+08:00",
"level": "INFO",
"logger": "app.services.job_service",
"traceId": "tracer-abc123xyz789",
"spanId": "span-001",
"userId": "U10086",
"message": "计算任务创建成功",
"fields": {
"jobId": "ft_a016cd8885cd",
"jobType": "fine_tuning",
"modelId": "model-llama2-7b",
"datasetId": "ds-20260819-001",
"gpuCount": 4,
"estimatedTime": "2h30m",
"costMs": 1523,
"source": "api"
},
"file": "job_service.py:234",
"thread": "MainThread",
"host": "compute-pod-7x9k2",
"app": "compute-service",
"env": "prod"
}
```
### 示例二可恢复的警告WARNING
```json
{
"@timestamp": "2026-08-19T08:38:27.074+08:00",
"level": "WARNING",
"logger": "app.workers.compute_poller",
"traceId": "tracer-xyz789abc123",
"spanId": "span-002",
"userId": "U10086",
"message": "计算任务状态轮询检测到失败,进入重试",
"fields": {
"jobId": "ft_a016cd8885cd",
"currentStatus": "FAILED",
"failureReason": "GPU节点不可用",
"errorCode": "NODE_UNAVAILABLE",
"retryCount": 2,
"maxRetries": 3,
"nextRetryDelay": 30
},
"file": "compute_poller.py:45",
"thread": "Thread-8",
"host": "compute-pod-7x9k2",
"app": "compute-service",
"env": "prod"
}
```
### 示例三严重错误ERROR+ 告警
```json
{
"@timestamp": "2026-08-19T08:38:30.456+08:00",
"level": "ERROR",
"logger": "app.services.payment_service",
"traceId": "tracer-pay-789xyz",
"spanId": "span-003",
"userId": "U10086",
"message": "支付调用失败,订单状态回滚",
"fields": {
"orderId": "ORD-20260819-001",
"amount": 299.00,
"paymentMethod": "wechat",
"retryCount": 3,
"hasRollback": true
},
"error": {
"type": "PaymentTimeoutException",
"message": "支付网关超时等待响应超过5000ms",
"stack_trace": "Traceback (most recent call last):\n File \"payment_service.py:89\" ...",
"root_cause": "upstream gateway 10.0.1.100:8080 connection timeout"
},
"file": "payment_service.py:156",
"thread": "http-nio-8080-exec-12",
"host": "order-pod-3f8k1",
"app": "order-service",
"env": "prod"
}
```
### 示例四完整的请求访问日志ACCESS
```json
{
"@timestamp": "2026-08-19T10:30:45.001+08:00",
"level": "INFO",
"logger": "app.middleware.access_log",
"traceId": "tracer-abc123xyz789",
"userId": "U10086",
"message": "HTTP 请求完成",
"fields": {
"method": "POST",
"path": "/api/v1/jobs",
"statusCode": 200,
"clientIp": "192.168.1.100",
"userAgent": "Mozilla/5.0 ...",
"requestSize": 2048,
"responseSize": 512,
"costMs": 1523,
"requestBody": {"modelId": "model-llama2-7b", "datasetId": "ds-001"}, // 已脱敏
"responseBody": {"jobId": "ft_a016cd8885cd", "status": "CREATED"} // 已脱敏
},
"app": "compute-service",
"env": "prod"
}
```
---
## 十一、附录:技术栈配置速查
### Python (Logging + JSON)
```python
import logging
import json
from pythonjsonlogger import jsonlogger
logger = logging.getLogger("app")
handler = logging.FileHandler("logs/app-biz.log")
formatter = jsonlogger.JsonFormatter(
fmt="%(asctime)s %(levelname)s %(name)s %(traceId)s %(message)s",
rename_fields={"asctime": "@timestamp", "name": "logger"}
)
handler.setFormatter(formatter)
logger.addHandler(handler)
```
### Java (Logback + JSON)
```xml
<!-- logback-spring.xml -->
<appender name="JSON" class="ch.qos.logback.core.ConsoleAppender">
<encoder class="net.logstash.logback.encoder.LogstashEncoder">
<fieldNames>
<timestamp>@timestamp</timestamp>
<level>level</level>
<thread>thread</thread>
<logger>logger</logger>
</fieldNames>
</encoder>
</appender>
```
### Go (Zap + JSON)
```go
logger, _ := zap.NewProduction()
logger.Info("订单创建成功",
zap.String("traceId", traceId),
zap.String("userId", userId),
zap.String("orderId", orderId),
zap.Int64("costMs", costMs),
)
```