强曰为道

与天地相似,故不违。知周乎万物,而道济天下,故不过。旁行而不流,乐天知命,故不忧.
文档目录

23 - 日志:log/slog、zap、zerolog、结构化日志

23 - 日志

23.1 标准 log 包

package main

import (
    "log"
    "os"
)

func main() {
    // 基本日志
    log.Println("这是一条日志")
    log.Printf("用户 %s 登录, ID: %d", "Alice", 1)

    // 带前缀
    log.SetPrefix("[APP] ")
    log.SetFlags(log.Ldate | log.Ltime | log.Lshortfile)
    log.Println("带前缀的日志")

    // Fatal(记录后 os.Exit(1))
    // log.Fatal("致命错误")

    // Panic(记录后 panic)
    // log.Panic("panic 错误")

    // 输出到文件
    f, _ := os.OpenFile("app.log", os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0644)
    log.SetOutput(f)
    log.Println("写入文件的日志")
}

23.2 log/slog(Go 1.21+)

slog 是 Go 1.21 引入的官方结构化日志包。

package main

import (
    "log/slog"
    "os"
)

func main() {
    // 文本格式(默认)
    slog.Info("服务器启动", "port", 8080, "env", "production")
    // 2024/01/15 10:00:00 INFO 服务器启动 port=8080 env=production

    // JSON 格式
    logger := slog.New(slog.NewJSONHandler(os.Stdout, nil))
    logger.Info("用户登录",
        "user_id", 123,
        "username", "alice",
        "ip", "192.168.1.100",
    )
    // {"time":"2024-01-15T10:00:00Z","level":"INFO","msg":"用户登录","user_id":123,"username":"alice","ip":"192.168.1.100"}

    // 日志级别
    slog.Debug("调试信息")           // 默认不输出
    slog.Info("信息")
    slog.Warn("警告")
    slog.Error("错误")

    // 带上下文的 logger
    logger2 := slog.With(
        "service", "user-service",
        "version", "1.0.0",
    )
    logger2.Info("处理请求", "method", "GET", "path", "/api/users")

    // 设置日志级别
    opts := &slog.HandlerOptions{
        Level: slog.LevelDebug,
    }
    logger3 := slog.New(slog.NewTextHandler(os.Stdout, opts))
    logger3.Debug("现在可以看到调试信息了")

    // WithGroup
    logger4 := slog.New(slog.NewJSONHandler(os.Stdout, nil))
    logger4.WithGroup("request").Info("请求信息",
        "method", "GET",
        "path", "/api/users",
        "status", 200,
    )
    // {"time":"...","level":"INFO","msg":"请求信息","request":{"method":"GET","path":"/api/users","status":200}}
}

slog 自定义 Handler

type CustomHandler struct {
    slog.Handler
}

func (h *CustomHandler) Handle(ctx context.Context, r slog.Record) error {
    // 添加 trace_id
    if traceID := ctx.Value("trace_id"); traceID != nil {
        r.AddAttrs(slog.String("trace_id", traceID.(string)))
    }
    return h.Handler.Handle(ctx, r)
}

func main() {
    base := slog.NewJSONHandler(os.Stdout, nil)
    handler := &CustomHandler{Handler: base}
    logger := slog.New(handler)

    ctx := context.WithValue(context.Background(), "trace_id", "abc-123")
    logger.InfoContext(ctx, "处理请求")
}

23.3 zap

Uber 开发的高性能日志库。

import (
    "go.uber.org/zap"
    "go.uber.org/zap/zapcore"
)

func main() {
    // 开发环境(可读格式)
    logger, _ := zap.NewDevelopment()
    defer logger.Sync()

    logger.Info("服务器启动",
        zap.Int("port", 8080),
        zap.String("env", "dev"),
    )

    // 生产环境(JSON 格式)
    prodLogger, _ := zap.NewProduction()
    defer prodLogger.Sync()

    prodLogger.Info("用户登录",
        zap.Int("user_id", 123),
        zap.String("username", "alice"),
    )

    // 自定义配置
    config := zap.Config{
        Level:       zap.NewAtomicLevelAt(zap.InfoLevel),
        Encoding:    "json",
        OutputPaths: []string{"stdout", "app.log"},
        EncoderConfig: zapcore.EncoderConfig{
            TimeKey:        "ts",
            LevelKey:       "level",
            NameKey:        "logger",
            MessageKey:     "msg",
            StacktraceKey:  "stacktrace",
            LineEnding:     zapcore.DefaultLineEnding,
            EncodeLevel:    zapcore.LowercaseLevelEncoder,
            EncodeTime:     zapcore.ISO8601TimeEncoder,
            EncodeDuration: zapcore.SecondsDurationEncoder,
        },
    }
    customLogger, _ := config.Build()
    defer customLogger.Sync()
    customLogger.Info("自定义配置日志")

    // Sugar Logger(printf 风格)
    sugar := prodLogger.Sugar()
    sugar.Infof("用户 %s 登录, ID: %d", "alice", 123)
    sugar.Warnw("慢查询",
        "duration", "2.5s",
        "query", "SELECT * FROM users",
    )
}

23.4 zerolog

零分配的高性能日志库。

import (
    "github.com/rs/zerolog"
    "github.com/rs/zerolog/log"
    "os"
    "time"
)

func main() {
    // 全局配置
    zerolog.TimeFieldFormat = zerolog.TimeFormatUnix
    log.Logger = log.Output(zerolog.ConsoleWriter{Out: os.Stderr})

    // 基本使用
    log.Info().Str("service", "api").Int("port", 8080).Msg("服务器启动")
    log.Error().Err(fmt.Errorf("连接失败")).Msg("数据库错误")

    // 链式调用
    log.Debug().
        Str("method", "GET").
        Str("path", "/api/users").
        Dur("duration", 150*time.Millisecond).
        Int("status", 200).
        Msg("请求完成")

    // 子 logger
    subLogger := log.With().
        Str("service", "user-service").
        Str("version", "1.0.0").
        Logger()

    subLogger.Info().Msg("处理请求")

    // 全局上下文
    zerolog.DefaultContextLogger = &subLogger
}

日志库对比

特性log/slogzapzerolog
标准库
性能最高最高
零分配部分
API 风格函数式链式链式
学习曲线
功能丰富度

23.5 结构化日志最佳实践

// 1. 使用 context 传递 trace_id
func handler(w http.ResponseWriter, r *http.Request) {
    ctx := r.Context()
    traceID := r.Header.Get("X-Request-ID")
    ctx = context.WithValue(ctx, "trace_id", traceID)

    slog.InfoContext(ctx, "处理请求",
        "method", r.Method,
        "path", r.URL.Path,
    )
}

// 2. 日志中间件
func loggingMiddleware(next http.Handler) http.Handler {
    return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
        start := time.Now()
        next.ServeHTTP(w, r)
        slog.Info("请求完成",
            "method", r.Method,
            "path", r.URL.Path,
            "duration", time.Since(start),
        )
    })
}

// 3. 错误日志
func processRequest(ctx context.Context) error {
    if err := doSomething(); err != nil {
        slog.ErrorContext(ctx, "处理失败",
            "error", err,
            "operation", "doSomething",
        )
        return err
    }
    return nil
}

🏢 业务场景

  1. 请求链路追踪:结构化日志 + trace_id 串联请求
  2. 错误监控:Error 级别日志发送到 Sentry/ELK
  3. 性能分析:记录慢请求的 duration
  4. 审计日志:记录用户操作(登录、修改、删除)

📖 扩展阅读