写点什么

探索 Go slog 标准库:设计与应用

作者:俞凡
  • 2024-06-12
    上海
  • 本文字数:6473 字

    阅读完需:约 21 分钟

本文主要介绍了 Go 语言新引入的 log/slog 标准库的设计理念、使用方法以及如何进行定制化开发,以提高日志操作的性能和灵活性。原文: Explore the Go slog Standard Library: Design and Usage



Go1.21 中引入的 log/slog 软件包试图弥补原有日志软件包的不足,即日志缺乏结构化和级别特性。正如提案中提到的,log/slog 包旨在创建一个零依赖、易用、高性能、灵活高效的日志系统。如果你对它的概念感兴趣,并想更好的利用它,那就请跟随我深入了解它的设计、实现和应用。

slog 设计

为实现其目的,slog 的设计具有高度灵活性。它支持结构化日志,能以 JSON 或其他格式输出日志,以便后续分析和处理。出色的模块化设计将日志功能分为多个组件,如日志级别管理、输出格式化、日志传输等,每个组件都可以独立配置和替换。其异步处理支持机制可确保日志操作不会阻碍主应用程序的执行。


// Logger contains all actions against a Log messagetype Logger struct {    New(h Handler) *Logger    Debug(msg string, args ...any)    Error(msg string, args ...any)    Info(msg string, args ...any)    //...    With(args ...any) *Logger}
// Handler defines how to type Handler interface { Handle(context.Context, Record) error WithAttrs(attrs []Attr) Handler WithGroup(name string) Handler}
// Record defines a Log message with many info。type Record struct { Time time.Time Message string Level Level PC uintptr}
复制代码


三个组件完成了整个流程。


Logger负责所有日志打印操作。Handler定义日志输出格式,默认实现为 TextHandlerJSONHandlerRecord定义日志的详细信息,如时间、级别等。这些接口不仅能以多种方式扩展 slog(如添加新的处理程序),还能满足不同日志框架的需求和配置。

slog 用法

slog 的基本用法与 zap 或 Golang 的其他第三方日志框架类似。

日志打印
func main() {    slog.Info("This is an informational message.")    slog.Warn("This is a warning message with context.", slog.String("user", "slaise"))    slog.Error("This is an error message with details.", slog.Int("code", 123))}// output(go-playground) https://go.dev/play/p/TLIur7rZFhi2009/11/10 23:00:00 INFO This is an informational message.2009/11/10 23:00:00 WARN This is a warning message with context. user=slaise2009/11/10 23:00:00 ERROR This is an error message with details. code=123
复制代码


slog 为不同级别的日志提供了不同的打印方式和相应的参数类型转换。

Handler

通过下面的代码,我们可以将默认打印方式修改为内置的 TextHandlerJSONHandler


logger := slog.New(slog.NewJSONHandler(os.Stdout, nil))// logger.Info/Debug/Error
复制代码


请看输出结果。


// JSONHandlerfunc main() { logger := slog.New(slog.NewJSONHandler(os.Stdout, nil)) logger.Info("This is an informational message.") logger.Warn("This is a warning message with context.", slog.String("user", "slaise")) logger.Error("This is an error message with details.", slog.Int("code", 123))
}// Output: https://go.dev/play/p/oCubHk77Sjw{"time":"2009-11-10T23:00:00Z","level":"INFO","msg":"This is an informational message."}{"time":"2009-11-10T23:00:00Z","level":"WARN","msg":"This is a warning message with context.","user":"slaise"}{"time":"2009-11-10T23:00:00Z","level":"ERROR","msg":"This is an error message with details.","code":123}
复制代码


// TextHandlerlogger := slog.New(slog.NewTextHandler(os.Stdout, nil))// Output: https://go.dev/play/p/ewil9ziZpsktime=2009-11-10T23:00:00.000Z level=INFO msg="This is an informational message."time=2009-11-10T23:00:00.000Z level=WARN msg="This is a warning message with context." user=slaisetime=2009-11-10T23:00:00.000Z level=ERROR msg="This is an error message with details." code=123
复制代码


可以通过 SetDefault 方法创建 Logger 来替换 slog 中的默认 Logger,默认 slog.Infoslog.Debug 的 Logger 也将同时被修改。


func main() { logger := slog.New(slog.NewJSONHandler(os.Stdout, nil)) slog.SetDefault(logger) slog.Info("This is an informational message.") slog.Warn("This is a warning message with context.", slog.String("user", "slaise")) slog.Error("This is an error message with details.", slog.Int("code", 123))}//output{"time":"2009-11-10T23:00:00Z","level":"INFO","msg":"This is an informational message."}{"time":"2009-11-10T23:00:00Z","level":"WARN","msg":"This is a warning message with context.","user":"slaise"}{"time":"2009-11-10T23:00:00Z","level":"ERROR","msg":"This is an error message with details.","code":123}
复制代码
消息处理

Recordattr 的处理是 slog 核心功能的一部分。每条日志记录都由时间、级别、消息等参数和一组键值对组成。处理这些参数的 API 提供了灵活性。使用 With 方法,用户可以轻松添加固定属性,这些属性将出现在该日志记录器生成的每条日志记录中。通过 Group 方法,用户可以汇总多个属性,进行统一处理。请看下面的示例。


func main() { logger := slog.New(slog.NewTextHandler(os.Stdout, nil))
// Build a group attrGroup := slog.Group("ops", slog.String("module", "authentication"), slog.String("method", "login"))
logger.Info("User login attempt", slog.String("service", "login-service"), slog.String("version", "1.0.2"), slog.String("status", "attempting"), attrGroup, )
// login failed // failed, err := login(user, pass) err := errors.New("login password err") if err != nil { // error can only be printed with slog.Any logger.Error("User login failed", slog.String("service", "login-service"), slog.String("version", "1.0.2"), slog.Any("error", err)) //output: service=login-service version=1.0.2 error="login password err }}
复制代码


Attr 提供了在 slog 中处理参数的主要 API,例如上例中的 slog.Stringslog.Any,以及 slog.Boolslog.Intslog.Float64slog.Durationslog.Time等。


Level 通过实现 Leveler 接口的 Level() 为 slog 提供默认日志级别设置。


const ( LevelDebug Level = -4 LevelInfo  Level = 0 LevelWarn  Level = 4 LevelError Level = 8)func (l Level) Level() Level { return l }
复制代码


日志应用本地机器的默认时区设置,但也可以使用 slog.Time 添加特定时间到日志中。


loc, err := time.LoadLocation("America/New_York")currentTime := time.Now().In(loc)slog.Info("Log message with NewYork timezone",  slog.Time("time", currentTime),  )
复制代码


不过,如果我们需要在不同于本地机器的时区打印日志,就需要自定义处理程序,因为 slog 软件包缺少用于全局修改的默认 API。

上下文处理

可以通过两种方式将上下文添加到日志记录中。


  • 通过在日志处理中加入 context.Context,可以在函数和 goroutine 之间传递上下文信息。

  • Logger 提供了带context的日志打印方法,如 DebugContextInfoContextWarnContextErrorContextcontext.Context 被添加到之前的方法参数列表中,并将 ctx 放在首位,这符合上下文的使用原则。


func (l *Logger) DebugContext(ctx context.Context, msg string, args ...any)
复制代码


但如果直接使用这些方法将context中的参数添加到日志中,就会像我一样感到困惑,因为这样做是行不通的。例如,在以下代码中,userId 不会显示在日志中。


func main() { ctx := context.WithValue(context.Background(), "userId", "123") slog.InfoContext(ctx, "reset password", slog.Time("time", time.Now()))}// output2009/11/10 23:00:00 INFO reset password time=2009-11-10T23:00:00.000Z
复制代码


阅读源代码后,你会惊讶的发现,默认 defaultHandler 只打印消息和参数列表,却不处理传入的 ctx。


func (h *defaultHandler) Handle(ctx context.Context, r Record) error { buf := buffer.New() buf.WriteString(r.Level.String()) buf.WriteByte(' ') buf.WriteString(r.Message) state := h.ch.newHandleState(buf, true, " ") defer state.free() state.appendNonBuiltIns(r) return h.output(r.PC, *buf)}
复制代码


go 1.21 中正式发布的 slog 只提供了相关 API 而没有提供实现,因此需要定制处理程序来实现相关功能。


  • 可通过父日志记录器构建具有层次关系的日志记录器,这些日志记录器继承并扩展了基本日志记录功能


例如,在前面的WithGroup示例中,在打印 Info 和 Error 日志的过程中重复传递了服务和版本信息,而日志程序的继承功能可以简化这一过程。


// Define a new Logger with service & service infologger = slog.With(logger, slog.String("service", "login-service"), slog.String("version", "1.0.2"))// Print logslogger.Info("User login attempt",  slog.String("status", "attempting"),  attrGroup, )
复制代码

slog 定制

Slog 的设计非常灵活,便于定制日志。

自定义日志级别

与官方 LogLevel 一样,可以通过实现 Logeler 接口来定制 LogLevel,而 slog.Level 本身也会返回 Level 类型。


const ( LevelTrace = slog.Level(-99) LevelFatal = slog.Level(99))
func main() { ctx := context.Background() // This will be hidden because default Log level is above trace slog.Log(ctx, LevelTrace, "Trace message") slog.Log(ctx, LevelFatal, "Fatal level")}// output2009/11/10 23:00:00 ERROR+91 Fatal level
复制代码


跟踪级别日志在未修改相应HandlerOptions时是隐藏的,可通过修改默认日志级别打开。


slog.SetLogLoggerLevel(LevelTrace)
复制代码


自定义 HandlerOptions HandlerOptions提供三种修改日志的方法。


  • 显示源代码。使用 AddSource:true,可以在日志中添加文件、行号和方法名称等信息。

  • 修改日志级别LevelTrace 的作用与上述 SetLogLoggerLevel 相同。

  • 定义 ReplaceAttr 可调用函数,用于修改日志中的 Attr 键值对。


对于缺少默认格式化支持的错误,可以通过实现 ReplaceAttr 来加以改进。


func replaceAttr(_ []string, a slog.Attr) slog.Attr {    switch a.Value.Kind() {    case slog.KindAny:        switch v := a.Value.Any().(type) {        case error:            a.Value = fmtErr(v) // provide error log format        }    }    return a}
复制代码


自定义 Handler 自定义日志的最后一种方法是实现自己的 Handler。下面的 ContextHandler 提供了将上下文中的参数整合到日志中的功能。


const ( UserId string = "userId")
type ContextHandler struct { slog.Handler}
func (h ContextHandler) Handle(ctx context.Context, r slog.Record) error { if id, ok := ctx.Value(UserId).(string); ok { r.AddAttrs(slog.String(UserId, id)) } return h.Handler.Handle(ctx, r)}
func main() { ctxHandler := &ContextHandler{slog.NewJSONHandler(os.Stdout, nil)} logger := slog.New(ctxHandler) ctx := context.WithValue(context.Background(), "userId", "123") logger.InfoContext(ctx, "User ops", slog.String("op", "login"))}
//output{"time":"2009-11-10T23:00:00Z","level":"INFO","msg":"User ops","op":"login","userId":"123"}
复制代码

性能

slog 支持通过延迟计算优化参数处理,只在实际需要日志信息时才执行参数的高性能字符串操作,以减少不必要的性能开销。


在默认日志方法中,只打印默认级别以上的日志。Attrs 将被添加到打印前创建的Record中。


func (l *Logger) log(ctx context.Context, level Level, msg string, args ...any) { if !l.Enabled(ctx, level) {  return } var pc uintptr if !internal.IgnorePC {  var pcs [1]uintptr  // skip [runtime.Callers, this function, this function's caller]  runtime.Callers(3, pcs[:])  pc = pcs[0] } r := NewRecord(time.Now(), level, msg, pc) r.Add(args...) if ctx == nil {  ctx = context.Background() } _ = l.Handler().Handle(ctx, r)}
复制代码


slog 利用内置 buffer.go 中的对象池技术重复使用日志条目对象,减少了内存应用和释放以及垃圾回收的频率,从而提高了应用性能。

slog 实践

到目前为止,我们已经做好了在项目中使用 slog 的准备,但要牢记以下几点。


  • 对敏感数据脱敏


数据安全在日志处理中至关重要。在将对象对象打印到日志时,应及时屏蔽或模糊敏感信息,以避免数据泄漏,这可以通过相应结构体中的 LogValue 接口来实现。下面我们来看一个包含电子邮件和密码的用户类型示例。


type User struct { ID       string `json:"id"` Email    string `json:"email"` Password string `json:"password"`}
func (u User) LogValue() slog.Value { return slog.StringValue(u.ID)}
func main() { h := slog.NewJSONHandler(os.Stdout, nil) logger := slog.New(h) u := &User{ ID: "1", Email: "slaise@gmail.com", Password: "111111", } logger.Info("info", "user", u)
}// output{"time":"2009-11-10T23:00:00Z","level":"INFO","msg":"info","user":"1"}
复制代码


  • 将 slog 级别提取为配置项,并为不同环境配置不同级别


项目通常部署在多个环境中,如 devstagingprod 等。通过将日志级别提取为环境参数,并通过 Kubernetes 的 configMap 注入,可以在不同环境中应用不同日志级别,从而减少开销。


  • 处理上下文信息


上例中的上下文实现是一个明智的选择,可以通过优化来动态加载上下文参数列表。在我们实现的代码及许多第三方日志框架中,Context 被广泛用于传递上下文信息,从而简化了从第三方库升级到 slog 的过程。


  • 正确配置日志输出目的地


根据需要配置日志输出,如输出到控制台、文件、网络服务等,或输出到多个输出目的地,以确保日志的可靠存储。


通过自定义处理程序的写入器,可以轻松修改日志输出地址。


file, err := os.OpenFile("app.log", os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0666) if err != nil {  panic("cannot open log file: " + err.Error()) } defer file.Close()
logger := slog.New(slog.JSONHandler(file))
复制代码

升级第三方库

活跃的 Golang 社区有许多为 slog 定制的升级框架,以下是一些常用框架。


  • slog-multi:处理器链,如流水线、路由器、扇出等。

  • slog-sampling:丢弃重复的日志条目。

  • slog-shim 为 1.21 以下的 Go 版本提供向后兼容的 slog 支持。

  • sloggen 生成各种辅助工具。

  • sloglint 可确保代码的一致性。

结论

slog 软件包为管理应用程序日志提供了强大而灵活的解决方案。其设计确保了高性能和多功能性,使开发人员能够自定义日志级别、Handler 配置以及 Handler 本身。使用 slog 不仅能加强消息处理和上下文信息集成,还能简化日志记录流程。为了有效实施,仔细考虑自定义日志级别和 Handler 以满足特定项目要求至关重要。通过明智利用 slog 的功能,开发人员可以显著提高日志实践的效率和清晰度,确保代码具有更好的可维护性和可读性。




你好,我是俞凡,在 Motorola 做过研发,现在在 Mavenir 做技术工作,对通信、网络、后端架构、云原生、DevOps、CICD、区块链、AI 等技术始终保持着浓厚的兴趣,平时喜欢阅读、思考,相信持续学习、终身成长,欢迎一起交流学习。为了方便大家以后能第一时间看到文章,请朋友们关注公众号"DeepNoMind",并设个星标吧,如果能一键三连(转发、点赞、在看),则能给我带来更多的支持和动力,激励我持续写下去,和大家共同成长进步!

发布于: 刚刚阅读数: 4
用户头像

俞凡

关注

公众号:DeepNoMind 2017-10-18 加入

俞凡,Mavenir Systems研发总监,关注高可用架构、高性能服务、5G、人工智能、区块链、DevOps、Agile等。公众号:DeepNoMind

评论

发布
暂无评论
探索 Go slog 标准库:设计与应用_golang_俞凡_InfoQ写作社区