登录
推荐 文章 Go 技术 课程 下载 专题 AI
首页 >  Golang >  Go教程

Go slog 怎样补充请求上下文字段

来源:17golang原创

时间:2026-10-21 21:44:03 455浏览 收藏

Go 的 log/slog 里,InfoContext 会把 context.Context 交给 Handler,但不会自动把 context 中的任意值变成日志字段。要让每条请求日志稳定带上 request_id、HTTP 方法和路径,最稳妥的做法是在请求入口提取允许记录的字段,用 Logger.With 创建请求级 logger,再把它沿调用链传给业务代码。

记住两层职责:context 负责携带请求生命周期和值,slog Attr 负责形成结构化日志字段。只调用 InfoContext 并不能替代把字段显式加入 logger 或 Handler。
迁移要点
  • 已有请求上下文时优先使用 InfoContext、LogAttrs,让 Handler 能拿到 context。
  • 稳定、低敏的请求字段在入口用 logger.With 固定下来,业务层只接收请求级 logger。
  • 只有需要统一策略时才包装 Handler 读取 context,并明确字段白名单、覆盖顺序和并发边界。

先分清 context 与日志属性

slog.Logger.Log、LogAttrs 以及顶层的 InfoContext 等方法都接收 context。标准 Handler 可以从这个参数读取 trace ID 一类的值,但它不会猜测你的业务键,更不会把 context.WithValue 中的所有内容遍历成日志。这样设计可以避免把取消信号、私密对象或不适合序列化的数据意外写入日志。

Go slog 请求从 HTTP context 到请求级 logger 再到结构化日志字段的关系说明图
图1:请求上下文、请求级 logger 与 slog 结构化字段的关系说明图,不是运行截图。

请求入口先创建请求级 logger

把字段加入 logger 的位置应尽量靠近请求入口。下面的中间件只提取可公开记录的字段:请求 ID 来自受控的请求头或服务端生成值,方法和路径来自请求本身;不要把 Cookie、Authorization 或完整查询参数直接塞进日志。

package main

import (
    "context"
    "log/slog"
    "net/http"
    "os"
)

type loggerKey struct{}

func requestLogger(base *slog.Logger, next http.Handler) http.Handler {
    return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
        requestID := r.Header.Get("X-Request-ID")
        if requestID == "" {
            requestID = "generated-by-service"
        }

        // 只把允许落盘的低敏字段放进请求级 logger,避免记录认证信息。
        reqLogger := base.With(
            slog.String("request_id", requestID),
            slog.String("method", r.Method),
            slog.String("path", r.URL.Path),
        )
        ctx := context.WithValue(r.Context(), loggerKey{}, reqLogger)

        // 把新的 context 绑定到请求,后续处理器可取出同一个 logger。
        next.ServeHTTP(w, r.WithContext(ctx))
    })
}

func loggerFrom(ctx context.Context, fallback *slog.Logger) *slog.Logger {
    // 缺少请求 logger 时使用 fallback,避免后台任务因类型断言失败而崩溃。
    if logger, ok := ctx.Value(loggerKey{}).(*slog.Logger); ok {
        return logger
    }
    return fallback
}

func main() {
    base := slog.New(slog.NewJSONHandler(os.Stdout, &slog.HandlerOptions{}))
    _ = requestLogger
    _ = loggerFrom
    _ = base
}

这里的 context 只保存请求级 logger,而不是把一组可变字段放进全局变量。每个请求得到一个新的 logger,Logger.With 返回新 logger,不会改变共享的基础 logger,因此并发请求之间不会互相串字段。

业务层用 InfoContext 保留请求语义

在处理器或服务方法中取出请求级 logger后,使用带 context 的方法写日志。这样 Handler 仍能获得取消状态、链路追踪等上下文;稳定字段由 logger 自己携带,临时字段用 slog.Attr 加到当前记录。

func handleOrder(w http.ResponseWriter, r *http.Request, base *slog.Logger) {
    logger := loggerFrom(r.Context(), base)

    // LogAttrs 只接收 Attr,适合固定字段类型并减少参数歧义。
    logger.LogAttrs(r.Context(), slog.LevelInfo, "开始处理订单",
        slog.String("order_id", "order-1001"),
    )

    // 记录结束状态时继续使用同一个请求级 logger,字段会自动跟随。
    logger.InfoContext(r.Context(), "订单处理完成",
        slog.Int("status", http.StatusOK),
    )
    w.WriteHeader(http.StatusOK)
}

使用 JSONHandler 时,结果会是结构化对象,例如包含 request_id、method、path 和 order_id 等键。换成 TextHandler 只改变序列化外观,不改变字段来源。对高频日志,LogAttrs 比交替传入键和值更容易保持类型清晰;不需要为了补字段而拼接字符串。

需要集中注入时再包装 Handler

如果团队要求所有调用点都自动补充同一组上下文字段,可以包装 Handler,在 Handle 阶段从 context 中读取白名单字段,再调用下层 Handler。这个方案要同时实现 WithAttrs 和 WithGroup 的委托,否则 logger 的分组或预格式化行为可能在包装层丢失。

type requestIDKey struct{}

type contextHandler struct {
    next slog.Handler
}

func (h *contextHandler) Enabled(ctx context.Context, level slog.Level) bool {
    // 先委托 Enabled,尽早过滤低级别日志,避免无谓构造字段。
    return h.next.Enabled(ctx, level)
}

func (h *contextHandler) Handle(ctx context.Context, record slog.Record) error {
    if requestID, ok := ctx.Value(requestIDKey{}).(string); ok && requestID != "" {
        // 只读取明确白名单键,不遍历 context,也不记录完整用户对象。
        record.AddAttrs(slog.String("request_id", requestID))
    }
    return h.next.Handle(ctx, record)
}

func (h *contextHandler) WithAttrs(attrs []slog.Attr) slog.Handler {
    // 返回新的包装器,保持原 handler 不变,符合 slog 的不可变链式语义。
    return &contextHandler{next: h.next.WithAttrs(attrs)}
}

func (h *contextHandler) WithGroup(name string) slog.Handler {
    // 继续委托分组,避免请求字段与业务字段落入错误层级。
    return &contextHandler{next: h.next.WithGroup(name)}
}

集中注入的优点是调用点少改,缺点是字段来源变得隐式,排查覆盖关系也更难。若同一键既由 logger.With 显式设置,又由 Handler 从 context 注入,要提前约定谁优先;生产代码还应给敏感字段做脱敏或直接禁止注入。

Go slog 基础 logger、请求级 logger、临时 Attr 与 contextHandler 的分层结构说明图
图2:基础 logger、请求级字段、单条 Attr 与 Handler 包装的分层说明图,不是运行截图。

迁移后的回归检查

检查项应确认的结果常见误区
字段是否稳定同一请求的多条日志都带同一个 request_id每个调用点重新读取请求头,导致字段遗漏或不一致
context 是否真的传入使用 InfoContext、LogAttrs 或 Log,而不是无 context 的快捷方法以为调用 Info 就能让 Handler 读取请求 context
并发是否隔离基础 logger 只读共享,请求 logger 每请求新建把请求字段写入全局 logger 或可变 map
字段是否安全只保留白名单,认证信息和完整用户对象不落盘把 context 中所有值都当作可记录数据
格式是否兼容JSON 和文本输出都能按键检索,分组语义保持一致包装 Handler 时漏掉 WithGroup 或覆盖字段顺序

小结

补充 Go slog 请求上下文字段时,推荐把“字段变成 Attr”的动作放在请求入口:从请求提取白名单字段,使用 Logger.With 创建请求级 logger,再在业务层用 InfoContext 或 LogAttrs 输出。只有在确实需要统一注入策略时,才让自定义 Handler 从 context 读取字段;无论选择哪种方案,都要保留 context 传递、并发隔离和敏感信息控制。

常见问题

只调用 slog.InfoContext 就会自动记录 context.Value 吗?

不会。它会把 context 传给 Handler,是否读取以及读取哪些键由 Handler 决定。普通 JSONHandler 不会遍历任意 context 值。

request_id 应该放在 context 还是 logger.With 里?

如果目标是让每条请求日志都出现,建议在入口提取后放进请求级 logger;context 可继续保存请求生命周期和需要被下游读取的值。两者职责不要混成“把 context 当日志字段容器”。

为什么不直接使用全局 slog.SetDefault?

全局默认 logger 适合应用级固定字段,但请求字段会随请求变化。把请求字段写入全局对象会产生串请求风险,应该使用请求级 logger 或 contextHandler。

声明:本文转载于:17golang原创 如有侵犯,请联系study_golang@163.com删除
相关阅读
更多>
最新阅读
更多>
课程推荐
更多>