一、前言
在 Go 社区中有很多优秀的开源日志框架(如: zap, logrus 等), 它们不仅功能丰富, 而且性能很好, 能够满足绝大数应用场景! 但是在日常开发过程中,因为自身需求或者项目体量的原因, 你也许会觉得这么高大上的日志库对自己来说有点功能过剩, 因为它们的优秀导致学习成本增加, 想要快速入门似乎需要花一些心思才行(学习成本上升并不是将其拒之门外的理由,相反,社区能够有这些优秀的成熟项目是值得每位Gopher为之高兴的事, 要想技术得到提升, 阅读并学习这些优秀的开源项目是必须的), 此时一个简单实用, 容易上手的日志库便成为了自己最想要的。 其实对于大多数人需要使用的日志场景无非以下几点:
- 能够分级别输出日志
- 能够根据时间或者文件大小对日志文件进行切割保存
- 能够根据配置选择需要输出的日志级别
为了探讨学习之用, 在下实践了一款简单实用的异步日志库: 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(既区分级别同时也记录在同一个文件)
三、关键代码
- 日志记录采用单独的一个协程进行记录, 日志文件切割也单独开启一个协程
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)
}
- 写日志时, 同时将通道中所有的消息都读取出来一并写入, 当句柄文件大小达到切割条件时, 将数据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)
}
}
- 因为日志写是异步的, 必须考虑程序退出时协程尚未执行完的问题, 这里采用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
网友评论