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

Go slog 的 WithAttrs 为什么没有出现在文本输出里

来源:17golang原创

时间:2026-09-07 19:21:15 167浏览 收藏

Go 的 log/slog 中,logger.With("request_id", id) 不是把字段直接写进缓冲区,而是让 Logger 创建一个带新 Handler 的副本。标准 TextHandler 会在后续处理记录时输出这些属性;如果消息出现但 request_id 不见了,优先排查 Handler 链路和属性改写规则,不要先改日志格式。

要点速览
  • 标准 TextHandler 的 WithAttrs 会保留属性,Info 记录应能看到 request_id。
  • 日志完全不出现,先看 HandlerOptions.Level;只有字段消失,再看 ReplaceAttr 和自定义 Handler。
  • 包装 Handler 必须返回 inner.WithAttrs(attrs) 的新实例,不能原样返回接收者。

先用标准 TextHandler 划清问题边界

先把业务 Handler 换成标准实现,并用固定的 Info 级别打印一行。如果这段输出包含 request_id=abc-42,说明 slog 的属性继承没有问题,故障在原来的包装器、过滤器或调用路径中。

package main

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

func main() {
    // 标准 TextHandler 会接收 Logger.With 传入的属性。
    handler := slog.NewTextHandler(os.Stdout, nil)
    logger := slog.New(handler).With("request_id", "abc-42")

    // 使用 Info,避免默认 LevelInfo 把 Debug 记录提前过滤掉。
    logger.InfoContext(context.Background(), "开始处理请求")
}

预期文本至少包含 level=INFO msg="开始处理请求" request_id=abc-42。如果整行没有输出,把问题归到级别过滤;如果整行存在但字段消失,继续看属性改写和 Handler 包装。

Go slog Logger WithAttrs、TextHandler 和文本输出之间的静态属性关系
图1:Logger 副本把 request_id 交给新的 Handler,TextHandler 在处理记录时将继承属性与消息一起组织成文本字段。

日志不出现和字段不出现是两种故障

HandlerOptions.Level 控制记录是否被处理。它不会只隐藏 request_id,而是让低于最低级别的整条记录都不进入 Handle。因此把最低级别设为 Warn 后,调用 logger.Info 看不到任何输出是正常现象。

另一条常见路径是 ReplaceAttr。它会在属性写出前接收已经解析的属性;如果针对 request_id 返回零值 slog.Attr{},这个字段就会被明确丢弃。调试时先临时移除 ReplaceAttr,再决定是否需要按分组路径过滤。

opts := &slog.HandlerOptions{
    Level: slog.LevelInfo,
    ReplaceAttr: func(groups []string, attr slog.Attr) slog.Attr {
        // 只隐藏密码,不要把所有业务属性误删。
        if attr.Key == "password" {
            return slog.Attr{}
        }
        return attr
    },
}
logger := slog.New(slog.NewTextHandler(os.Stdout, opts)).With("request_id", "abc-42")
logger.Info("登录检查")

如果回调里写了“非白名单都返回零值”,或只判断 groups 而忽略了属性键,WithAttrs 产生的字段就会被过滤掉。分组日志还要注意:WithGroup("request") 后,回调看到的路径会包含 request,不能继续按顶层字段名猜测。

自定义 Handler 必须复制属性继承关系

很多项目会用一个轻量包装器增加采样、路由或脱敏。这里最容易出现“消息有了,WithAttrs 没了”:包装器的 WithAttrs 直接返回了自己,或者创建了新包装器却把原始 inner 放了进去。

type routeHandler struct {
    inner slog.Handler
}

func (h *routeHandler) Enabled(ctx context.Context, level slog.Level) bool {
    // 级别判断交给内层 Handler,保持原有过滤策略。
    return h.inner.Enabled(ctx, level)
}

func (h *routeHandler) Handle(ctx context.Context, record slog.Record) error {
    // 这里可以做路由,但不要重新构造时丢掉 record 的属性。
    return h.inner.Handle(ctx, record)
}

func (h *routeHandler) WithAttrs(attrs []slog.Attr) slog.Handler {
    // 必须把属性继续交给内层,并返回新的包装器。
    return &routeHandler{inner: h.inner.WithAttrs(attrs)}
}

func (h *routeHandler) WithGroup(name string) slog.Handler {
    // 分组也要沿同一条 Handler 链传递。
    return &routeHandler{inner: h.inner.WithGroup(name)}
}

修复后不要直接调用 Handler 的方法做业务日志,仍然用 slog.New(h).With(...).Info(...),这样可以同时验证 Logger 副本、级别判断和 Handler 的属性继承。

按这张清单定位实际输出路径

现象优先检查处理方式
整行没有Level、Enabled、是否用了 Debug先换 Info 或降低 Level
消息有,WithAttrs 没有自定义 Handler.WithAttrs返回 inner.WithAttrs(attrs) 的新包装器
只有某些键没有ReplaceAttr、脱敏白名单保留非目标键,检查 groups
分组字段位置异常WithGroup 调用顺序统一 logger.WithGroup 后再 With
改了代码却无变化实际使用的 Logger 实例避免混用默认 Logger 和局部 Logger
Go slog 的级别过滤、ReplaceAttr 和自定义 Handler 属性边界关系
图2:把整条记录过滤、单个属性改写和包装 Handler 转发分成三个边界,能快速判断字段在哪一层消失。

常见问题

WithAttrs 会影响 Handler.Enabled 吗?

不会。Enabled 负责判断某个级别是否处理,WithAttrs 负责构造带属性的 Handler;低级别记录会在属性处理前被整体跳过。

为什么把 Debug 改成 Info 后字段就出现了?

默认 TextHandler 的最低级别是 Info。Debug 记录被过滤时,字段并非单独丢失,而是整条记录没有进入 Handle。

WithGroup 后 ReplaceAttr 应该判断什么?

同时看 groupsattr.Key。同一个键在不同分组下可能代表不同字段,不能只用键名做全局删除。

标准 Handler 正常,自定义 Handler 该怎么回滚?

先临时换回标准 TextHandler 保证日志可见,再逐个恢复包装器;每恢复一层都用同一个 Info 记录检查 request_id 是否仍在。

排查这类问题时,先问“整条记录是否被处理”,再问“属性在哪一层被改写”。标准实现能输出字段,就说明重点不在文本格式,而在 Handler 链路、过滤规则和实际 Logger 实例。

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