开源日志包

标准库 log 包

标准库自带,无需安装

  1. 只提供 PrintPanicFatal 函数用于日志输出
  2. Go 标准库大量使用了该 log 包

glog

Kubernetes 使用的 klog 是基于 glog 进行封装

  1. Google 推出的轻量级日志包
  2. 特性
    • 支持 4 种日志级别: InfoWarningErrorFatal
    • 支持命令行选项
    • 支持根据文件大小切割日志文件
    • 支持日志按级别分类输出
    • 支持 V level – 开发者自定义日志级别
    • 支持 vmodule – 开发者对不同的文件使用不同的日志级别
    • 支持 traceLocation – 打印指定位置的栈信息

logrus

Github star 数量最多的日志包,DockerPrometheus 也在使用 logrus

  1. 支持常用的日志级别
  2. 可扩展:允许使用者通过 Hook 的方式,将日志分发到任意地方
  3. 支持自定义的日志格式:内置支持 JSONTEXT
  4. 结构化日志记录:Field 机制允许使用者自定义字段
  5. 预设日志字段:Default Field 机制,可以给一部分或者全部日志统一添加共同的日志字段
  6. Fatal handlers:允许注册一个或多个 Handler,当产生 Fatal 级别的日志时调用,常用于优雅关闭

zap

Uber 开源,以高性能著称,子包 zapcore 提供很多底层的日志接口,适合二次封装

  1. 支持常用的日志级别
  2. 性能非常高
  3. 支持针对特定的日志级别,输出调用堆栈
  4. 与 logrus 类似:结构化日志、预设日志字段、支持 Hook

设计实现

源码:https://github.com/marmotedu/gopractise-demo/tree/master/log/cuslog

功能需求

  1. 支持自定义配置
  2. 支持文件名行号
  3. 支持日志级别:Debug、Info、Warn、Error、Panic、Fatal
  4. 支持输出到本地文件标准输出
  5. 支持 JSONTEXT 的日志输出,支持自定义日志格式
  6. 支持选项模式

级别 + 选项

为了方便比较,几乎所有的日志包都用常量计数器 iota 来定义日志级别

options.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
type Level uint8

const (
DebugLevel Level = iota
InfoLevel
WarnLevel
ErrorLevel
PanicLevel
FatalLevel
)

var LevelNameMapping = map[Level]string{
DebugLevel: "DEBUG",
InfoLevel: "INFO",
WarnLevel: "WARN",
ErrorLevel: "ERROR",
PanicLevel: "PANIC",
FatalLevel: "FATAL",
}

常见的日志选项:日志级别输出位置(标准输出 or 文件)、输出格式(JSON or Text)、是否开启文件名行号

options.go
1
2
3
4
5
6
7
type options struct {
output io.Writer
level Level
stdLevel Level
formatter Formatter
disableCaller bool
}
formatter.go
1
2
3
type Formatter interface {
Format(entry *Entry) error
}

通过选项模式,可以灵活地设置日志选项

options.go
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
type Option func(*options)

func initOptions(opts ...Option) (o *options) {
o = &options{}
for _, opt := range opts {
opt(o)
}

if o.output == nil {
o.output = os.Stderr
}

if o.formatter == nil {
o.formatter = &TextFormatter{}
}

return
}

func WithLevel(level Level) Option {
return func(o *options) {
o.level = level
}
}

func WithDisableCaller(caller bool) Option {
return func(o *options) {
o.disableCaller = caller
}
}
logger.go
1
2
3
4
5
6
7
8
9
10
11
12
func SetOptions(opts ...Option) {
std.SetOptions(opts...)
}

func (l *logger) SetOptions(opts ...Option) {
l.mu.Lock()
defer l.mu.Unlock()

for _, opt := range opts {
opt(l.opt)
}
}

具有选项模式的日志包,可以动态地修改日志选项

main.go
1
cuslog.SetOptions(cuslog.WithLevel(cuslog.DebugLevel), cuslog.WithDisableCaller(true))

Logger + 打印

创建 Logger,日志包都会有一个默认的全局 Logger

logger.go
1
2
3
4
5
6
7
8
9
10
11
12
13
var std = New()

type logger struct {
opt *options
mu sync.Mutex
entryPool *sync.Pool
}

func New(opts ...Option) *logger {
logger := &logger{opt: initOptions(opts...)}
logger.entryPool = &sync.Pool{New: func() interface{} { return entry(logger) }}
return logger
}

非格式化打印 + 格式化打印

logger.go
1
2
3
4
5
6
7
func (l *logger) Debug(args ...interface{}) {
l.entry().write(DebugLevel, FmtEmptySeparate, args...)
}

func (l *logger) Debugf(format string, args ...interface{}) {
l.entry().write(DebugLevel, format, args...)
}

Panic、Panicf 要调用 panic() 函数;Fatal、Fatalf 要调用 os.Exit(1) 函数

写入输出

Entry 用来保存所有的日志信息:日志配置 + 日志内容

entry.go
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
type Entry struct {
logger *logger
Buffer *bytes.Buffer
Map map[string]interface{}
Level Level
Time time.Time
File string
Line int
Func string
Format string
Args []interface{}
}

func (e *Entry) write(level Level, format string, args ...interface{}) {
if e.logger.opt.level > level {
return
}
e.Time = time.Now()
e.Level = level
e.Format = format
e.Args = args
if !e.logger.opt.disableCaller {
if pc, file, line, ok := runtime.Caller(2); !ok {
e.File = "???"
e.Func = "???"
} else {
e.File, e.Line, e.Func = file, line, runtime.FuncForPC(pc).Name()
e.Func = e.Func[strings.LastIndex(e.Func, "/")+1:]
}
}
e.format()
e.writer()
e.release()
}

func (e *Entry) format() {
_ = e.logger.opt.formatter.Format(e)
}

func (e *Entry) writer() {
e.logger.mu.Lock()
_, _ = e.logger.opt.output.Write(e.Buffer.Bytes())
e.logger.mu.Unlock()
}

func (e *Entry) release() {
e.Args, e.Line, e.File, e.Format, e.Func = nil, 0, "", "", ""
e.Buffer.Reset()
e.logger.entryPool.Put(e)
}
io.go
1
2
3
type Writer interface {
Write(p []byte) (n int, err error)
}

自定义输出格式

image-20220502203406325

测试

example.go
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
package main

import (
"log"
"os"

"github.com/marmotedu/gopractise-demo/log/cuslog"
)

func main() {
cuslog.Info("std log")
cuslog.SetOptions(cuslog.WithLevel(cuslog.DebugLevel))
cuslog.Debug("change std log to debug level")
cuslog.SetOptions(cuslog.WithFormatter(&cuslog.JsonFormatter{IgnoreBasicFields: false}))
cuslog.Debug("log in json format")
cuslog.Info("another log in json format")

// 输出到文件
fd, err := os.OpenFile("test.log", os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0644)
if err != nil {
log.Fatalln("create file test.log failed")
}
defer fd.Close()

l := cuslog.New(cuslog.WithLevel(cuslog.InfoLevel),
cuslog.WithOutput(fd),
cuslog.WithFormatter(&cuslog.JsonFormatter{IgnoreBasicFields: false}),
)
l.Info("custom log with json formatter")
}
1
2
3
4
5
$ go run example.go        
2022-05-02T20:38:54+08:00 INFO example.go:11 std log
2022-05-02T20:38:54+08:00 DEBUG example.go:13 change std log to debug level
{"message":"log in json format","level":"DEBUG","time":"2022-05-02T20:38:54+08:00","file":"/Users/zhongmingmao/workspace/go/src/github.com/marmotedu/gopractise-demo/log/cuslog/example/example.go:15","func":"main.main"}
{"file":"/Users/zhongmingmao/workspace/go/src/github.com/marmotedu/gopractise-demo/log/cuslog/example/example.go:16","func":"main.main","message":"another log in json format","level":"INFO","time":"2022-05-02T20:38:54+08:00"}