Go 日志处理
1、背景
日志文件一般用于记录操作系统或其他软件运行时发生的事件,或通信软件不同用户之间的消息。如果有一些问题需要对程序进行调试或故障排查时,日志是必不可少的,这是我们分析程序问题常用的手段。
2、操作前了解相关配置和要求
zap + lumberjack 实现日志切割归档的机制:
3、操作步骤
3.1 配置 Logger
zap 提供了两种类型的日志记录器 Logger 和 Sugared Logger,区别是:
在每一微秒和每一次内存分配都很重要的上下文中,使用 Logger。它比 Sugared Logger 更快,内存分配次数也更少,但它只支持强类型的结构化日志记录。
在性能很好但不是很关键的上下文中,使用 SugaredLogger。它比其他结构化日志记录包快 4~10 倍,并且支持结构化和 printf 风格的日志记录。
3.1.1 Logger
通过调用
zap.NewProduction()
,zap.NewDevelopment()
或者zap.NewExample()
创建一个 Logger。上面的每一个函数都将创建一个 Logger,唯一的区别在于它将记录的信息不同。
我们可以通过 Logger 调用 Debug, Info, Warn, Error 等方法来记录不同级别的日志。
3.1.1.1 NewExample
package main import ( "go.uber.org/zap" ) func main() { log := zap.NewExample() log.Debug("this is debug message") log.Info("this is info message") log.Info("this is info message with fileds", zap.Int("age", 24), zap.String("agender", "man")) log.Warn("this is warn message") log.Error("this is error message") log.Panic("this is panic message") }
{"level":"debug","msg":"this is debug message"} {"level":"info","msg":"this is info message"} {"level":"info","msg":"this is info message with fileds","age":24,"agender":"man"} {"level":"warn","msg":"this is warn message"} {"level":"error","msg":"this is error message"} {"level":"panic","msg":"this is panic message"} panic: this is panic message
3.1.1.2 NewDevelopment
package main import ( "go.uber.org/zap" ) func main() { log, _ := zap.NewDevelopment() log.Debug("this is debug message") log.Info("this is info message") log.Info("this is info message with fileds", zap.Int("age", 24), zap.String("agender", "man")) log.Warn("this is warn message") log.Error("this is error message") // log.DPanic("This is a DPANIC message") // log.Panic("this is panic message") // log.Fatal("This is a FATAL message") }
2023-01-02T16:24:07.904+0800 DEBUG main/demo.go:9 this is debug message 2023-01-02T16:24:07.905+0800 INFO main/demo.go:10 this is info message 2023-01-02T16:24:07.905+0800 INFO main/demo.go:11 this is info message with fileds {"age": 24, "agender": "man"} 2023-01-02T16:24:07.905+0800 WARN main/demo.go:12 this is warn message main.main /Users/zhangyy/Documents/Gitlab/yannic/golang/zhangyydev.com/golang-basics/main/demo.go:12 runtime.main /opt/homebrew/Cellar/go/1.19.4/libexec/src/runtime/proc.go:250 2023-01-02T16:24:07.905+0800 ERROR main/demo.go:13 this is error message main.main /Users/zhangyy/Documents/Gitlab/yannic/golang/zhangyydev.com/golang-basics/main/demo.go:13 runtime.main /opt/homebrew/Cellar/go/1.19.4/libexec/src/runtime/proc.go:250
3.1.1.3 NewProduction
package main import ( "go.uber.org/zap" ) func main() { log, _ := zap.NewProduction() log.Debug("this is debug message") log.Info("this is info message") log.Info("this is info message with fileds", zap.Int("age", 24), zap.String("agender", "man")) log.Warn("this is warn message") log.Error("this is error message") // log.DPanic("This is a DPANIC message") // log.Panic("this is panic message") // log.Fatal("This is a FATAL message") }
{"level":"info","ts":1672648032.9237802,"caller":"main/demo.go:10","msg":"this is info message"} {"level":"info","ts":1672648032.923952,"caller":"main/demo.go:11","msg":"this is info message with fileds","age":24,"agender":"man"} {"level":"warn","ts":1672648032.9239638,"caller":"main/demo.go:12","msg":"this is warn message"} {"level":"error","ts":1672648032.9239721,"caller":"main/demo.go:13","msg":"this is error message","stacktrace":"main.main\n\t/Users/zhangyy/Documents/Gitlab/yannic/golang/zhangyydev.com/golang-basics/main/demo.go:13\nruntime.main\n\t/opt/homebrew/Cellar/go/1.19.4/libexec/src/runtime/proc.go:250"}
3.1.1.4 对比总结
Example 和 Production 使用的是 JSON 格式输出,Development 使用行的形式输出。
Example
Debug 级别日志不记录
Caller 信息、时间信息不记录
以小写形式打印级别名称
Production
Debug 级别日志不记录
Error, Dpanic 级别的记录,会在堆栈中跟踪文件,Warn 不会
始终将调用者添加到文件中
以时间戳格式打印日期
以小写形式打印级别名称
Development
从警告级别向上打印到堆栈中来跟踪
始终打印包/文件/行(方法)
在行尾添加任何额外字段作为 JSON 字符串
以大写形式打印级别名称
以毫秒为单位打印 ISO8601 格式的时间戳
3.1.2 Sugared Logger
Sugared Logger 对 Logger 进行了封装,以提供更符合人体工程学但速度稍慢的 API。Sugaring Logger 的成本非常低,因此单个应用程序同时使用 Loggers 和 SugaredLoggers 是合理的,并在性能敏感代码的边界上在它们之间进行转换。
默认的 zap 记录器需要结构化标签,需要使用特定值类型的函数:
log := zap.NewExample() log.Info("this is info message with fields", zap.Int("age",24), zap.String("agender","man"))
这样虽然会显的很长,但是对性能要求较高的话,这是最快的选择。
当然我们也可以使用 Sugared Logger,它基于 printf 分割的反射类型检测,提供更简单的语法来添加混合类型的标签。
package main import ( "go.uber.org/zap" ) func main() { logger, _ := zap.NewDevelopment() slogger := logger.Sugar() slogger.Debugf("debug message age is %d, agender is %s", 19, "man") slogger.Info("Info() uses sprint") slogger.Infof("Infof() uses %s", "sprintf") slogger.Infow("Infow() allows tags", "name", "Legolas", "type", 1) }
2023-01-02T17:12:55.345+0800 DEBUG main/demo.go:11 debug message age is 19, agender is man 2023-01-02T17:12:55.345+0800 INFO main/demo.go:12 Info() uses sprint 2023-01-02T17:12:55.345+0800 INFO main/demo.go:13 Infof() uses sprintf 2023-01-02T17:12:55.346+0800 INFO main/demo.go:14 Infow() allows tags {"name": "Legolas", "type": 1}
如果需要,可以随时使用记录器上的 slogger.Desugar()
方法从 sugar logger 切换到标准记录器。
logger, _ := zap.NewDevelopment() // Logger >>> Sugared Logger slogger := logger.Sugar() // Sugared Logger >>> Logger logger = slogger.Desugar()
3.2 日志写入文件
我们将使用 zap.New() 方法来手动传递所有配置,而不是使用像 zap.NewProduction() 这样的预置方法来创建 Logger。
func New(core zapcore.Core, options ...Option) *Logger
zapcore.Core
需要三个配置:Encoder, WriteSyncer, LogLevel
Encoder(编码器),我们将使用开箱即用的
NewConsoleEncoder()
,并使用预先设置的ProductionEncoderConfig()
。
zapcore.NewConsoleEncoder(zap.NewProductionEncoderConfig())
writeSyncer,指定日志将写到那里去,我们使用
zapcore.AddSync()
函数并且将打开的文件句柄传进去。
file,_ := os.Create("./test.log") writeSyncer := zapcore.AddSync(file)
Log Level,定义哪种级别的日志将被写入。
实例:
package main import ( "os" "go.uber.org/zap" "go.uber.org/zap/zapcore" ) var sugarLogger *zap.SugaredLogger func InitLogger() { encoder := getEncoder() writeSyncer := getLogWriter() core := zapcore.NewCore(encoder, writeSyncer, zapcore.DebugLevel) logger := zap.New(core, zap.AddCaller()) // 添加将调用函数信息记录到日志中的功能 sugarLogger = logger.Sugar() } func getEncoder() zapcore.Encoder { encoderConfig := zap.NewProductionEncoderConfig() encoderConfig.EncodeTime = zapcore.ISO8601TimeEncoder // 修改时间编码器 encoderConfig.EncodeLevel = zapcore.CapitalLevelEncoder // 在日志文件中使用大写字母记录日志级别 return zapcore.NewConsoleEncoder(encoderConfig) // NewConsoleEncoder 打印更符合人们观察的方式 } func getLogWriter() zapcore.WriteSyncer { file, _ := os.Create("./test.log") return zapcore.AddSync(file) } func main() { InitLogger() sugarLogger.Info("this is info message") sugarLogger.Infof("this is info message, %s, %d", "Infof", 666) sugarLogger.Error("this is error message") sugarLogger.Info("this is info message") }
2023-01-02T17:42:36.952+0800 INFO main/demo.go:36 this is info message 2023-01-02T17:42:36.953+0800 INFO main/demo.go:37 this is info message, Infof, 666 2023-01-02T17:42:36.953+0800 ERROR main/demo.go:38 this is error message 2023-01-02T17:42:36.953+0800 INFO main/demo.go:39 this is info message
3.3 日志切割归档
因为 zap 本身不支持切割归档日志文件,为了添加日志切割归档功能,我们将使用第三方库 lumberjack 来实现。
3.3.1 安装
命令:go get github.com/natefinch/lumberjack
3.3.2 lumberjack + zap
要在 zap 中加入 lumberjack 支持,我们需要修改 WriteSyncer 代码。我们将按照下面的代码修改 getLogWriter()
函数:
func getLogWriter() zapcore.WriteSyncer { lumberJackLogger := &lumberjack.Logger{ Filename: "./test.log", // 日志文件的位置 MaxSize: 10, // 在进行切割之前,日志文件的最大大小(以MB为单位) MaxBackups: 5, // 保留旧文件的最大个数 MaxAge: 30, // 保留旧文件的最大天数 Compress: false, // 是否压缩/归档旧文件 } return zapcore.AddSync(lumberJackLogger) }
完整代码:
package main import ( "github.com/natefinch/lumberjack" "go.uber.org/zap" "go.uber.org/zap/zapcore" ) var sugarLogger *zap.SugaredLogger func InitLogger() { encoder := getEncoder() writeSyncer := getLogWriter() core := zapcore.NewCore(encoder, writeSyncer, zapcore.DebugLevel) logger := zap.New(core, zap.AddCaller()) // 添加将调用函数信息记录到日志中的功能 sugarLogger = logger.Sugar() } func getEncoder() zapcore.Encoder { encoderConfig := zap.NewProductionEncoderConfig() encoderConfig.EncodeTime = zapcore.ISO8601TimeEncoder // 修改时间编码器 encoderConfig.EncodeLevel = zapcore.CapitalLevelEncoder // 在日志文件中使用大写字母记录日志级别 return zapcore.NewConsoleEncoder(encoderConfig) // NewConsoleEncoder 打印更符合人们观察的方式 } func getLogWriter() zapcore.WriteSyncer { lumberJackLogger := &lumberjack.Logger{ Filename: "./test.log", // 日志文件的位置 MaxSize: 10, // 在进行切割之前,日志文件的最大大小(以MB为单位) MaxBackups: 5, // 保留旧文件的最大个数 MaxAge: 30, // 保留旧文件的最大天数 Compress: false, // 是否压缩/归档旧文件 } return zapcore.AddSync(lumberJackLogger) } func main() { InitLogger() sugarLogger.Info("this is info message") sugarLogger.Infof("this is info message, %s, %d", "Infof", 666) sugarLogger.Error("this is error message") sugarLogger.Info("this is info message") }
4、注意事项
应用程序同时使用 Loggers 和 SugaredLoggers 是合理的,但请注意在性能敏感代码的边界上在它们之间进行转换。
5、结果检查
对所配置的日志系统进行测试运行。