15 KiB
15 KiB
好的,这是一份可以直接放在项目根目录的 日志规范要求.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 结构,字段名不得随意变更:
{
"@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 时,必须包含:
{
"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(结果如何)
✅ 正例:
logger.info(
"任务日志拉取成功",
extra={
"userId": "U10086",
"fields": {
"jobId": "ft_a016cd8885cd",
"tailLines": 5000,
"logSize": "2.3MB",
"costMs": 42,
"source": "frontend"
}
}
)
❌ 反例(禁止):
logger.info("get logs success")
logger.info(f"job {job_id} status is {status}") # 禁止字符串拼接
4.3 WARNING/ERROR 日志内容要求
必须包含三要素:
- 发生了什么(what)
- 为什么发生(why)—— 异常类型/错误码
- 业务上下文(context)—— 哪个业务对象失败了
✅ 正例:
logger.warning(
"计算轮询检测到任务失败",
extra={
"fields": {
"jobId": "ft_a016cd8885cd",
"failureReason": "GPU资源不足",
"errorCode": "RESOURCE_INSUFFICIENT",
"retryCount": 3,
"lastRetryTime": "2026-08-19T08:38:25.495+08:00"
}
}
)
❌ 反例(禁止):
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 示例:使用 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 脱敏实现示例
# 脱敏工具函数
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)
{
"@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)
{
"@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)+ 告警
{
"@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)
{
"@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)
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)
<!-- 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)
logger, _ := zap.NewProduction()
logger.Info("订单创建成功",
zap.String("traceId", traceId),
zap.String("userId", userId),
zap.String("orderId", orderId),
zap.Int64("costMs", costMs),
)