← [ BACK_TO_NOTES ]

Go 日志

从 log 到 log/slog 的结构化日志:级别、Handler、字段、与 context/trace 关联,以及生产环境脱敏、动态级别与性能边界。

#concept #growing #dev #backend #go

[!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":...}

结合场景再看三个关注点

  1. JSON Handler 面向检索
    下单日志要能按 order_id / request_id 过滤;独立字段可减少解析与命名歧义。

  2. With = 请求域固定字段
    中间件设一次,业务里只补 order_id、错误码等差异字段。

  3. 级别在 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,逐步迁移。

设计动机

  1. 可查询性
    字段化后才能按 order_id/code 过滤,而不是正则啃字符串。
  2. 关联性
    同一 request_id/trace_id 串起跨服务日志。
  3. 成本可控
    级别与采样避免“全量 Debug”烧盘与烧 CPU。
  4. 与 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 输出给人看的临时打印,不是运维数据面

工程实践

  1. 新项目默认 slog + JSON(生产)/ Text(本地)。
  2. Logger 依赖注入:结构体字段或参数传递;请求级 With。
  3. 统一字段字典:request_id、peer、error_code 命名一致。
  4. 错误日志带操作名:msg 稳定可聚合,细节放字段。
  5. 动态级别:LevelVar + 受控 admin 接口(务必鉴权)。
  6. 与中间件配合:记录 method/path/status/latency,不在每个业务里复制。
  7. 测试:对 Handler 用 bytes.Buffer 断言关键字段存在。
  8. 性能:热路径避免昂贵字段计算;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、无无界刷盘。
  • 下一步:可观测性 把日志与指标/追踪对齐;配置 管理级别与输出。

自测题

概念题

  1. Logger 与 Handler 如何分工?
  2. 为什么生产更常选 JSONHandler?
  3. log.Fatal 的主要工程危害是什么?

代码推理题

l := slog.Default().With("k", 1)
l = l.With("k", 2)
l.Info("x")

内置 JSONHandler 会输出几个 k?下游解析行为是否可依赖?设计上应否重复同名键?

工程思考题

QPS 5 万的服务要把每个请求 body 打 Debug。有什么风险?如何改?

参考答案

展开
  1. Logger 提供调用 API 与字段叠加;Handler 负责级别过滤、编码与输出。
  2. 便于集中采集、索引字段查询、与告警流水线集成。
  3. 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外部规范与可观测性对齐(扩展)

笔记元信息

  • 建议文件名:go-logging.md
  • 所属阶段:服务工程
  • 学习顺序:可观测性入口
  • 建议下一篇:Go 可观测性 或 Go 配置管理
  • 本篇状态:已深化(结构完整;以 slog 为默认)
创建于 2026/6/25 更新于 2026/10/7
Digital Biome
Access

键盘导航

j / k / ↑ / ↓
选择对象
Enter / o
打开选择
/ 或 Ctrl+K
全站搜索
i / f / 1–4
筛选当前列表
g → h / l / p / t / a
首页 / 笔记 / 项目 / 工具 / About
r → 1–9
打开所选对象的关联
u
返回上一页
Esc
关闭浮层 / 取消命令;输入框内清空文本