Go 1.21 结构化日志

Go 1.21 在标准库log/slog包中新增了对结构化日志的支持。

日志等级

1
2
3
4
slog.Debug("S")
slog.Info(" L")
slog.Warn(" O")
slog.Error("G")

结构化参数

1
2
3
slog.Info("hello world", "time", time.Now().Unix())

2024/01/03 22:05:04 INFO hello world time=1704290704

改变slog默认logger的格式,使用JSONHandler输出json对象。

1
2
3
4
5
6
logger := slog.New(slog.NewJSONHandler(os.Stdout, nil))
slog.SetDefault(logger)

slog.Info("hello world", "unix_time", time.Now().Unix())

{"time":"2024-01-03T22:11:24.739111+08:00","level":"INFO","msg":"hello world","unix_time":1704291084}

HandlerOptions

HandlerOptions提供了我们可以用来修改内置TextHandlerJSONHandlermain hooks

设置等级

1
2
3
opts := &slog.HandlerOptions{
	Level: slog.LevelWarn,
}

改变时间格式

 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
opts := &slog.HandlerOptions{
    // Use the ReplaceAttr function on the handler options
    // to be able to replace any single attribute in the log output
    ReplaceAttr: func(groups []string, a slog.Attr) slog.Attr {
        // check that we are handling the time key
        if a.Key != slog.TimeKey {
            return a
        }

        t := a.Value.Time()

        // change the value from a time.Time to a String
        // where the string has the correct time format.
        a.Value = slog.StringValue(t.Format(time.DateTime))

        return a
    },
}

logger := slog.New(slog.NewJSONHandler(os.Stdout, opts))
slog.SetDefault(logger)

slog.Info("hello world", "foo", 42)

{"time":"2024-01-03 22:27:53","level":"INFO","msg":"hello world","foo":42}

修改key名

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
opts := &slog.HandlerOptions{
    ReplaceAttr: func(groups []string, a slog.Attr) slog.Attr {
    	if a.Key != slog.MessageKey {
    		return a
    	}
    
    	// change the key from "msg" to "message"
    	a.Key = "message"
    
    	return a
    },
}

嵌套日志

使用slog.Group方法。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
textLogger := slog.New(slog.NewTextHandler(os.Stdout, nil))
jsonLogger := slog.New(slog.NewJSONHandler(os.Stdout, nil))

slog.SetDefault(textLogger)

slog.Info("outside the group",
    // The Group method takes the outer key, then kv pairs
    slog.Group("request",
        "method", "GET",
        "url", "https://berylyvos.icu/",
    ),
)

slog.SetDefault(jsonLogger)

slog.Info("outside the group",
    slog.Group("request",
        "method", "GET",
        "url", "https://berylyvos.icu/",
    ),
)

textLogger 输出:

1
time=2024-01-03T22:40:04.665+08:00 level=INFO msg="outside the group" request.method=GET request.url=https://berylyvos.icu/

jsonLogger 输出:

1
{"time":"2024-01-03T22:40:04.665753+08:00","level":"INFO","msg":"outside the group","request":{"method":"GET","url":"https://berylyvos.icu/"}}

携带context

访问context,需要创建自定义handler。

slog.Handler接口

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
type Handler interface {
  // Enabled reports if the specific log event should 
  // be printed for this Handler and the given log level.
  // We can also choose to use info from the Context that
  // we passed in the InfoContext(...) call. If we used
  // Info(...) not InfoContext(...) then the context will 
  // be context.Background().
  Enabled(context.Context, slog.Level) bool

  // Handle takes the log record and prints it. The handle
  // function has access to the context, so we can extract
  // request specific info that we want to include in our
  // log event here.
  Handle(context.Context, Record) error

  // WithAttrs are the key value pairs that we're including in the structured logging
  // e.g. user=5
  WithAttrs(attrs []Attr) Handler

  // WithGroup is a sub-handler, with the group/object name
  WithGroup(name string) Handler
}

定义包含context的自定义JSON 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
// myHandler embeds the anonymous struct member
// *slog.JSONHandler. This gives us method overriding
// where all methods of the handler interface we don't
// implement will be dispatched to the built-in handler
type myHandler struct {
	*slog.JSONHandler
}

type contextKey string

const userIDKey contextKey = "userid"

// Handle passes the userID key and value as an extra
// attribute to the standard json handler then calls handle.
func (h *myHandler) Handle(ctx context.Context, r slog.Record) error {
	userID := ctx.Value(userIDKey).(string)

	return h.
		JSONHandler.
		WithAttrs([]slog.Attr{slog.String("userID", userID)}).
		Handle(ctx, r)
}

func main() {
	// make the default json handler
	jsonHandler := slog.NewJSONHandler(os.Stdout, nil)

	// wrap the default json handler in a handler that logs context
	myHandler := &myHandler{JSONHandler: jsonHandler}

	logger := slog.New(myHandler)
	slog.SetDefault(logger)

	// add our context fields
	ctx := context.Background()
	ctx = context.WithValue(ctx, userIDKey, "14")

	// call the context log method
	slog.InfoContext(ctx, "hello world", "foo", 42)
}

输出携带了context中的userID作为额外的key:

1
{"time":"2024-01-03T23:05:22.844931+08:00","level":"INFO","msg":"hello world","userID":"14","foo":42}

中间件

github.com/zknill/slogmwlog/slog包进行了封装,让其更易使用。


原文:https://zknill.io/posts/go-1-21-slog/