0%

Golang-异步日志库

一、前言

Go 社区中有很多优秀的开源日志框架(如: zap, logrus 等), 它们不仅功能丰富, 而且性能很好, 能够满足绝大数应用场景! 但是在日常开发过程中,因为自身需求或者项目体量的原因, 你也许会觉得这么高大上的日志库对自己来说有点功能过剩, 因为它们的优秀导致学习成本增加, 想要快速入门似乎需要花一些心思才行(学习成本上升并不是将其拒之门外的理由,相反,社区能够有这些优秀的成熟项目是值得每位Gopher为之高兴的事, 要想技术得到提升, 阅读并学习这些优秀的开源项目是必须的), 此时一个简单实用, 容易上手的日志库便成为了自己最想要的。 其实对于大多数人需要使用的日志场景无非以下几点:

  1. 能够分级别输出日志
  2. 能够根据时间或者文件大小对日志文件进行切割保存
  3. 能够根据配置选择需要输出的日志级别

为了探讨学习之用, 在下实践了一款简单实用的异步日志库: plogs

代码采用Golang Channel实现异步写, 如果需要, 可以通过配置同步输出到stdout

二、功能

  • 格式化日志输出
  • 日志输出级别可配置
  • 日志缓冲区大小可配置
  • 日志缓冲区flush周期可配置
  • 异步记录日志(终端输出可选且采用同步输出)
  • temp.log总是当前正在输出的日志文件
  • 配置项可选, 可根据自己的需求选择不同Option进行初始化
  • 日志级别划分: Fatal(致命错误), Error(错误), Warn(警告), Info(流水), Debug(调试信息)
  • 在控制台使用颜色对不同级别的日志进行区分: Fatal(红色), Error(紫红色), Warn(黄色), Info(绿色), Debug(蓝色)
  • 提供多种日志文件切割周期: CutDaily(24小时), CutHourly(每小时), CutHalfAnHour(每半小时), CutTenMin(每10分钟), CutPer10M(每10M), CutPer60M(每60M), CutPer100M(每100M)
  • 提供不同的日志记录方式: WriteByLevel(区分级别记录在不同的文件), WriteByMerged(所有日志记录在一起), WriteByAll(既区分级别同时也记录在同一个文件)

三、关键代码

  1. 日志记录采用单独的一个协程进行记录, 日志文件切割也单独开启一个协程

    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
    func (log *Logger) run() {
    log.wg.Add(2)
    go func(wg *sync.WaitGroup) {
    ticker := time.NewTimer(log.config.flushDuration)
    defer ticker.Stop()
    for {
    //第一个select,固定周期进行flush,或者周期内通道缓冲达到了容量的90%时进行flush
    //收到flush信号量时需要停止ticker
    //第二个select,收到日志即write,没有日志则进入下一个flush周期
    //每个周期完毕,需要重置ticker
    //日志消息通道关闭时直接return,结束协程
    select {
    case <-ticker.C:
    break
    case _, ok := <-log.flushChan:
    if ok {
    ticker.Stop()
    break
    }
    }
    select {
    case msg, ok := <-log.msgChan:
    log.write(msg)
    if !ok {
    wg.Done()
    return
    }
    default:
    break
    }
    ticker.Reset(log.config.flushDuration)
    }
    }(log.wg)

    go func(wg *sync.WaitGroup) {
    ticker := time.NewTicker(defaultCutDuration)
    defer ticker.Stop()
    for {
    select {
    case <-ticker.C:
    log.cut()
    ticker.Reset(defaultCutDuration)
    case <-log.closeChan:
    wg.Done()
    return
    default:
    break
    }
    }
    }(log.wg)
    }
  2. 写日志时, 同时将通道中所有的消息都读取出来一并写入, 当句柄文件大小达到切割条件时, 将数据sync到硬盘中

    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
    func (log *Logger) write(msgData *logMessage) {
    var msgs []*logMessage

    // 将第一条日志添加进队列中
    if msgData != nil {
    msgs = append(msgs, msgData)
    }
    // 这里将通道中的所有能读取到的日志都读取出来
    select {
    case data, ok := <-log.msgChan:
    if ok {
    msgs = append(msgs, data)
    }
    default:
    break
    }

    mu := log.levelConfigs.mu
    mu.Lock()
    defer mu.Unlock()

    for _, m := range msgs {
    var levels []Level
    var configs []*levelConfig
    switch log.config.writeOption {
    case WriteByLevel:
    levels = append(levels, m.level)
    case WriteByMerged:
    levels = append(levels, _LevelEnd)
    case WriteByAll:
    levels = append(levels, _LevelEnd, m.level)
    }
    configs = log.levelConfigs.getConfig(levels...)
    // 将message写入句柄
    for _, cg := range configs {
    n, _ := cg.fd.WriteString(m.message)
    cg.size += int64(n)
    }
    log.releaseLogMessage(m)
    }
    }
  3. 因为日志写是异步的, 必须考虑程序退出时协程尚未执行完的问题, 这里采用sync.WaitGroup保证协程的正确退出!

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    func (log *Logger) Close() {
    log.closed = true
    close(log.msgChan)
    close(log.flushChan)
    close(log.closeChan)
    log.wg.Wait()
    for _, config := range log.levelConfigs.levels {
    config.close()
    }
    }

四、使用

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 (
"time"

"github.com/pyihe/plogs"
)

func main() {
opts := []plogs.Option{
plogs.WithCutOption(plogs.CutTenMin),
plogs.WithAppName("ALTIMA"),
plogs.WithBufferSize(1024),
plogs.WithFlushDuration(500 * time.Millisecond),
plogs.WithWriteOption(plogs.WriteByMerged),
plogs.WithLogPath("./files"),
plogs.WithWriteLevel(plogs.LevelFatal | plogs.LevelError | plogs.LevelWarning | plogs.LevelInfo | plogs.LevelDebug),
plogs.WithStdout(true),
}

logger := plogs.NewLogger(opts...)
defer logger.Close()

plogs.Fatalf("hello, this is level fatal!")
plogs.Errorf("hello, this is level error")
plogs.Warnf("hello, this is level warn!")
plogs.Infof("hello, this is level info!")
plogs.Debugf("hello, this is level debug!")
}

效果截图:

五、TODO

  • 提供日志文件保存最多个数限制以及保存最长时间限制
  • 优化文件及代码行号: 不显示系统绝对路径,只显示文件及代码在项目中的相对路径
  • 优化日志文件切割
  • 代码效率优化