前置知识: Go

Go与日志

38 minBeginner2026/6/14

slog与结构化日志

学习目标

本章节对标 MIT 6.5840(Distributed Systems)与 Stanford CS110L(Safety in Systems Programming)的可观测性教学水准,融合 Go 1.21 log/slog 的工程实践细节。完成本章学习后,读者应能够达成以下 Bloom 认知层级目标:

Remember(记忆)

  • R1:复述 Go 日志库的演进历史(loglogruszap/zerologslog
  • R2:列出 log/slog 包的四个核心日志级别(DEBUG、INFO、WARN、ERROR)及其语义
  • R3:背诵 slog.Handlerslog.Loggerslog.Recordslog.Attr 四个核心抽象的职责
  • R4:识别 TextHandlerJSONHandler 的输出格式差异

Understand(理解)

  • U1:解释结构化日志相较于格式化日志的可观测性优势
  • U2:阐述 slog.Handler 接口的 HandleEnabledWithAttrsWithGroup 四个方法的协作机制
  • U3:说明 slog.LogAttrsslog.Info 的延迟计算差异
  • U4:推演 slog.Logger 在 context 中的传播路径

Apply(应用)

  • A1:使用 slog.SetDefault 配置全局日志器
  • A2:通过 slog.With 绑定请求级别的默认属性
  • A3:实现自定义 slog.Handler 过滤敏感字段
  • A4:编写 HTTP 中间件集成结构化日志

Analyze(分析)

  • An1:分析 slogzerolog/zap 的零分配设计差异
  • An2:对比 log/slog 与 Rust tracing、Java SLF4J、Python logging 的抽象层次
  • An3:解构 slog.Record 的内部数据结构与零拷贝设计
  • An4:剖析 slog.HandlerOptionsAddSourceLevelReplaceAttr 三大参数的作用

Evaluate(评估)

  • E1:评估 slog 在高并发场景下的性能开销(纳秒/调用)
  • E2:评判 JSONHandlerTextHandler 在生产环境的取舍
  • E3:权衡 slog 与第三方库(zap/zerolog)的迁移成本
  • E4:评估日志采样、日志级别动态调整的策略

Create(创造)

  • C1:设计一个支持多 sink(stdout、file、Kafka、Loki)的复合 slog.Handler
  • C2:实现一个基于 OpenTelemetry 的 slog.Handler,将日志与 trace 关联
  • C3:构建一个日志采样与降级机制(高 QPS 场景下自动降级)
  • C4:为微服务架构设计统一日志规范(字段命名、级别策略、采样规则)

历史动机与发展脉络

史前时代:标准库 log 包(2009-2012)

Go 1.0 发布时仅提供 log 包,功能极为简陋:

import "log"

log.Println("服务器启动")
log.Printf("请求处理 %s %s", method, path)
log.Fatal("无法连接数据库")  // 输出后调用 os.Exit(1)
log.Panic("意外状态")        // 输出后 panic

log 包的三大局限:

  1. 无日志级别:所有日志同等对待,无法区分 INFO/WARN/ERROR
  2. 非结构化:仅支持 Printf 风格的格式化输出,难以被日志系统解析
  3. 无 Context 集成:无法传递 trace_id、request_id 等关联信息

第三方库爆发期(2013-2017)

社区为弥补标准库不足,涌现多个日志库:

  • logrus(2013,Simon Eskildsen):首个主流结构化日志库,引入 FieldsLevels
  • zap(2016,Uber):高性能零分配日志库,Benchmark 比 logrus 快 10 倍
  • zerolog(2017,Olivier Poitrey):基于 JSON 的零分配日志库,链式 API
  • apex/log(2017,TJ Holowaychuk):基于 logrus 接口的精简实现

zap 的性能优势源于关键设计:

// zap 使用 sync.Pool 复用 buffer,避免 GC 压力
// zap 使用字节级别 JSON 编码,避免反射
logger.Info("请求处理",
    zap.String("method", "GET"),
    zap.Int("status", 200),
    zap.Duration("duration", elapsed),
)

标准库结构化日志的呼声(2019-2022)

随着云原生与可观测性兴起,社区对标准库结构化日志的呼声渐高:

  • 2019 年 Go 官方调查:日志库是开发者最希望标准化的领域之一
  • 2020 年 issue #56286 提出 log/slog 提案
  • 2022 年 9 月,Russ Cox 接受提案并开始设计

log/slog 提案(2022 年 9 月)

Jonathan Amsterdam(Google)提出的 proposal 核心目标:

  1. 结构化:原生支持键值对,输出 JSON 或文本格式
  2. 零分配:通过 slog.LogAttrsslog.Attr 类型安全 API 减少分配
  3. 可组合:通过 Handler 接口允许第三方实现(如对接 zap/zerolog)
  4. Context 集成:原生支持 context.Context,便于传递 trace 信息
  5. 不破坏兼容:保留 log 包,slog 作为新模块独立演进

Go 1.21(2023 年 8 月):log/slog 正式发布

import "log/slog"

slog.Info("服务器启动", "port", 8080, "mode", "debug")
slog.Error("数据库连接失败", "error", err, "retry", 3)

slog 的核心抽象:

  • slog.Logger:高层 API,提供 Info/Error/Debug 等方法
  • slog.Handler:底层接口,决定日志的输出格式与目标
  • slog.Record:日志记录的内部表示,零拷贝传递
  • slog.Attr:类型安全的属性,避免 interface{} 装箱

Go 1.22(2024 年 2 月):slog 稳定化

  • 修复 slog.Handler 实现的多个边缘 case
  • 性能优化:slog.LogAttrsslog.Info 快约 30%
  • 标准库其他包(如 net/http)开始集成 slog

Go 1.23(2025 年 2 月):slog 默认集成

  • net/http.Server 提供 ErrorLog 字段,支持 *slog.Logger
  • database/sql 开始输出 slog 格式的慢查询日志
  • 标准库全面拥抱结构化日志

演进时间线总结

时间事件重要性
2009log 包发布基线
2013logrus 发布结构化先驱
2016zap 发布性能标杆
2017zerolog 发布零分配
2022-09log/slog 提案标准化起点
2023-08Go 1.21 发布 slog里程碑
2024-02Go 1.22 稳定化生产可用
2025-02Go 1.23 全面集成生态成熟

形式化定义

日志记录的形式化定义

定义以下概念:

  • Log Record:一个五元组 R=(T,L,M,A,C)R = (T, L, M, A, C),其中:

    • TR+T \in \mathbb{R}^+ 是时间戳(Unix 纳秒)
    • L{DEBUG,INFO,WARN,ERROR}L \in \{\text{DEBUG}, \text{INFO}, \text{WARN}, \text{ERROR}\} 是日志级别
    • MΣM \in \Sigma^* 是消息字符串
    • A={(k1,v1),(k2,v2),,(kn,vn)}A = \{(k_1, v_1), (k_2, v_2), \dots, (k_n, v_n)\} 是属性集合(键值对)
    • CC 是 context(可空,携带 trace_id 等)
  • Handler:一个函数 H:R{emit,drop}×OutputH: R \to \{\text{emit}, \text{drop}\} \times \text{Output},决定是否输出以及输出到何处

  • Logger:一个绑定了一组默认属性 A0A_0 的日志器,每次调用时合并传入属性:

Rfinal=(T,L,M,A0Apassed,C)R_{\text{final}} = (T, L, M, A_0 \cup A_{\text{passed}}, C)

日志级别的形式化偏序

日志级别构成偏序集 (L,)(L, \leq)

DEBUG<INFO<WARN<ERROR\text{DEBUG} < \text{INFO} < \text{WARN} < \text{ERROR}

Handler.Enabled(ctx, level) 的判定:

Enabled(L)=(LLthreshold)filter(L,C)\text{Enabled}(L) = (L \geq L_{\text{threshold}}) \land \text{filter}(L, C)

其中 LthresholdL_{\text{threshold}} 是 Handler 配置的最低级别,filter\text{filter} 是自定义过滤逻辑(如采样)。

slog.Attr 的类型安全

slog.Attr 是一个带类型的属性,避免 interface{} 装箱:

type Attr struct {
    key   string
    value Value  // 封装 String/Int/Float64/Bool/Duration/Time/Any
}

形式化:

Attr=(key:string,value:Value)\text{Attr} = (\text{key}: \text{string}, \text{value}: \text{Value}) Value=StringInt64Float64BoolDurationTimeAny\text{Value} = \text{String} \mid \text{Int64} \mid \text{Float64} \mid \text{Bool} \mid \text{Duration} \mid \text{Time} \mid \text{Any}

Handler 接口的形式化契约

slog.Handler 接口的四个方法构成如下契约:

Handler=Enabled,Handle,WithAttrs,WithGroup\text{Handler} = \langle \text{Enabled}, \text{Handle}, \text{WithAttrs}, \text{WithGroup} \rangle

满足:

  1. Enabled 单调性:若 Enabled(L1)=true\text{Enabled}(L_1) = \text{true}L2L1L_2 \geq L_1,则 Enabled(L2)=true\text{Enabled}(L_2) = \text{true}
  2. Handle 副作用Handle(R)\text{Handle}(R) 可能产生 I/O 副作用,但不得修改 RR
  3. WithAttrs 不可变性WithAttrs(A)\text{WithAttrs}(A) 返回新 Handler,原 Handler 不变
  4. WithGroup 嵌套性WithGroup(g).WithGroup(h)\text{WithGroup}(g).WithGroup(h) 等价于所有属性前缀为 g.hg.h

理论推导与原理解析

1. 零分配日志的性能模型

传统 Printf 风格日志的瓶颈在于字符串拼接:

// 即使日志级别不满足,Format 也会执行
log.Printf("查询耗时 %v", expensiveCompute())

性能开销分解:

Tlog=Tcheck+Tformat+TemitT_{\text{log}} = T_{\text{check}} + T_{\text{format}} + T_{\text{emit}}

当级别不满足时,理想情况应满足:

TlogTcheck1nsT_{\text{log}} \approx T_{\text{check}} \approx 1\text{ns}

slog 通过 slog.LogAttrs 实现延迟计算:

// 正确:仅在 DEBUG 级别时计算 expensiveCompute
slog.LogAttrs(ctx, slog.LevelDebug, "调试",
    slog.String("data", expensiveCompute()),
)

陷阱:上述代码仍然会调用 expensiveCompute(),因为 Go 是严格求值语言。真正的延迟计算需要包装为闭包:

// 真正的延迟计算
if slog.Default().Enabled(ctx, slog.LevelDebug) {
    slog.Debug("调试", "data", expensiveCompute())
}

2. JSON 编码的性能分析

JSONHandler 的编码路径:

Record → json.Encoder → bytes.Buffer → io.Writer

性能优化点:

  1. sync.Pool 复用bytes.Buffer 通过 pool 复用,避免分配
  2. 字节级编码:直接操作字节切片,避免 reflect
  3. 零拷贝字符串:字符串字段直接写入 buffer,不经过 interface{}

基准测试对比(Go 1.22,M2 Pro):

BenchmarkSlogText-8         3000000     450 ns/op     0 B/op     0 allocs/op
BenchmarkSlogJSON-8         2500000     600 ns/op     0 B/op     0 allocs/op
BenchmarkZapSugar-8         2000000     750 ns/op     0 B/op     0 allocs/op
BenchmarkZlogrus-8           500000   3200 ns/op   680 B/op     8 allocs/op

3. Handler 链式传播的代数性质

slog.With(attrs)slog.WithGroup(name) 构成代数结构:

Logger×AttrLogger\text{Logger} \times \text{Attr}^* \to \text{Logger}

满足:

  • 结合律(L.With(A1)).With(A2)=L.With(A1A2)(L.\text{With}(A_1)).\text{With}(A_2) = L.\text{With}(A_1 \frown A_2)
  • 单位元L.With()=LL.\text{With}(\emptyset) = L
  • Group 嵌套WithGroup(g1).WithGroup(g2).With(A)\text{WithGroup}(g_1).\text{WithGroup}(g_2).\text{With}(A) 输出为 {g1.g2:A}\{g_1.g_2: A\}

4. 采样算法的形式化

高 QPS 场景下的日志采样:

emit(R)={trueif rand(0,1)<rate(L)falseotherwise\text{emit}(R) = \begin{cases} \text{true} & \text{if } \text{rand}(0, 1) < \text{rate}(L) \\ \text{false} & \text{otherwise} \end{cases}

其中 rate(L)\text{rate}(L) 是按级别配置的采样率(如 ERROR=1.0, INFO=0.01)。

更高级的令牌桶采样:

tokens(t)=min(capacity,tokens(tΔt)+rateΔt)\text{tokens}(t) = \min(\text{capacity}, \text{tokens}(t - \Delta t) + \text{rate} \cdot \Delta t)

tokens(t)1\text{tokens}(t) \geq 1 时输出,并扣减 1 个 token。

5. Context 集成的传播语义

slogcontext.Context 的集成通过 slog.Logger 显式传递:

// 从 context 提取 logger
func FromContext(ctx context.Context) *slog.Logger {
    if l, ok := ctx.Value(loggerKey).(*slog.Logger); ok {
        return l
    }
    return slog.Default()
}

这与 OpenTelemetry 的 trace context 传播对齐:

trace_idcontextlogger.attrslog_record\text{trace\_id} \to \text{context} \to \text{logger.attrs} \to \text{log\_record}

使得日志与 trace 可通过 trace_id 关联查询。


代码示例

示例 1:基础用法(Go 1.21+)

package main

import "log/slog"

func main() {
    // 使用默认 Logger 输出日志
    slog.Info("服务器启动", "port", 8080, "mode", "debug")
    slog.Warn("内存使用率高", "usage", "85%")
    slog.Error("数据库连接失败", "error", "timeout", "retry", 3)
    slog.Debug("调试信息", "detail", "仅开发环境可见")

    // 输出示例(TextHandler 格式):
    // time=2026-06-14T10:30:00.000Z level=INFO msg=服务器启动 port=8080 mode=debug
}

示例 2:四个日志级别

// DEBUG:调试信息,仅开发环境使用
slog.Debug("查询参数", "sql", querySQL, "args", args)

// INFO:正常运行信息
slog.Info("请求处理完成", "method", "GET", "path", "/users", "duration", elapsed)

// WARN:警告信息,不影响运行但需要关注
slog.Warn("缓存未命中", "key", cacheKey, "fallback", "数据库查询")

// ERROR:错误信息,需要立即处理
slog.Error("支付失败", "order_id", orderID, "error", err)

示例 3:创建自定义 Logger

默认 Logger 使用 TextHandler 输出到标准错误。可以自定义格式和输出目标:

package main

import (
    "log/slog"
    "os"
)

func main() {
    // 创建 JSON 格式的 Logger(推荐生产环境使用)
    jsonHandler := slog.NewJSONHandler(os.Stdout, &slog.HandlerOptions{
        Level: slog.LevelInfo, // 最低日志级别
    })
    logger := slog.New(jsonHandler)

    // 设置为全局默认 Logger
    slog.SetDefault(logger)

    // 现在所有 slog 调用都使用 JSON 格式
    slog.Info("请求处理", "method", "GET")
    // 输出:{"time":"2026-06-14T10:30:00Z","level":"INFO","msg":"请求处理","method":"GET"}
}

示例 4:带默认属性的 Logger

为 Logger 绑定默认属性,后续所有日志自动携带:

// 创建带默认属性的 Logger
requestLogger := slog.With(
    "request_id", "abc-123",
    "user_id", "456",
    "ip", "192.168.1.1",
)

// 所有日志自动携带 request_id、user_id、ip
requestLogger.Info("查询用户", "action", "get_user")
requestLogger.Warn("权限不足", "resource", "/admin")

示例 5:使用 slog.Attr 类型安全属性

直接传键值对时类型不安全,可以使用 slog.Attr

slog.Info("订单创建",
    slog.String("order_id", "ORD-001"),
    slog.Int("items", 3),
    slog.Float64("total", 299.9),
    slog.Bool("paid", true),
    slog.Duration("processing_time", 150*time.Millisecond),
    slog.Time("created_at", time.Now()),
)

示例 6:日志组(Group)

将相关属性分组:

slog.Info("请求信息",
    slog.Group("request",
        slog.String("method", "POST"),
        slog.String("path", "/orders"),
        slog.Int("status", 201),
    ),
    slog.Group("response",
        slog.Int("size", 1024),
        slog.Duration("duration", 50*time.Millisecond),
    ),
)
// JSON 输出中 request 和 response 会是嵌套对象

示例 7:动态属性计算

仅在日志级别满足时才计算属性值:

// 使用 LogAttrs 避免在低级别时计算属性
slog.LogAttrs(context.Background(), slog.LevelDebug,
    "调试信息",
    slog.String("big_data", expensiveOperation()), // 仅在 DEBUG 级别时调用
)

示例 8:自定义 Handler(敏感字段过滤)

实现 slog.Handler 接口自定义日志处理:

package main

import (
    "context"
    "log/slog"
)

// FilterHandler 过滤敏感字段的 Handler
type FilterHandler struct {
    handler     slog.Handler
    sensitiveKeys map[string]bool
}

func NewFilterHandler(h slog.Handler, keys []string) *FilterHandler {
    m := make(map[string]bool, len(keys))
    for _, k := range keys {
        m[k] = true
    }
    return &FilterHandler{handler: h, sensitiveKeys: m}
}

func (h *FilterHandler) Enabled(ctx context.Context, level slog.Level) bool {
    return h.handler.Enabled(ctx, level)
}

func (h *FilterHandler) Handle(ctx context.Context, r slog.Record) error {
    // 过滤敏感字段,构造新 Record
    newRecord := slog.NewRecord(r.Time, r.Level, r.Message, r.PC)
    r.Attrs(func(attr slog.Attr) bool {
        if h.sensitiveKeys[attr.Key] {
            newRecord.AddAttrs(slog.String(attr.Key, "[REDACTED]"))
        } else {
            newRecord.AddAttrs(attr)
        }
        return true
    })
    return h.handler.Handle(ctx, newRecord)
}

func (h *FilterHandler) WithAttrs(attrs []slog.Attr) slog.Handler {
    return &FilterHandler{
        handler:        h.handler.WithAttrs(attrs),
        sensitiveKeys:  h.sensitiveKeys,
    }
}

func (h *FilterHandler) WithGroup(name string) slog.Handler {
    return &FilterHandler{
        handler:        h.handler.WithGroup(name),
        sensitiveKeys:  h.sensitiveKeys,
    }
}

func main() {
    base := slog.NewJSONHandler(os.Stdout, nil)
    handler := NewFilterHandler(base, []string{"password", "token", "credit_card"})
    logger := slog.New(handler)

    logger.Info("用户登录", "username", "alice", "password", "secret123")
    // 输出:{"password":"[REDACTED]","username":"alice",...}
}

示例 9:HTTP 请求日志中间件(企业级)

package middleware

import (
    "context"
    "crypto/rand"
    "encoding/hex"
    "log/slog"
    "net/http"
    "time"
)

type contextKey string

const loggerKey contextKey = "logger"

// responseRecorder 记录响应状态码与大小
type responseRecorder struct {
    http.ResponseWriter
    status int
    size   int
}

func (r *responseRecorder) WriteHeader(status int) {
    r.status = status
    r.ResponseWriter.WriteHeader(status)
}

func (r *responseRecorder) Write(b []byte) (int, error) {
    n, err := r.ResponseWriter.Write(b)
    r.size += n
    return n, err
}

// generateRequestID 生成随机 request ID
func generateRequestID() string {
    b := make([]byte, 8)
    rand.Read(b)
    return hex.EncodeToString(b)
}

// LoggingMiddleware HTTP 请求日志中间件
func LoggingMiddleware(next http.Handler) http.Handler {
    return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
        start := time.Now()
        requestID := r.Header.Get("X-Request-ID")
        if requestID == "" {
            requestID = generateRequestID()
        }

        // 创建请求级别的 Logger
        logger := slog.Default().With(
            "request_id", requestID,
            "method", r.Method,
            "path", r.URL.Path,
            "remote_addr", r.RemoteAddr,
            "user_agent", r.UserAgent(),
        )

        // 将 Logger 存入 Context
        ctx := context.WithValue(r.Context(), loggerKey, logger)
        recorder := &responseRecorder{ResponseWriter: w, status: 200}

        // 写入 response header
        w.Header().Set("X-Request-ID", requestID)

        next.ServeHTTP(recorder, r.WithContext(ctx))

        duration := time.Since(start)
        logger.Info("请求完成",
            "status", recorder.status,
            "size", recorder.size,
            "duration_ms", duration.Milliseconds(),
        )
    })
}

// FromContext 从 context 提取 logger
func FromContext(ctx context.Context) *slog.Logger {
    if l, ok := ctx.Value(loggerKey).(*slog.Logger); ok {
        return l
    }
    return slog.Default()
}

示例 10:多 sink 复合 Handler(企业级)

package logging

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

// MultiHandler 将日志同时输出到多个 sink
type MultiHandler struct {
    handlers []slog.Handler
}

func NewMultiHandler(handlers ...slog.Handler) *MultiHandler {
    return &MultiHandler{handlers: handlers}
}

func (h *MultiHandler) Enabled(ctx context.Context, level slog.Level) bool {
    for _, handler := range h.handlers {
        if handler.Enabled(ctx, level) {
            return true
        }
    }
    return false
}

func (h *MultiHandler) Handle(ctx context.Context, r slog.Record) error {
    var wg sync.WaitGroup
    errCh := make(chan error, len(h.handlers))

    for _, handler := range h.handlers {
        if !handler.Enabled(ctx, r.Level) {
            continue
        }
        wg.Add(1)
        go func(hd slog.Handler) {
            defer wg.Done()
            if err := hd.Handle(ctx, r.Clone()); err != nil {
                errCh <- err
            }
        }(handler)
    }

    wg.Wait()
    close(errCh)
    for err := range errCh {
        if err != nil {
            return err
        }
    }
    return nil
}

func (h *MultiHandler) WithAttrs(attrs []slog.Attr) slog.Handler {
    newHandlers := make([]slog.Handler, len(h.handlers))
    for i, hd := range h.handlers {
        newHandlers[i] = hd.WithAttrs(attrs)
    }
    return NewMultiHandler(newHandlers...)
}

func (h *MultiHandler) WithGroup(name string) slog.Handler {
    newHandlers := make([]slog.Handler, len(h.handlers))
    for i, hd := range h.handlers {
        newHandlers[i] = hd.WithGroup(name)
    }
    return NewMultiHandler(newHandlers...)
}

// SetupMultiSink 配置多 sink 日志(stdout + file + error file)
func SetupMultiSink() {
    file, _ := os.OpenFile("app.log", os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0644)
    errFile, _ := os.OpenFile("error.log", os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0644)

    stdoutHandler := slog.NewJSONHandler(os.Stdout, &slog.HandlerOptions{
        Level: slog.LevelInfo,
    })
    fileHandler := slog.NewJSONHandler(file, &slog.HandlerOptions{
        Level: slog.LevelDebug,
    })
    errHandler := slog.NewJSONHandler(errFile, &slog.HandlerOptions{
        Level: slog.LevelError,
    })

    slog.SetDefault(slog.New(NewMultiHandler(stdoutHandler, fileHandler, errHandler)))
}

示例 11:日志采样(高 QPS 场景)

package logging

import (
    "context"
    "log/slog"
    "sync/atomic"
    "time"
)

// SamplingHandler 日志采样 Handler
type SamplingHandler struct {
    handler    slog.Handler
    rate       int32  // 每 N 条输出 1 条
    counter    int32
    levelThreshold slog.Level  // 仅对低于此级别采样
}

func NewSamplingHandler(h slog.Handler, rate int, threshold slog.Level) *SamplingHandler {
    return &SamplingHandler{
        handler:         h,
        rate:            int32(rate),
        levelThreshold:  threshold,
    }
}

func (h *SamplingHandler) Enabled(ctx context.Context, level slog.Level) bool {
    if level >= h.levelThreshold {
        // 高级别日志不采样
        return h.handler.Enabled(ctx, level)
    }
    // 低级别日志采样
    n := atomic.AddInt32(&h.counter, 1)
    if n%h.rate != 0 {
        return false
    }
    return h.handler.Enabled(ctx, level)
}

func (h *SamplingHandler) Handle(ctx context.Context, r slog.Record) error {
    return h.handler.Handle(ctx, r)
}

func (h *SamplingHandler) WithAttrs(attrs []slog.Attr) slog.Handler {
    return &SamplingHandler{
        handler:         h.handler.WithAttrs(attrs),
        rate:            h.rate,
        levelThreshold:  h.levelThreshold,
    }
}

func (h *SamplingHandler) WithGroup(name string) slog.Handler {
    return &SamplingHandler{
        handler:         h.handler.WithGroup(name),
        rate:            h.rate,
        levelThreshold:  h.levelThreshold,
    }
}

示例 12:OpenTelemetry 集成

package logging

import (
    "context"
    "log/slog"

    "go.opentelemetry.io/otel/trace"
)

// OTelHandler 将日志与 OpenTelemetry trace 关联
type OTelHandler struct {
    handler slog.Handler
}

func NewOTelHandler(h slog.Handler) *OTelHandler {
    return &OTelHandler{handler: h}
}

func (h *OTelHandler) Enabled(ctx context.Context, level slog.Level) bool {
    return h.handler.Enabled(ctx, level)
}

func (h *OTelHandler) Handle(ctx context.Context, r slog.Record) error {
    // 从 context 提取 span context
    span := trace.SpanFromContext(ctx)
    spanCtx := span.SpanContext()

    if spanCtx.IsValid() {
        // 添加 trace_id 与 span_id
        r.AddAttrs(
            slog.String("trace_id", spanCtx.TraceID().String()),
            slog.String("span_id", spanCtx.SpanID().String()),
        )
    }
    return h.handler.Handle(ctx, r)
}

func (h *OTelHandler) WithAttrs(attrs []slog.Attr) slog.Handler {
    return &OTelHandler{handler: h.handler.WithAttrs(attrs)}
}

func (h *OTelHandler) WithGroup(name string) slog.Handler {
    return &OTelHandler{handler: h.handler.WithGroup(name)}
}

对比分析

Go slog 与其他语言日志库对比

维度Go slogRust tracingJava SLF4J/LogbackPython loggingC++ spdlog
结构化原生键值对span/event 模型MDC + 键值对LogRecord attrsfmt + 自定义
零分配LogAttrs 优化零分配高分配高分配中等分配
Context 集成context.Contexttokio contextMDC ThreadLocallogging.Logger无标准
级别数量4(DEBUG/INFO/WARN/ERROR)5(TRACE/DEBUG/INFO/WARN/ERROR)5(TRACE/DEBUG/INFO/WARN/ERROR)5(DEBUG/INFO/WARN/ERROR/CRITICAL)6(TRACE+)
Handler 接口4 方法2 traitAppenderHandlerSink
采样支持自定义 Handler内置内置内置自定义
生态成熟度标准(2023+)成熟极成熟极成熟成熟
学习成本

slog vs zap vs zerolog 内部对比

维度slogzapzerolog
API 风格键值对 + AttrSugar + 强类型链式调用
性能(ns/op)450-600200-300100-200
分配(allocs/op)000
Context 集成原生通过 helper通过 helper
Handler 互操作是(标准接口)是(实现 slog.Handler)是(实现 slog.Handler)
配置复杂度
生态绑定标准库Uber 生态独立

推荐选型

  • 新项目(Go 1.21+):优先使用 slog,需要极致性能时通过 slog.New(zap.New(...).GetSlogHandler()) 桥接 zap
  • 存量项目(zap/zerolog):保持现状,逐步引入 slog.Handler 适配层
  • 云原生微服务slog + OpenTelemetry 集成是最佳实践

常见陷阱与最佳实践

陷阱 1:级别不满足时仍计算参数

错误代码

// 即使 INFO 级别被屏蔽,expensiveCompute() 仍会执行
slog.Debug("调试", "data", expensiveCompute())

正确做法

if slog.Default().Enabled(ctx, slog.LevelDebug) {
    slog.Debug("调试", "data", expensiveCompute())
}

陷阱 2:在热路径拼接字符串

错误代码

slog.Info("用户 " + name + " 登录")  // 字符串拼接

正确做法

slog.Info("用户登录", "name", name)  // 结构化属性

陷阱 3:日志中包含敏感信息

错误代码

slog.Info("用户认证", "password", password, "token", jwt)

正确做法

slog.Info("用户认证", "username", username, "auth_method", "jwt")
// 或使用 FilterHandler 自动过滤

陷阱 4:Error 日志缺少错误原因

错误代码

slog.Error("操作失败")  // 无 error 字段

正确做法

slog.Error("操作失败", "error", err, "step", "validate")

陷阱 5:在循环中频繁创建 Logger

错误代码

for _, item := range items {
    logger := slog.With("item_id", item.ID)  // 每次循环创建新 Logger
    logger.Info("处理")
}

正确做法

logger := slog.With("batch_id", batchID)
for _, item := range items {
    logger.Info("处理", "item_id", item.ID)  // 在调用时传入属性
}

陷阱 6:生产环境输出到文件

错误做法

file, _ := os.OpenFile("app.log", ...)
logger := slog.New(slog.NewJSONHandler(file, nil))

正确做法:输出到 stdout,由容器/系统(如 Docker logs、kubectl logs、journald)收集:

logger := slog.New(slog.NewJSONHandler(os.Stdout, nil))

陷阱 7:忽略 context 取消

错误代码

func handler(w http.ResponseWriter, r *http.Request) {
    slog.Info("开始处理")
    time.Sleep(10 * time.Second)  // 客户端已断开
    slog.Info("处理完成")  // 仍输出日志
}

正确做法

func handler(w http.ResponseWriter, r *http.Request) {
    logger := slog.With("request_id", requestID)
    select {
    case <-r.Context().Done():
        logger.Info("请求被取消")
        return
    case result := <-process():
        logger.Info("处理完成", "result", result)
    }
}

最佳实践总结

  1. 生产环境用 JSON 格式:便于 ELK/Loki 等系统解析
  2. 开发环境用 Text 格式:人类可读
  3. 统一字段命名user_idrequest_idtrace_idduration_ms
  4. 级别策略:生产 INFO,开发 DEBUG,ERROR 仅用于需要立即处理的错误
  5. 日志关联:所有日志携带 trace_id,与 OpenTelemetry 集成
  6. 采样策略:高 QPS 场景下,INFO 级别采样 1%,ERROR 不采样
  7. 不输出 PII:用户身份证号、信用卡号等不记录

工程实践

1. 项目日志架构

Application Code


   slog.Logger  ──→  slog.Handler (JSON)
       │                    │
       │                    ├──→ stdout (容器收集)
       │                    ├──→ file (本地调试)
       │                    └──→ Kafka (日志聚合)


  OpenTelemetry  ──→  Jaeger / Tempo

2. 配置管理

// config/logging.go
package config

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

type LogConfig struct {
    Level  string `yaml:"level"`   // debug/info/warn/error
    Format string `yaml:"format"`  // text/json
    Output string `yaml:"output"`  // stdout/stderr/file
}

func SetupLogger(cfg LogConfig) *slog.Logger {
    var level slog.Level
    switch strings.ToLower(cfg.Level) {
    case "debug":
        level = slog.LevelDebug
    case "warn":
        level = slog.LevelWarn
    case "error":
        level = slog.LevelError
    default:
        level = slog.LevelInfo
    }

    var output *os.File
    switch strings.ToLower(cfg.Output) {
    case "stderr":
        output = os.Stderr
    case "file":
        f, _ := os.OpenFile("app.log", os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0644)
        output = f
    default:
        output = os.Stdout
    }

    opts := &slog.HandlerOptions{
        Level: level,
        AddSource: true,
    }

    var handler slog.Handler
    if cfg.Format == "json" {
        handler = slog.NewJSONHandler(output, opts)
    } else {
        handler = slog.NewTextHandler(output, opts)
    }

    // 添加全局默认属性
    handler = handler.WithAttrs([]slog.Attr{
        slog.String("service", "fandex-api"),
        slog.String("version", "1.0.0"),
        slog.String("env", os.Getenv("APP_ENV")),
    })

    logger := slog.New(handler)
    slog.SetDefault(logger)
    return logger
}

3. 请求级别日志传播

// middleware/logger.go
package middleware

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

type contextKey string

const loggerKey contextKey = "logger"

func LoggingMiddleware(next http.Handler) http.Handler {
    return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
        requestID := r.Header.Get("X-Request-ID")
        if requestID == "" {
            requestID = generateRequestID()
        }

        logger := slog.Default().With(
            "request_id", requestID,
            "method", r.Method,
            "path", r.URL.Path,
        )

        ctx := context.WithValue(r.Context(), loggerKey, logger)
        next.ServeHTTP(w, r.WithContext(ctx))
    })
}

// FromContext 从 context 提取 logger
func FromContext(ctx context.Context) *slog.Logger {
    if l, ok := ctx.Value(loggerKey).(*slog.Logger); ok {
        return l
    }
    return slog.Default()
}

业务代码使用:

func GetUser(ctx context.Context, id string) (*User, error) {
    logger := middleware.FromContext(ctx)
    logger.Info("查询用户", "user_id", id)

    user, err := db.FindUser(id)
    if err != nil {
        logger.Error("查询失败", "user_id", id, "error", err)
        return nil, err
    }
    return user, nil
}

4. 日志轮转(容器环境推荐 stdout,无需轮转)

非容器环境使用 lumberjack 实现轮转:

import "gopkg.in/natefinch/lumberjack.v2"

writer := &lumberjack.Logger{
    Filename:   "app.log",
    MaxSize:    100, // MB
    MaxBackups: 10,
    MaxAge:     30, // days
    Compress:   true,
}
handler := slog.NewJSONHandler(writer, nil)

5. 与 Prometheus 指标集成

// 监控日志级别分布
var logCounter = prometheus.NewCounterVec(
    prometheus.CounterOpts{
        Name: "log_events_total",
        Help: "Total log events by level",
    },
    []string{"level"},
)

type MetricsHandler struct {
    handler slog.Handler
}

func (h *MetricsHandler) Handle(ctx context.Context, r slog.Record) error {
    logCounter.WithLabelValues(r.Level.String()).Inc()
    return h.handler.Handle(ctx, r)
}

6. 与 ELK/Loki 集成

JSON 格式日志可直接被 Filebeat/Promtail 解析:

{
  "time": "2026-06-14T10:30:00Z",
  "level": "INFO",
  "msg": "请求处理",
  "request_id": "abc-123",
  "user_id": "456",
  "method": "GET",
  "path": "/users",
  "duration_ms": 45,
  "service": "fandex-api",
  "trace_id": "1a2b3c..."
}

在 Loki 中查询:

{service="fandex-api"} |= "ERROR" | json | duration_ms > 1000

案例研究

案例 1:Kubernetes 的日志实践

Kubernetes 组件(kube-apiserver、kubelet 等)使用 klog(基于 log 包的封装),并逐步迁移到 log/slog

// klog v2 集成 slog
import (
    "k8s.io/klog/v2"
    "log/slog"
)

// klog 提供 slog.Handler 适配
handler := klog.NewSlogHandler(klog.Background())
logger := slog.New(handler)
logger.Info("调度 Pod", "pod", "nginx-abc", "node", "node-1")

K8s 的日志规范:

  • 强制 JSON 格式(--logging-format=json
  • 字段命名遵循 lowerCamelCase(如 podNamenodeName
  • 错误日志必须包含 err 字段
  • 采样策略:高频事件(如 watch)采样 1:100

案例 2:Docker 的日志驱动

Docker 容器日志通过日志驱动收集:

# 配置 Docker 使用 json-file 驱动并轮转
docker run --log-driver=json-file \
           --log-opt max-size=10m \
           --log-opt max-file=3 \
           my-app

应用端只需输出到 stdout:

slog.SetDefault(slog.New(slog.NewJSONHandler(os.Stdout, nil)))

Docker 自动收集并打上容器元数据:

{
  "log": "{\"level\":\"INFO\",\"msg\":\"启动\"}\n",
  "stream": "stdout",
  "time": "2026-06-14T10:30:00Z",
  "container_id": "abc123...",
  "container_name": "/my-app"
}

案例 3:TiDB 的日志架构

TiDB 作为分布式数据库,日志要求极高:

  • 结构化:所有日志 JSON 格式
  • 可关联:每条日志携带 session_idtxn_id
  • 采样:高频 SQL 日志采样
  • 慢日志:单独输出到 tidb-slow.log

TiDB 使用自定义 log/slog Handler:

// TiDB 慢日志 Handler
type SlowLogHandler struct {
    handler slog.Handler
    threshold time.Duration
}

func (h *SlowLogHandler) Handle(ctx context.Context, r slog.Record) error {
    // 提取 duration 属性
    var duration time.Duration
    r.Attrs(func(a slog.Attr) bool {
        if a.Key == "duration" {
            duration = a.Value.Duration()
        }
        return true
    })

    if duration > h.threshold {
        // 写入慢日志文件
        return h.handler.Handle(ctx, r)
    }
    return nil
}

案例 4:etcd 的日志迁移

etcd 从 capnslog 迁移到 zap,再到 slog

// etcd v3.5+ 使用 zap
logger := zap.NewProduction()
logger.Info("leader changed", zap.String("old", oldLeader), zap.String("new", newLeader))

// etcd v3.6+ 迁移到 slog(通过 zap 的 slog 适配)
slogLogger := slog.New(zapcore.NewSlogHandler(zapLogger))
slogLogger.Info("leader changed", "old", oldLeader, "new", newLeader)

案例 5:Prometheus 的日志规范化

Prometheus 2.0 重构日志系统,遵循以下原则:

  1. 错误日志必须可定位:包含足够上下文(如 querystartend
  2. 不记录重复信息:上层已记录的错误,下层不再重复
  3. 采样高频事件scrape 失败采样 1:100
slog.Warn(" scrape 失败",
    "target", target,
    "error", err,
    "duration", duration,
)

案例 6:Uber 的 zap 在生产环境的实践

Uber 工程团队分享的 zap 使用经验:

  • 字段命名snake_case,与 JSON 标准对齐
  • 必填字段:每条日志必须包含 serviceversionenv
  • 禁止字段time(zap 自动添加)、level(zap 自动添加)
  • 错误处理zap.Error(err) 而非 zap.String("error", err.Error())

习题

习题 1:基础概念(难度:易)

题目:描述 slog.Loggerslog.Handler 的职责划分,并说明为何采用这种分层设计。

参考答案

slog.Logger 是高层 API,提供 Info/Error/Debug 等便捷方法,负责:

  • 维护默认属性集合(通过 With
  • 构造 slog.Record 对象
  • 将 Record 传递给 Handler

slog.Handler 是底层接口,负责:

  • 判定日志级别是否启用(Enabled
  • 实际输出日志(Handle
  • 派生新 Handler(WithAttrs/WithGroup

分层设计的优势:

  1. 关注点分离:Logger 关注 API 易用性,Handler 关注输出策略
  2. 可组合:不同 Handler 可与同一 Logger 配合(如 JSON/Text/自定义)
  3. 可互操作:第三方库(zap/zerolog)可实现 Handler 接口,与 slog 生态互通

习题 2:性能分析(难度:中)

题目:以下两段代码,哪段性能更高?为什么?

// 代码 A
slog.Info("用户登录", "user_id", userID, "ip", ip)

// 代码 B
if slog.Default().Enabled(ctx, slog.LevelInfo) {
    slog.Info("用户登录", "user_id", userID, "ip", ip)
}

参考答案

代码 A 性能更高。

分析:

  • slog.Info 内部会调用 Enabled 判定,若级别不满足则直接返回,不会执行后续逻辑
  • 代码 B 多了一次 slog.Default() 调用与 Enabled 判定,引入额外开销
  • 代码 B 的优势在于避免参数计算(如 expensiveCompute()),但本题中参数是简单变量,无需延迟计算

何时使用代码 B 的模式:当参数计算昂贵时:

if slog.Default().Enabled(ctx, slog.LevelDebug) {
    slog.Debug("调试", "data", expensiveCompute())
}

习题 3:自定义 Handler(难度:中)

题目:实现一个 slog.Handler,将所有 ERROR 级别日志同时输出到 stdout 与一个错误告警通道(channel)。

参考答案

package logging

import (
    "context"
    "log/slog"
)

type AlertHandler struct {
    handler slog.Handler
    alertCh chan slog.Record
}

func NewAlertHandler(h slog.Handler, ch chan slog.Record) *AlertHandler {
    return &AlertHandler{handler: h, alertCh: ch}
}

func (h *AlertHandler) Enabled(ctx context.Context, level slog.Level) bool {
    return h.handler.Enabled(ctx, level)
}

func (h *AlertHandler) Handle(ctx context.Context, r slog.Record) error {
    // 先输出到原始 handler
    if err := h.handler.Handle(ctx, r.Clone()); err != nil {
        return err
    }

    // ERROR 级别同时发送到告警通道
    if r.Level >= slog.LevelError {
        select {
        case h.alertCh <- r.Clone():
        default:
            // 通道满时丢弃,避免阻塞主流程
        }
    }
    return nil
}

func (h *AlertHandler) WithAttrs(attrs []slog.Attr) slog.Handler {
    return &AlertHandler{
        handler: h.handler.WithAttrs(attrs),
        alertCh: h.alertCh,
    }
}

func (h *AlertHandler) WithGroup(name string) slog.Handler {
    return &AlertHandler{
        handler: h.handler.WithGroup(name),
        alertCh: h.alertCh,
    }
}

习题 4:日志采样设计(难度:难)

题目:设计一个令牌桶采样 Handler,要求:

  • 每秒最多输出 100 条 INFO 级别日志
  • WARN 与 ERROR 不采样
  • 写出完整实现

参考答案

package logging

import (
    "context"
    "log/slog"
    "sync"
    "time"
)

type TokenBucketHandler struct {
    handler    slog.Handler
    mu         sync.Mutex
    tokens     float64
    maxTokens  float64
    rate       float64  // tokens per second
    lastRefill time.Time
}

func NewTokenBucketHandler(h slog.Handler, rate float64, capacity float64) *TokenBucketHandler {
    return &TokenBucketHandler{
        handler:    h,
        tokens:     capacity,
        maxTokens:  capacity,
        rate:       rate,
        lastRefill: time.Now(),
    }
}

func (h *TokenBucketHandler) refill() {
    now := time.Now()
    elapsed := now.Sub(h.lastRefill).Seconds()
    h.tokens += h.rate * elapsed
    if h.tokens > h.maxTokens {
        h.tokens = h.maxTokens
    }
    h.lastRefill = now
}

func (h *TokenBucketHandler) Enabled(ctx context.Context, level slog.Level) bool {
    // WARN 及以上不采样
    if level >= slog.LevelWarn {
        return h.handler.Enabled(ctx, level)
    }

    h.mu.Lock()
    h.refill()
    if h.tokens >= 1 {
        h.tokens -= 1
        h.mu.Unlock()
        return h.handler.Enabled(ctx, level)
    }
    h.mu.Unlock()
    return false
}

func (h *TokenBucketHandler) Handle(ctx context.Context, r slog.Record) error {
    return h.handler.Handle(ctx, r)
}

func (h *TokenBucketHandler) WithAttrs(attrs []slog.Attr) slog.Handler {
    return &TokenBucketHandler{
        handler:    h.handler.WithAttrs(attrs),
        tokens:     h.maxTokens,
        maxTokens:  h.maxTokens,
        rate:       h.rate,
        lastRefill: time.Now(),
    }
}

func (h *TokenBucketHandler) WithGroup(name string) slog.Handler {
    return &TokenBucketHandler{
        handler:    h.handler.WithGroup(name),
        tokens:     h.maxTokens,
        maxTokens:  h.maxTokens,
        rate:       h.rate,
        lastRefill: time.Now(),
    }
}

习题 5:Context 集成(难度:中)

题目:实现一个 WithLogger(ctx, logger)FromContext(ctx) 函数,使 logger 在 HTTP 请求处理链中自动传播。

参考答案

package logging

import (
    "context"
    "log/slog"
)

type contextKey string

const loggerKey contextKey = "logger"

// WithLogger 将 logger 存入 context
func WithLogger(ctx context.Context, logger *slog.Logger) context.Context {
    return context.WithValue(ctx, loggerKey, logger)
}

// FromContext 从 context 提取 logger,若不存在返回默认 logger
func FromContext(ctx context.Context) *slog.Logger {
    if l, ok := ctx.Value(loggerKey).(*slog.Logger); ok {
        return l
    }
    return slog.Default()
}

// 使用示例
func LoggingMiddleware(next http.Handler) http.Handler {
    return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
        logger := slog.Default().With("request_id", generateID())
        ctx := WithLogger(r.Context(), logger)
        next.ServeHTTP(w, r.WithContext(ctx))
    })
}

// 业务代码
func GetUser(ctx context.Context, id string) (*User, error) {
    logger := FromContext(ctx)
    logger.Info("查询用户", "id", id)
    // ...
}

习题 6:综合设计(难度:难)

题目:为微服务架构设计统一日志规范,要求:

  1. 字段命名规范
  2. 级别使用策略
  3. 采样规则
  4. 与 OpenTelemetry 集成方案

参考答案

1. 字段命名规范(snake_case):

字段类型说明
timestamptime日志时间(Handler 自动添加)
levelstring日志级别(Handler 自动添加)
msgstring日志消息
servicestring服务名
versionstring服务版本
envstring环境(dev/staging/prod)
trace_idstringOpenTelemetry trace ID
span_idstringOpenTelemetry span ID
request_idstring请求 ID
user_idstring用户 ID(可空)
duration_msint耗时(毫秒)
errorstring错误信息

2. 级别使用策略

  • DEBUG:开发环境,详细参数、SQL、内部状态
  • INFO:生产环境,请求开始/结束、关键业务事件
  • WARN:可恢复异常(如缓存未命中、限流触发)
  • ERROR:不可恢复异常、需立即告警

3. 采样规则

级别采样率
ERROR100%(不采样)
WARN100%
INFO10%(高 QPS 服务 1%)
DEBUG0%(生产环境不输出)

4. OpenTelemetry 集成

// 启动时配置
func SetupLogger() *slog.Logger {
    baseHandler := slog.NewJSONHandler(os.Stdout, nil)
    otelHandler := NewOTelHandler(baseHandler)
    return slog.New(otelHandler).With(
        "service", "fandex-api",
        "version", version,
        "env", env,
    )
}

// 业务代码自动携带 trace_id
func handler(w http.ResponseWriter, r *http.Request) {
    ctx := r.Context()
    logger := slog.Default().With("request_id", requestID)
    ctx = WithLogger(ctx, logger)
    // trace_id 由 OTelHandler 自动从 ctx 提取并添加
    logger.InfoContext(ctx, "处理请求")
}

参考文献

标准与规范

学术论文

性能基准

教学资源

官方文档

工程实践


延伸阅读

官方资源

第三方库文档

可观测性生态

相关章节

推荐书籍

  • Beyer, B., et al. (2016). Site Reliability Engineering: How Google Runs Production Systems. O’Reilly Media.(第 6 章:Monitoring)
  • Sigelman, B. H., et al. (2010). Dapper, a Large-Scale Distributed Systems Tracing Infrastructure. Google Technical Report.
  • Majors, C., & Fong-Jones, L. (2022). Observability Engineering. O’Reilly Media.
返回入门指南