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

Go log/slog 如何把请求字段加入结构化日志

来源:17golang原创

时间:2026-09-12 20:37:37 322浏览 收藏

如果每条 Go 请求日志都要补上 trace_idrequest_id 和租户编号,最稳妥的做法不是在每次 Info 调用时手写一遍,而是在请求入口创建一个带公共属性的 slog.Logger,随后沿着 context.Context 传递。这样既能保持 JSON 字段统一,也不会把业务函数改成“先拼日志字符串再输出”。

要点速览
  • Logger.With 适合固化一条请求链路都会出现的字段。
  • 有上下文时优先使用 InfoContextErrorContextLogAttrs
  • trace 字段可以检索,但密码、Cookie 和完整令牌不能直接写入日志。

先把日志分成请求字段和事件字段

log/slog 的一条记录由消息、级别和键值属性组成。请求字段描述“这条链路是谁”,例如 trace_idrequest_id、HTTP 方法和路径;事件字段描述“此刻发生了什么”,例如订单号、耗时或错误。前者应该在请求入口绑定,后者在具体业务点追加。

下面的初始化只负责输出 JSON。示例中的代码和日志都是文章操作示意,字段值请替换成你的网关或链路追踪系统提供的真实值。

package main

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

type loggerKey struct{}

func main() {
    // JSONHandler 让采集系统按字段解析日志,而不是依赖字符串切割。
    handler := slog.NewJSONHandler(os.Stdout, &slog.HandlerOptions{
        Level: slog.LevelInfo,
    })
    slog.SetDefault(slog.New(handler))

    http.HandleFunc("/orders", requestLogger(orderHandler))
    _ = http.ListenAndServe(":8080", nil) // 示例服务;生产环境要处理返回错误。
}

func requestLogger(next http.HandlerFunc) http.HandlerFunc {
    return func(w http.ResponseWriter, r *http.Request) {
        // 实际项目应优先读取可信网关注入的 trace_id,缺失时再生成 request_id。
        traceID := r.Header.Get("X-Trace-ID")
        if traceID == "" {
            traceID = "trace-demo"
        }
        requestID := r.Header.Get("X-Request-ID")
        if requestID == "" {
            requestID = "request-demo"
        }

        reqLog := slog.Default().With(
            "trace_id", traceID,
            "request_id", requestID,
            "http_method", r.Method,
            "http_path", r.URL.Path,
        )
        ctx := context.WithValue(r.Context(), loggerKey{}, reqLog)
        next(w, r.WithContext(ctx))
    }
}

func orderHandler(w http.ResponseWriter, r *http.Request) {
    logger := r.Context().Value(loggerKey{}).(*slog.Logger)
    // 事件字段只在真正处理订单的地方追加,避免污染所有请求日志。
    logger.InfoContext(r.Context(), "order accepted", "order_id", "ord-1001")
    w.WriteHeader(http.StatusAccepted)
}
Go log slash slog 请求入口把 trace_id 和 request_id 绑定到 logger 并通过 context 传入业务处理器的结构示意图
图1:请求级字段绑定到 slog.Logger,再沿 context 进入业务处理器的操作示意图。

为什么要用 With,而不是每行重复传参

With 会返回一个使用同一 Handler、但已经附带公共属性的新 Logger。后续日志调用都会带上这些属性,代码只需要在事件位置添加订单号、结果或耗时。它还比每行重复传参更容易统一字段名,避免一处写 requestId、另一处写 request_id

如果一个子系统还会使用通用键名,可以再调用 WithGroup,例如把支付模块的 id 放到 payment.id 或 JSON 的 payment 对象下,减少跨模块键名冲突。

有 context 时优先用 Context 方法

InfoContextErrorContextLogAttrs 接受调用点的上下文,便于自定义 Handler 或链路系统读取当前 span。若你已经把 Logger 放进 context,取出后仍要把同一个请求上下文传给日志方法;不要为了“带字段”而创建一个永远不会取消的新 context。

func loadOrder(ctx context.Context, orderID string) error {
    logger, ok := ctx.Value(loggerKey{}).(*slog.Logger)
    if !ok {
        logger = slog.Default() // 没有请求包装时仍保持可用的兜底行为。
    }

    // Attr 适合固定类型,也能避免把参数顺序写错。
    logger.LogAttrs(ctx, slog.LevelInfo, "loading order",
        slog.String("order_id", orderID),
        slog.String("component", "order-service"),
    )
    return nil
}
Go log slash slog JSONHandler 输出 trace_id request_id http_path 和 order_id 的结构化日志结果示意图
图2:同一请求的公共字段与业务事件字段出现在一条可检索的 JSON 日志中的结果示意图。

字段命名和敏感数据要先定规则

建议在团队内固定一份字段表:trace_id 用于跨服务串联,request_id 用于单次入口定位,http_methodhttp_path 用于筛选,order_id 等业务字段只在相关事件中出现。不要把完整 Authorization、Cookie、密码或身份证号放进公共 Logger;需要记录时先脱敏或只保留不可逆摘要。

字段类型绑定位置示例
请求公共字段中间件trace_id、request_id
路由字段请求入口http_method、http_path
业务事件字段具体函数order_id、result、latency_ms

常见问题

直接调用 slog.Info 会丢掉请求字段吗?

如果没有把公共 Logger 设为默认,直接调用顶层函数不会自动获得请求级字段。业务代码应显式使用请求 Logger,或在请求入口调用 slog.SetDefault 之外采用清晰的依赖传递方案。

为什么日志里有重复的 trace_id?

通常是同一个键既通过 With 绑定,又在某次事件调用中再次传入。统一约定公共字段只在入口绑定,事件字段只在事件点追加。

什么时候用 LogAttrs?

字段类型明确、调用频繁或需要减少交替键值参数时可以用它。普通低频日志用 InfoContext 也足够清楚。

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