Go语言slog结构化日志库的Handler自定义
Go 1.21 版本引入了 log/slog 标准库,提供了强大的结构化日志功能。虽然内置的 TextHandler 和 JSONHandler 能够满足大部分基础需求,但在实际生产环境中,我们往往需要自定义日志格式(例如添加特定的分隔符)、过滤敏感信息或对接第三方日志平台。这需要通过实现 slog.Handler 接口来完成。
以下将通过具体步骤,演示如何从零编写一个自定义的 Handler。
1. 理解 Handler 接口的工作流程
在编写代码前,必须了解 slog.Handler 接口包含的核心方法及其职责。Handler 就像是日志的“加工厂”,它接收原始的日志记录,决定是否处理,并将其格式化后写入指定位置。
以下是 Handler 接口四个核心方法的职责说明:
| 方法名 | 职责描述 | 返回值 |
|---|---|---|
Enabled |
判断日志级别是否满足输出条件,用于性能优化。 | bool |
Handle |
处理单条日志记录,核心逻辑所在(格式化与写入)。 | error |
WithAttrs |
返回一个附加了新属性的新 Handler 实例。 | Handler |
WithGroup |
返回一个设置了分组名称的新 Handler 实例。 | Handler |
为了直观理解日志处理的流转过程,可以参考以下执行流程:
2. 定义自定义 Handler 结构体
首先,创建一个结构体来保存 Handler 的状态。通常需要持有 io.Writer(用于写入输出)以及相关的配置选项。
- 打开 Go 项目文件,新建
handler.go文件。 - 定义
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 // 存储当前的组名
}
- 编写构造函数
NewMyHandler,方便初始化。
func NewMyHandler(w io.Writer, level slog.Level) *MyHandler {
return &MyHandler{
writer: w,
minLevel: level,
}
}
3. 实现 Enabled 方法
此方法用于过滤低级别的日志,避免不必要的性能消耗。
- 实现
Enabled方法,比较当前日志级别与 Handler 设定的最低级别。 - 返回比较结果。
func (h *MyHandler) Enabled(ctx context.Context, level slog.Level) bool {
return level >= h.minLevel
}
4. 实现 Handle 方法(核心逻辑)
这是最重要的步骤,决定了日志最终的样子。我们将实现一个简单的格式:时间 | 级别 | 消息 | 属性键=值。
- 实现
Handle方法,接收context和slog.Record。 - 调用
r.Time()、r.Level()和r.Message()获取基础信息。 - 构建格式化字符串,使用
strings.Builder提高拼接效率。 - 遍历 Record 中的属性,解析并追加到字符串中。
- 写入最终内容到
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")。
- 实现
WithAttrs方法。创建MyHandler的副本,追加新属性到副本的attrs列表中。 - 实现
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 中进行验证。
- 创建
main.go文件。 - 实例化
MyHandler,传入os.Stdout和日志级别。 - 设置
slog的默认 Logger。 - 调用
slog.Info、slog.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()。虽然这能处理大部分类型,但针对复杂对象(如 Group、LogValuer),slog 提供了更底层的 Value.Resolve() 方法。
若需要处理嵌套的 Group 结构,修改 writeAttr 逻辑如下:
- 调用
attr.Value.Resolve()获取完全解析后的值。 - 判断
value.Kind()是否为slog.KindGroup。 - 递归处理 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,可以根据业务需求随意调整日志格式与输出逻辑。

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