log/slog 是 Go 1.21 引入的一个新的日志记录包

slog相较于传统log包,slog提供了更丰富的功能,如:支持结构化日志,能输出json等格式的日志,

同时也吸收很多社区的优秀日志库的优秀功能,如:glog、zap

在实际项目中,很多日志系统都是将不同级别日志写入单独的文件,这样才能更好的查看日志,

是一个比较实用的能力,就算写入到ELK等系统,也是要能方便搜索。

glog 库实现

在slog之前,glog是有这个能力的,非常好用,早期版本实现也是比较简单:

实现源代码https://github.com/golang/glog/blob/v1.0.0/glog.go#L670

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
        switch s {
		case fatalLog:
			l.file[fatalLog].Write(data)
			fallthrough
		case errorLog:
			l.file[errorLog].Write(data)
			fallthrough
		case warningLog:
			l.file[warningLog].Write(data)
			fallthrough
		case infoLog:
			l.file[infoLog].Write(data)
		}

对于高级别的日志,会写入到对应的文件,低级别的包含高级别的日志,这样就能保证日志的完整性。

zap 库实现

zap 库可以使用 zapcore 库来实现,示例如下:

 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
package main
 
import (
    "go.uber.org/zap"
    "go.uber.org/zap/zapcore"
    "os"
)
 
func main() {
    // 创建不同级别的日志配置
    highPriority := zap.LevelEnablerFunc(func(lvl zapcore.Level) bool {
        return lvl >= zapcore.ErrorLevel
    })
    lowPriority := zap.LevelEnablerFunc(func(lvl zapcore.Level) bool {
        return lvl < zapcore.ErrorLevel
    })
 
    // 创建核心(core)并写入不同文件
    debugLog, _ := os.OpenFile("debug.log", os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0666)
    infoLog, _ := os.OpenFile("info.log", os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0666)
    errorLog, _ := os.OpenFile("error.log", os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0666)
    defer debugLog.Close()
    defer infoLog.Close()
    defer errorLog.Close()
 
    debugCore := zapcore.NewCore(zapcore.NewJSONEncoder(zap.NewProductionEncoderConfig()), zapcore.AddSync(debugLog), lowPriority)
    infoCore := zapcore.NewCore(zapcore.NewJSONEncoder(zap.NewProductionEncoderConfig()), zapcore.AddSync(infoLog), lowPriority)
    errorCore := zapcore.NewCore(zapcore.NewJSONEncoder(zap.NewProductionEncoderConfig()), zapcore.AddSync(errorLog), highPriority)
 
    // 组合核心,创建Logger实例
    logger := zap.New(zapcore.NewTee(debugCore, infoCore, errorCore))
    defer logger.Sync() // 刷新缓冲区中的日志信息到文件。
 
    logger.Debug("This is a debug message") // 将会写入 debug.log 或 info/error 日志文件(取决于具体配置)
    logger.Info("This is an info message")  // 将会写入 info/error 日志文件(取决于具体配置)
    logger.Error("This is an error message") // 将会写入 error 日志文件(取决于具体配置)
}

log/slog 库实现

在 Go 1.21 版本中,Go 官方提供了名为 slog 的日志库,它提供了一种更简洁的日志记录方式。

在新的项目中,完全可以使用 slog 库来记录日志,不再需要引入第三方库,这样也可以避免引入额外的依赖,导致后续的维护和兼容性问题。

那么要保留分文件记录日志,如何实现? log/slog 库提供了一种简单的方式来实现,通过使用 slog.Handler 来实现。

参考: https://pkg.go.dev/log/slog#example-Handler-LevelHandler

示例代码:

 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
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
func main() {
	var v = &slog.LevelVar{}
	v.Set(slog.LevelDebug)

	handler := NewLevelHandler(v, fileHandler2("./app.debug.log", v))
	logger := slog.New(handler)

	slog.SetDefault(logger)

	logger.Debug("This is a debug message")
	logger.Info("Starting qkp platform log", "version", "v3.2.2")
	logger.Warn("Low disk space", "remaining", "500MB")
	logger.Error("Failed to connect to ETCD cluster", "error", "connection timeout")
}

func fileHandler(filename string, l slog.Leveler) slog.Handler {
	lumberjackLogger := &lumberjack.Logger{
		Filename:   filename, // 日志文件路径
		MaxSize:    10,       // 单个日志文件最大大小(单位:MB)
		MaxBackups: 5,        // 保留的旧日志文件个数
		MaxAge:     30,       // 保留的旧日志文件最大天数(单位:天)
		Compress:   true,     // 是否压缩旧日志文件
	}

	handler := slog.NewJSONHandler(lumberjackLogger, &slog.HandlerOptions{
		AddSource: true,
		Level:     l,
		ReplaceAttr: func(groups []string, a slog.Attr) slog.Attr {
			if a.Key == slog.TimeKey {
				return slog.Time("timestamp", a.Value.Time())
			}
			return a
		},
	})
	return handler
}

// A LevelHandler wraps a Handler with an Enabled method
// that returns false for levels below a minimum.
type LevelHandler struct {
	level       slog.Leveler
	handler     slog.Handler
	infohandler slog.Handler
	warnhandler slog.Handler
	errhandler  slog.Handler
}

// NewLevelHandler returns a LevelHandler with the given level.
// All methods except Enabled delegate to h.
func NewLevelHandler(level slog.Leveler, h slog.Handler) *LevelHandler {
	// Optimization: avoid chains of LevelHandlers.
	if lh, ok := h.(*LevelHandler); ok {
		h = lh.Handler()
	}
	i := fileHandler2("./app.info.log", level)
	w := fileHandler2("./app.warn.log", level)
	e := fileHandler2("./app.error.log", level)
	return &LevelHandler{level, h, i, w, e}
}

// Enabled implements Handler.Enabled by reporting whether
// level is at least as large as h's level.
func (h *LevelHandler) Enabled(_ context.Context, level slog.Level) bool {
	return level >= h.level.Level()
}

// Handle implements Handler.Handle.
func (h *LevelHandler) Handle(ctx context.Context, r slog.Record) error {
	switch r.Level {
	case slog.LevelError:
		h.errhandler.Handle(ctx, r)
		fallthrough
	case slog.LevelWarn:
		h.warnhandler.Handle(ctx, r)
		fallthrough
	case slog.LevelInfo:
		h.infohandler.Handle(ctx, r)
	}
	return h.handler.Handle(ctx, r)
}

// WithAttrs implements Handler.WithAttrs.
func (h *LevelHandler) WithAttrs(attrs []slog.Attr) slog.Handler {
	return NewLevelHandler(h.level, h.handler.WithAttrs(attrs))
}

// WithGroup implements Handler.WithGroup.
func (h *LevelHandler) WithGroup(name string) slog.Handler {
	return NewLevelHandler(h.level, h.handler.WithGroup(name))
}

// Handler returns the Handler wrapped by h.
func (h *LevelHandler) Handler() slog.Handler {
	return h.handler
}

参考