上一篇文章学习了 go 标准库的 log
包和 log/syslog
包,log
包提供了简单的日志记录功能,而 log/syslog
则提供了将日志记录到系统 syslog 服务的能力。这篇文章将继续学习标准库的 log/slog
库,log/slog
库提供了结构化日志功能,它所记录的日志包含了消息、日志级别以及通过键值对所表示的各种属性。
属性 首先通过如下示例直观地感受下什么叫做结构化日志:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 package mainimport ( "log/slog" "os" ) func main () { tl := slog.New(slog.NewTextHandler(os.Stderr, nil )) gl := slog.New(slog.NewJSONHandler(os.Stderr, nil )) tl.Info("request done" , slog.Int("id" , 100 ), slog.String("os" , "Windows" )) gl.Info("request done" , slog.Int("id" , 100 ), slog.String("os" , "Windows" )) }
1 2 3 time=2024-12-15T17:17:05.096+08:00 level=INFO msg="request done" id =100 os=Windows {"time" :"2024-12-15T17:17:05.09655238+08:00" ,"level" :"INFO" ,"msg" :"request done" ,"id" :100,"os" :"Windows" }
在该示例中,创建一个了 TextHandler
和 JSONHandler
的 Logger
对象,分别用它们输出两条相同内容的日志:
对于 TextHandler
,它以 key=value
的形式输出日志中的各个属性,这些属性包括 time
、level
、msg
、id
、os
对于 JSONHandler
,它以 JSON 格式输出日志内容,各个属性就是 JSON 对象中的 键值对
可以看到,结构化的日志的关键就是认为日志是由一系列的 属性
构成的,而属性就是通过 键值对
表示的。键永远都是字符串类型,而值可以是多种数据类型。
日志中的 属性
可以是 slog 内建的属性,也可以是用户自定义的属性 。如下就是 slog
所有内建属性的 key:
1 2 3 4 5 6 7 const ( TimeKey = "time" LevelKey = "level" MessageKey = "msg" SourceKey = "source" )
这就是为什么上面示例的日志输出中包含 time
、level
、msg
等属性,只要创建的 Logger
对象开启了这些内建属性对应的开关,那么输出的日志内容里会自动包含相应的内建属性。
Attr 类型 结构化日志的核心就是 属性
,一条日志其实就是由任意个属性组成的。上面的实例中,我们通过 slog.Int()
、slog.String()
等函数为日志内容添加自定义属性。slog.Attr
类型用来表示日志属性,它的定义如下:
1 2 3 4 5 type Attr struct { Key string Value Value }
从 Attr
的定义也可以看出,属性的确就是 键值对
,其中 Key
代表属性的名称,它总是字符串类型,而 Value
则代表属性的值,它的类型为 Value
。
Value 类型 Value
用来表示属性中的值,该类型定义如下:
1 2 3 4 5 type Value struct { _ [0 ]func () num uint64 any any }
Value
可以表示任何 Go 类型的值,但是不同于直接使用 any
类型,Value
类型会尽量尝试使用其 num
字段来保存某些预定义类型的值(也称为 small value
),从而避免内存分配。只有当的确无法使用 num
来保存该类型的值时,才会直接使用 any
接口来保存该类型的值。使用 any
接口类型的确可以保存任意类型的值,但是这种赋值会触发内存申请来保存值的副本(即接口的装箱操作 )。
slog
库提供了以下函数来创建 Value
:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 func AnyValue (v any) Valuefunc BoolValue (v bool ) Valuefunc DurationValue (v time.Duration) Valuefunc Float64Value (v float64 ) Valuefunc Int64Value (v int64 ) Valuefunc IntValue (v int ) Valuefunc StringValue (value string ) Valuefunc TimeValue (v time.Time) Valuefunc Uint64Value (v uint64 ) Valuefunc GroupValue (as ...Attr) Value
这些函数中最特殊的就是 GroupValue
,它允许将一系列 Attr([]Attr
)直接打包成一个 Group Value
。另外,slog
库也提供了对应的方法来获取 Value
所表示的值:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 func (v Value) Any() anyfunc (v Value) Bool() bool func (v Value) Duration() time.Durationfunc (v Value) Float64() float64 func (v Value) Int64() int64 func (v Value) String() string func (v Value) Time() time.Timefunc (v Value) Uint64() uint64 func (v Value) Group() []Attr
另外,slog
库提供了一个接口类型 LogValuer
,任何类型都可以通过实现该接口类型来定义如何将自身转换为 Value
:
1 2 3 type LogValuer interface { LogValue() Value }
Value 也提供了如下方法来操作 LogValuer
类型的 Value:
1 2 3 4 5 func (v Value) LogValuer() LogValuerfunc (v Value) Resolve() (rv Value)
Value.Kind()
方法可以返回 Value 的种类:
1 func (v Value) Kind() Kind
下面的实例展示了 value
的用法:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 package mainimport ( "fmt" "log/slog" ) type Token string func (Token) LogValue() slog.Value { return slog.StringValue("******" ) } type A struct { a int } func main () { v1 := slog.Int64Value(100 ) v2 := slog.AnyValue(Token("secret" )) v3 := slog.AnyValue(A{}) fmt.Println(v1.Kind(), v1.Int64()) fmt.Println(v2.Kind(), v2.Resolve().String()) fmt.Println(v3.Kind(), v3.Any().(A)) }
Attr 的创建 在介绍完 Value
类型后,我们再来看 Attr
的创建。slog
库提供了一系列工具函数来简化 Attr 的创建。我们首先看 Any
函数的实现,它可以为任意类型的值生成 Attr
:
1 2 3 4 func Any (key string , value any) Attr { return Attr{key, AnyValue(value)} }
了解了 Any
函数的实现后,其他 Attr 创建函数都是类似的,这里简单列举如下:
1 2 3 4 5 6 7 8 9 func Bool (key string , v bool ) Attrfunc Duration (key string , v time.Duration) Attrfunc Float64 (key string , v float64 ) Attrfunc Int (key string , value int ) Attrfunc Int64 (key string , value int64 ) Attrfunc String (key, value string ) Attrfunc Time (key string , v time.Time) Attrfunc Uint64 (key string , v uint64 ) Attrfunc Group (key string , args ...any) Attr
Group Attr 上面的 Attr
创建函数中有一个特别的 Group
函数,它用于创建一个 Group Attr
,即 key 是字符串,但是 value 是一组属性(即上文所说的 Group Value
)。该函数的第一个参数就是 Group Attr
的 key,而之后的 args 参数会按照每两个一组的方式来创建一个 Attr,所有的 args 就可以转换为一组 Attr,从而创建 Group Attr
出的 Value。
Group Attr
可以将一组属性进行分组,而 Group Attr
的 key 则会限定这组属性的名称,至于这个 限定
是如何显示的,则取决于所使用的 Handler:
对于 TextHandler
,会以 GroupKey.AttrKey=AttrValue
的方式显示 Group 中的某个属性
对于 JSONHandler
,则会将 Group 作为单独一个 JSON 对象,Group Attr
的 Key 就是该 JSON 对象的 Key,例如 "GroupKey": { "AttrKey": AttrValue }
如下是一个示例:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 package mainimport ( "log/slog" "os" ) func main () { tl := slog.New(slog.NewTextHandler(os.Stderr, nil )) gl := slog.New(slog.NewJSONHandler(os.Stderr, nil )) tl.Info("request done" , slog.Group("request" , "id" , 100 , "os" , "Windows" )) gl.Info("request done" , slog.Group("request" , "id" , 100 , "os" , "Windows" )) }
记录日志 为了使用 slog
库记录日志,首先需要有一个 slog.Logger
类型的对象。slog.Logger
类型定义如下:
1 2 3 type Logger struct { handler Handler }
可以看到,slog.Logger
类型只包含一个 Handler
类型的成员 handler
,它决定了处理每一条日志记录的具体行为。通过如下函数来基于某个 Handler
创建 slog.Logger
对象:
1 func New (h Handler) *Logger
Handler Handler
是一个接口类型,它的定义如下
1 2 3 4 5 6 7 8 9 10 11 12 13 type Handler interface { Enabled(context.Context, Level) bool Handle(context.Context, Record) error WithAttrs(attrs []Attr) Handler WithGroup(name string ) Handler }
Handler
的 Handle
方法用来真正处理日志,日志记录通过 Record
类型来表示。虽然 slog
提供了多种参数形式的 API 来处理日志,但是其内部最终还是使用 Record 类型来表示 一条日志
。它的定义如下:
1 2 3 4 5 6 7 8 9 10 type Record struct { Time time.Time Message string Level Level PC uintptr }
slog
包内置了两种 Handler
的实现,即 TextHandler
和 JSONHandler
,分别以普通文本格式和 JSON 格式来输出日志。它们的定义如下:
1 2 3 4 5 6 7 type TextHandler struct { *commonHandler } type JSONHandler struct { *commonHandler }
可以看到,这两种类型都是通过内嵌 *commonHandler
来实现的,因为 TextHandler
、JSONHandler
只是最终的日志输出形式不一样,其对结构化日志 Attr
、Group
等特性的处理都是类似的,因此 slog
包通过提供 commonHandler
类型,用来实现结构化日志的核心逻辑:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 type commonHandler struct { json bool opts HandlerOptions preformattedAttrs []byte groupPrefix string groups []string nOpenGroups int mu *sync.Mutex w io.Writer }
TextHandler
和 JSONHandler
的创建函数如下:
1 2 func NewTextHandler (w io.Writer, opts *HandlerOptions) *TextHandlerfunc NewJSONHandler (w io.Writer, opts *HandlerOptions) *JSONHandler
这两个函数都支持 HandlerOptions
参数,可以控制 Handler
的某些行为。如果 opts 参数为空,会使用 HandlerOptions
的默认值:
1 2 3 4 5 6 7 8 9 type HandlerOptions struct { AddSource bool Level Leveler ReplaceAttr func (groups []string , a Attr) Attr }
HandlerOptions
的 Level
字段决定了该 Handler 所处理的最小日志级别,它的类型是一个接口类型:
1 2 3 type Leveler interface { Level() Level }
处理每一条日志时都会调用 Leveler.Level()
方法,将结果与当前日志记录的级别进行判断,从而决定当前日志是否需要处理,这样就为日志输出提供了动态控制能力。
WithAttrs/WithGroup 有时候,某些 Attr、Group 对所有日志都是通用的。对于这些公共 Attr、Group,如果每次记录日志时都显式添加/设置的话,会显得非常冗余,因此 Handler 定义了如下两个方法,支持直接在 Handler 级别上添加公共 Attr、打开 Group:
1 2 3 4 5 WithAttrs(attrs []Attr) Handler WithGroup(name string ) Handler
如下是一个简单示例,可以帮助理解这两个函数的行为:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 package mainimport ( "log/slog" "os" ) func main () { tl := slog.New(slog.NewTextHandler(os.Stdout, nil )) tl.Info("text log" , "id" , 100 ) tl.Info("text log" , "id" , 200 ) tl1 := slog.New( tl.Handler().WithAttrs([]slog.Attr{ slog.String("os" , "linux" ), }), ) tl1.Info("text log" , "id" , 100 ) tl1.Info("text log" , "id" , 200 ) tl2 := slog.New( tl.Handler().WithGroup("request" ), ) tl2.Info("text log" , "id" , 100 ) tl2.Info("text log" , "id" , 200 ) tl3 := slog.New( tl.Handler(). WithAttrs([]slog.Attr{slog.String("os" , "linux" )}). WithGroup("request" ), ) tl3.Info("text log" , "id" , 100 ) tl3.Info("text log" , "id" , 200 ) tl4 := slog.New( tl.Handler(). WithGroup("request" ). WithAttrs([]slog.Attr{slog.String("os" , "linux" )}), ) tl4.Info("text log" , "id" , 100 ) tl4.Info("text log" , "id" , 200 ) tl4.Info("text log" , slog.Group("subrequest" , "subid" , "100" )) }
WithGroup
函数会在 Handler 上打开一个 Group,打开的 Group 会对后续的 Attr/Group 生效 。后续的 Attr/Group 既可以是在 Handler
上添加的 Attr/Group,也可以是来自日志记录本身的 Attr/Group。
无论是在 Handler
上添加 Attr/Group
,还是后面处理日志本身的 Attr/Group
,slog
库都是借助 handleState
来实现日志内容的输出控制的。理解了 handleState
的相关逻辑,就理解了 slog
库的核心原理:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 type handleState struct { h *commonHandler buf *buffer.Buffer freeBuf bool sep string prefix *buffer.Buffer groups *[]string }
(*commonHandler).withAttrs()
以及 (*commonHandler).handle()
方法都会借助 handleState
来实现日志输出内容的控制,只不过 withAttrs
是在 Handler 级别上控制日志的输出,而 handle
则是在 日志记录
级别上控制日志的输出。
1 2 3 4 5 6 7 func (h *commonHandler) withAttrs(as []Attr) *commonHandlerfunc (h *commonHandler) handle(r Record) error
slog.Logger
的 API上面的分析就是 slog
库的底层实现原理,接下来快速列举一下 slog.Logger
所提供的 API,通过这些 API,来实现日志的记录功能:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 func New (h Handler) *Loggerfunc (l *Logger) Handler() Handlerfunc (l *Logger) With(args ...any) *Loggerfunc (l *Logger) WithGroup(name string ) *Loggerfunc (l *Logger) Enabled(ctx context.Context, level Level) bool func (l *Logger) Log(ctx context.Context, level Level, msg string , args ...any)func (l *Logger) LogAttrs(ctx context.Context, level Level, msg string , attrs ...Attr) {func (l *Logger) Debug(msg string , args ...any)func (l *Logger) DebugContext(ctx context.Context, msg string , args ...any)func (l *Logger) Info(msg string , args ...any)func (l *Logger) InfoContext(ctx context.Context, msg string , args ...any)func (l *Logger) Warn(msg string , args ...any)func (l *Logger) WarnContext(ctx context.Context, msg string , args ...any)func (l *Logger) Error(msg string , args ...any)func (l *Logger) ErrorContext(ctx context.Context, msg string , args ...any)
这些日志记录函数都提供了 XXXContext
版本,方便 Handler 从 context.Context
中获取信息。但是内置的 TextHanler
和 JSONHandler
本身都忽略了 ctx 参数,所以如果要用上该参数,可能需要自己重新实现 Handler(可以基于已有的 TextHandler
或者 JSONHandler
实现):
1 2 3 4 5 6 7 func (h *TextHandler) Handle(_ context.Context, r Record) error { return h.commonHandler.handle(r) } func (h *JSONHandler) Handle(_ context.Context, r Record) error { return h.commonHandler.handle(r) }
默认 Logger 对象 同样 slog 库也提供了一个预定义的、默认的 Logger
对象,slog
库的 Info、Error 等顶级函数就是调用这个预定义 Logger 对象的相应方法来实现的。这样就简化了 slog 库的使用,省去了手动创建 Logger
对象的步骤。
这个预定义的 logger 对象是在 slog 库的 init
函数创建的:
1 2 3 4 5 6 7 8 9 10 11 12 13 var defaultLogger atomic.Pointer[Logger]func init () { defaultLogger.Store(New(newDefaultHandler(loginternal.DefaultOutput))) } func newDefaultHandler (output func (uintptr , []byte ) error ) *defaultHandler { return &defaultHandler{ ch: &commonHandler{json: false }, output: output, } }
默认的 logger 对象是基于 defaultHandler
来实现的,它的核心还是基于 commonHandler
来实现的日志 Attr/Group
的处理,唯一需要注意的是它的 output
函数是调用 log
库的 std.output
函数来实现的,即调用的是 log 库默认 logger 对象的输出函数:
1 2 3 4 5 6 7 8 9 10 11 func init () { internal.DefaultOutput = func (pc uintptr , data []byte ) error { return std.output(pc, 0 , func (buf []byte ) []byte { return append (buf, data...) }) } }
因此,虽然 slog
的 defaultLogger
是基于 commonHandler
(非 json 格式)来实现的,但是其输出格式还是与 TextHandler
有所差异:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 package mainimport ( "log/slog" "os" ) func main () { tl := slog.New(slog.NewTextHandler(os.Stderr, nil )) tl.Info("log message" , "id" , 100 ) slog.Info("log message" , "id" , 100 ) }
slog 库也提供了一些与默认 logger 对象相关的 API:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 func Default () *Loggerfunc SetDefault (l *Logger) func Log (ctx context.Context, level Level, msg string , args ...any) func LogAttrs (ctx context.Context, level Level, msg string , attrs ...Attr) func Debug (msg string , args ...any) func DebugContext (ctx context.Context, msg string , args ...any) func Info (msg string , args ...any) func InfoContext (ctx context.Context, msg string , args ...any) func Warn (msg string , args ...any) func WarnContext (ctx context.Context, msg string , args ...any) func Error (msg string , args ...any) func ErrorContext (ctx context.Context, msg string , args ...any)
slog 库桥接到 log 库 如果我们的应用程序代码已经是基于 log
库的 API 来输出日志了,那么怎么能让其使用上 slog 库提供的结构化日志能力呢?slog 库提供了 NewLogLogger
函数来实现这个需求:
1 func NewLogLogger (h Handler, level Level) *log.Logger
NewLogLogger
函数内部实现是通过将用户提供的 Handler 封装为一个 handlerWriter
来实现的,handlerWriter
实现了 io.Writer
接口,这样 log 库的 logger 对象在输出日志时,就会调用 handlerWriter
的 Write 方法来输出日志,而 handlerWriter
内部则继续调用 Handler
的 Handle 方法来处理该日志:
1 2 3 4 5 6 7 8 9 func NewLogLogger (h Handler, level Level) *log.Logger { return log.New(&handlerWriter{h, level, true }, "" , 0 ) } type handlerWriter struct { h Handler level Leveler capturePC bool }
如下是一个简单的示例
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 package mainimport ( "log" "log/slog" "os" ) func main () { log.Println("hello" ) ll := slog.NewLogLogger(slog.NewTextHandler(os.Stdout, nil ), slog.LevelInfo) ll.Println("hello" ) }
小结 这篇文章详细介绍了 slog 库的使用方法和基本原理,重点介绍了其 Attr、Handler 等核心数据类型,方便我们理解 slog 库的核心设计思路。slog 库提供了结构化日志的核心抽象(Attr/Group),内置了 TextHandler 和 JSONHandler 两种日志处理器,同时也允许我们通过自定义 Handler 来扩展 slog 的能力。