这是我参与「第三届青训营 -后端场」笔记创作活动的第三篇笔记
程序运行状况是个黑盒,无论是在项目开发过程中,或是项目在线上运行时需要通过日志了解程序运行状况,日志库则是必不可少的组件。 对于一个可用的日志库,基本的功能需求有:
- 日志对象构建
- 日志分级
- 打印各个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{})
}
其中的日志级别,可用采用 const 配合 iota 实现枚举定义
文件日志
定义文件日志 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 Debug 以下级别日志文件
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 warning 及以上级别日志文件
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
}
需要注意的的是,获取代码执行的行号是通过 runtime 包下的 Caller 方法实现 其中的 (skip int) 表示调用栈的深度,也就是说,每一次对这个方法封装,实际执行的代码的深度就会 + 1 这个参数也必须 + 1 才能获取到真正执行代码的行
可优化的地方
易用性封装
日志模块通常是单例存在的,可以考虑封装一个初始化日志对象的函数来获得日志对象
异步写入
日志文件写入的 IO 操作相对内存来说耗时长,当并发量足够大时文件操作可能会阻塞,从而影响到业务代码的执行效率
解决方法是把同步写日志这个操作替换成异步写入:
- 将日志数据写入 channel
- 运行goroutine取出channel的日志数据写入到日志文件中