文章目录

Go语言slog结构化日志库的Handler自定义

发布于 2026-05-03 15:18:40 · 浏览 21 次 · 评论 0 条

Go语言slog结构化日志库的Handler自定义

Go 1.21 版本引入了 log/slog 标准库,提供了强大的结构化日志功能。虽然内置的 TextHandlerJSONHandler 能够满足大部分基础需求,但在实际生产环境中,我们往往需要自定义日志格式(例如添加特定的分隔符)、过滤敏感信息或对接第三方日志平台。这需要通过实现 slog.Handler 接口来完成。

以下将通过具体步骤,演示如何从零编写一个自定义的 Handler。


1. 理解 Handler 接口的工作流程

在编写代码前,必须了解 slog.Handler 接口包含的核心方法及其职责。Handler 就像是日志的“加工厂”,它接收原始的日志记录,决定是否处理,并将其格式化后写入指定位置。

以下是 Handler 接口四个核心方法的职责说明:

方法名 职责描述 返回值
Enabled 判断日志级别是否满足输出条件,用于性能优化。 bool
Handle 处理单条日志记录,核心逻辑所在(格式化与写入)。 error
WithAttrs 返回一个附加了新属性的新 Handler 实例。 Handler
WithGroup 返回一个设置了分组名称的新 Handler 实例。 Handler

为了直观理解日志处理的流转过程,可以参考以下执行流程:

graph LR A["Start: Log Call"] --> B{"Check: Enabled?"} B -- false --> F["End: Skip Log"] B -- true --> C["Run: Handle Method"] C --> D["Format: Time, Level, Msg, Attrs"] D --> E["Write: Output to IO"] E --> F

2. 定义自定义 Handler 结构体

首先,创建一个结构体来保存 Handler 的状态。通常需要持有 io.Writer(用于写入输出)以及相关的配置选项。

  1. 打开 Go 项目文件,新建 handler.go 文件。
  2. 定义 MyHandler 结构体,包含 writer 和用于存储预定义属性的 attrs 切片,以及 minLevel 用于控制日志级别。
package main

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

// MyHandler 自定义结构体,实现 slog.Handler 接口
type MyHandler struct {
    writer    io.Writer
    minLevel  slog.Level
    attrs     []slog.Attr // 存储预定义的属性
    groupName string      // 存储当前的组名
}
  1. 编写构造函数 NewMyHandler,方便初始化。
func NewMyHandler(w io.Writer, level slog.Level) *MyHandler {
    return &MyHandler{
        writer:   w,
        minLevel: level,
    }
}

3. 实现 Enabled 方法

此方法用于过滤低级别的日志,避免不必要的性能消耗。

  1. 实现 Enabled 方法,比较当前日志级别与 Handler 设定的最低级别。
  2. 返回比较结果。
func (h *MyHandler) Enabled(ctx context.Context, level slog.Level) bool {
    return level >= h.minLevel
}

4. 实现 Handle 方法(核心逻辑)

这是最重要的步骤,决定了日志最终的样子。我们将实现一个简单的格式:时间 | 级别 | 消息 | 属性键=值

  1. 实现 Handle 方法,接收 contextslog.Record
  2. 调用 r.Time()r.Level()r.Message() 获取基础信息。
  3. 构建格式化字符串,使用 strings.Builder 提高拼接效率。
  4. 遍历 Record 中的属性,解析并追加到字符串中。
  5. 写入最终内容到 h.writer
func (h *MyHandler) Handle(ctx context.Context, r slog.Record) error {
    var builder strings.Builder

    // 1. 写入时间
    builder.WriteString(r.Time().Format("2006-01-02 15:04:05"))
    builder.WriteString(" | ")

    // 2. 写入级别
    builder.WriteString(r.Level().String())
    builder.WriteString(" | ")

    // 3. 写入消息
    builder.WriteString(r.Message())

    // 4. 处理 Record 中的属性
    r.Attrs(func(attr slog.Attr) bool {
        h.writeAttr(&builder, attr)
        return true
    })

    // 5. 处理 Handler 预定义的属性
    for _, attr := range h.attrs {
        h.writeAttr(&builder, attr)
    }

    builder.WriteString("\n")

    // 6. 写入输出流
    _, err := h.writer.Write([]byte(builder.String()))
    return err
}

// 辅助方法:格式化单个属性
func (h *MyHandler) writeAttr(builder *strings.Builder, attr slog.Attr) {
    key := attr.Key
    if h.groupName != "" {
        key = h.groupName + "." + key
    }

    value := attr.Value.String()
    builder.WriteString(" | ")
    builder.WriteString(key)
    builder.WriteString("=")
    builder.WriteString(value)
}

5. 实现 WithAttrs 和 WithGroup 方法

这两个方法用于支持链式调用,如 slog.With("key", "value")

  1. 实现 WithAttrs 方法。创建 MyHandler 的副本,追加新属性到副本的 attrs 列表中。
  2. 实现 WithGroup 方法。创建副本,设置副本的 groupName
func (h *MyHandler) WithAttrs(attrs []slog.Attr) slog.Handler {
    // 复制当前 Handler,避免修改原实例
    newHandler := *h
    // 追加新属性
    newHandler.attrs = append(newHandler.attrs, attrs...)
    return &newHandler
}

func (h *MyHandler) WithGroup(name string) slog.Handler {
    newHandler := *h
    // 设置组名
    newHandler.groupName = name
    return &newHandler
}

6. 集成与测试

完成自定义 Handler 后,需要将其注册到 slog 的默认 Logger 中进行验证。

  1. 创建 main.go 文件。
  2. 实例化 MyHandler,传入 os.Stdout 和日志级别。
  3. 设置 slog 的默认 Logger。
  4. 调用 slog.Infoslog.Error 等方法输出日志。
package main

import (
    "log/slog"
    "os"
)

func main() {
    // 1. 实例化自定义 Handler
    opts := slog.HandlerOptions{AddSource: true}
    handler := NewMyHandler(os.Stdout, slog.LevelInfo)

    // 2. 包装为 slog.Logger (可选,也可以直接用 Default)
    logger := slog.New(handler)
    slog.SetDefault(logger)

    // 3. 输出不同级别的日志
    slog.Debug("This is a debug message", "status", "skipped") // 低于 Info 级别,不会输出
    slog.Info("User login", "id", 1001, "ip", "192.168.1.1")

    // 4. 测试 WithAttr 链式调用
    slog.With("service", "payment").Warn("Transaction failed", "amount", 50.00, "error", "timeout")
}

运行上述代码后,控制台将输出如下格式的日志:

2023-10-27 10:00:00 | INFO | User login | id=1001 | ip=192.168.1.1
2023-10-27 10:00:01 | WARN | Transaction failed | service=payment | amount=50 | error=timeout

7. 进阶优化:处理 Value 类型

在步骤 4 的 writeAttr 方法中,我们使用了 attr.Value.String()。虽然这能处理大部分类型,但针对复杂对象(如 GroupLogValuer),slog 提供了更底层的 Value.Resolve() 方法。

若需要处理嵌套的 Group 结构,修改 writeAttr 逻辑如下:

  1. 调用 attr.Value.Resolve() 获取完全解析后的值。
  2. 判断 value.Kind() 是否为 slog.KindGroup
  3. 递归处理 Group 内部的属性。
func (h *MyHandler) writeAttr(builder *strings.Builder, attr slog.Attr) {
    value := attr.Value.Resolve()
    key := attr.Key
    if h.groupName != "" {
        key = h.groupName + "." + key
    }

    if value.Kind() == slog.KindGroup {
        // 如果是 Group,递归处理内部属性
        for _, innerAttr := range value.Group() {
            // 这里简单处理,实际应用中可能需要临时修改 groupName 逻辑
            h.writeAttr(builder, innerAttr)
        }
        return
    }

    builder.WriteString(" | ")
    builder.WriteString(key)
    builder.WriteString("=")
    builder.WriteString(value.String())
}

通过以上步骤,你已经拥有了一个完全可控、结构清晰的 Go slog 自定义 Handler,可以根据业务需求随意调整日志格式与输出逻辑。

评论 (0)

暂无评论,快来抢沙发吧!

扫一扫,手机查看

扫描上方二维码,在手机上查看本文