Files
OpenFlare/.agents/skills/go-logging/references/LOGGING-PATTERNS.md
T
2026-08-04 11:40:32 +08:00

7.4 KiB

日志模式

关于 slog 设置、handler 配置、测试、HTTP 中间件以及从旧版 log 包迁移的详细模式。

设置 slog

基本配置

package main

import (
    "log/slog"
    "os"
)

func main() {
    // JSON handler 用于生产(机器可解析)
    logger := slog.New(slog.NewJSONHandler(os.Stdout, &slog.HandlerOptions{
        Level: slog.LevelInfo,
    }))
    slog.SetDefault(logger)

    slog.Info("server started", "addr", ":8080")
    // 输出:{"time":"...","level":"INFO","msg":"server started","addr":":8080"}
}

用于开发的 Text Handler

// 本地开发的人类可读输出
logger := slog.New(slog.NewTextHandler(os.Stderr, &slog.HandlerOptions{
    Level: slog.LevelDebug,
}))
slog.SetDefault(logger)
// 输出:time=... level=DEBUG msg="cache lookup" key=user:42 hit=true

动态级别控制

使用 slog.LevelVar 在运行时更改最低级别(例如通过管理端点或信号处理器):

var programLevel = new(slog.LevelVar) // 默认 Info

func init() {
    logger := slog.New(slog.NewJSONHandler(os.Stdout, &slog.HandlerOptions{
        Level: programLevel,
    }))
    slog.SetDefault(logger)
}

// 从管理端点或信号处理器调用
func enableDebug() {
    programLevel.Set(slog.LevelDebug)
}

自定义 Handler 模式

添加源位置

logger := slog.New(slog.NewJSONHandler(os.Stdout, &slog.HandlerOptions{
    AddSource: true,
    Level:     slog.LevelInfo,
}))
// 输出包含:"source":{"function":"main.handleRequest","file":"server.go","line":42}

使用默认属性包装 Handler

使用 slog.Handler 中间件向每条日志记录注入字段:

type contextHandler struct {
    inner   slog.Handler
    attrs   []slog.Attr
}

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

func (h *contextHandler) Handle(ctx context.Context, r slog.Record) error {
    r.AddAttrs(h.attrs...)
    return h.inner.Handle(ctx, r)
}

func (h *contextHandler) WithAttrs(attrs []slog.Attr) slog.Handler {
    return &contextHandler{inner: h.inner.WithAttrs(attrs), attrs: h.attrs}
}

func (h *contextHandler) WithGroup(name string) slog.Handler {
    return &contextHandler{inner: h.inner.WithGroup(name), attrs: h.attrs}
}

多 Handler(扇出)

写入多个目标(例如 stdout + 文件):

type multiHandler struct {
    handlers []slog.Handler
}

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

func (m *multiHandler) Handle(ctx context.Context, r slog.Record) error {
    var errs []error
    for _, h := range m.handlers {
        if h.Enabled(ctx, r.Level) {
            if err := h.Handle(ctx, r); err != nil {
                errs = append(errs, err)
            }
        }
    }
    return errors.Join(errs...)
}

func (m *multiHandler) WithAttrs(attrs []slog.Attr) slog.Handler {
    handlers := make([]slog.Handler, len(m.handlers))
    for i, h := range m.handlers {
        handlers[i] = h.WithAttrs(attrs)
    }
    return &multiHandler{handlers: handlers}
}

func (m *multiHandler) WithGroup(name string) slog.Handler {
    handlers := make([]slog.Handler, len(m.handlers))
    for i, h := range m.handlers {
        handlers[i] = h.WithGroup(name)
    }
    return &multiHandler{handlers: handlers}
}

使用 slogtest 测试

Go 1.22+ 提供了 testing/slogtest 来验证 handler 实现:

package myhandler_test

import (
    "testing"
    "testing/slogtest"
)

func TestHandler(t *testing.T) {
    // newHandler 返回你的自定义 slog.Handler 和一个
    // 将输出解析为 []map[string]any 的函数用于验证。
    results := func(t *testing.T) map[string]any {
        // 在此解析你的 handler 输出
    }

    h := NewMyHandler(buf, nil)
    slogtest.Run(t, func(t *testing.T) slog.Handler { return h }, results)
}

在测试中捕获日志

对于断言日志输出的单元测试,写入 buffer:

func TestOrderProcessing(t *testing.T) {
    var buf bytes.Buffer
    logger := slog.New(slog.NewJSONHandler(&buf, nil))

    processOrder(logger, order)

    if !strings.Contains(buf.String(), `"order_id"`) {
        t.Error("expected order_id in log output")
    }
}

HTTP 请求日志中间件

一个完整的中间件,记录每个请求的计时、状态和请求作用域字段:

func loggingMiddleware(next http.Handler) http.Handler {
    return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
        start := time.Now()
        reqID := r.Header.Get("X-Request-ID")
        if reqID == "" {
            reqID = uuid.NewString()
        }

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

        // 包装 response writer 以捕获状态码
        rw := &responseWriter{ResponseWriter: w, status: http.StatusOK}

        // 将日志器存入 context 供下游 handler 使用
        ctx := context.WithValue(r.Context(), loggerKey, logger)
        next.ServeHTTP(rw, r.WithContext(ctx))

        logger.Info("request completed",
            "status", rw.status,
            "elapsed_ms", time.Since(start).Milliseconds(),
        )
    })
}

type responseWriter struct {
    http.ResponseWriter
    status int
}

func (rw *responseWriter) WriteHeader(code int) {
    rw.status = code
    rw.ResponseWriter.WriteHeader(code)
}

从 Context 获取日志器

type ctxKey struct{}

var loggerKey = ctxKey{}

func loggerFromCtx(ctx context.Context) *slog.Logger {
    if l, ok := ctx.Value(loggerKey).(*slog.Logger); ok {
        return l
    }
    return slog.Default()
}

从 log.Printf 迁移到 slog

第 1 步:替换直接调用

// 迁移前
log.Printf("user %s logged in from %s", userID, ip)

// 迁移后
slog.Info("user logged in", "user_id", userID, "ip", ip)

第 2 步:替换 main() 中的 log.Fatalf

// 迁移前
log.Fatalf("failed to connect: %v", err)

// 迁移后——slog 没有 Fatal;在 main 中使用 slog + os.Exit
slog.Error("failed to connect", "err", err)
os.Exit(1)

第 3 步:桥接旧代码

如果逐步迁移,将标准 log 包的输出通过 slog 重定向:

// 在 main() 中,设置 slog 之后:
slog.SetDefault(logger)

// 标准 log 包现在通过 slog 的默认 handler 写入。
// 这是因为 slog.SetDefault 也会更新 log.Default()。

第 4 步:替换日志器参数

// 迁移前:传递 *log.Logger
func NewServer(addr string, logger *log.Logger) *Server

// 迁移后:显式传递 *slog.Logger
func NewServer(addr string, logger *slog.Logger) *Server

// 或从 handler 中的 context 派生
func (s *Server) handleRequest(ctx context.Context) {
    logger := loggerFromCtx(ctx)
    logger.Info("handling request")
}

迁移清单

步骤 更改什么 验证
1 log.Printf → slog.Info/Warn/Error rg 'log\.Printf' 返回 0 个匹配
2 log.Fatalf → slog.Error + os.Exit(1) 在 main 中 仅在 main() 中
3 在 main 中尽早设置 slog.SetDefault 旧版 log 调用通过 slog 路由
4 *log.Logger 参数 → *slog.Logger 所有构造函数已更新
5 移除已替换处的 "log" 导入 goimports 会自动处理