美文网首页
Golang-异步日志库

Golang-异步日志库

作者: pyihe | 来源:发表于2022-02-25 00:28 被阅读0次

    一、前言

    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. 日志记录采用单独的一个协程进行记录, 日志文件切割也单独开启一个协程
    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)
    }
    
    1. 写日志时, 同时将通道中所有的消息都读取出来一并写入, 当句柄文件大小达到切割条件时, 将数据sync到硬盘中
    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)
        }
    }
    
    1. 因为日志写是异步的, 必须考虑程序退出时协程尚未执行完的问题, 这里采用sync.WaitGroup保证协程的正确退出!
    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()
        }
    }
    

    四、使用

    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!")
    }
    
    

    效果截图:

    log.png

    相关文章

      网友评论

          本文标题:Golang-异步日志库

          本文链接:https://www.haomeiwen.com/subject/rdztrrtx.html