0%

golang笔记-logrus组件

最近看golang1.21的发布时,看到已经有结构化日志库提供了,不过在项目中我经常用的到日志库是logrus。本文总结logrus的使用,并用笔记记录。logrus除了是结构化日志以外,我更看重的是有很多外部的hook支持,最初选择是因为可以向logstash发送日志。

1. 使用方法

日志最基础的使用就是文件日志,这里使用了另外两个库,以协助logrus进行日志的分割。一个是按照每小时进行滚动切割,这样在找问题时,可以按照一小时内去查找,另外如果一个小时内所产生的日志条数过多,也可以限制日志条数,超过条数要求日志文件切割。

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
func newLfsHook(logName string, logLevel log.Level, 
maxRemainCnt uint) (log.Hook, error) {
writer, err := rotatelogs.New(
logName+".%Y%m%d%H",
// WithLinkName为最新的日志建立软连接,以方便随着找到当前日志文件
rotatelogs.WithLinkName(logName),

// WithRotationTime设置日志分割的时间,这里设置为一小时分割一次
rotatelogs.WithRotationTime(time.Hour),

// WithMaxAge和WithRotationCount二者只能设置一个,
// WithMaxAge设置文件清理前的最长保存时间,
// WithRotationCount设置文件清理前最多保存的个数。
//rotatelogs.WithMaxAge(time.Hour*24*3),
rotatelogs.WithRotationCount(maxRemainCnt),
)

if err != nil {
log.Errorf("config local filesystem for logger error: %v", err)
return nil, err
}

writerMap := make(map[logrus.Level]io.Writer, 0)
for i := 0; i < int(logLevel); i++ {
writerMap[log.Level(i)] = writer
}

lfsHook := lfshook.NewHook(lfshook.WriterMap(writerMap),
&log.TextFormatter{DisableColors: true})
return lfsHook, nil
}

初始化logger,设置日志的等级,一般情况是错误日志单独一个文件处理。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
import (
log "github.com/sirupsen/logrus"
)

func initLogger(logFilename string, level log.Level) error {
log.SetLevel(level)
fileHook, err := newLfsHook(logFilename, level, 1024)
if err != nil {
fmt.Printf("log init fatal!")
return err
}
log.AddHook(fileHook)
// 一般情况可以给错误日志专门开一个hook
errHook, err := newLfsHook("logs/error.log",
log.ErrorLevel, 1024)
if err != nil {
fmt.Printf("errlog init fatal!")
return err
}
log.AddHook(errHook)
return nil
}

2. 原理简介

2.1 最重要的三个结构。以下结构中列出重要的字段,部分省略
  • 第一个就是logger结构
    1
    2
    3
    4
    5
    6
    7
    type Logger struct {
    // 写日志的处理器
    Out io.Writer
    // Reusable empty entry
    entryPool sync.Pool
    // ...
    }
  • 第二个类就是entry结构,表示日志的一个条目
    1
    2
    3
    4
    5
    6
    7
    8
    type Entry struct {
    // ...
    Logger *Logger
    //...
    // When formatter is called in entry.log(), a Buffer may be set to entry
    Buffer *bytes.Buffer
    // ...
    }
  • 第三个类是hook,就是外部可以添加的各种支持的日志记录处理器.
    1
    2
    3
    4
    5
    6
    7
    type Hook interface {
    Levels() []Level
    Fire(*Entry) error
    }

    // Internal type for storing the hooks on a logger instance.
    type LevelHooks map[Level][]Hook
2.2 日志处理逻辑

主要的逻辑是先复制entry条目,如果要报告调用的函数,则要加锁获取Buffer,这个buffer主要用于结构化。
在这里优先钩子上的记录处理器先处理,在进行默认处理。

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
func (entry *Entry) log(level Level, msg string) {
var buffer *bytes.Buffer
// 复制entry
newEntry := entry.Dup()

if newEntry.Time.IsZero() {
newEntry.Time = time.Now()
}

newEntry.Level = level
newEntry.Message = msg

// 获得缓存和logger的是否报告调用者配置
newEntry.Logger.mu.Lock()
reportCaller := newEntry.Logger.ReportCaller
bufPool := newEntry.getBufferPool()
newEntry.Logger.mu.Unlock()

if reportCaller {
newEntry.Caller = getCaller()
}

// hook先处理
newEntry.fireHooks()
buffer = bufPool.Get()
defer func() {
newEntry.Buffer = nil
buffer.Reset()
bufPool.Put(buffer)
}()
buffer.Reset()
newEntry.Buffer = buffer
//日志的主处理器进行处理,见下面的注释。
newEntry.write()

newEntry.Buffer = nil
if level <= PanicLevel {
panic(newEntry)
}
}

newEntry.write函数最后调用logger结构体的Write来处理日志。这里加锁的原因是,写日志有可能是不同的线程里,为了保持其输出的一致性,在这里加锁。这里要指出来的是,hook的线程安全由hook组件自己承担。

1
2
3
4
5
6
7
8
9
10
11
12
func (entry *Entry) write() {
entry.Logger.mu.Lock()
defer entry.Logger.mu.Unlock()
serialized, err := entry.Logger.Formatter.Format(entry)
if err != nil {
fmt.Fprintf(os.Stderr, "Failed to obtain reader, %v\n", err)
return
}
if _, err := entry.Logger.Out.Write(serialized); err != nil {
fmt.Fprintf(os.Stderr, "Failed to write to log, %v\n", err)
}
}

3. 结语

最简单的组件,使用最常见,对性能的要求也比较苛刻,通常在前期的使用日志文件,到后期使用日志中心,日志组件要能够灵活支持,平滑过度,项目之初就要制定好日志结构化的一些基本字段,规则也相当重要,以便于日志采集,日志分析,日志提取相关工作可以顺利开展,这也是日志系统之外的事情了。

Hooks列表
logstash