Skip to content

Zap

Zap — это быстрый, структурированный, уровневый компонент логирования, созданный на Go.

Официальный репозиторий: uber-go/zap: Blazing fast, structured, leveled logging in Go. (github.com)

Официальная документация: zap package - go.uber.org/zap - Go Packages

Установка

go get -u go.uber.org/zap

Быстрый старт

Официальная документация предоставляет два примера быстрого старта. Оба являются производственными логгерами. Первый — Sugar, который поддерживает стиль printf, но с относительно более низкой производительностью.

go
logger, _ := zap.NewProduction()
defer logger.Sync() // Сброс буферизованных записей лога в файл при выходе программы
sugar := logger.Sugar()
sugar.Infow("failed to fetch URL",
  "url", url,
  "attempt", 3,
  "backoff", time.Second,
)
sugar.Infof("Failed to fetch URL: %s", url)

Второй — logger, который имеет лучшую производительность, но поддерживает только строго типизированный вывод.

go
logger, _ := zap.NewProduction()
defer logger.Sync()
logger.Info("failed to fetch URL",
  // Структурированный контекст как строго типизированные значения Field.
  zap.String("url", url),
  zap.Int("attempt", 3),
  zap.Duration("backoff", time.Second),
)

TIP

Zap очень прост в использовании. Сложная часть — настройка логгера, подходящего для вашего проекта. Официальных примеров мало, поэтому читайте комментарии к исходному коду больше.

Конфигурация

Обычно конфигурация логгера пишется в файлах конфигурации. Конфигурация Zap также поддерживает десериализацию из файлов конфигурации, но поддерживает только базовую конфигурацию. Даже для расширенной конфигурации официальные примеры очень кратки и недостаточны для производственного использования, поэтому нам нужно обсудить детальную конфигурацию.

Сначала посмотрим на общую структуру конфигурации. Нам нужно понять значение каждого поля.

go
type Config struct {
    // Минимальный уровень логирования
   Level AtomicLevel `json:"level" yaml:"level"`
    // Режим разработки, в основном влияет на трассировку стека
   Development bool `json:"development" yaml:"development"`
    // Отслеживание вызывающего
   DisableCaller bool `json:"disableCaller" yaml:"disableCaller"`
    // Трассировка стека
   DisableStacktrace bool `json:"disableStacktrace" yaml:"disableStacktrace"`
    // Выборка, запись только представительных логов при ограничении влияния на производительность логов
   Sampling *SamplingConfig `json:"sampling" yaml:"sampling"`
    // Кодирование, либо json, либо режим консоли
   Encoding string `json:"encoding" yaml:"encoding"`
    // Конфигурация кодировщика, в основном конфигурация форматирования вывода
   EncoderConfig zapcore.EncoderConfig `json:"encoderConfig" yaml:"encoderConfig"`
    // Пути вывода файлов логов
   OutputPaths []string `json:"outputPaths" yaml:"outputPaths"`
    // Пути вывода файлов ошибок
   ErrorOutputPaths []string `json:"errorOutputPaths" yaml:"errorOutputPaths"`
    // Добавление некоторого содержимого вывода по умолчанию в логи
   InitialFields map[string]interface{} `json:"initialFields" yaml:"initialFields"`
}

Ниже приведены детали конфигурации кодировщика

go
type EncoderConfig struct {
   // Значение ключа, если ключ пустой, соответствующее свойство не будет выведено
   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"`
   // Некоторые пользовательские кодировщики
   EncodeLevel    LevelEncoder    `json:"levelEncoder" yaml:"levelEncoder"`
   EncodeTime     TimeEncoder     `json:"timeEncoder" yaml:"timeEncoder"`
   EncodeDuration DurationEncoder `json:"durationEncoder" yaml:"durationEncoder"`
   EncodeCaller   CallerEncoder   `json:"callerEncoder" yaml:"callerEncoder"`
   // Кодировщик имени логгера
   EncodeName NameEncoder `json:"nameEncoder" yaml:"nameEncoder"`
   // Кодировщик рефлексии, в основном для типов interface{}, если нет jsonencoder по умолчанию
   NewReflectedEncoder func(io.Writer) ReflectedEncoder `json:"-" yaml:"-"`
   // Строка разделителя вывода консоли
   ConsoleSeparator string `json:"consoleSeparator" yaml:"consoleSeparator"`
}

Option — это переключатели конфигурации и приложения, с множеством реализаций.

go
type Option interface {
   apply(*Logger)
}

// Реализация Option
type optionFunc func(*Logger)

func (f optionFunc) apply(log *Logger) {
  f(log)
}

// Приложение
func Development() Option {
  return optionFunc(func(log *Logger) {
    log.development = true
  })
}

Это наиболее часто используемое ядро логгера. Его внутренние поля в основном представляют наши шаги конфигурации. Вы также можете обратиться к официальным шагам для десериализации конфигурации, которые примерно одинаковы.

go
type ioCore struct {
   // Уровень логирования
   LevelEnabler
   // Кодирование лога
   enc Encoder
   // Запись лога
   out WriteSyncer
}

zap.Encoder отвечает за форматирование и кодирование лога

zap.WriteSyncer отвечает за вывод лога, в основном в файлы и консоль

zap.LevelEnabler — это минимальный уровень логирования; логи ниже этого уровня больше не выводятся через syncer.

Кодирование лога

Кодирование лога в основном включает детали форматирования логов. Сначала посмотрим на вывод при использовании оригинального логгера напрямую.

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"}

Вы обнаружите, что у этого лога есть несколько проблем:

  • Нет временной метки
  • Нет информации о вызывающем, не знаете, откуда был выведен этот лог, что затрудняет устранение ошибок
  • Нет трассировки стека

Далее решим эти проблемы пошагово, в основном изменяя zapcore.EncoderConfig. Сначала нам нужно написать собственный файл конфигурации вместо использования официальной прямой десериализации. Создайте файл конфигурации config.yml

yml
# Конфигурация логгера Zap
zap:
  prefix: ZapLogTest
  timeFormat: 2006/01/02 - 15:04:05.00000
  level: debug
  caller: true
  stackTrace: false
  encode: console
  # Куда выводить логи: file | console | both
  writer: both
  logFile:
    maxSize: 20
    backups: 5
    compress: true
    output:
      - "./log/output.log"

Сопоставленная структура

go
// ZapConfig
// @Date: 2023-01-09 16:37:05
// @Description: структура конфигурации логгера 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: структура конфигурации файла лога
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

Используйте Viper для чтения конфигурации; код здесь опущен.

go
type TimeEncoder func(time.Time, PrimitiveArrayEncoder)

TimeEncoder на самом деле является функцией. Мы можем использовать другие кодировщики времени, предоставленные официальными, или написать собственный.

go
func CustomTimeFormatEncoder(t time.Time, encoder zapcore.PrimitiveArrayEncoder) {
   encoder.AppendString(global.Config.ZapConfig.Prefix + "\t" + t.Format(global.Config.ZapConfig.TimeFormat))
}

Общая часть следующая

go
func zapEncoder(config *ZapConfig) zapcore.Encoder {
   // Создать новую конфигурацию
   encoderConfig := zapcore.EncoderConfig{
      TimeKey:       "Time",
      LevelKey:      "Level",
      NameKey:       "Logger",
      CallerKey:     "Caller",
      MessageKey:    "Message",
      StacktraceKey: "StackTrace",
      LineEnding:    zapcore.DefaultLineEnding,
      FunctionKey:   zapcore.OmitKey,
   }
   // Пользовательский формат времени
   encoderConfig.EncodeTime = CustomTimeFormatEncoder
   // Уровень лога в верхнем регистре
   encoderConfig.EncodeLevel = zapcore.CapitalColorLevelEncoder
   // Интервал времени в секундах
   encoderConfig.EncodeDuration = zapcore.SecondsDurationEncoder
   // Краткий вывод вызывающего
   encoderConfig.EncodeCaller = zapcore.ShortCallerEncoder
   // Полная сериализация имени логгера
   encoderConfig.EncodeName = zapcore.FullNameEncoder
   // Окончательное кодирование лога: json или console
   switch config.Encode {
   case "json":
      {
         return zapcore.NewJSONEncoder(encoderConfig)
      }
   case "console":
      {
         return zapcore.NewConsoleEncoder(encoderConfig)
      }
   }
   // Консоль по умолчанию
   return zapcore.NewConsoleEncoder(encoderConfig)
}

Вывод лога

Вывод лога делится на вывод консоли и вывод файла. Мы можем настроить динамически на основе файла конфигурации. Если нам нужна ротация файлов логов, нужно использовать другую стороннюю зависимость.

go get -u github.com/natefinch/lumberjack

Окончательный код следующий

go
func zapWriteSyncer(cfg *ZapConfig) zapcore.WriteSyncer {
   syncers := make([]zapcore.WriteSyncer, 0, 2)
   // Если включен вывод консоли, добавить writer консоли
   if cfg.Writer == config.WriteBoth || cfg.Writer == config.WriteConsole {
      syncers = append(syncers, zapcore.AddSync(os.Stdout))
   }

   // Если включено логирование в файл, добавить writers на основе путей файлов
   if cfg.Writer == config.WriteBoth || cfg.Writer == config.WriteFile {
      // Добавить вывод лога
      for _, path := range cfg.LogFile.Output {
         logger := &lumberjack.Logger{
            Filename:   path, // Путь файла
            MaxSize:    cfg.LogFile.MaxSize, // Размер разделённых файлов
            MaxBackups: cfg.LogFile.BackUps, // Количество резервных копий
            Compress:   cfg.LogFile.Compress, // Сжимать ли
            LocalTime:  true, // Использовать локальное время
         }
         syncers = append(syncers, zapcore.Lock(zapcore.AddSync(logger)))
      }
   }
   return zap.CombineWriteSyncers(syncers...)
}

Уровень лога

В официальной документации есть элементы enum для уровней логов; просто используйте их напрямую.

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
   }
   // Уровень Debug по умолчанию
   return zap.DebugLevel
}

Окончательная сборка

go
func InitZap(config *ZapConfig) *zap.Logger {
   // Собрать кодировщик
   encoder := zapEncoder(config)
   // Собрать уровень лога
   levelEnabler := zapLevelEnabler(config)
   // Наконец получить Core и Options
   subCore, options := tee(config, encoder, levelEnabler)
   // Создать Logger
   return zap.New(subCore, options...)
}

// Объединить всё
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)
}

// Собрать Options
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
}

Окончательный результат

ZapLogTest      2023/01/09 - 19:44:00.91076     INFO    demo/zap.go:49     Logger initialization complete

Golang by www.golangdev.cn edit