打印日志是最常规的需求,GoLang 的基础库只提供了简单的格式化功能,但是很多基础功能不存在,例如日志级别、文件切割等等。
这里介绍 Uber 开发的一个日志库 ZAP 。
简介
通常来说,一个日志系统需要具有如下的功能:
- 将事件记录到文件中,而不是应用程序控制台。
- 日志切割,可以根据文件大小、时间等来切割日志文件。
- 支持不同的日志级别,例如 DEBUG、INFO、ERROR 等。
- 能够打印基本信息,如调用文件、函数名、行号、时间等。
在 GoLang 中提供了基本的 log 模块,不过功能很简单。
log
官方标准的日志库,使用方式与 fmt
库类似,只是默认输出时会添加时间信息。
package main
import "log"
func main() {
log.Println("Hello World")
}
其输出类似 2019/03/01 11:15:08 Hello World
。
格式定制
可以在启动时通过 init()
函数进行一些初始化操作,其中 log 库提供了一些常见的配置项,可以通过 func SetFlags(flag int)
设置。
const (
Ldate = 1 << iota // 日期示例: 2009/01/23
Ltime // 时间示例: 01:23:23
Lmicroseconds // 毫秒示例: 01:23:23.123123.
Llongfile // 绝对路径和行号: /a/b/c/d.go:23
Lshortfile // 文件和行号: d.go:23.
LUTC // 日期时间转为UTC时区的
LstdFlags = Ldate | Ltime // 默认的格式输出
)
也可以通过 func SetPrefix(prefix string)
设置日志的输出前缀。
package main
import "log"
func init() {
log.SetPrefix("[APP] ")
log.SetFlags(log.Ldate | log.Lmicroseconds | log.Lshortfile)
}
func main() {
log.Println("Hello World")
}
最终的输出为 [APP] 2019/03/01 11:17:10.316400 test.go:11: Hello World
。
输出到文件
上述都只是打印到终端,当然,可以在初始化时指定文件。
package main
import (
"fmt"
"log"
"os"
)
func init() {
fileName := "/tmp/test.log"
logFile, err := os.OpenFile(fileName, os.O_CREATE|os.O_APPEND|os.O_RDWR, 0744)
if err != nil {
fmt.Printf("Open log file '%s' failed, %s.", fileName, err)
} else {
log.SetOutput(logFile)
}
log.SetPrefix("[APP] ")
log.SetFlags(log.Ldate | log.Lmicroseconds | log.Lshortfile)
}
func main() {
log.Println("Hello World")
}
其它
最大的优势是简单,可以打印简单日志,提供了 Fatal
(通过 os.Exit(1)
退出程序)和 Panic
(消息写入日志后抛出 panic 报错)。前者会在打印日志后直接退出;后者如果没有使用 recover()
函数则会打印错误栈信息后退出。
ZAP
其它最常用的三方库有 logrus 以及 ZAP,后者的性能更好,而且通过三方库可以支持文件的自动切割,可以满足绝大部分的场景,这里就使用后者。
Zap 是 Uber 开源的高性能日志库,对性能和内存分配做了极致的优化,提供了日志常见的基础功能,而且配合一些三方库可以作为日志切割,这里简单介绍 ZAP 的使用。
安装
可以通过 go get -u go.uber.org/zap
命令安装,提供了两种类型的日志记录器,官网有关于两者的性能介绍。
Sugared Logger
通用日志格式,支持结构化和printf
风格的日志记录。Logger
性能更好,但是只支持强类型和结构化日志。
由于 fmt.Printf
之类的方法大量使用了 interface{}
和反射,会有性能损失,包括了内存分配次数,所以,zap 为了提供性能,没有使用反射,默认只支持强类型、结构化的日志。
package main
import (
"go.uber.org/zap"
)
var logger *zap.Logger
func main() {
logger, _ = zap.NewProduction()
defer logger.Sync()
logger.Info("Hello", zap.String("Name", "World"))
}
默认输出的是 JSON 结构,输出为。
{"level":"info","ts":1614570428.602909,"caller":"BasicAgent/main.go:38","msg":"Hello","Name":"World"}
上述就是所谓的强结构化类型,通过 zap.String
标识这个是字符串类型,还包括了一些其它的基础类型。
使用结构化类型就是复杂,所以也提供了 SugarLogger
方法,可以使用类似 fmt.Printf
的格式化。
package main
import (
"go.uber.org/zap"
)
var logger *zap.Logger
func main() {
logger, _ = zap.NewProduction()
defer logger.Sync()
sugar := logger.Sugar()
sugar.Infow("Hello", zap.String("Name", "World"))
sugar.Infof("Hello %s", "World")
}
其输出为。
{"level":"info","ts":1614570676.1768792,"caller":"BasicAgent/main.go:39","msg":"Hello","Name":"World"}
{"level":"info","ts":1614570676.1769674,"caller":"BasicAgent/main.go:40","msg":"Hello World"}
可以支持格式化以及 printf
类似语法。
日志的格式可以进行定制,除了上述的 NewProduction
之外,还有 NewDevelopment
和 NewExample
总共三个函数,其输出的格式有所区别,分别用在生产、开发、单元测试中,还可以通过 New
进行高度定制。
日志定制
直接通过 New()
进行定制,其关键数据结构是 zapcore.Core
,通过 NewCore()
函数创建,其关键参数为:
Encoder
决定如何写入日志,也即是日志格式。WriterSyncer
日志写到哪里,文件还是终端等。LogLevel
日志级别。
示例如下。
package main
import (
"os"
"go.uber.org/zap"
"go.uber.org/zap/zapcore"
)
var logger *zap.Logger
func main() {
file, err := os.Create("/tmp/test.log")
if err != nil {
return
}
logger := zap.New(zapcore.NewCore(
zapcore.NewJSONEncoder(zap.NewProductionEncoderConfig()),
zapcore.AddSync(file),
zapcore.DebugLevel,
))
defer logger.Sync()
sugar := logger.Sugar()
sugar.Infow("Hello", zap.String("Name", "World"))
sugar.Infof("Hello %s", "World")
}
上述因为日志文件打开方式不是 APPEND
的,所以每次运行都会覆盖。
日志级别动态修改
Zap 允许动态修改日志级别,不过需要有些特殊设置。
package main
import (
"os"
"go.uber.org/zap"
"go.uber.org/zap/zapcore"
)
func main() {
atom := zap.NewAtomicLevel()
// To keep the example deterministic, disable timestamps in the output.
encoderCfg := zap.NewProductionEncoderConfig()
encoderCfg.TimeKey = ""
logger := zap.New(zapcore.NewCore(
zapcore.NewJSONEncoder(encoderCfg),
zapcore.Lock(os.Stdout),
atom,
))
defer logger.Sync()
logger.Info("info logging enabled")
atom.SetLevel(zap.ErrorLevel)
logger.Info("info logging disabled")
}
日志切割
Zap 默认是不支持日志切割的,可以通过 github.com/natefinch/lumberjack
库实现,包括了标准的日志输出,也可以通过该库实现切割。
package main
import (
"go.uber.org/zap"
"go.uber.org/zap/zapcore"
"gopkg.in/natefinch/lumberjack.v2"
)
var logger *zap.Logger
func main() {
logger := zap.New(zapcore.NewCore(
zapcore.NewJSONEncoder(zap.NewProductionEncoderConfig()),
zapcore.AddSync(&lumberjack.Logger{
Filename: "/tmp/test.log",
MaxSize: 50,
MaxBackups: 4,
MaxAge: 30,
Compress: true,
}),
zapcore.DebugLevel,
))
defer logger.Sync()
sugar := logger.Sugar()
sugar.Infow("Hello", zap.String("Name", "World"))
sugar.Infof("Hello %s", "World")
}
在使用时,可以将日志设置单独抽象一个包,然后其它模块直接使用即可。