Skip to content

Zap

Zap es un componente de registros rápido, estructurado y nivelado construido con Go.

Repositorio oficial: uber-go/zap: Blazing fast, structured, leveled logging in Go. (github.com)

Documentación oficial: zap package - go.uber.org/zap - Go Packages

Instalación

go get -u go.uber.org/zap

Inicio rápido

Oficial proporciona dos ejemplos de inicio rápido, ambos son registros de nivel de producción, el primero es un Sugar que soporta estilo printf pero con rendimiento relativamente bajo.

go
logger, _ := zap.NewProduction()
defer logger.Sync() // Sincronizar el búfer al archivo al final del programa
sugar := logger.Sugar()
sugar.Infow("failed to fetch URL",
  "url", url,
  "attempt", 3,
  "backoff", time.Second,
)
sugar.Infof("Failed to fetch URL: %s", url)

El segundo es de mejor rendimiento, pero solo soporta salida fuertemente tipificada logger

go
logger, _ := zap.NewProduction()
defer logger.Sync()
logger.Info("failed to fetch URL",
  // Contexto estructurado como valores de campo fuertemente tipificados.
  zap.String("url", url),
  zap.Int("attempt", 3),
  zap.Duration("backoff", time.Second),
)

TIP

El uso de Zap es muy simple, el punto problemático es configurar un registro adecuado para tu proyecto, los ejemplos oficiales son pocos, hay que leer más los comentarios del código fuente.

Configuración

Generalmente la configuración de registros se escribe en archivos de configuración, la configuración de Zap también soporta deserialización a través de archivos de configuración, pero solo soporta configuración básica, incluso la configuración avanzada que los ejemplos oficiales proporcionan es muy simple, no es suficiente para poner en uso, así que hay que explicar los detalles de la configuración.

Primero veamos la estructura del cuerpo de configuración total, necesitamos entender el significado de cada campo

go
type Config struct {
    // Nivel mínimo de registros
   Level AtomicLevel `json:"level" yaml:"level"`
    // Modo desarrollo, afecta principalmente el seguimiento de pila
   Development bool `json:"development" yaml:"development"`
    // Seguimiento de llamador
   DisableCaller bool `json:"disableCaller" yaml:"disableCaller"`
    // Seguimiento de pila
   DisableStacktrace bool `json:"disableStacktrace" yaml:"disableStacktrace"`
    // Muestreo, registrar solo algunos registros más representativos bajo la condición de limitar el uso de rendimiento de registros, igual a registro selectivo
   Sampling *SamplingConfig `json:"sampling" yaml:"sampling"`
    // Codificación, dividida en modo json y console
   Encoding string `json:"encoding" yaml:"encoding"`
    // Configuración de codificación, principalmente configuración de formateo de salida
   EncoderConfig zapcore.EncoderConfig `json:"encoderConfig" yaml:"encoderConfig"`
    // Ruta de salida de archivo de registros
   OutputPaths []string `json:"outputPaths" yaml:"outputPaths"`
    // Ruta de salida de archivo de errores
   ErrorOutputPaths []string `json:"errorOutputPaths" yaml:"errorOutputPaths"`
    // Agregar algunos contenidos de salida predeterminados a los registros
   InitialFields map[string]interface{} `json:"initialFields" yaml:"initialFields"`
}

A continuación están los detalles sobre la configuración de codificación

go
type EncoderConfig struct {
   // Clave de valor, si la clave está vacía, la propiedad correspondiente no se escribirá
   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"`
   // Algunos codificadores personalizados
   EncodeLevel    LevelEncoder    `json:"levelEncoder" yaml:"levelEncoder"`
   EncodeTime     TimeEncoder     `json:"timeEncoder" yaml:"timeEncoder"`
   EncodeDuration DurationEncoder `json:"durationEncoder" yaml:"durationEncoder"`
   EncodeCaller   CallerEncoder   `json:"callerEncoder" yaml:"callerEncoder"`
   // Codificador de nombre de registrador
   EncodeName NameEncoder `json:"nameEncoder" yaml:"nameEncoder"`
   // Codificador de reflexión, principalmente para tipo interface{}, si no hay jsonencoder predeterminado
   NewReflectedEncoder func(io.Writer) ReflectedEncoder `json:"-" yaml:"-"`
   // Cadena de separación de salida de consola
   ConsoleSeparator string `json:"consoleSeparator" yaml:"consoleSeparator"`
}

Option es sobre interruptores y aplicaciones de algunas configuraciones, hay muchas implementaciones.

go
type Option interface {
   apply(*Logger)
}

// Implementación de Option
type optionFunc func(*Logger)

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

// Aplicación
func Development() Option {
  return optionFunc(func(log *Logger) {
    log.development = true
  })
}

Este es el núcleo de registro más utilizado, sus campos internos básicamente representan nuestros pasos de configuración, también se puede referir a los pasos de la configuración oficial al deserializar, son aproximadamente los mismos.

go
type ioCore struct {
   // Nivel de registros
   LevelEnabler
   // Codificación de registros
   enc Encoder
   // Escritura de registros
   out WriteSyncer
}

zap.Encoder es responsable de la formateo y codificación de registros

zap.WriteSyncer es responsable de la salida de registros, principalmente salida a archivo y consola

zap.LevelEnabler nivel mínimo de registros, los registros por debajo de este nivel ya no se escriben a través de syncer.

Codificación de registros

La codificación de registros implica principalmente el formateo de algunos detalles de registros, primero veamos la salida del registro original directamente.

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

Se encontrará que este registro tiene varios problemas:

  • No tiene tiempo
  • No tiene situación del llamador, no se sabe de dónde se escribió este registro, de lo contrario no se puede investigar cuando ocurre un error
  • No tiene situación de pila

A continuación se resuelve el problema paso a paso, principalmente modificar zapcore.EncoderConfig, primero hay que escribir el archivo de configuración propio, no usar la deserialización directa oficial. Primero crear un archivo de configuración config.yml

yml
# Configuración de registros Zap
zap:
  prefix: ZapLogTest
  timeFormat: 2006/01/02 - 15:04:05.00000
  level: debug
  caller: true
  stackTrace: false
  encode: console
  # Dónde escribir los registros file | console | both
  writer: both
  logFile:
    maxSize: 20
    backups: 5
    compress: true
    output:
      - "./log/output.log"

Estructura mapeada

go
// ZapConfig
// @Date: 2023-01-09 16:37:05
// @Description: Estructura de configuración de registros 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: Estructura de configuración de archivo de registros
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

Leer configuración usando Viper, el código específico se omite.

go
type TimeEncoder func(time.Time, PrimitiveArrayEncoder)

TimerEncoder en realidad es una función, podemos usar otros codificadores de tiempo proporcionados oficialmente, o escribir los propios.

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

La parte general es la siguiente

go
func zapEncoder(config *ZapConfig) zapcore.Encoder {
   // Crear una configuración
   encoderConfig := zapcore.EncoderConfig{
      TimeKey:       "Time",
      LevelKey:      "Level",
      NameKey:       "Logger",
      CallerKey:     "Caller",
      MessageKey:    "Message",
      StacktraceKey: "StackTrace",
      LineEnding:    zapcore.DefaultLineEnding,
      FunctionKey:   zapcore.OmitKey,
   }
   // Formato de tiempo personalizado
   encoderConfig.EncodeTime = CustomTimeFormatEncoder
   // Nivel de registros en mayúsculas
   encoderConfig.EncodeLevel = zapcore.CapitalColorLevelEncoder
   // Intervalo de tiempo en segundos
   encoderConfig.EncodeDuration = zapcore.SecondsDurationEncoder
   // Salida abreviada del llamador
   encoderConfig.EncodeCaller = zapcore.ShortCallerEncoder
   // Serialización completa del nombre del registrador
   encoderConfig.EncodeName = zapcore.FullNameEncoder
   // Codificación final de registros json o console
   switch config.Encode {
   case "json":
      {
         return zapcore.NewJSONEncoder(encoderConfig)
      }
   case "console":
      {
         return zapcore.NewConsoleEncoder(encoderConfig)
      }
   }
   // console predeterminado
   return zapcore.NewConsoleEncoder(encoderConfig)
}

Salida de registros

La salida de registros se divide en salida de consola y salida de archivo, podemos configurar dinámicamente según el archivo de configuración, y si se desea realizar corte de archivos de registros también hay que usar otra dependencia de terceros.

go get -u github.com/natefinch/lumberjack

Finalmente el código es el siguiente

go
 func zapWriteSyncer(cfg *ZapConfig) zapcore.WriteSyncer {
   syncers := make([]zapcore.WriteSyncer, 0, 2)
   // Si se habilita la salida de consola de registros, agregar escritor de consola
   if cfg.Writer == config.WriteBoth || cfg.Writer == config.WriteConsole {
      syncers = append(syncers, zapcore.AddSync(os.Stdout))
   }

   // Si se habilita el almacenamiento de archivos de registros, agregar escritores según la ruta de archivo
   if cfg.Writer == config.WriteBoth || cfg.Writer == config.WriteFile {
      // Agregar escritor de registros
      for _, path := range cfg.LogFile.Output {
         logger := &lumberjack.Logger{
            Filename:   path, // Ruta de archivo
            MaxSize:    cfg.LogFile.MaxSize, // Tamaño de archivo dividido
            MaxBackups: cfg.LogFile.BackUps, // Número de copias de seguridad
            Compress:   cfg.LogFile.Compress, // Si comprimir
            LocalTime:  true, // Usar hora local
         }
         syncers = append(syncers, zapcore.Lock(zapcore.AddSync(logger)))
      }
   }
   return zap.CombineWriteSyncers(syncers...)
}

Nivel de registros

Oficial tiene elementos de enumeración sobre niveles de registros, se pueden usar directamente.

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
   }
   // Nivel Debug predeterminado
   return zap.DebugLevel
}

Construcción final

go
func InitZap(config *ZapConfig) *zap.Logger {
   // Construir codificador
   encoder := zapEncoder(config)
   // Construir nivel de registros
   levelEnabler := zapLevelEnabler(config)
   // Finalmente obtener Core y Options
   subCore, options := tee(config, encoder, levelEnabler)
    // Crear Logger
   return zap.New(subCore, options...)
}

// Combinar todo
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)
}

// Construir 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
}

Efecto final

ZapLogTest      2023/01/09 - 19:44:00.91076     INFO    demo/zap.go:49     Registro inicializado correctamente

Golang editado por www.golangdev.cn