Skip to content

Zap

Zap è un componente di logging veloce, strutturato e livellato costruito in Go.

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

Documentazione ufficiale: zap package - go.uber.org/zap - Go Packages

Installazione

go get -u go.uber.org/zap

Guida Rapida

Gli autori forniscono due esempi di livello production, il primo è uno Sugar che supporta lo stile printf ma con prestazioni relativamente inferiori.

go
logger, _ := zap.NewProduction()
defer logger.Sync() // Sincronizza il buffer nel file alla fine del programma
sugar := logger.Sugar()
sugar.Infow("failed to fetch URL",
  "url", url,
  "attempt", 3,
  "backoff", time.Second,
)
sugar.Infof("Failed to fetch URL: %s", url)

Il secondo ha prestazioni migliori, ma supporta solo output fortemente tipizzato logger

go
logger, _ := zap.NewProduction()
defer logger.Sync()
logger.Info("failed to fetch URL",
  // Contesto strutturato come valori Field fortemente tipizzati.
  zap.String("url", url),
  zap.Int("attempt", 3),
  zap.Duration("backoff", time.Second),
)

TIP

L'uso di Zap è molto semplice, la parte complicata è configurarlo per il proprio progetto. Gli esempi ufficiali sono pochi, bisogna leggere attentamente i commenti del codice sorgente.

Configurazione

Generalmente la configurazione del logging viene scritta nei file di configurazione, anche la configurazione di Zap supporta la deserializzazione tramite file di configurazione, ma supporta solo configurazioni di base. Anche gli esempi forniti dagli autori sono molto semplici e non sufficienti per l'uso in produzione, quindi analizziamo i dettagli della configurazione.

Per prima cosa esaminiamo la struttura di configurazione complessiva, dobbiamo comprendere il significato di ogni campo.

go
type Config struct {
    // Livello minimo di logging
   Level AtomicLevel `json:"level" yaml:"level"`
    // Modalità sviluppo, influenza principalmente il tracking dello stack trace
   Development bool `json:"development" yaml:"development"`
    // Tracciamento del chiamante
   DisableCaller bool `json:"disableCaller" yaml:"disableCaller"`
    // Stack trace
   DisableStacktrace bool `json:"disableStacktrace" yaml:"disableStacktrace"`
    // Campionamento, registra solo una parte rappresentativa dei log per limitare l'impatto sulle prestazioni
   Sampling *SamplingConfig `json:"sampling" yaml:"sampling"`
    // Codifica, può essere json o console
   Encoding string `json:"encoding" yaml:"encoding"`
    // Configurazione del codificatore, principalmente per la formattazione dell'output
   EncoderConfig zapcore.EncoderConfig `json:"encoderConfig" yaml:"encoderConfig"`
    // Percorso di output dei file di log
   OutputPaths []string `json:"outputPaths" yaml:"outputPaths"`
    // Percorso di output dei file di errore
   ErrorOutputPaths []string `json:"errorOutputPaths" yaml:"errorOutputPaths"`
    // Aggiunge alcuni contenuti di output predefiniti ai log
   InitialFields map[string]interface{} `json:"initialFields" yaml:"initialFields"`
}

Di seguito i dettagli sulla configurazione del codificatore

go
type EncoderConfig struct {
   // Chiave del valore, se la chiave è vuota, la proprietà corrispondente non verrà 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"`
   // Alcuni codificatori personalizzati
   EncodeLevel    LevelEncoder    `json:"levelEncoder" yaml:"levelEncoder"`
   EncodeTime     TimeEncoder     `json:"timeEncoder" yaml:"timeEncoder"`
   EncodeDuration DurationEncoder `json:"durationEncoder" yaml:"durationEncoder"`
   EncodeCaller   CallerEncoder   `json:"callerEncoder" yaml:"callerEncoder"`
   // Codificatore del nome del logger
   EncodeName NameEncoder `json:"nameEncoder" yaml:"nameEncoder"`
   // Codificatore reflection, principalmente per tipo interface{}, se non c'è jsonencoder predefinito
   NewReflectedEncoder func(io.Writer) ReflectedEncoder `json:"-" yaml:"-"`
   // Stringa di separazione per output console
   ConsoleSeparator string `json:"consoleSeparator" yaml:"consoleSeparator"`
}

Option riguarda gli switch e le applicazioni di alcune configurazioni, ci sono molte implementazioni.

go
type Option interface {
   apply(*Logger)
}

// Implementazione di Option
type optionFunc func(*Logger)

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

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

Questo è il core del logger più comunemente usato, i suoi campi interni rappresentano essenzialmente i passaggi della nostra configurazione. Si può anche fare riferimento ai passaggi forniti dagli autori durante la deserializzazione della configurazione, sono più o meno gli stessi.

go
type ioCore struct {
   // Livello di log
   LevelEnabler
   // Codifica del log
   enc Encoder
   // Scrittura del log
   out WriteSyncer
}

zap.Encoder è responsabile della formattazione e codifica del log

zap.WriteSyncer è responsabile dell'output del log, principalmente su file e console

zap.LevelEnabler è il livello minimo di log, i log al di sotto di questo livello non vengono più output tramite syncer.

Codifica del Log

La codifica del log riguarda principalmente la formattazione dei dettagli del log. Innanzitutto vediamo l'output del log originale.

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

Si noterà che questo log ha diversi problemi:

  • Non c'è il tempo
  • Non ci sono informazioni sul chiamante, non si sa da dove viene output questo log, altrimenti in caso di errore non si può fare troubleshooting
  • Non c'è lo stack trace

Procediamo passo dopo passo per risolvere il problema, principalmente modificando zapcore.EncoderConfig. Per prima cosa dobbiamo scrivere il nostro file di configurazione, non usare la deserializzazione diretta degli autori. Creiamo un file di configurazione config.yml

yml
# Configurazione Log Zap
zap:
  prefix: ZapLogTest
  timeFormat: 2006/01/02 - 15:04:05.00000
  level: debug
  caller: true
  stackTrace: false
  encode: console
  # Dove output il log file | console | both
  writer: both
  logFile:
    maxSize: 20
    backups: 5
    compress: true
    output:
      - "./log/output.log"

Struttura mappata

go
// ZapConfig
// @Date: 2023-01-09 16:37:05
// @Description: Struttura di configurazione 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: Struttura di configurazione file di 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

Per leggere la configurazione si usa Viper, il codice specifico viene omesso.

go
type TimeEncoder func(time.Time, PrimitiveArrayEncoder)

TimeEncoder è essenzialmente una funzione, possiamo usare altri encoder di tempo forniti dagli autori o scriverne uno personalizzato.

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

La parte complessiva è la seguente

go
func zapEncoder(config *ZapConfig) zapcore.Encoder {
   // Crea una nuova configurazione
   encoderConfig := zapcore.EncoderConfig{
      TimeKey:       "Time",
      LevelKey:      "Level",
      NameKey:       "Logger",
      CallerKey:     "Caller",
      MessageKey:    "Message",
      StacktraceKey: "StackTrace",
      LineEnding:    zapcore.DefaultLineEnding,
      FunctionKey:   zapcore.OmitKey,
   }
   // Formato tempo personalizzato
   encoderConfig.EncodeTime = CustomTimeFormatEncoder
   // Livello log maiuscolo
   encoderConfig.EncodeLevel = zapcore.CapitalColorLevelEncoder
   // Intervallo di tempo in secondi
   encoderConfig.EncodeDuration = zapcore.SecondsDurationEncoder
   // Output breve del chiamante
   encoderConfig.EncodeCaller = zapcore.ShortCallerEncoder
   // Serializzazione completa del nome del logger
   encoderConfig.EncodeName = zapcore.FullNameEncoder
   // Codifica finale del log json o console
   switch config.Encode {
   case "json":
      {
         return zapcore.NewJSONEncoder(encoderConfig)
      }
   case "console":
      {
         return zapcore.NewConsoleEncoder(encoderConfig)
      }
   }
   // Default console
   return zapcore.NewConsoleEncoder(encoderConfig)
}

Output del Log

L'output del log si divide in output su console e su file. Possiamo configurarlo dinamicamente in base al file di configurazione e, se si desidera effettuare il taglio dei file di log, è necessario utilizzare un'altra dipendenza di terze parti.

go get -u github.com/natefinch/lumberjack

Il codice finale è il seguente

go
func zapWriteSyncer(cfg *ZapConfig) zapcore.WriteSyncer {
   syncers := make([]zapcore.WriteSyncer, 0, 2)
   // Se è abilitato l'output su console del log, aggiungi lo scrittore di console
   if cfg.Writer == config.WriteBoth || cfg.Writer == config.WriteConsole {
      syncers = append(syncers, zapcore.AddSync(os.Stdout))
   }

   // Se è abilitato il salvataggio su file del log, aggiungi gli scrittori in base ai percorsi dei file
   if cfg.Writer == config.WriteBoth || cfg.Writer == config.WriteFile {
      // Aggiungi output del log
      for _, path := range cfg.LogFile.Output {
         logger := &lumberjack.Logger{
            Filename:   path, // Percorso file
            MaxSize:    cfg.LogFile.MaxSize, // Dimensione file per divisione
            MaxBackups: cfg.LogFile.BackUps, // Numero di backup
            Compress:   cfg.LogFile.Compress, // Compressione
            LocalTime:  true, // Usa ora locale
         }
         syncers = append(syncers, zapcore.Lock(zapcore.AddSync(logger)))
      }
   }
   return zap.CombineWriteSyncers(syncers...)
}

Livello di Log

Gli autori forniscono le enumerazioni per i livelli di log, basta usarle direttamente.

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
   }
   // Default livello Debug
   return zap.DebugLevel
}

Costruzione Finale

go
func InitZap(config *ZapConfig) *zap.Logger {
   // Costruisci encoder
   encoder := zapEncoder(config)
   // Costruisci livello di log
   levelEnabler := zapLevelEnabler(config)
   // Infine ottieni Core e Options
   subCore, options := tee(config, encoder, levelEnabler)
    // Crea Logger
   return zap.New(subCore, options...)
}

// Unisci tutto
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)
}

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

Output finale

ZapLogTest      2023/01/09 - 19:44:00.91076     INFO    demo/zap.go:49     Log inizializzazione completata

Golang by www.golangdev.cn edit