构造一个golang logger
一个实用的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 }