type
Post
status
Published
date
Apr 2, 2022
slug
summary
利用接口、channel和文件操作实现异步写日志的模块
tags
Golang
category
技术分享
icon
password
需求
程序运行状况是个黑盒,无论是在项目开发过程中,或是项目在线上运行时需要通过日志了解程序运行状况,日志库则是必不可少的组件
对于一个可用的日志库,基本的功能需求有:
- 日志对象构建
- 日志分级
- 打印各个level的日志
- 设置日志级别
- 日志存储
实现
日志接口
根据日志存储的位置不同有不同的实现,例如文件,控制台,消息中间件等
定义一个日志接口来规范日志的行为,易于扩展和维护
type LogInterface interface { SetLevel(level int) Debug(format string, args ...interface{}) Trace(format string, args ...interface{}) Info(format string, args ...interface{}) Warning(format string, args ...interface{}) Error(format string, args ...interface{}) Fatal(format string, args ...interface{}) }
日志级别,使用 constant 定义枚举来:
const ( LogLevelDebug = iota LogLevelTrace LogLevelInfo LogLevelWarn LoglevelError LoglevelFatal )
文件日志
定义文件日志 FileLogger 结构体和构造方法
将日志文件拆分为两部分:
- debugFile 存放 [debug,warning) 级别的日志文件
- warnFile 存放 [warning,fatal] 级别的日志文件
type FileLogger struct { level int //日志级别 logPath string //日志路径 logName string //日志文件名 debugFile *os.File //[debug,warning) 级别的日志文件句柄 warnFile *os.File //[warning,fatal] 级别的日志文件句柄 } func NewFileLog(level int, logPath string, logName string) LogInterface { logger := &FileLogger{ level: level, logPath: logPath, logName: logName, } logger.init() return logger }
其中 init() 函数用来初始化日志文件句柄
func (f *FileLogger) init() { //DebugFile filename := fmt.Sprintf("%s/%s.debug", f.logPath, f.logName) file, err := os.OpenFile(filename, os.O_CREATE|os.O_APPEND|os.O_WRONLY, 0755) if err != nil { panic(fmt.Sprintf("open %s failed,err: %v", filename, err)) } f.debugFile = file //WarnFile filename = fmt.Sprintf("%s/%s.warn", f.logPath, f.logName) file, err = os.OpenFile(filename, os.O_CREATE|os.O_APPEND|os.O_WRONLY, 0755) if err != nil { panic(fmt.Sprintf("open %s failed,err: %v", filename, err)) } f.warnFile = file }
核心日志输出方法:
func (f *FileLogger) printLog(level int, format string, args ...interface{}) { //如果当前的日志级别比需要打印的日志级别更高,则不打印(低级别日志) if f.level > level { return } //日志消息 msg := fmt.Sprintf(format, args) //当前时间 now := time.Now() nowStr := now.Format("2006-1-02 15:04:05") //日志级别字符串 levelStr := getLevelText(level) //当前代码执行信息 fileName, funcName, lineNo := GetLineInfo() var filePath *os.File if level >= LogLevelWarn { filePath = f.warnFile } else { filePath = f.debugFile } fmt.Fprintf(filePath, "[%s] 「%s」- (%s:%d) Func: %s:%s \n", levelStr, nowStr, fileName, lineNo, funcName, msg) } func GetLineInfo() (fileName string, funcName string, lineNo int) { //skip 参数表示调用栈的深度 pc, file, line, ok := runtime.Caller(3) if ok { //path.Base 去掉包和函数的全路径 fileName = path.Base(file) funcName = path.Base(runtime.FuncForPC(pc).Name()) lineNo = line } return }

在不同级别的日志方法中,只需要调用printLog,传入参数即可
func (f *FileLogger) Debug(format string, args ...interface{}) { f.printLog(LogLevelDebug, format, args) } func (f *FileLogger) Trace(format string, args ...interface{}) { f.printLog(LogLevelTrace, format, args) } .....
控制台日志
无论是输出到文件还是控制台,区别仅仅是输出的文件句柄是标准控制台输出还是文件
因此可以将上面的 printLog 方法作为普通函数,提取到单独的文件中,将原本的方法绑定参数作为普通参数传递
func printLog(log LogInterface, level int, format string, args ...interface{}) { if log.GetLevel() > level { return } var out io.Writer if fileLog, ok := log.(FileLogger); ok { if level < LogLevelWarn { out = fileLog.debugFile } else { out = fileLog.warnFile } } else { out = os.Stdout } //日志消息 msg := fmt.Sprintf(format, args) //当前时间 now := time.Now() nowStr := now.Format("2006-1-02 15:04:05") //日志级别字符串 levelStr := getLevelText(level) //当前代码执行信息 fileName, funcName, lineNo := GetLineInfo() fmt.Fprintf(out, "[%s] 「%s」- (%s:%d) Func: %s:%s \n", levelStr, nowStr, fileName, lineNo, funcName, msg) }
优化
易用性封装
日志模块通常是单例存在的,封装一个初始化日志对象的函数来获得日志对象
异步写入
日志文件写入的 IO 操作相对内存来说耗时长,当并发量足够大时文件操作可能会阻塞,从而影响到业务代码的执行效率
解决方法是把同步写日志这个操作替换成异步写入:
- 将日志数据写入 channel
- 运行goroutine取出channel的日志数据写入到日志文件中
type LogData struct { Message string //日志消息 TimeStr string //日志时间 LevelStr string //日志级别 Filename string //日志输出语句的行号 FuncName string //函数名 LineNo int //日志输出语句的行号 AboveWarning bool // 是否是 warn以上的日志级别 } func printLog(level int, format string, args ...interface{}) *LogData { //日志消息 msg := fmt.Sprintf(format, args...) //当前时间 now := time.Now() nowStr := now.Format("2006-1-02 15:04:05") //日志级别字符串 levelStr := getLevelText(level) //当前代码执行信息 fileName, funcName, lineNo := GetLineInfo() logData := &LogData{ Message: msg, TimeStr: nowStr, LevelStr: levelStr, Filename: fileName, FuncName: funcName, LineNo: lineNo, AboveWarning: false, } if level >= LogLevelWarn { logData.AboveWarning = true } return logData }
/* 初始化日志文件句柄 */ func (f *FileLogger) Init() { //DebugFile filename := fmt.Sprintf("%s/%s.debug", f.logPath, f.logName) file, err := os.OpenFile(filename, os.O_CREATE|os.O_APPEND|os.O_WRONLY, 0755) if err != nil { panic(fmt.Sprintf("open %s failed,err: %v", filename, err)) } f.debugFile = file //WarnFile filename = fmt.Sprintf("%s/%s.warn", f.logPath, f.logName) file, err = os.OpenFile(filename, os.O_CREATE|os.O_APPEND|os.O_WRONLY, 0755) if err != nil { panic(fmt.Sprintf("open %s failed,err: %v", filename, err)) } f.warnFile = file //异步写入日志 go f.writeLogBg() } func (f *FileLogger) writeLogBg() { //遍历 channel for logData := range f.LogDataChan { var file = f.debugFile if logData.AboveWarning { file = f.warnFile } //f.checkSplitFile(logData.WarnAndFatal) fmt.Fprintf(file, "%s %s (%s:%s:%d) %s\n", logData.TimeStr, logData.LevelStr, logData.Filename, logData.FuncName, logData.LineNo, logData.Message) } }
日志文件切分
控制日志文件,根据不同的切分策略,例如达到指定大小或指定时间间隔拆分为一个日志文件
const ( LogSplitTypeHour = iota LogSplitTypeSize )