Files
OpenFlare/.agent/skills/go-logging/references/LEVELS-AND-CONTEXT.md
T
2026-06-18 15:24:48 +08:00

245 lines
7.1 KiB
Markdown
Raw 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.
# 级别与上下文
关于日志级别语义、基于 context 的日志模式、性能考虑以及哪些内容不应出现在日志中的详细指导。
## 级别语义
### Debug
仅开发人员的诊断。生产中默认禁用。用于跟踪在开发或故障排查期间有帮助的内部状态:
```go
slog.Debug("cache lookup", "key", key, "hit", hit)
slog.Debug("parsed config", "fields", len(cfg.Fields))
slog.Debug("SQL query", "query", q, "args", args)
```
**何时使用**:内部状态转换、缓存行为、开发期间的详细请求/响应数据。
### Info
确认系统按预期运行的重要事件。这些应在生产中对理解系统行为有用:
```go
slog.Info("server started", "addr", addr, "version", version)
slog.Info("config loaded", "path", cfgPath, "env", env)
slog.Info("migration completed", "version", v, "elapsed_ms", elapsed)
slog.Info("user registered", "user_id", uid)
```
**何时使用**:启动/关闭、配置变更、重要业务事件、周期性健康摘要。
### Warn
发生了意外的事情,但系统已恢复或优雅降级。运维人员可能想要调查但不需要立即行动:
```go
slog.Warn("retry succeeded", "attempt", n, "endpoint", url)
slog.Warn("deprecated endpoint called", "path", r.URL.Path, "user_id", uid)
slog.Warn("rate limit approaching", "current", rate, "limit", max)
slog.Warn("fallback to default config", "err", err)
```
**何时使用**:最终成功的重试、弃用的代码路径、接近资源限制、回退行为。
### Error
操作失败并需要运维人员关注。系统无法完成请求或任务:
```go
slog.Error("payment failed", "err", err, "order_id", id, "amount", amt)
slog.Error("database connection lost", "err", err, "host", dbHost)
slog.Error("message processing failed", "err", err, "msg_id", msgID)
```
**何时使用**:影响用户的失败操作、丢失的连接、数据完整性问题、未恢复的外部服务故障。
**始终包含错误**:`slog.Error` 调用应始终带有包含实际错误值的 `"err"` 属性。
### 在 Warn 和 Error 之间选择
```
操作最终是否成功?
├─ 是(经过重试/回退后)→ Warn
└─ 否(调用者收到错误)→ Error
├─ 需要立即关注 → Error
└─ 可以等到下次审查 → Warn
```
---
## 自定义详细级别
slog 级别是整数。在标准级别之间定义自定义子级别以实现细粒度控制:
```go
const (
LevelTrace = slog.Level(-8) // 低于 Debug
LevelNotice = slog.Level(2) // 在 Info 和 Warn 之间
)
slog.Log(ctx, LevelTrace, "detailed trace", "span_id", spanID)
```
使用 `HandlerOptions.Level` 配合 `slog.LevelVar` 在运行时控制最低级别。
---
## 基于 Context 的日志
### 模式 1:Context 中的日志器
在 context 中存储增强后的 `*slog.Logger`。每个中间件层添加自己的字段:
```go
func authMiddleware(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
userID := authenticate(r)
logger := loggerFromCtx(r.Context()).With("user_id", userID)
ctx := context.WithValue(r.Context(), loggerKey, logger)
next.ServeHTTP(w, r.WithContext(ctx))
})
}
```
**优点**:简单,与任何 handler 链配合使用。
**缺点**:需要纪律来始终使用 `loggerFromCtx`。
### 模式 2:显式日志器参数
将 `*slog.Logger` 作为函数参数与 context 一起传递:
```go
func processOrder(ctx context.Context, logger *slog.Logger, order *Order) error {
logger.Info("processing order", "order_id", order.ID)
// ...
}
```
**优点**:显式依赖,更易测试,无需 context 键。
**缺点**:每个函数签名中都有额外参数。
### 何时使用哪种
| 场景 | 推荐 |
|------|------|
| HTTP 处理器 / 中间件链 | Context 中的日志器 |
| 无 HTTP 依赖的库代码 | 显式参数 |
| 后台工作器 / 批处理任务 | 显式参数 |
| 深层调用链(5 层以上) | Context 中的日志器 |
---
## 性能考虑
### 使用 Enabled() 预检查
当日志级别被禁用时避免分配日志参数:
```go
// 开销大:参数始终被求值,即使 Debug 被禁用
slog.Debug("request details",
"headers", fmt.Sprintf("%v", r.Header),
"body", string(bodyBytes),
)
// 更好:禁用时完全跳过
if slog.Default().Enabled(ctx, slog.LevelDebug) {
slog.Debug("request details",
"headers", fmt.Sprintf("%v", r.Header),
"body", string(bodyBytes),
)
}
```
当参数构造开销大(格式化、序列化或读取数据)时,这很重要。对于简单属性(`slog.String`、`slog.Int`),开销可以忽略不计。
### 在热路径上使用 LogAttrs
`slog.LogAttrs` 避免了便捷方法(`slog.Info` 等)产生的 `[]any` 分配:
```go
// 标准——为键值对分配一个 []any
slog.Info("request handled", "method", r.Method, "status", code)
// 更快——类型化属性,无 []any 分配
slog.LogAttrs(ctx, slog.LevelInfo, "request handled",
slog.String("method", r.Method),
slog.Int("status", code),
)
```
### 避免在紧凑循环中记录日志
如果循环处理数千个项目,记录摘要而不是每次迭代:
```go
// 不好:10k 项目批次中每个项目一条日志
for _, item := range items {
slog.Debug("processing item", "id", item.ID)
process(item)
}
// 好:记录摘要
slog.Info("batch started", "count", len(items))
processed, failed := processBatch(items)
slog.Info("batch completed", "processed", processed, "failed", failed)
```
---
## 不应记录的内容
### 密钥和凭证
永远不要记录:
- 密码、API 密钥、令牌(OAuth、JWT、会话)
- 私钥、证书
- 包含凭证的数据库连接字符串
```go
// 不好
slog.Info("connecting", "dsn", dsn) // 可能包含密码
// 好
slog.Info("connecting", "host", dbHost, "database", dbName)
```
### 个人身份信息(PII)
除非调试所需且你的保留策略允许,否则避免记录:
- 电子邮件地址、电话号码
- 完整姓名、物理地址
- IP 地址(在某些司法管辖区)
- 信用卡号、社会安全号
如果必须记录用户标识符,使用不透明 ID 而非 PII。
### 高基数无界数据
不要记录完整的请求体、Info 级别的完整栈跟踪或无界集合:
```go
// 不好:无界数据
slog.Info("received", "body", string(requestBody))
slog.Info("users loaded", "users", users) // 可能有 10 万条记录
// 好:有界摘要
slog.Info("received", "content_length", len(requestBody), "content_type", ct)
slog.Info("users loaded", "count", len(users))
```
### 决策表
| 数据类型 | 记录吗? | 替代方案 |
|----------|----------|----------|
| 请求 ID / 跟踪 ID | 是 | — |
| 用户 ID(不透明的) | 是 | — |
| HTTP 方法、路径、状态 | 是 | — |
| 错误消息 | 是 | — |
| 密码 / 令牌 | **永不** | 记录令牌前缀或 "已脱敏" |
| 完整请求体 | **否** | 记录内容长度和类型 |
| PII(邮箱、姓名) | **避免** | 记录不透明用户 ID |
| 大型集合 | **否** | 记录数量或摘要 |
| 栈跟踪 | 仅 Debug | 使用 `slog.Debug` |