Zap
Zap là một thành phần log nhanh, có cấu trúc và phân cấp mức độ được xây dựng bằng Go.
Kho lưu trữ chính thức: uber-go/zap: Blazing fast, structured, leveled logging in Go. (github.com)
Tài liệu chính thức: zap package - go.uber.org/zap - Go Packages
Cài đặt
go get -u go.uber.org/zapBắt đầu nhanh
Official đã cung cấp hai ví dụ bắt đầu nhanh, cả hai đều là log cấp production, đầu tiên là Sugar hỗ trợ phong cách printf nhưng hiệu suất thấp hơn.
logger, _ := zap.NewProduction()
defer logger.Sync() // Đồng bộ bộ đệm vào file khi kết thúc chương trình
sugar := logger.Sugar()
sugar.Infow("failed to fetch URL",
"url", url,
"attempt", 3,
"backoff", time.Second,
)
sugar.Infof("Failed to fetch URL: %s", url)Thứ hai là logger có hiệu suất tốt hơn nhưng chỉ hỗ trợ output kiểu mạnh.
logger, _ := zap.NewProduction()
defer logger.Sync()
logger.Info("failed to fetch URL",
// Structured context as strongly typed Field values.
zap.String("url", url),
zap.Int("attempt", 3),
zap.Duration("backoff", time.Second),
)TIP
Việc sử dụng Zap rất đơn giản, điểm phức tạp là cấu hình log phù hợp cho dự án của bạn, các ví dụ chính thức rất ít, cần đọc nhiều comment trong source code.
Cấu hình
Nói chung cấu hình log thường được viết trong file cấu hình, Zap cũng hỗ trợ deserialize từ file cấu hình, nhưng chỉ hỗ trợ cấu hình cơ bản, ngay cả các ví dụ về cấu hình nâng cao mà official cung cấp cũng rất đơn giản, không đủ để đưa vào sử dụng, vì vậy cần phải nói chi tiết về các cấu hình.
Đầu tiên hãy xem cấu trúc tổng thể của cấu hình, cần phải hiểu ý nghĩa của từng trường
type Config struct {
// Mức log tối thiểu
Level AtomicLevel `json:"level" yaml:"level"`
// Chế độ phát triển, chủ yếu ảnh hưởng đến stack trace
Development bool `json:"development" yaml:"development"`
// Theo dõi caller
DisableCaller bool `json:"disableCaller" yaml:"disableCaller"`
// Stack trace
DisableStacktrace bool `json:"disableStacktrace" yaml:"disableStacktrace"`
// Sampling, trong trường hợp giới hạn log chỉ ghi nhận một phần log có tính đại diện, tương đương với log chọn lọc
Sampling *SamplingConfig `json:"sampling" yaml:"sampling"`
// Encoding, chia thành hai chế độ json và console
Encoding string `json:"encoding" yaml:"encoding"`
// Cấu hình encoder, chủ yếu là một số cấu hình định dạng output
EncoderConfig zapcore.EncoderConfig `json:"encoderConfig" yaml:"encoderConfig"`
// Đường dẫn output file log
OutputPaths []string `json:"outputPaths" yaml:"outputPaths"`
// Đường dẫn file lỗi output
ErrorOutputPaths []string `json:"errorOutputPaths" yaml:"errorOutputPaths"`
// Thêm một số nội dung output mặc định cho log
InitialFields map[string]interface{} `json:"initialFields" yaml:"initialFields"`
}Dưới đây là chi tiết về cấu hình encoding
type EncoderConfig struct {
// Key value, nếu key rỗng thì thuộc tính tương ứng sẽ không được output
MessageKey string `json:"messageKey" yaml:"messageKey"`
LevelKey string `json:"levelKey" yaml:"levelKey"`
TimeKey string `json:"timeKey" yaml:"timeKey"`
NameKey string `json:"nameKey" yaml:"nameKey"`
CallerKey string `json:"callerKey" yaml:"callerKey"`
FunctionKey string `json:"functionKey" yaml:"functionKey"`
StacktraceKey string `json:"stacktraceKey" yaml:"stacktraceKey"`
SkipLineEnding bool `json:"skipLineEnding" yaml:"skipLineEnding"`
LineEnding string `json:"lineEnding" yaml:"lineEnding"`
// Một số encoder tùy chỉnh
EncodeLevel LevelEncoder `json:"levelEncoder" yaml:"levelEncoder"`
EncodeTime TimeEncoder `json:"timeEncoder" yaml:"timeEncoder"`
EncodeDuration DurationEncoder `json:"durationEncoder" yaml:"durationEncoder"`
EncodeCaller CallerEncoder `json:"callerEncoder" yaml:"callerEncoder"`
// Encoder tên logger
EncodeName NameEncoder `json:"nameEncoder" yaml:"nameEncoder"`
// Encoder reflection, chủ yếu cho kiểu interface{}, nếu không có mặc định jsonencoder
NewReflectedEncoder func(io.Writer) ReflectedEncoder `json:"-" yaml:"-"`
// Chuỗi cách ly output console
ConsoleSeparator string `json:"consoleSeparator" yaml:"consoleSeparator"`
}Option là công tắc và ứng dụng về một số cấu hình, có nhiều implementation.
type Option interface {
apply(*Logger)
}
// Implementation của Option
type optionFunc func(*Logger)
func (f optionFunc) apply(log *Logger) {
f(log)
}
// Ứng dụng
func Development() Option {
return optionFunc(func(log *Logger) {
log.development = true
})
}Đây là logger core được sử dụng phổ biến nhất, các trường bên trong về cơ bản đại diện cho các bước cấu hình của chúng ta, cũng có thể tham khảo các bước mà official thực hiện khi deserialize cấu hình, đại khái đều giống nhau.
type ioCore struct {
// Mức log
LevelEnabler
// Encoding log
enc Encoder
// Ghi log
out WriteSyncer
}zap.Encoder chịu trách nhiệm định dạng và encoding log
zap.WriteSyncer chịu trách nhiệm output log, chủ yếu là output ra file và console
zap.LevelEnabler mức log tối thiểu, log dưới mức này sẽ không được output thông qua syncer.
Encoding Log
Encoding log chủ yếu liên quan đến việc định dạng chi tiết log, đầu tiên hãy xem output của log gốc trực tiếp.
func TestQuickStart(t *testing.T) {
rawJSON := []byte(`{
"level": "debug",
"encoding": "json",
"outputPaths": ["stdout"],
"errorOutputPaths": ["stderr"],
"initialFields": {"foo": "bar"},
"encoderConfig": {
"messageKey": "message",
"levelKey": "level",
"levelEncoder": "lowercase"
}
}`)
var cfg zap.Config
if err := json.Unmarshal(rawJSON, &cfg); err != nil {
panic(err)
}
logger := zap.Must(cfg.Build())
defer logger.Sync()
logger.Info("logger construction succeeded")
}{"level":"info","message":"logger construction succeeded","foo":"bar"}Sẽ thấy log này có một số vấn đề:
- Không có thời gian
- Không có thông tin caller, không biết log này được output từ đâu, nếu xảy ra lỗi sẽ không thể debug
- Không có stack trace
Tiếp theo từng bước giải quyết vấn đề, chủ yếu là cải tạo zapcore.EncoderConfig, đầu tiên chúng ta cần tự viết file cấu hình, không sử dụng deserialize trực tiếp của official. Đầu tiên tạo một file cấu hình config.yml
# Cấu hình Log Zap
zap:
prefix: ZapLogTest
timeFormat: 2006/01/02 - 15:04:05.00000
level: debug
caller: true
stackTrace: false
encode: console
# Log output đi đâu file | console | both
writer: both
logFile:
maxSize: 20
backups: 5
compress: true
output:
- "./log/output.log"Mapping đến struct
// ZapConfig
// @Date: 2023-01-09 16:37:05
// @Description: Struct cấu hình log zap
type ZapConfig struct {
Prefix string `yaml:"prefix" mapstructure:"prefix"`
TimeFormat string `yaml:"timeFormat" mapstructure:"timeFormat"`
Level string `yaml:"level" mapstructure:"level"`
Caller bool `yaml:"caller" mapstructure:"caller"`
StackTrace bool `yaml:"stackTrace" mapstructure:"stackTrace"`
Writer string `yaml:"writer" mapstructure:"writer"`
Encode string `yaml:"encode" mapstructure:"encode"`
LogFile *LogFileConfig `yaml:"logFile" mapstructure:"logFile"`
}
// LogFileConfig
// @Date: 2023-01-09 16:38:45
// @Description: Struct cấu hình file log
type LogFileConfig struct {
MaxSize int `yaml:"maxSize" mapstructure:"maxSize"`
BackUps int `yaml:"backups" mapstructure:"backups"`
Compress bool `yaml:"compress" mapstructure:"compress"`
Output []string `yaml:"output" mapstructure:"output"`
Errput []string `yaml:"errput" mapstructure:"errput"`
}TIP
Đọc cấu hình sử dụng Viper, code cụ thể được bỏ qua.
type TimeEncoder func(time.Time, PrimitiveArrayEncoder)TimeEncoder thực chất là một hàm, chúng ta có thể sử dụng các encoder thời gian khác do official cung cấp, cũng có thể tự viết.
func CustomTimeFormatEncoder(t time.Time, encoder zapcore.PrimitiveArrayEncoder) {
encoder.AppendString(global.Config.ZapConfig.Prefix + "\t" + t.Format(global.Config.ZapConfig.TimeFormat))
}Phần tổng thể như sau
func zapEncoder(config *ZapConfig) zapcore.Encoder {
// Tạo mới một cấu hình
encoderConfig := zapcore.EncoderConfig{
TimeKey: "Time",
LevelKey: "Level",
NameKey: "Logger",
CallerKey: "Caller",
MessageKey: "Message",
StacktraceKey: "StackTrace",
LineEnding: zapcore.DefaultLineEnding,
FunctionKey: zapcore.OmitKey,
}
// Định dạng thời gian tùy chỉnh
encoderConfig.EncodeTime = CustomTimeFormatEncoder
// Mức log viết hoa
encoderConfig.EncodeLevel = zapcore.CapitalColorLevelEncoder
// Khoảng thời gian tính bằng giây
encoderConfig.EncodeDuration = zapcore.SecondsDurationEncoder
// Output caller ngắn gọn
encoderConfig.EncodeCaller = zapcore.ShortCallerEncoder
// SerializedName logger đầy đủ
encoderConfig.EncodeName = zapcore.FullNameEncoder
// Encoding log cuối cùng json hoặc console
switch config.Encode {
case "json":
{
return zapcore.NewJSONEncoder(encoderConfig)
}
case "console":
{
return zapcore.NewConsoleEncoder(encoderConfig)
}
}
// Mặc định console
return zapcore.NewConsoleEncoder(encoderConfig)
}Output Log
Output log chia thành output console và output file, chúng ta có thể cấu hình động dựa trên file cấu hình, và nếu muốn cắt file log còn cần sử dụng một dependency bên thứ ba khác.
go get -u github.com/natefinch/lumberjackCuối cùng code như sau
func zapWriteSyncer(cfg *ZapConfig) zapcore.WriteSyncer {
syncers := make([]zapcore.WriteSyncer, 0, 2)
// Nếu bật output console log, thêm writer console
if cfg.Writer == config.WriteBoth || cfg.Writer == config.WriteConsole {
syncers = append(syncers, zapcore.AddSync(os.Stdout))
}
// Nếu bật lưu file log, thêm writer theo đường dẫn file
if cfg.Writer == config.WriteBoth || cfg.Writer == config.WriteFile {
// Thêm output log
for _, path := range cfg.LogFile.Output {
logger := &lumberjack.Logger{
Filename: path, // Đường dẫn file
MaxSize: cfg.LogFile.MaxSize, // Kích thước file cắt
MaxBackups: cfg.LogFile.BackUps, // Số lần backup
Compress: cfg.LogFile.Compress, // Có nén hay không
LocalTime: true, // Sử dụng thời gian địa phương
}
syncers = append(syncers, zapcore.Lock(zapcore.AddSync(logger)))
}
}
return zap.CombineWriteSyncers(syncers...)
}Mức Log
Official có các enum về mức log, chỉ cần sử dụng trực tiếp.
func zapLevelEnabler(cfg *ZapConfig) zapcore.LevelEnabler {
switch cfg.Level {
case config.DebugLevel:
return zap.DebugLevel
case config.InfoLevel:
return zap.InfoLevel
case config.ErrorLevel:
return zap.ErrorLevel
case config.PanicLevel:
return zap.PanicLevel
case config.FatalLevel:
return zap.FatalLevel
}
// Mặc định mức Debug
return zap.DebugLevel
}Cuối cùng Build
func InitZap(config *ZapConfig) *zap.Logger {
// Build encoder
encoder := zapEncoder(config)
// Build mức log
levelEnabler := zapLevelEnabler(config)
// Cuối cùng lấy Core và Options
subCore, options := tee(config, encoder, levelEnabler)
// Tạo Logger
return zap.New(subCore, options...)
}
// Gộp tất cả lại
func tee(cfg *ZapConfig, encoder zapcore.Encoder, levelEnabler zapcore.LevelEnabler) (core zapcore.Core, options []zap.Option) {
sink := zapWriteSyncer(cfg)
return zapcore.NewCore(encoder, sink, levelEnabler), buildOptions(cfg, levelEnabler)
}
// Build Option
func buildOptions(cfg *ZapConfig, levelEnabler zapcore.LevelEnabler) (options []zap.Option) {
if cfg.Caller {
options = append(options, zap.AddCaller())
}
if cfg.StackTrace {
options = append(options, zap.AddStacktrace(levelEnabler))
}
return
}Hiệu quả cuối cùng
ZapLogTest 2023/01/09 - 19:44:00.91076 INFO demo/zap.go:49 Log đã khởi tạo xongBest Practices
- Sử dụng Sync: Luôn gọi
logger.Sync()khi kết thúc chương trình - Cấu hình phù hợp: Chọn encoding và output phù hợp với môi trường
- Log rotation: Sử dụng lumberjack để quản lý kích thước file log
- Mức log: Sử dụng mức log phù hợp cho từng môi trường (debug/dev/production)
- Performance: Sử dụng logger thay vì sugar khi cần hiệu suất cao
Kết luận
Zap là một thư viện log mạnh mẽ và nhanh chóng cho Go. Với khả năng cấu hình linh hoạt, hỗ trợ cả structured logging và printf-style logging, Zap là lựa chọn tuyệt vời cho các ứng dụng Go cần logging hiệu suất cao.
