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

2023-10-16 00:25:19 浏览数 (2)

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

前言

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

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

slog 包

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

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

  • 结构化日志
代码语言:go复制
// 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() 等输出不同级别日志的函数。

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

代码语言:txt复制
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 的形式表示,并且它们之间用空格分隔。

代码语言:go复制
// 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 实例,通过该实例可以执行日志输出的相关操作。

程序运行结果如下所示:

代码语言:txt复制
time=2023-10-08T21:09:03.912 08:00 level=INFO msg=TextHandler 姓名=陈明勇

JsonHandler 处理器

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

代码语言:go复制
// 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 实例,通过该实例可以执行日志输出的相关操作。

程序运行结果如下所示:

代码语言:txt复制
{"time":"2023-10-08T21:09:22.614686104 08:00","level":"INFO","msg":"JsonHandler","姓名":"陈明勇"}

全局的 Logger 实例

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

代码语言:go复制
logger := slog.Default()

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

代码语言:go复制
// 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 分组

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

代码语言:go复制
// 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))  
}

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

代码语言:txt复制
{"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 类型的去输出日志会更 高效,因为减少了类型解析的过程。

代码语言:go复制
// 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 设置统一的属性

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

代码语言:go复制
// 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 实例,后面通过新实例输出的日志都会包含 被添加的固定属性,从而 避免 每条输出的日志语句添加 相同 的键值对。

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

代码语言:txt复制
{"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,通过它,我们可以配置是否显示日志语句的源代码位置信息、最低的日志输出级别以及键值对属性的重写操作。

代码语言:go复制
// 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" 的形式。

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

代码语言:txt复制
{"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 包还支持使用另一种方式进行修改。

代码语言:go复制
// 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 方法的返回值所覆盖。

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

代码语言:txt复制
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

参考资料

  • log/slog: https://pkg.go.dev/log/slog

我正在参与2023腾讯技术创作特训营第二期有奖征文,瓜分万元奖池和键盘手表

0 人点赞