Files
OpenFlare/.agents/skills/go-logging/references/LEVELS-AND-CONTEXT.md
T
2026-08-04 11:40:32 +08:00

7.1 KiB
Raw Blame History

级别与上下文

关于日志级别语义、基于 context 的日志模式、性能考虑以及哪些内容不应出现在日志中的详细指导。

级别语义

Debug

仅开发人员的诊断。生产中默认禁用。用于跟踪在开发或故障排查期间有帮助的内部状态:

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

确认系统按预期运行的重要事件。这些应在生产中对理解系统行为有用:

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

发生了意外的事情,但系统已恢复或优雅降级。运维人员可能想要调查但不需要立即行动:

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

操作失败并需要运维人员关注。系统无法完成请求或任务:

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 级别是整数。在标准级别之间定义自定义子级别以实现细粒度控制:

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。每个中间件层添加自己的字段:

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 一起传递:

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() 预检查

当日志级别被禁用时避免分配日志参数:

// 开销大:参数始终被求值,即使 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 分配:

// 标准——为键值对分配一个 []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),
)

避免在紧凑循环中记录日志

如果循环处理数千个项目,记录摘要而不是每次迭代:

// 不好: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、会话)
  • 私钥、证书
  • 包含凭证的数据库连接字符串
// 不好
slog.Info("connecting", "dsn", dsn) // 可能包含密码

// 好
slog.Info("connecting", "host", dbHost, "database", dbName)

个人身份信息(PII)

除非调试所需且你的保留策略允许,否则避免记录:

  • 电子邮件地址、电话号码
  • 完整姓名、物理地址
  • IP 地址(在某些司法管辖区)
  • 信用卡号、社会安全号

如果必须记录用户标识符,使用不透明 ID 而非 PII。

高基数无界数据

不要记录完整的请求体、Info 级别的完整栈跟踪或无界集合:

// 不好:无界数据
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