mirror of
https://github.com/Rain-kl/OpenFlare.git
synced 2026-10-04 15:06:37 +08:00
245 lines
7.1 KiB
Markdown
245 lines
7.1 KiB
Markdown
# 级别与上下文
|
||
|
||
关于日志级别语义、基于 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` |
|