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

Go slog.NewJSONHandler 怎样输出结构化 JSON 日志

来源:17golang原创

时间:2026-10-29 09:43:16 440浏览 收藏

如果 Go 服务的日志要交给采集系统按字段检索,关键不是把字符串拼成“看起来像 JSON”,而是让日志记录由标准库统一编码。slog.NewJSONHandler 会把每次日志调用写成一行 JSON 对象,再交给指定的 io.Writer。这样 level、msg 和业务属性都能保持字段边界。

官方文档:https://pkg.go.dev/log/slog

小型服务可以从 os.Stdout 加默认选项开始;进入生产采集后,再按日志量、来源定位和敏感字段要求配置 Level、AddSource 与 ReplaceAttr。

先把 JSON 日志的输入与输出边界定下来

NewJSONHandler(w, opts) 接收一个输出目标和可选配置,返回实现了 slog.Handler 的 JSON 处理器。opts 传 nil 时使用默认选项;处理器负责把记录编码成逐行 JSON,而不是让业务代码自己拼接引号、逗号和转义符。

下面的初始化把应用日志绑定到标准输出。标准输出只是一个 Writer 选择,容器运行时、日志代理或本地进程可以在边界外继续接收它:

package main

import (
    "log/slog"
    "os"
)

func newLogger() *slog.Logger {
    // nil 使用默认 HandlerOptions,适合先建立最小 JSON 输出边界。
    handler := slog.NewJSONHandler(os.Stdout, nil)
    return slog.New(handler)
}

func main() {
    logger := newLogger()
    // 键值属性会成为 JSON 字段,调用方不需要拼接 JSON 字符串。
    logger.Info("请求完成", "request_id", "req-42", "status", 200)
}

架构上可以把它拆成三层:业务调用方产生 Record,JSONHandler 负责编码,io.Writer 负责承接结果。这样将来把输出从标准输出换成文件或自定义 Writer 时,业务日志调用不必跟着改。

slog.Logger、JSONHandler、HandlerOptions、Record、逐行 JSON 对象和 io.Writer 的静态关系说明图
图1:slog.Logger 经过 JSONHandler 写入 io.Writer 的输出边界说明图,展示结构化日志的静态关系,不是截图。

按采集负载配置 HandlerOptions

默认配置能输出 JSON,但生产环境通常需要先做三项决策。Level 决定最低记录级别;低于它的记录会被处理器忽略。AddSource 会增加日志调用位置,适合排查问题但会带来额外字段和定位开销。ReplaceAttr 则可以重命名、改写或丢弃属性,包括内置的 time、level、source 和 msg。

package main

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

func newProductionLogger() *slog.Logger {
    options := &slog.HandlerOptions{
        Level:     slog.LevelInfo,
        AddSource: true,
        ReplaceAttr: func(groups []string, attr slog.Attr) slog.Attr {
            // 只示意脱敏边界:不要把令牌、密码等原始值写入日志。
            if strings.EqualFold(attr.Key, "token") || strings.EqualFold(attr.Key, "password") {
                return slog.String(attr.Key, "[REDACTED]")
            }
            return attr
        },
    }
    // Writer 决定日志交给哪里,选项决定写出哪些字段。
    return slog.New(slog.NewJSONHandler(os.Stdout, options))
}

脱敏函数要按你的字段命名规范扩展,并考虑分组属性;不能只依赖“调用方自觉不打印”。如果开启 AddSource,先确认采集端允许并索引该字段;如果日志量很大,可以让 LevelVar 参与动态调节,而不是把所有调试信息长期写入生产出口。

HandlerOptions、Level、AddSource、ReplaceAttr、字段契约、输出 Writer 和日志采集端的静态关系说明图
图2:HandlerOptions 与日志采集边界说明图,展示级别过滤、来源字段和属性改写如何连接到下游 Writer。

把结构化字段设计成稳定契约

JSON 只是编码格式,真正方便检索的是稳定的字段名和类型。建议把 request_id、service、operation、status 这类跨模块都会使用的字段固定下来;数值就保持数值,不要一会儿写成 200、一会儿写成 "200"。

同时要区分三类内容:msg 描述人类可读的事件,业务属性承担机器检索,错误对象只记录必要的错误信息。不要把一整段已经序列化的 JSON 再作为字符串传入,否则采集端看到的会是嵌套转义文本,失去字段级查询价值。

设计项推荐做法常见问题
事件名称短而稳定,放在 msg把动态参数拼进长句
业务字段使用固定键名和稳定类型同一字段在字符串和数字间切换
敏感信息在 HandlerOptions 统一改写或删除只在少数调用点手工脱敏
错误信息保留可定位的摘要和分类直接输出凭据、请求体或完整隐私数据

根据运行场景选择落地方案

在容器或进程管理环境里,标准输出通常是最简单的出口:应用只负责写 JSON,采集层负责转发、索引和保留策略。写本地文件时,要额外面对文件轮转、磁盘容量和进程权限;把 Writer 换成自定义实现时,则要明确并发安全、短写入和错误处理契约。

可以按下面的决策收口:

  • 服务交给平台采集:优先 os.Stdout,保持单条日志一行,避免混入非 JSON 文本。
  • 需要源码定位:开启 AddSource,但把它当成排障字段,不要让业务查询依赖路径字符串。
  • 日志字段受合规约束:集中使用 ReplaceAttr 做删减或替换,并把新增敏感字段纳入评审清单。
  • 日志量不可控:先用级别和采集端过滤控制体量;不要为了“完整”永久保留所有调试日志。

落地前至少确认输出 Writer、最低级别、字段命名、脱敏策略和采集端的单行约束。这样 slog.NewJSONHandler 才是一个清晰的日志架构边界,而不是把格式转换责任散落在每个业务函数里。

常见问题

NewJSONHandler 会输出一整个 JSON 数组吗?

不会。它按日志记录逐行写出 JSON 对象,采集端应按行读取,而不是等待一个完整数组结束。

为什么已经使用 JSONHandler,还要固定字段类型?

JSONHandler 只负责编码;如果同一个键在不同事件里使用不同类型,检索、聚合和告警规则仍然会变得不稳定。

AddSource 要不要一直开启?

它适合需要源码位置的排障场景。是否长期开启应结合日志量、采集字段和定位收益决定,不要把它当成业务字段的替代品。

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