起因
日志库是最基础和最重要的库,go 官方的日志库功能太简陋,项目中经常使用的功能:日志级别,json 格式,按日期旋转文件等都不支持。
而使用最多的 logrus
库也不支持按时间旋转文件,还要依赖另外三方包去实现。
所以我决定重新设计一个简单易用的日志库 ylog.
目标
一个日志库要保证最常用的功能和一定的灵活性, 所以实现的目标如下:
- 支持日志级别
- 支持 JSON 格式
- 支持按日期旋转文件
- 自定义格式化和输出
设计
1. 日志记录
日志写入一条信息就是一条日志记录,日志记录应该是一个最基础和核心的结构体。
1
2
3
4
5
6
7
|
type Record struct {
Time time.Time // 时间
Msg string // 原始消息
Fmsg string // 格式化后的消息内容
Level Level // 级别
Fields Fields // 自定义字段
}
|
2. 日志级别
日志级别类型 Level 应该是一个 int 类型,这样才能对级别进行对比和排序, 定义如下:
1
2
3
4
5
6
7
8
|
type Level uint32
const (
DebugLevel Level = iota
InfoLevel
WarnLevel
ErrorLevel
)
|
为什么这里的 Level
使用的是 uint32
的类型,而不是 uint8
或者 uint16
或者其他, 这是因为 atomic
包对应的函数都是 32 或者 64 位的, 为了能够使用到原子操作相关的函数, 所以这里使用 uint32
。
3. 自定义字段
自定义字段本质就是 key-value
, 使用 map 即可:
1
|
type Fields map[string]interface{}
|
4. 接口
Logger 接口作为对外使用的接口,提供基本使用方法和设置。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
|
type Logger interface {
// 写日志
Info(msg string)
Debug(msg string)
Warn(msg string)
Error(msg string)
// 设置格式化
SetFormatter(f Formatter)
// 设置输出
SetOuter(o Outer)
// 设置级别
SetLevel(l Level)
// 设置自定义字段
With(fields Fields) Logger
}
|
With 方法,设置自定义字段后返回一个新的 Logger。
自定义字段存储在 Logger 中, 在写日志的时候,新的 Logger 会复制自定义字段到日志记录(Record)中。
由于自定义字段 Fields 本质是一个 map ,map 不是线程安全的,所以这里只对 map 进行复制操作。
而同样的 Logger 还可以设置公共的字段,而不影响上层的 Logger。
Formatter 和 Outer 接口都比较简单,Formatter 格式化日志记录,Outer 负责输出日志。
1
2
3
4
5
6
7
8
|
type (
Formatter interface {
Format(r Record) (string, error)
}
Outer interface {
Write(r Record) error
}
)
|
实现
接口定义完之后,实现对应的接口方法, 这里需要考虑线程安全,在该枷锁的地方要加锁。 看一下 Logger 接口的核心代码:
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
|
Log struct {
f Formatter
o Outer
level Level
fields Fields
mu sync.RWMutex
}
func (l *Log) SetOuter(o Outer) {
l.mu.Lock()
defer l.mu.Unlock()
l.o = o
}
func (l *Log) GetOuter() Outer {
l.mu.RLock()
defer l.mu.RUnlock()
return l.o
}
func (l *Log) SetLevel(level Level) {
atomic.StoreUint32((*uint32)(&l.level), uint32(level))
}
func (l *Log) GetLevel() Level {
return Level(atomic.LoadUint32((*uint32)(&l.level)))
}
func (l *Log) Info(msg string) {
l.Write(msg, InfoLevel)
}
func (l *Log) Write(msg string, level Level) error {
if level >= l.GetLevel() {
record := Record{
Time: time.Now(),
Msg: msg,
Level: level,
Fields: l.GetFields(),
}
// 格式化
fmsg, err := l.GetFormatter().Format(record)
if err != nil {
return fmt.Errorf("format record err: %w", err)
}
record.Fmsg = fmsg
// 输出
return l.GetOuter().Write(record)
}
return nil
}
...
|
封装一些函数方便使用
1
2
3
4
5
6
7
8
9
10
11
12
|
var defaultLog Logger
func init() {
defaultLog = New()
defaultLog.SetLevel(InfoLevel)
}
func Info(msg string) {
defaultLog.Info(msg)
}
func Infof(format string, a ...interface{}) {
defaultLog.Info(fmt.Sprintf(format, a...))
}
|
其他代码具体实现查看项目: https://github.com/lyuangg/ylog
性能测试
与 logrus 性能对比:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
|
goos: darwin
goarch: amd64
pkg: github.com/lyuangg/ylog/benchmarks
cpu: Intel(R) Core(TM) i5-8279U CPU @ 2.40GHz
BenchmarkDefaultLogger/logrus-8 1528854 2652 ns/op 576 B/op 17 allocs/op
BenchmarkDefaultLogger/ylog-8 11159875 307.4 ns/op 276 B/op 6 allocs/op
BenchmarkRoateLogger/logrus-8 250870 13076 ns/op 945 B/op 21 allocs/op
BenchmarkRoateLogger/ylog-8 622275 5385 ns/op 292 B/op 8 allocs/op
BenchmarkWithField/logrus-8 1000000 3094 ns/op 881 B/op 18 allocs/op
BenchmarkWithField/ylog-8 8655415 387.6 ns/op 500 B/op 7 allocs/op
BenchmarkJsonFormat/logrus-8 879865 3747 ns/op 1682 B/op 29 allocs/op
BenchmarkJsonFormat/ylog-8 3995971 999.0 ns/op 1585 B/op 25 allocs/op
PASS
ok github.com/lyuangg/ylog/benchmarks 32.649s
|
项目
github: https://github.com/lyuangg/ylog