构造一个golang logger

作者: adm 分类: go 发布时间: 2023-03-28

一个实用的logger需要提供以下这些功能:

支持把日志写入多个输出流中,比如可以选择性的让测试、开发环境同时向控制台和日志文件输出日志,生产环境只输出到日志文件中
支持多级别的日志等级,常见的有:TRACE、DEBUG、INFO、WARN、ERROR、PANIC等
支持结构化输出,结构化输出常用的就是JSON格式,这样可以让统一日志平台通过logstash之类的组件把日志聚合到日志平台上
需要支持日志切割log rotation
在log entry中除了主动记录的信息外,还要包括如打印日志的函数、所在的文件、行号、记录时间等

1. Log日志库
使用log记录日志,默认会输出到控制台,比如下面这个例子:

func main() {
	simpleHTTPGet("www.baidu.com")
	simpleHTTPGet("https://www.baidu.com")
}
 
func simpleHTTPGet(url string) {
	resp, err := http.Get(url)
	if err != nil {
		log.Printf("Error fetching url %s: %s", url, err.Error())
	} else {
		log.Printf("Status Code for %s: %s", url, resp.Status)
		resp.Body.Close()
	}
	return
}
 

输出信息如下:

2022/05/31 10:40:45 Error fetching url www.baidu.com: Get "www.baidu.com": unsupported protocol scheme ""
2022/05/31 10:40:45 Status Code for https://www.baidu.com: 200 OK
 

go原生的logger也支持把日志输出到指定的文件中,通过log.SetOutput可以把任何io.Writer的实现设置成日志的输出。我们把日志输出到一个指定文件:

func main() {
	setupLogger()
	simpleHTTPGet("www.baidu.com")
	simpleHTTPGet("https://www.baidu.com")
}
 
func setupLogger() {
	logFileLocation, _ := os.OpenFile("/tmp/test.log", os.O_CREATE|os.O_APPEND|os.O_RDWR, 0644)
	log.SetOutput(logFileLocation)
}
 

原生logger用法非常简单,对于一些简单的开发调试来讲基本是适用的,但是用在项目中存在着以下不足:

仅限基本的日志级别,只有一个Print选项
对于错误日志,有Fatal和Panic,不支持Error
无结构化能力,只是简单的文本输出
没有日志切割能力

2. Zap日志库
zap是Uber开源的日志库,具备高性能的特性

zap高性能的一大原因是,不用反射。日志里每个要写入的字段都要携带类型

logger.Info(
    "Success...",
    zap.String("statusCode", resp.Status),
    zap.String("url",url))
 

上面向日志里写入了一条记录,Message是”Success…”,另外写入了两个字符串键值对。

zap针对日志里要写入的字段,每个类型都有一个对应的方法将字段转成zap.Field类型,比如:

zap.Int('key', 123)
zap.Bool('key', true)
zap.Error('err', err)
zap.Any('arbitraryType', &User{})
 

2.1 zap的简单使用
首先需要引入依赖

$ go get -u go.uber.org/zap
 

之后我们做一下简单的初始化工作就可以使用zap logger了,其实zap提供了三种初始化方法,我们就使用zap.NewProduction()即可

我们简单修改一下之前的代码,引入zap.logger:

var logger *zap.Logger
 
func main() {
	simpleHttpGet("www.baidu.com")
	simpleHttpGet("https://www.baidu.com")
}
 
func simpleHttpGet(url string) {
	resp, err := http.Get(url)
	if err != nil {
		logger.Error("Failed...", zap.String("Error", err.Error()))
	} else {
		logger.Info("Success...", zap.String("StatusCode", resp.Status), zap.String("Url", url))
		resp.Body.Close()
	}
}
 
func init() {
	logger, _ = zap.NewProduction()
}

运行程序,可以在控制台看到更加详细的输出,其中包括了go原生log不支持的一些信息,包括调用栈信息、时间戳、日志等级,json格式化输出

{"level":"error","ts":1654150127.123799,"caller":"logger/main.go:27","msg":"Failed...","Error":"Get \"www.baidu.com\": unsupported protocol scheme \"\"","stacktrace":"main.simpleHttpGet\n\t/Users/pangjiping/gopath/src/blog/logger/main.go:27\nmain.main\n\t/Users/pangjiping/gopath/src/blog/logger/main.go:14\nruntime.main\n\t/usr/local/go/src/runtime/proc.go:255"}
{"level":"info","ts":1654150127.293559,"caller":"logger/main.go:30","msg":"Success...","StatusCode":"200 OK","Url":"https://www.baidu.com"}
 

2.2 zap的定制化
对zap做简单定制,让其将日志输出到指定文件,并且将时间戳转为日期的格式

修改init()函数完成logger的初始化工作即可

func init() {
	encoderConfig := zap.NewProductionEncoderConfig()
	// 设置日志记录中时间的格式
	encoderConfig.EncodeTime = zapcore.ISO8601TimeEncoder
	// 日志encoder还是json encoder,把日志行格式化程json格式的
	encoder := zapcore.NewJSONEncoder(encoderConfig)
 
	file, _ := os.OpenFile("./test.log", os.O_CREATE|os.O_APPEND|os.O_WRONLY, 0644)
	fileWriteSyncer := zapcore.AddSync(file)
 
	core := zapcore.NewTee(
		// 控制台输出
		zapcore.NewCore(encoder, zapcore.AddSync(os.Stdout), zapcore.DebugLevel),
		// 文件输出
		zapcore.NewCore(encoder, fileWriteSyncer, zapcore.DebugLevel),
	)
	logger = zap.New(core)
}
 

现在我们在控制台得到的输出日志为:

{"level":"error","ts":"2022-06-02T14:23:19.639+0800","msg":"Failed...","Error":"Get \"www.baidu.com\": unsupported protocol scheme \"\""}
{"level":"info","ts":"2022-06-02T14:23:19.815+0800","msg":"Success...","StatusCode":"200 OK","Url":"https://www.baidu.com"}
 

2.3 日志切割
zap本身不支持日志切割,可以借助另一个库lumberjack完成日志切割

func getFileLogWriter() (writeSyncer zapcore.WriteSyncer) {
	// 使用 lumberjack 实现 log rotate
	lumberJackLogger := &lumberjack.Logger{
		Filename:   "/tmp/test.log",
		MaxSize:    100, // 单个文件最大100M
		MaxBackups: 60,  // 多于 60 个日志文件后,清理较旧的日志
		MaxAge:     1,   // 一天一切割
		Compress:   false,
	}
 
	return zapcore.AddSync(lumberJackLogger)
}
 

2.4 封装
package zlog

import (
	"os"
	"path"
	"runtime"
 
	"go.uber.org/zap"
	"go.uber.org/zap/zapcore"
	"gopkg.in/natefinch/lumberjack.v2"
)
 
var logger *zap.Logger
 
func init() {
	encoderConfig := zap.NewProductionEncoderConfig()
	encoderConfig.EncodeTime = zapcore.ISO8601TimeEncoder
	encoder := zapcore.NewJSONEncoder(encoderConfig)
 
	fileWriteSyncer := getFileLogWriter()
 
	core := zapcore.NewTee(
		zapcore.NewCore(encoder, zapcore.AddSync(os.Stdout), zap.DebugLevel),
		zapcore.NewCore(encoder, fileWriteSyncer, zapcore.DebugLevel),
	)
 
	logger = zap.New(core)
}
 
func getFileLogWriter() (writeSyncer zapcore.WriteSyncer) {
	lumberJackLogger := &lumberjack.Logger{
		Filename:   "./debug.log",
		MaxSize:    100,
		MaxBackups: 60,
		MaxAge:     1,
		Compress:   false,
	}
 
	return zapcore.AddSync(lumberJackLogger)
}
 
func Info(message string, fields ...zap.Field) {
	callerFields := getCallerInfoForLog()
	fields = append(fields, callerFields...)
	logger.Info(message, fields...)
}
 
func Debug(message string, fields ...zap.Field) {
	callerFields := getCallerInfoForLog()
	fields = append(fields, callerFields...)
	logger.Debug(message, fields...)
}
 
func Error(message string, fields ...zap.Field) {
	callerFields := getCallerInfoForLog()
	fields = append(fields, callerFields...)
	logger.Error(message, fields...)
}
 
func Warn(message string, fields ...zap.Field) {
	callerFields := getCallerInfoForLog()
	fields = append(fields, callerFields...)
	logger.Warn(message, fields...)
}
 
func getCallerInfoForLog() (callerFields []zap.Field) {
	pc, file, line, ok := runtime.Caller(2)
	if !ok {
		return
	}
 
	funcName := runtime.FuncForPC(pc).Name()
	funcName = path.Base(funcName) // 只保留函数名
 
	callerFields = append(callerFields, zap.String("func", funcName), zap.String("file", file), zap.Int("line", line))
	return
}

如果觉得我的文章对您有用,请随意赞赏。您的支持将鼓励我继续创作!