Go 日志
从 log 到 log/slog 的结构化日志:级别、Handler、字段、与 context/trace 关联,以及生产环境脱敏、动态级别与性能边界。
[!info] related notes
Go 日志
这个概念为什么出现
服务出了问题,你需要在海量并发请求里回答:
- 发生了什么、在哪个请求上
- 关键上下文是什么(user、order、latency)
- 能否被检索系统高效过滤
- 能否在不重启的情况下临时提高详细度
- 会不会因为日志本身拖垮延迟与 GC
log.Printf 可以用于生产,但缺少内建级别与结构化字段,检索和过滤通常需要额外约定。Go 1.21 起标准库 log/slog 把结构化、可级别过滤、可替换 Handler 变成默认路径;第三方 zerolog/zap 仍在极致性能与生态里占位。
[!abstract] 一句话理解 生产日志应结构化(键值字段 + 级别 + 时间 + msg),用可替换 Handler 输出 JSON/文本;通过 Logger.With/context 关联请求,并严禁写入密钥与未脱敏 PII。
最小可运行示例
先把示例放进业务场景,再看代码:
场景:下单接口打可检索的结构化访问日志
订单服务启动时要写一条进程级启动日志;
每个 POST /orders 成功后,还要带着 request_id、order_id、耗时写一条业务日志,
方便在 Loki/ELK 里按请求串联排查。
slog 的 JSON Handler + Logger.With 就是把「固定字段」绑在请求域 Logger 上,避免每行手写拼接。
package main
import (
"context"
"log/slog"
"os"
"time"
)
// handleCreateOrder 模拟“创建订单成功后的日志侧写”。
//
// 业务意图:
// - 入口已有 request_id / route;
// - 落库成功后补 order_id 与 latency;
// - 字段必须是键值,不能是 printf 糊成一串。
//
// 教学点:
// - With 产生子 Logger,固定字段自动出现在后续每条日志;
// - Duration 等类型化 Attr 便于索引与聚合。
func handleCreateOrder(base *slog.Logger, requestID, orderID string) {
// 请求域 Logger:中间件解析出的 request_id / route 在此挂上。
reqLog := base.With(
slog.String("request_id", requestID),
slog.String("route", "POST /orders"),
)
start := time.Now()
// 假装业务:写库、发事件……这里只演示日志落点。
// 真实 handler 会在 return 前打成功/失败日志。
reqLog.Info("order created",
slog.String("order_id", orderID),
slog.Duration("latency", time.Since(start)),
)
}
func main() {
// 生产默认:JSON 到 stdout,由采集侧收走。
handler := slog.NewJSONHandler(os.Stdout, &slog.HandlerOptions{
Level: slog.LevelInfo, // 低于 Info 的 Debug 在 Handler 侧直接丢弃
})
logger := slog.New(handler)
// 可选:把全局默认也换成 slog,兼容 slog.Info 调用点。
slog.SetDefault(logger)
// 进程启动日志(无 request 域):端口、环境便于对照发布。
ctx := context.Background()
slog.InfoContext(ctx, "server starting",
slog.Int("port", 8080),
slog.String("env", "production"),
)
// 模拟一次下单请求完成。
handleCreateOrder(logger, "req-1", "o-456")
}
需 Go 1.21+;将代码保存到新模块的 main.go,先 go mod init example.com/log-demo,再运行:
go run .
期望输出(时间戳与 latency 数值会变):
{"time":"...","level":"INFO","msg":"server starting","port":8080,"env":"production"}
{"time":"...","level":"INFO","msg":"order created","request_id":"req-1","route":"POST /orders","order_id":"o-456","latency":...}
结合场景再看三个关注点
-
JSON Handler 面向检索
下单日志要能按order_id/request_id过滤;独立字段可减少解析与命名歧义。 -
With= 请求域固定字段
中间件设一次,业务里只补 order_id、错误码等差异字段。 -
级别在 Handler 过滤
LevelInfo 时 Debug 不进入 Handle,但调用参数已先求值;昂贵计算仍需 Enabled 判断或 LogValuer 延迟求值。
核心概念与准确模型
标准库 log 的位置
log.Println("something")
log.Printf("count=%d", n)
log 支持包级默认 Logger,也支持 log.New 创建独立实例;可配置输出、前缀与 flags,但没有内建级别或结构化字段。是否迁移到 slog 取决于检索、关联与过滤需求。
log/slog 模型
| 概念 | 作用 |
|---|---|
Logger | 门面:Info/Warn/Error/Log/With |
Handler | 真正输出:过滤级别、编码、写到 io.Writer |
Attr / slog.String 等 | 类型化字段 |
Level / LevelVar | 静态或运行时可变阈值 |
Record | 一条日志的时间、级别、消息、字段 |
内置 Handler:
TextHandler:人类可读JSONHandler:机器友好
自定义 Handler 可做:采样、脱敏、冗余字段剥离、桥接到 syslog/OTel。
级别
常见:Debug < Info < Warn < Error。
var level slog.LevelVar
level.Set(slog.LevelInfo)
h := slog.NewJSONHandler(os.Stdout, &slog.HandlerOptions{Level: &level})
// 运维接口或信号里:level.Set(slog.LevelDebug)
原则:
- Info:普通运行与业务事件;日志级别本身不提供审计完整性、持久性或不可抵赖保证。
- Warn:可恢复异常、降级
- Error:失败需关注
- Debug:诊断细节,默认关
避免用 Fatal/panic 当日志级别:log.Fatal 会 os.Exit,跳过 defer,破坏优雅关闭。
Context 集成
slog.InfoContext(ctx, "query done", slog.Int64("rows", n))
内置 JSONHandler/TextHandler 不自动读取 context 字段;自定义 Handler 或 OTel 适配器可从 ctx 提取 trace_id/span_id。context 取消不应阻止日志记录。中间件负责把 request id 放进 context 或 Logger.With。见 中间件、可观测性。
错误字段
if err != nil {
logger.Error("save failed", slog.Any("err", err))
// 或 slog.String("err", err.Error()) 视检索需求
}
JSONHandler 对顶层 error Attr 调用 Error() 输出字符串,不自动导出错误原因链、栈或类型化错误码。%w 保留可供 errors.Is/As 检查的原因链,但不会采集调用栈(错误包装);对外响应仍要脱敏。
第三方库速览
| 库 | 特点 | 何时考虑 |
|---|---|---|
| slog | 标准库、零额外依赖、生态增长快 | 新项目默认 |
| zap | 高性能、生态成熟、API 双模式 | 已有 Uber 栈/极致分配控制 |
| zerolog | 以低分配链式 API 为目标,实际分配取决于字段与输出路径 | 超高 QPS 且团队接受其风格 |
桥接:可用适配器把旧 log 或第三方输出接到 slog,逐步迁移。
设计动机
- 可查询性
字段化后才能按order_id/code过滤,而不是正则啃字符串。 - 关联性
同一request_id/trace_id串起跨服务日志。 - 成本可控
级别与采样避免“全量 Debug”烧盘与烧 CPU。 - 与 Go 显式风格一致
不鼓励隐式全局魔法;但提供SetDefault降低改造成本。
边界情况与反直觉行为
1. 默认 Logger 与测试
测试中 SetDefault 会影响进程全局;并行测试需隔离或注入 Logger。
2. 高基数标签
把 user_id 当指标标签会爆炸;日志字段可以有高基数,但检索与保留策略要设计。指标与日志职责不同。
3. 结构化不是“把整对象塞进去”
slog.Any("req", bigStruct) 可能深反射、循环引用、泄露密码字段。显式白名单字段。
4. 异步日志丢数据
有些封装异步写盘,进程崩溃可能丢尾部日志。关键路径要理解 flush 与持久化承诺。内置 JSON/Text Handler 同步调用 Writer,slog.Logger 会忽略 Handler 返回的错误;需要交付保证时应在 Handler/采集链路独立处理失败。
5. 多 Writer
同时写 stdout 与文件时,注意多写失败语义与阻塞;容器环境常只打 stdout,交给采集层。
常见误区
[!warning] 常见误区:字符串拼接当结构 错误:
fmt.Sprintf("user=%s order=%s", u, o)。
正确:独立 Attr,便于索引与类型一致。
[!warning] 常见误区:密码/Token 进日志 错误:
Authorization头原样打印。
正确:只采集必要的白名单字段,按类型脱敏;Handler 过滤可补防线,不能仅靠 password 键黑名单处理嵌套对象或任意错误文本。
[!warning] 常见误区:到处
log.Fatal错误:初始化失败直接 Exit,跳过资源清理。
正确:return err到main,统一退出码与清理。
[!warning] 常见误区:Info 刷屏 错误:每条循环/每字节一条 Info。
正确:聚合、采样、Debug 下沉、指标补量。
[!warning] 常见误区:无关联 ID 错误:全是“save failed”无法对齐请求。
正确:请求入口注入request_id/trace_id。
与相邻概念对比
| 概念 | 差异 |
|---|---|
| 指标 metrics | 聚合数值;低基数;告警主通道之一 |
| 追踪 tracing | 跨服务跨度与时序;日志用 id 关联 |
| 审计 audit | 需要明确完整性、访问控制与保留策略;普通业务日志不自动具备这些保证 |
fmt 输出 | 给人看的临时打印,不是运维数据面 |
工程实践
- 新项目默认 slog + JSON(生产)/ Text(本地)。
- Logger 依赖注入:结构体字段或参数传递;请求级
With。 - 统一字段字典:
request_id、peer、error_code命名一致。 - 错误日志带操作名:
msg稳定可聚合,细节放字段。 - 动态级别:
LevelVar+ 受控 admin 接口(务必鉴权)。 - 与中间件配合:记录 method/path/status/latency,不在每个业务里复制。
- 测试:对 Handler 用
bytes.Buffer断言关键字段存在。 - 性能:热路径避免昂贵字段计算;Debug 字段可用惰性或先
Enabled判断。
if logger.Enabled(ctx, slog.LevelDebug) {
logger.DebugContext(ctx, "payload summary", slog.Int("bytes", len(b)))
}
可验证实验
实验 1:JSON 输出
运行最小示例,确认一行一个 JSON 对象且含 level/msg。
实验 2:级别过滤
Level=Info 时 Debug 不应出现;改为 Debug 后出现。
实验 3:With 继承
子 Logger 打日志应同时含父字段与新字段。
实验 4:LevelVar
运行中修改 LevelVar,观察后续日志详细度变化。
实验 5:脱敏
写一个包装 Handler,drop/replace password 键,用单测验证。
本节总结
- 本质:结构化事件流,不是
printf日记。 - 标准答案:
log/slog+ 合适 Handler + 请求关联字段。 - 红线:无密钥、无滥用 Fatal、无无界刷盘。
- 下一步:可观测性 把日志与指标/追踪对齐;配置 管理级别与输出。
自测题
概念题
Logger与Handler如何分工?- 为什么生产更常选 JSONHandler?
log.Fatal的主要工程危害是什么?
代码推理题
l := slog.Default().With("k", 1)
l = l.With("k", 2)
l.Info("x")
内置 JSONHandler 会输出几个 k?下游解析行为是否可依赖?设计上应否重复同名键?
工程思考题
QPS 5 万的服务要把每个请求 body 打 Debug。有什么风险?如何改?
参考答案
展开
- Logger 提供调用 API 与字段叠加;Handler 负责级别过滤、编码与输出。
- 便于集中采集、索引字段查询、与告警流水线集成。
os.Exit跳过 defer,资源与在途请求无法优雅收尾。
代码题:内置 JSONHandler 保留两个 k 字段,不会替你覆盖或去重;下游解析器处理重复 JSON 键的方式可能不同。自定义 Handler 也可能采取别的策略,应避免同一组内重复键。 工程题:体积极大、含 PII、CPU/IO 打满;应默认只记必要的元数据与脱敏摘要;截断、哈希、仅失败时记录或换通道都不自动消除敏感信息风险。对昂贵摘要计算先检查 Enabled,并限定采样率与大小。
延伸阅读与资料来源
| 资料 | 类型 | 支撑 |
|---|---|---|
| Package log/slog | 标准库文档 | Logger/Handler/Level |
| Structured Logging with slog | 官方博客 | 设计与用法 |
| Package log | 标准库文档 | 旧 API 边界 |
| OpenTelemetry Logs concepts | 外部规范 | 与可观测性对齐(扩展) |