Skip to content

Zap

Zap é um componente de logs rápido, estruturado e nivelado construído em Go.

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

Documentação oficial: zap package - go.uber.org/zap - Go Packages

Instalação

go get -u go.uber.org/zap

Início Rápido

A equipe oficial forneceu dois exemplos de logs de nível de produção. O primeiro é um Sugar que suporta o estilo printf mas com desempenho relativamente menor.

go
logger, _ := zap.NewProduction()
defer logger.Sync() // Sincroniza o buffer para o arquivo no final do 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)

O segundo é um logger com melhor desempenho, mas suporta apenas saída fortemente tipada.

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

TIP

O uso do Zap é muito simples, o ponto problemático é configurar logs adequados para seu projeto. Os exemplos oficiais são poucos, então é necessário ler mais os comentários do código-fonte.

Configuração

Geralmente, a configuração de logs é escrita em arquivos de configuração. A configuração do Zap também suporta desserialização através de arquivos de configuração, mas suporta apenas configurações básicas. Mesmo as configurações avançadas nos exemplos oficiais são muito simples, insuficientes para uso em produção, então vamos detalhar as configurações.

Primeiro, vamos ver a estrutura geral de configuração, precisamos entender o significado de cada campo.

go
type Config struct {
    // Nível mínimo de log
   Level AtomicLevel `json:"level" yaml:"level"`
    // Modo de desenvolvimento, afeta principalmente o rastreamento de pilha
   Development bool `json:"development" yaml:"development"`
    // Rastreamento de chamador
   DisableCaller bool `json:"disableCaller" yaml:"disableCaller"`
    // Rastreamento de pilha
   DisableStacktrace bool `json:"disableStacktrace" yaml:"disableStacktrace"`
    // Amostragem, registra apenas alguns logs mais representativos enquanto limita o uso de desempenho
   Sampling *SamplingConfig `json:"sampling" yaml:"sampling"`
    // Codificação,分为 json e console
   Encoding string `json:"encoding" yaml:"encoding"`
    // Configuração de codificação, principalmente formatação de saída
   EncoderConfig zapcore.EncoderConfig `json:"encoderConfig" yaml:"encoderConfig"`
    // Caminho de saída do arquivo de log
   OutputPaths []string `json:"outputPaths" yaml:"outputPaths"`
    // Caminho de saída do arquivo de erro
   ErrorOutputPaths []string `json:"errorOutputPaths" yaml:"errorOutputPaths"`
    // Adiciona alguns campos de saída padrão ao log
   InitialFields map[string]interface{} `json:"initialFields" yaml:"initialFields"`
}

Abaixo estão os detalhes sobre a configuração de codificação.

go
type EncoderConfig struct {
   // Chave de valor, se a chave estiver vazia, a propriedade correspondente não será exibida
   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"`
   // Alguns 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 nome do logger
   EncodeName NameEncoder `json:"nameEncoder" yaml:"nameEncoder"`
   // Codificador de reflexão, principalmente para tipo interface{}, se não houver jsonencoder padrão
   NewReflectedEncoder func(io.Writer) ReflectedEncoder `json:"-" yaml:"-"`
   // String de separação de saída do console
   ConsoleSeparator string `json:"consoleSeparator" yaml:"consoleSeparator"`
}

Option é sobre switches e aplicações de algumas configurações, há muitas implementações.

go
type Option interface {
   apply(*Logger)
}

// Implementação de Option
type optionFunc func(*Logger)

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

// Aplicação
func Development() Option {
  return optionFunc(func(log *Logger) {
    log.development = true
  })
}

Este é o núcleo de log mais comumente usado, seus campos internos basicamente representam nossas etapas de configuração, também podemos nos referir às etapas da equipe oficial ao desserializar configurações, são aproximadamente as mesmas.

go
type ioCore struct {
   // Nível de log
   LevelEnabler
   // Codificação de log
   enc Encoder
   // Escrita de log
   out WriteSyncer
}

zap.Encoder é responsável pela formatação e codificação de logs

zap.WriteSyncer é responsável pela saída de logs, principalmente para arquivo e console

zap.LevelEnabler é o nível mínimo de log, logs abaixo deste nível não serão mais exibidos através de syncer.

Codificação de Logs

A codificação de logs envolve principalmente a formatação de detalhes de logs. Primeiro, vamos ver a saída do log original.

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

Você notará que este log tem alguns problemas:

  • Sem tempo
  • Sem informações do chamador, não sabemos de onde este log foi gerado, o que dificulta a depuração em caso de erro
  • Sem informações de pilha

Agora vamos resolver os problemas passo a passo, principalmente modificando zapcore.EncoderConfig. Primeiro, precisamos escrever nosso próprio arquivo de configuração, não usar a desserialização direta oficial. Primeiro, crie um arquivo de configuração config.yml

yml
# Configuração de Log Zap
zap:
  prefix: ZapLogTest
  timeFormat: 2006/01/02 - 15:04:05.00000
  level: debug
  caller: true
  stackTrace: false
  encode: console
  # Para onde o log é enviado file | console | both
  writer: both
  logFile:
    maxSize: 20
    backups: 5
    compress: true
    output:
      - "./log/output.log"

Mapeado para a estrutura

go
// ZapConfig
// @Date: 2023-01-09 16:37:05
// @Description: Estrutura de configuração de 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: Estrutura de configuração de arquivo de 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

A leitura de configuração usa Viper, o código foi omitido.

go
type TimeEncoder func(time.Time, PrimitiveArrayEncoder)

TimeEncoder é essencialmente uma função, podemos usar outros codificadores de tempo fornecidos oficialmente ou escrever os nossos próprios.

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

A parte geral é a seguinte

go
func zapEncoder(config *ZapConfig) zapcore.Encoder {
   // Cria uma nova configuração
   encoderConfig := zapcore.EncoderConfig{
      TimeKey:       "Time",
      LevelKey:      "Level",
      NameKey:       "Logger",
      CallerKey:     "Caller",
      MessageKey:    "Message",
      StacktraceKey: "StackTrace",
      LineEnding:    zapcore.DefaultLineEnding,
      FunctionKey:   zapcore.OmitKey,
   }
   // Formato de tempo personalizado
   encoderConfig.EncodeTime = CustomTimeFormatEncoder
   // Nível de log em maiúsculas
   encoderConfig.EncodeLevel = zapcore.CapitalColorLevelEncoder
   // Intervalo de tempo em segundos
   encoderConfig.EncodeDuration = zapcore.SecondsDurationEncoder
   // Saída de chamador abreviada
   encoderConfig.EncodeCaller = zapcore.ShortCallerEncoder
   // Serialização completa do nome do logger
   encoderConfig.EncodeName = zapcore.FullNameEncoder
   // Codificação final de log json ou console
   switch config.Encode {
   case "json":
      {
         return zapcore.NewJSONEncoder(encoderConfig)
      }
   case "console":
      {
         return zapcore.NewConsoleEncoder(encoderConfig)
      }
   }
   // Console padrão
   return zapcore.NewConsoleEncoder(encoderConfig)
}

Saída de Logs

A saída de logs é dividida entre console e arquivo. Podemos configurar dinamicamente com base no arquivo de configuração e, se quisermos realizar rotação de arquivos de log, precisamos usar outra dependência de terceiros.

go get -u github.com/natefinch/lumberjack

O código final é o seguinte

go
func zapWriteSyncer(cfg *ZapConfig) zapcore.WriteSyncer {
   syncers := make([]zapcore.WriteSyncer, 0, 2)
   // Se a saída do console de log estiver habilitada, adiciona o gravador do console
   if cfg.Writer == config.WriteBoth || cfg.Writer == config.WriteConsole {
      syncers = append(syncers, zapcore.AddSync(os.Stdout))
   }

   // Se o armazenamento de arquivo de log estiver habilitado, adiciona gravadores com base nos caminhos dos arquivos
   if cfg.Writer == config.WriteBoth || cfg.Writer == config.WriteFile {
      // Adiciona saída de log
      for _, path := range cfg.LogFile.Output {
         logger := &lumberjack.Logger{
            Filename:   path, // Caminho do arquivo
            MaxSize:    cfg.LogFile.MaxSize, // Tamanho do arquivo para divisão
            MaxBackups: cfg.LogFile.BackUps, // Número de backups
            Compress:   cfg.LogFile.Compress, // Comprimir ou não
            LocalTime:  true, // Usar hora local
         }
         syncers = append(syncers, zapcore.Lock(zapcore.AddSync(logger)))
      }
   }
   return zap.CombineWriteSyncers(syncers...)
}

Nível de Log

A equipe oficial tem enumerações sobre níveis de log, basta usá-las.

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
   }
   // Padrão Debug
   return zap.DebugLevel
}

Construção Final

go
func InitZap(config *ZapConfig) *zap.Logger {
   // Constrói o codificador
   encoder := zapEncoder(config)
   // Constrói o nível de log
   levelEnabler := zapLevelEnabler(config)
   // Finalmente obtém Core e Options
   subCore, options := tee(config, encoder, levelEnabler)
    // Cria Logger
   return zap.New(subCore, options...)
}

// Combina tudo
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)
}

// Constrói 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
}

Resultado final

ZapLogTest      2023/01/09 - 19:44:00.91076     INFO    demo/zap.go:49     日志初始化完成

Golang por www.golangdev.cn edit