2

Logrus 日志分析

 2 years ago
source link: https://liqiang.io/post/logrus-logger-library-analysis?lang=ZH_CN
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

Logrus 日志分析

@SOLUTION· 2021-08-14 02:25 · 59 min read

这是我在了解 Logrus 这个日志库时做的一个笔记记录,主要是介绍一些 logrus 的特性,以及我尝试这些特性时的一些个人观点,同时,我还会简单解析一下 logrus 是如何实现这些特性的。

  1. Logger
  2. - Out io.Writer
  3. - Hooks LevelHooks
  4. - Formatter Formatter
  5. - Level Level
  6. - entryPool sync.Pool
  7. - BufferPool BufferPool
  8. Hook <interface>
  9. - Levels() []Level
  10. - Fire(*Entry) error
  11. Formatter <interface>
  12. - Format(*Entry) ([]byte, error)
  13. Entry
  14. - Logger *Logger
  15. - Data Fields
  16. - Time time.Time
  17. - Level Level
  18. - Caller *runtime.Frame
  19. - Buffer *bytes.Buffer
  20. - Context context.Context

组件交互顺序

所以从核心组件的字段来看,大概也可以了解到顺序是怎么样的,这里再给出一张图以打印一个 Info 级别的日志内容来看下实际顺序是怎么样的:

代码版本:v1.8.1

  1. [[email protected]]# logrus.Infof("xxxx")
  2. logger.go:164 func (logger *Logger) Infof(format string, args ...interface{}) {
  3. --> logger.go:165 logger.Logf(InfoLevel, format, args...)
  4. --> logger.go:148 func (logger *Logger) Logf(level Level, format string, args ...interface{}) {
  5. --> logger.go:149 if logger.IsLevelEnabled(level) {
  6. --> logger.go:150 entry := logger.newEntry()
  7. --> logger.go:96 entry, ok := logger.entryPool.Get().(*Entry)
  8. --> logger.go:151 entry.Logf(level, format, args...)
  9. --> entry.go:336 func (entry *Entry) Logf(level Level, format string, args ...interface{}) {
  10. --> entry.go:337 if entry.Logger.IsLevelEnabled(level) {
  11. --> entry.go:338 entry.Log(level, fmt.Sprintf(format, args...))
  12. --> entry.go:292 if entry.Logger.IsLevelEnabled(level) {
  13. --> entry.go:293 entry.log(level, fmt.Sprint(args...))
  14. --> entry.go:221 func (entry *Entry) log(level Level, msg string) {
  15. --> entry.go:224 newEntry := entry.Dup()
  16. --> entry.go:233 newEntry.Logger.mu.Lock()
  17. --> entry.go:234 reportCaller := newEntry.Logger.ReportCaller
  18. --> entry.go:235 newEntry.Logger.mu.Unlock()
  19. --> entry.go:237 if reportCaller {
  20. --> entry.go:238 newEntry.Caller = getCaller()
  21. --> entry.go:241 newEntry.fireHooks()
  22. --> entry.go:265 entry.Logger.mu.Lock()
  23. --> entry.go:266 tmpHooks = make(LevelHooks, len(entry.Logger.Hooks))
  24. --> entry.go:267 for k, v := range entry.Logger.Hooks {
  25. --> entry.go:268 tmpHooks[k] = v
  26. --> entry.go:270 entry.Logger.mu.Unlock()
  27. --> entry.go:272 err := tmpHooks.Fire(entry.Level, entry)
  28. --> hooks.go:27 for _, hook := range hooks[level] {
  29. --> hooks.go:28 if err := hook.Fire(entry); err != nil {
  30. --> entry.go:243 buffer = getBuffer()
  31. --> entry.go:249 newEntry.Buffer = buffer
  32. --> entry.go:251 newEntry.write()
  33. --> entry.go:279 serialized, err := entry.Logger.Formatter.Format(entry)
  34. --> entry.go:284 entry.Logger.mu.Lock()
  35. --> entry.go:285 defer entry.Logger.mu.Unlock()
  36. --> entry.go:286 if _, err := entry.Logger.Out.Write(serialized); err != nil {
  37. --> logger.go:152 logger.releaseEntry(entry)
  38. --> logger.go:104 entry.Data = map[string]interface{}{}
  39. --> logger.go:105 logger.entryPool.Put(entry)

从上面可以看出,主要的业务逻辑还是在 Entry 中完成,然后 Entry 中包含了一个 Logger 的引用,formatter 和 hook 都是通过这个引用来获取的,然后在 entry 中执行,同时还需要注意到的是这里面调用了好几次的锁,但是持续时间都很短,可能性能影响也不是很大(这个需要 benchmark 一下看看)。

总的来说,整体链条都还比较清晰,没有过于复杂的设计,概念也比较明确,所以就不看再多的东西了,值得学习的一个是这里面用到了几个池,这个对于高性能场景还是很有借鉴意义的,减少内存的分配和回收,还有比如一些小细节:tmpHooks = make(LevelHooks, len(entry.Logger.Hooks)),这些都是值得学习的。

hook 是什么

hook 是对特定的 log level 执行的一段代码,可以针对特定的 log level 做特定的工作,例如 Error 的时候,向 错误跟踪服务发送一个信息之类的。(我觉得不是太必要,可以通过多个 handler 来实现?)

Formatter 是什么

  • interface 定义:
  1. [[email protected]]# cat formatter.go
  2. type Formatter interface {
  3. Format(*Entry) ([]byte, error)
  4. }

就是定义怎么将 Entry 转化成 []byte 的结构。

比较独特的特性

  • Fatal 处理器
  • 线程安全
    • 不管是不注册 hook,还是注册的 hook 执行都是线程安全的.
    • 写出 logger.Out 也是安全的:
      • 从上面可以看到这个写出 logger.Out 过程是加锁的,当然,这在一定程度上降低了性能;
      • 但是如果 logger.Out 是一个以 O_APPEND 标志打开的 os 文件的话,并且每次写出数据都小于 4K,其实也可以不加锁,这样性能会高很多。
  • 完全兼容 Go 的标准库日志模块,日志级别也比较全面;
  • 结构化的日志管理,和一条日志记录不一样,有利于日志分析;
  • 允许使用者通过 hook 的方式将日志分发到其他的地方,但是,我觉得不是必须的;
  • Entry 的实现对于共同上下文的场景很有帮助;
  • 可以通过 Formmater 自定义日志的格式;
  • 线程安全:日志并发写操作通过 mutex 进行保护的。
  • 对于日志输出,没有携带行号和文件名,这个比较遗憾,但是通过 Caller 可以获取到函数名(大概可以定位到哪个文件了);
  • 输出到本地文件系统没有提供日志分割功能,这是别人吐槽的一个观点,但是我不支持,我认为者不属于日志库的功能;
  • 结构化的日志有利于分析,但是其实也不利于开发阅读,所以这其实是一种选择,但是对于真实工业环境,我觉得这也不算一个缺点吧。

About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK