实现可在项目中使用的日志库|青训营笔记

202 阅读3分钟

这是我参与「第三届青训营 -后端场」笔记创作活动的第三篇笔记

程序运行状况是个黑盒,无论是在项目开发过程中,或是项目在线上运行时需要通过日志了解程序运行状况,日志库则是必不可少的组件。 对于一个可用的日志库,基本的功能需求有:

  • 日志对象构建
  • 日志分级
    • 打印各个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 才能获取到真正执行代码的行

image.png

可优化的地方

易用性封装

日志模块通常是单例存在的,可以考虑封装一个初始化日志对象的函数来获得日志对象

异步写入

日志文件写入的 IO 操作相对内存来说耗时长,当并发量足够大时文件操作可能会阻塞,从而影响到业务代码的执行效率

解决方法是把同步写日志这个操作替换成异步写入:

  • 将日志数据写入 channel
  • 运行goroutine取出channel的日志数据写入到日志文件中