Skip to content

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/zap

Bắ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.

go
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.

go
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

go
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

go
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.

go
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.

go
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.

go
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

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

go
// 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.

go
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.

go
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

go
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/lumberjack

Cuối cùng code như sau

go
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.

go
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

go
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 xong

Best Practices

  1. Sử dụng Sync: Luôn gọi logger.Sync() khi kết thúc chương trình
  2. Cấu hình phù hợp: Chọn encoding và output phù hợp với môi trường
  3. Log rotation: Sử dụng lumberjack để quản lý kích thước file log
  4. Mức log: Sử dụng mức log phù hợp cho từng môi trường (debug/dev/production)
  5. 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.

Golang by www.golangdev.cn edit