Go slog 包:开启结构化日志的奇妙之旅

简介: 本文对 go 语言里的 slog 包进行了详细介绍,包括基本的使用、Logger 实例的创建和高效输出日志以及自定义日志信息等内容。

本文中涉及到的相关代码,都已上传至:https://github.com/chenmingyong0423/blog/tree/master/tutorial-code/slog

前言

go 1.21.0 版本引入了一个新的包 log/slog,该包提供了结构化日志的功能。相比于普通的日志,结构化日志更受欢迎,因为它具有更高的可读性,并且在处理、分析和搜索等方面具有显著的优势。

接下来让我们深入探讨 log/slog 包的使用,准备好了吗?准备一杯你最喜欢的咖啡或茶,随着本文一探究竟吧。

slog 包

slog 包提供了结构化日志,其中的日志记录包含了 消息严重级别 以及 各种其他属性,这些属性以 键值对 的形式表示。

slog 包的主要功能如下所示:

  • 结构化日志
  • 日志严重级别
  • 日志自定义处理
  • 日志分组

    初体验

// github.com/chenmingyong0423/blog/blob/master/tutorial-code/slog/demo1/main.go
package main

import (
    "context"
    "log/slog"
)

func main() {
   
    slog.Info("slog msg", "greeting", "hello slog")
    // 携带 context 上下文
    slog.InfoContext(context.Background(), "slog msg with context", "greeting", "hello slog")
}

在上述示例中,我们直接通过调用包函数 slog.Info 去输出一条 info 等级的日志。在该函数内部,会使用默认提供的一个 Logger 实例去执行日志输出的操作。除此之外,我们还能使用 slog.InfoContext 携带上下文进行日志输出。

除了 Info()InfoContext() 函数,还有 Debug()Warn()Error() 等输出不同级别日志的函数。

运行上面这段程序,会得到以下输出:

2023/10/08 21:08:08 INFO slog msg greeting="hello slog"
2023/10/08 21:08:08 INFO slog msg with context greeting="hello slog"

Logger 的创建

默认情况下,使用 slog 包函数输出日志,仅仅是普通的文本格式,若想实现 JSON 或者 key=value 的格式输出,需要使用 slog.New() 函数创建 Logger 实例,使用该函数时需要传入一个 slog.Handler 的实现,slog 包提供两个实现:TextHandlerJsonHandler

TextHandler 处理器

TextHandler 是一个日志记录处理器,它将记录以一系列键值对的形式写入到一个 io.Writer 中。每个键值对都以 key=value 的形式表示,并且它们之间用空格分隔。

// github.com/chenmingyong0423/blog/blob/master/tutorial-code/slog/demo2/main.go
package main  

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

func main() {
     
    textLogger := slog.New(slog.NewTextHandler(os.Stdout, nil))  
    textLogger.InfoContext(context.Background(), "TextHandler", "姓名", "陈明勇")  
}

在上述示例中,我们通过 slog.NewTextHandler 函数创建一个日志处理器,第一个参数 os.Stdout 表示将日志输出到控制台,然后将处理器作为参数,传递到 slog.New 函数里创建一个 Logger 实例,通过该实例可以执行日志输出的相关操作。

程序运行结果如下所示:

time=2023-10-08T21:09:03.912+08:00 level=INFO msg=TextHandler 姓名=陈明勇

JsonHandler 处理器

JsonHandler 是一个日志处理器, 它将记录以 json 的形式写入到一个 io.Writer 中。

// github.com/chenmingyong0423/blog/blob/master/tutorial-code/slog/demo3/main.go
package main

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

func main() {
   
    jsonLogger := slog.New(slog.NewJSONHandler(os.Stdout, nil))
    jsonLogger.InfoContext(context.Background(), "JsonHandler", "姓名", "陈明勇")
}

在上述示例中,我们通过 slog.NewJsonHandler 函数创建一个 json 日志处理器,第一个参数 os.Stdout 表示将日志输出到控制台,然后将处理器作为参数传递到 slog.New 函数里创建一个 Logger 实例,通过该实例可以执行日志输出的相关操作。

程序运行结果如下所示:

{"time":"2023-10-08T21:09:22.614686104+08:00","level":"INFO","msg":"JsonHandler","姓名":"陈明勇"}

全局的 Logger 实例

slog 有一个默认的 Logger 实例,如果我们想要获取默认的 Logger 实例,可以参考以下代码:

logger := slog.Default()

在前面的示例中,我们一直使用创建的一个 Logger 实例去输出日志。然而,如果我们不想每次都需要通过特定的 Logger 实例来记录日志,而是希望能够全局操作,我们可以使用 slog.SetDefault 函数来设置并替换默认的 Logger 实例。这种方式可以使日志记录更加方便和灵活。

// github.com/chenmingyong0423/blog/blob/master/tutorial-code/slog/demo4/main.go
package main

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

func main() {
   
    jsonLogger := slog.New(slog.NewJSONHandler(os.Stdout, nil))
    slog.SetDefault(jsonLogger)
    slog.InfoContext(context.Background(), "JsonHandler", "姓名", "陈明勇") //{"time":"2023-10-08T21:11:22.41760604+08:00","level":"INFO","msg":"JsonHandler","姓名":"陈明勇"}
}

Group 分组

分组指的给日志记录相关联的属性(键值对)进行分组,通过示例感受一下:

// github.com/chenmingyong0423/blog/blob/master/tutorial-code/slog/demo5/main.go
package main  

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

func main() {
     
    jsonLogger := slog.New(slog.NewJSONHandler(os.Stdout, nil)).WithGroup("information")  
    jsonLogger.InfoContext(context.Background(), "json-log", slog.String("name", "chenmingyong"), slog.Int("phone", 1234567890))  

    textLogger := slog.New(slog.NewTextHandler(os.Stdout, nil)).WithGroup("information")  
    textLogger.InfoContext(context.Background(), "json-log", slog.String("name", "chenmingyong"), slog.Int("phone", 1234567890))  
}

运行这段程序的结果如下所示:

{"time":"2023-10-08T21:12:23.124255258+08:00","level":"INFO","msg":"json-log","information":{"name":"chenmingyong","phone":1234567890}}
time=2023-10-08T21:12:23.127+08:00 level=INFO msg=json-log information.name=chenmingyong information.phone=1234567890

根据运行结果可知,如果是对具有 JsonHandler 处理器的 Logger 实例进行分组操作,输出日志时,组名 group name 将作为 keyvalue 则是所有键值对组成的一个 json 对象。

如果是对具有 TextHandler 处理器的 Logger 实例进行分组操作,组名 group name 将与所有键值对的键进行组合,最终以 groupName.key=value 的形式展示。

LogAttrs 高效输出日志

如果需要频繁输出日志,相比之前的例子,我们使用 slog.LogAttrs 函数结合 slog.Attr 类型的去输出日志会更 高效,因为减少了类型解析的过程。

// github.com/chenmingyong0423/blog/blob/master/tutorial-code/slog/demo6/main.go
package main

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

func main() {
   
    jsonLogger := slog.New(slog.NewJSONHandler(os.Stdout, nil))
    jsonLogger.LogAttrs(context.Background(), slog.LevelInfo, "高效输出日志", slog.String("姓名", "陈明勇"), slog.Int("联系方式", 12345678901))
}

在上面的示例中,我们使用了 LogAttrs 方法去输出一条日志,该方法的签名为:func (l *Logger) LogAttrs(ctx context.Context, level Level, msg string, attrs ...Attr)

结合方法签名我们可以知道,第一个参数为 context.Context 上下文类型,第二个参数为 Level 类型,即 slog 包里面的日志严重级别类型,第三个参数为 Attr 键值对类型。

在使用其他方法如 Info 输出日志时,内部会将键值对转成 Attr 类型,而使用 LogAttrs 方法,我们直接指定了 Attr 类型,减少了转换的过程,因此会更 高效

With 设置统一的属性

如果每条日志都包含相同的一个键值对,我们可以考虑设置一个统一的属性。

// github.com/chenmingyong0423/blog/blob/master/tutorial-code/slog/demo7/main.go
package main  

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

func main() {
     
    jsonLogger := slog.New(slog.NewJSONHandler(os.Stdout, nil))  
    logger := jsonLogger.With("systemID", "s1")  
    logger.LogAttrs(context.Background(), slog.LevelInfo, "json-log", slog.String("k1", "v1"))  
    logger.LogAttrs(context.Background(), slog.LevelInfo, "json-log", slog.String("k2", "v2"))
}

我们可以使用 With 方法添加一个或多个固定属性,并返回一个新的 Logger 实例,后面通过新实例输出的日志都会包含 被添加的固定属性,从而 避免 每条输出的日志语句添加 相同 的键值对。

运行这段程序的结果如下所示:

{"time":"2023-10-08T21:19:51.338328238+08:00","level":"INFO","msg":"json-log","systemID":"s1","k1":"v1"}
{"time":"2023-10-08T21:19:51.338604943+08:00","level":"INFO","msg":"json-log","systemID":"s1","k2":"v2"}

HandlerOptions 日志处理器的配置选项

细心的读者也许能发现,在前面的示例中,无论是 NewJSONHandler,还是 NewTextHandler,第二个参数都被设置为 nil,这是为了使用默认的配置。

这个参数的类型为 *HandlerOptions,通过它,我们可以配置是否显示日志语句的源代码位置信息、最低的日志输出级别以及键值对属性的重写操作。

// github.com/chenmingyong0423/blog/blob/master/tutorial-code/slog/demo8/main.go
package main  

import (
    "context"  
    "log/slog"  
    "os"  
    "time"  
)

func main() {
     
    jsonLogger := slog.New(slog.NewJSONHandler(os.Stdout, &slog.HandlerOptions{
     
        AddSource: true,  
        Level: slog.LevelError,  
        ReplaceAttr: func(groups []string, a slog.Attr) slog.Attr {
     
            if a.Key == slog.TimeKey {
     
                if t, ok := a.Value.Any().(time.Time); ok {
     
                    a.Value = slog.StringValue(t.Format(time.DateTime))  
                }  
            }  
            return a  
        },  
    }))  
    jsonLogger.InfoContext(context.Background(), "json-log", slog.String("姓名", "陈明勇")) 
    jsonLogger.ErrorContext(context.Background(), "json-log", slog.String("姓名", "陈明勇")) 
}

在上述示例中,我们创建了一个具有 JsonHanlder 处理器的 Logger 实例。在创建 JsonHanlder 时,通过 HandlerOptions 参数指定了以下配置:

  • 输出日志语句的源代码配置 Source 信息
  • 设置最低日志等级为 Error
  • key"time" 的属性值的格式重写为 "2006-01-02 15:04:05" 的形式。

运行这段程序得到的结果如下所示:

{"time":"2023-10-08 21:21:31","level":"ERROR","source":{"function":"main.main","file":"D:/goproject/src/gocode/play/main.go","line":24},"msg":"json-log"
,"姓名":"陈明勇"}

输出结果与预期相同,INFO 等级的日志没有被输出、输出 Source 信息以及重写 key"time" 的属性值。

自定义 key-value 对中 value 的值

在前面的一个案例中,我们有通过 HandlerOptions 配置来修改 key-value 对中 value 的值,除此之外,slog 包还支持使用另一种方式进行修改。

// github.com/chenmingyong0423/blog/blob/master/tutorial-code/slog/demo9/main.go
package main

import (
    "context"
    "log/slog"
)

type Password string

func (Password) LogValue() slog.Value {
   
    return slog.StringValue("REDACTED_PASSWORD")
}

func main() {
   
    slog.LogAttrs(context.Background(), slog.LevelInfo, "敏感数据", slog.Any("password", Password("1234567890")))
}

在上述案例中,我们通过实现 slog.LogValuer 接口(为某个类型添加 LogValue() slog.Value 方法),将 key-value 对中 value 的值进行重写。日志输出时,value 的值将会被 LogValue 方法的返回值所覆盖。

运行这段程序输出的结果如下所示:

2023/10/08 21:37:11 INFO 敏感数据 password=REDACTED_PASSWORD

输出结果与预期结果相同,passwordvalue 已经被修改。

小结

本文对 go 语言里的 slog 包进行了详细介绍,包括基本的使用、Logger 实例的创建和高效输出日志以及自定义日志信息等内容。

阅读完本文,相信你对 slog 包有更深入地理解,可以更好地使用它来管理和记录日志。

如果你还有其他高级用法,欢迎评论区留言探讨!

本文中涉及到的相关代码,都已上传至:https://github.com/chenmingyong0423/blog/tree/master/tutorial-code/slog

参考资料

作者:陈明勇

个人网站:https://chenmingyong.cn

文章持续更新,如果本文能让您有所收获,欢迎点赞收藏加关注本号。

微信阅读可搜《Go技术干货》。这篇文章已被收录于 GitHub https://github.com/chenmingyong0423/blog ,欢迎大家 Star 催更并持续关注。

相关实践学习
日志服务之使用Nginx模式采集日志
本文介绍如何通过日志服务控制台创建Nginx模式的Logtail配置快速采集Nginx日志并进行多维度分析。
目录
相关文章
|
2月前
|
Java 开发工具 Windows
Windows环境下面启动jar包,输出的日志出现乱码的解决办法
Windows环境下面启动jar包,输出的日志出现乱码的解决办法
|
7天前
|
安全 测试技术 Go
Golang深入浅出之-Go语言单元测试与基准测试:testing包详解
【4月更文挑战第27天】Go语言的`testing`包是单元测试和基准测试的核心,简化了测试流程并鼓励编写高质量测试代码。本文介绍了测试文件命名规范、常用断言方法,以及如何进行基准测试。同时,讨论了测试中常见的问题,如状态干扰、并发同步、依赖外部服务和测试覆盖率低,并提出了相应的避免策略,包括使用`t.Cleanup`、`t.Parallel()`、模拟对象和检查覆盖率。良好的测试实践能提升代码质量和项目稳定性。
12 1
|
7天前
|
运维 监控 Go
Golang深入浅出之-Go语言中的日志记录:log与logrus库
【4月更文挑战第27天】本文比较了Go语言中标准库`log`与第三方库`logrus`的日志功能。`log`简单但不支持日志级别配置和多样化格式,而`logrus`提供更丰富的功能,如日志级别控制、自定义格式和钩子。文章指出了使用`logrus`时可能遇到的问题,如全局logger滥用、日志级别设置不当和过度依赖字段,并给出了避免错误的建议,强调理解日志级别、合理利用结构化日志、模块化日志管理和定期审查日志配置的重要性。通过这些实践,开发者能提高应用监控和故障排查能力。
81 1
|
7天前
|
安全 Go
Golang深入浅出之-Go语言标准库中的文件读写:io/ioutil包
【4月更文挑战第27天】Go语言的`io/ioutil`包提供简单文件读写,适合小文件操作。本文聚焦`ReadFile`和`WriteFile`函数,讨论错误处理、文件权限、大文件处理和编码问题。避免错误的关键在于检查错误、设置合适权限、采用流式读写及处理编码。遵循这些最佳实践能提升代码稳定性。
13 0
|
7天前
|
Prometheus 监控 Cloud Native
【Go语言专栏】Go语言中的日志记录与监控
【4月更文挑战第30天】Go语言在软件开发和运维中扮演重要角色,提供灵活的日志记录机制和与多种监控工具的集成。内置`log`包支持基本日志记录,而第三方库如`zap`、`zerolog`和`logrus`则扩展了更多功能。监控方面,Go应用可与Prometheus、Grafana、Jaeger等工具配合,实现系统指标收集、可视化和分布式追踪。健康检查通过HTTP端点确保服务可用性。结合日志和监控,能有效提升Go应用的稳定性和性能。
|
7天前
|
缓存 编译器 Go
【Go语言专栏】理解Go语言的包管理机制
【4月更文挑战第30天】Go语言包管理是构建可维护应用的关键,从基本概念如包导入、初始化到版本管理和依赖管理,再到Go Modules的引入,简化了过程。包的可见性规则和社区生态也至关重要。理解并掌握这些机制对于编写高质量Go代码具有决定性影响。随着Go语言的持续发展,包管理将更加强大易用。
|
8天前
|
安全 Unix Go
Golang深入浅出之-Go语言中的时间与日期处理:time包详解
【4月更文挑战第26天】Go语言的`time`包提供处理日期和时间的功能,包括`time.Time`类型、时间戳、格式化与解析。本文讨论了核心概念、常见问题(如时区处理、格式字符串混淆、超时控制和并发安全)及解决方法。推荐使用`time.LoadLocation`管理时区,熟悉时间格式规则,用`context`精确控制超时,并注意并发安全。文中通过代码示例展示了如何获取格式化时间、计算时间差以及创建定时任务。学习和应用这些知识可提高程序的健壮性和准确性。
25 2
|
8天前
|
XML JSON Go
Golang深入浅出之-XML处理在Go语言中的实现:encoding/xml包
【4月更文挑战第26天】Go语言的`encoding/xml`库提供XML处理,包括序列化和反序列化。本文讨论了XML处理的基础,如`xml.Marshal`和`xml.Unmarshal`函数,以及常见问题和易错点,如标签命名、结构体嵌套、omitempty标签和命名空间。建议遵循标签命名规则,正确处理嵌套和属性,谨慎使用omitempty,以及理解并有效利用命名空间。文中还给出了基础示例和处理XML属性的代码示例,帮助读者掌握XML处理技巧。
20 1
Golang深入浅出之-XML处理在Go语言中的实现:encoding/xml包
|
9天前
|
数据管理 Go 开发者
Golang深入浅出之-Go语言上下文(context)包:处理取消与超时
【4月更文挑战第25天】Go语言中的`context`包在并发、网络请求和长任务中至关重要,提供取消、截止时间和元数据管理。本文探讨`context`基础,如`Background()`、`TODO()`、`WithCancel()`、`WithDeadline()`和`WithTimeout()`。常见问题包括不当传递、过度使用`Background()`和`TODO()`以及忽略错误处理。通过取消和超时示例,强调正确传递上下文、处理取消错误和设置超时以提高应用健壮性和响应性。正确使用`context`是构建稳定高效Go应用的关键。
18 1
|
11天前
|
Go 开发者
Golang深入浅出之-Go语言上下文(context)包:处理取消与超时
【4月更文挑战第23天】Go语言的`context`包提供`Context`接口用于处理任务取消、超时和截止日期。通过传递`Context`对象,开发者能轻松实现复杂控制流。本文解析`context`包特性,讨论常见问题和解决方案,并给出代码示例。关键点包括:1) 确保将`Context`传递给所有相关任务;2) 根据需求选择适当的`Context`创建函数;3) 定期检查`Done()`通道以响应取消请求。正确使用`context`包能提升Go程序的控制流管理效率。
20 1