本文目录导读:

制定大型开源项目的日志规范,核心目标是可搜索、可过滤、可关联、可定级,因为参与者众多、环境复杂,一个好的日志规范能让开发者快速定位问题,运维人员有效监控,而贡献者能遵循统一标准。
以下是一个通用的、经过实践检验的日志规范制定框架,适用于 Kubernetes、Apache 系列、OpenStack 等大型项目。
核心原则(定基调)
- 一致性:所有模块、所有贡献者必须采用相同的格式和级别。
- 机器可读:日志不仅是给人看的,更是给 Logstash、Splunk、ELK 等工具解析的。结构化日志是必须的。
- 人肉可读:在机器可读的基础上,格式化输出要清晰,便于在终端直接
grep和观察。 - 无副作用:日志记录本身不能导致程序崩溃、性能严重下降或引发死锁。
日志级别定义(定严重程度)
这是最基础也最容易打架的地方,建议采用 Syslog 级别 + 行业惯例,强烈建议项目 只使用 4-5 个级别,避免滥用。
| 级别 | 推荐名称 | 触发条件(举例) | 使用者 |
|---|---|---|---|
| 0 | FATAL / CRITICAL | 程序无法继续运行,必须立即退出(如数据库连接丢失且无法恢复)。 | 开发/运维 |
| 1 | ERROR | 功能不可用,但程序仍可继续运行(如 API 调用失败、配置解析错误)。 | 开发/运维 |
| 2 | WARN | 潜在问题,但当前功能正常(如磁盘即将写满、使用废弃接口、重试操作)。 | 开发/运维 |
| 3 | INFO | 关键业务节点的状态变化(如服务启动、请求处理完成、用户登录)。这是生产环境默认级别。 | 运维/支持 |
| 4 | DEBUG | 详细状态信息,用于排查问题(如变量值、SQL 语句、请求参数)。生产环境默认关闭。 | 开发者 |
禁止使用的陷阱:
- TRACE:除非项目是网络框架或数据库引擎,否则 DEBUG 基本够用,TRACE 级别的日志量巨大,容易淹没系统。
- NOTICE:和 INFO 界限模糊,容易引发争论,建议合并到 INFO。
规范(定格式)
这是大型项目成败的关键。必须采用键值对(Key-Value)或 JSON 格式。
标准字段(所有日志必须包含)
{
"timestamp": "2023-10-27T10:00:00.123Z",
"level": "ERROR",
"logger": "com.example.module.UserService",
"thread": "http-nio-8080-exec-3",
"message": "Failed to create user",
"trace_id": "a1b2c3d4-... (用于全链路追踪)",
"span_id": "e5f6g7h8-...",
"error": { // 如果是 ERROR 级别
"type": "DuplicateEntryException",
"stack_trace": "com.example... at line 123",
"cause": "User email already exists"
}
}
关键字段要求
timestamp:必须是 ISO 8601 格式,包含毫秒/微秒,且明确时区(推荐 UTC)。绝不能用2023-10-27 18:00:00这种无时区格式。level:使用字符串,不要用数字代码。logger/source:明确记录来自哪个模块、类或组件。message:人类可读的描述。要求:必须是固定的模板,变量用占位符表示,绝不要拼接字符串(否则难以搜索)。- ❌
log.info("User " + userId + " created order " + orderId); - ✅
log.info("User created order", "user_id", userId, "order_id", orderId);
- ❌
trace_id/span_id:必须支持 OpenTelemetry 或 OpenTracing 标准,这是分布式系统中将不同服务日志串联起来的生命线。
结构化日志最佳实践
- 使用 Log4j2 / Logback 的 StructuredArguments(Java) 或 logrus / zap 的 Fields(Go)。
- 禁止在日志消息中使用
->、、 等自定义分隔符,这会导致后续解析极其痛苦。 - 敏感信息脱敏:密码、Token、身份证号、信用卡号等必须用 或
[REDACTED]替代,在String.valueOf()之前强制检查。
性能与安全规范(定红线)
-
参数化日志:
- ✅
logger.error("Failed to connect to {}", hostname); - ❌
logger.error("Failed to connect to " + hostname + "..."); - 参数化日志在级别未开启时不会执行字符串拼接,能极大提升性能。
- ✅
-
条件判断:
- 对于非常频繁的 DEBUG 日志,使用
if (log.isDebugEnabled()) { ... }。
- 对于非常频繁的 DEBUG 日志,使用
-
异常日志:
- 日志包含异常时,必须传递 Throwable 对象,而不是只输出
e.getMessage()。- ✅
logger.error("Failed to process request", exception); - ❌
logger.error("Failed to process request: " + e.getMessage());
- ✅
- 禁止记录异常后又
throw或return,在一个地方记录,在另一个合适的地方处理,重复记录会导致日志爆炸。
- 日志包含异常时,必须传递 Throwable 对象,而不是只输出
-
避免
System.out.println和e.printStackTrace():这在大型开源项目中被视为 BUG。
管理流程(定规矩)
-
代码审查(Code Review):日志代码必须纳入 CR 范围,Reviewer 需要检查:
- 日志级别是否合理(不要将 ERROR 当 INFO 用)。
- 是否包含
trace_id等上下文信息。 - 是否泄露敏感信息。
- 是否使用了参数化日志。
-
默认配置文件:项目应提供开箱即用的日志配置(如
logback-spring.xml或log4j2.xml),并包含以下策略:- 生产配置:仅输出 INFO 及以上级别到文件。
- 开发配置:输出 DEBUG 及 ERROR 到控制台,带颜色。
- 滚动策略:基于时间(如每天)和大小(如 100MB)滚动,保留最近 30 天。
- 异步日志:默认启用
AsyncAppender或AsyncLogger,防止 I/O 阻塞主线程。
-
日志即文档:对于关键模块(如网络层、存储层),要求模块开发者提供
LOGGING.md,说明该模块会输出哪些关键日志(如启动、连接、切换、错误模式),方便运维人员解读。
一个具体的规范示例(Go 语言)
// 推荐使用 slog (Go 1.21+) 或 zap
import "log/slog"
// 1. 定义结构化日志
slog.Info("request completed",
"method", r.Method,
"path", r.URL.Path,
"status", http.StatusOK,
"duration_ms", 120,
"trace_id", traceID,
)
// 2. 错误日志必须包含异常
slog.Error("failed to create user",
"user_id", userId,
"error", err, // 标准库
"stack", debug.Stack(), // 可选,但建议在 DEBUG 级别使用
)
// 3. 动态级别生成
if slog.Level.Enabled(ctx, slog.LevelDebug) {
slog.Debug("sensitive debug info", "payload", formatPayload(req.Target))
}
总结清单
| 类别 | 必须做 | 绝对禁止 |
|---|---|---|
| 格式 | JSON 或 Key-Value 结构化 | 自由文本、无固定分隔符 |
| 时间 | ISO 8601 UTC 含毫秒 | 2023/10/27 或 HH:mm:ss |
| ID | 始终包含 trace_id 和 span_id |
无上下文 ID |
| 异常 | 直接传入 Throwable/error 对象 | 只记录 e.getMessage() 或 e.stack() |
| 拼接 | 占位符 或 %s |
字符串 拼接 |
| 敏感信息 | 必须脱敏 | 打印 Token、密码、密钥 |
| 级别 | INFO 及以上用于生产,DEBUG 用于开发 | 将大量 DEBUG 日志带入生产 |
规范不在于完美,而在于执行,只要团队约定好以上框架,并在 CI 流程中加入日志检查(如禁止 System.out、检查必须包含 trace_id),就能建立起一套可靠的大型项目日志体系。