Skip to content

Zap

Zap ist eine in Go geschriebene, schnelle, strukturierte und levelbasierte Protokollierungskomponente.

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

Offizielle Dokumentation: zap package - go.uber.org/zap - Go Packages

Installation

go get -u go.uber.org/zap

Schnellstart

Die offizielle Dokumentation zeigt zwei Schnellstart-Beispiele, beide sind produktionsreife Logger. Der erste ist Sugar, der printf-Stil unterstützt, aber relativ langsam ist.

go
logger, _ := zap.NewProduction()
defer logger.Sync() // Synchronisiert den Puffer in die Datei beim Programmende
sugar := logger.Sugar()
sugar.Infow("failed to fetch URL",
  "url", url,
  "attempt", 3,
  "backoff", time.Second,
)
sugar.Infof("Failed to fetch URL: %s", url)

Der zweite ist logger, der bessere Leistung bietet, aber nur stark typisierte Ausgaben unterstützt.

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

Die Verwendung von Zap ist sehr einfach. Die Schwierigkeit liegt darin, einen für das eigene Projekt geeigneten Logger zu konfigurieren. Die offiziellen Beispiele sind spärlich, daher sollten Sie die Quellcode-Kommentare sorgfältig lesen.

Konfiguration

Im Allgemeinen wird die Logger-Konfiguration in einer Konfigurationsdatei gespeichert. Zap unterstützt auch die Deserialisierung aus Konfigurationsdateien, aber nur grundlegende Konfigurationen. Selbst bei erweiterten Konfigurationen sind die offiziellen Beispiele sehr einfach und nicht für den Produktionseinsatz geeignet. Daher werden wir die Details der Konfiguration ausführlich erläutern.

Schauen wir uns zunächst die Gesamtstruktur der Konfiguration an und verstehen die Bedeutung jedes Feldes.

go
type Config struct {
    // Minimale Protokollebene
   Level AtomicLevel `json:"level" yaml:"level"`
    // Entwicklungsmodus, beeinflusst hauptsächlich die Stack-Traces
   Development bool `json:"development" yaml:"development"`
    // Aufrufer-Tracking
   DisableCaller bool `json:"disableCaller" yaml:"disableCaller"`
    // Stack-Trace
   DisableStacktrace bool `json:"disableStacktrace" yaml:"disableStacktrace"`
    // Sampling, zeichnet bei begrenzter Protokoll-Performance nur repräsentative Protokolle auf
   Sampling *SamplingConfig `json:"sampling" yaml:"sampling"`
    // Kodierung, unterteilt in json und console
   Encoding string `json:"encoding" yaml:"encoding"`
    // Kodierungskonfiguration, hauptsächlich einige Formatierungseinstellungen
   EncoderConfig zapcore.EncoderConfig `json:"encoderConfig" yaml:"encoderConfig"`
    // Protokolldatei-Ausgabepfade
   OutputPaths []string `json:"outputPaths" yaml:"outputPaths"`
    // Fehlerdatei-Ausgabepfade
   ErrorOutputPaths []string `json:"errorOutputPaths" yaml:"errorOutputPaths"`
    // Fügt Protokollen einige Standardinhalte hinzu
   InitialFields map[string]interface{} `json:"initialFields" yaml:"initialFields"`
}

Im Folgenden sind die Details der Kodierungskonfiguration aufgeführt:

go
type EncoderConfig struct {
   // Schlüsselwerte, wenn der Schlüssel leer ist, wird das entsprechende Attribut nicht ausgegeben
   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"`
   // Einige benutzerdefinierte Encoder
   EncodeLevel    LevelEncoder    `json:"levelEncoder" yaml:"levelEncoder"`
   EncodeTime     TimeEncoder     `json:"timeEncoder" yaml:"timeEncoder"`
   EncodeDuration DurationEncoder `json:"durationEncoder" yaml:"durationEncoder"`
   EncodeCaller   CallerEncoder   `json:"callerEncoder" yaml:"callerEncoder"`
   // Logger-Namen-Encoder
   EncodeName NameEncoder `json:"nameEncoder" yaml:"nameEncoder"`
   // Reflexions-Encoder, hauptsächlich für interface{} Typen, Standard ist jsonencoder
   NewReflectedEncoder func(io.Writer) ReflectedEncoder `json:"-" yaml:"-"`
   // Trennzeichen für Konsolenausgabe
   ConsoleSeparator string `json:"consoleSeparator" yaml:"consoleSeparator"`
}

Option bezieht sich auf einige Konfigurationsschalter und deren Anwendung, es gibt viele Implementierungen.

go
type Option interface {
   apply(*Logger)
}

// Option-Implementierung
type optionFunc func(*Logger)

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

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

Dies ist der am häufigsten verwendete Protokollkern. Seine internen Felder repräsentieren im Wesentlichen unsere Konfigurationsschritte. Sie können auch die offiziellen Schritte bei der Deserialisierung der Konfiguration als Referenz nehmen, sie sind im Wesentlichen gleich.

go
type ioCore struct {
   // Protokollebene
   LevelEnabler
   // Protokollkodierung
   enc Encoder
   // Protokollausgabe
   out WriteSyncer
}

zap.Encoder ist verantwortlich für die Formatierung und Kodierung der Protokolle.

zap.WriteSyncer ist verantwortlich für die Protokollausgabe, hauptsächlich in Dateien und auf der Konsole.

zap.LevelEnabler ist die minimale Protokollebene. Protokolle unter dieser Ebene werden nicht über den syncer ausgegeben.

Protokollkodierung

Die Protokollkodierung bezieht sich hauptsächlich auf einige detaillierte Formatierungen der Protokolle. Schauen wir uns zunächst die Ausgabe mit den einfachsten Einstellungen an.

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

Es gibt einige Probleme mit dieser Protokollzeile:

  • Keine Zeitangabe
  • Keine Informationen zum Aufrufer, unklar wo diese Protokollzeile ausgegeben wurde, was bei Fehlern die Fehlersuche erschwert
  • Keine Stack-Trace-Informationen

Als Nächstes werden wir diese Probleme Schritt für Schritt lösen, hauptsächlich durch Anpassung von zapcore.EncoderConfig. Zuerst erstellen wir unsere eigene Konfigurationsdatei config.yml, anstatt die offizielle Direkt-Deserialisierung zu verwenden.

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

Zugehörige Struktur:

go
// ZapConfig
// @Date: 2023-01-09 16:37:05
// @Description: zap Protokollkonfigurationsstruktur
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: Protokolldatei-Konfigurationsstruktur
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

Zum Lesen der Konfiguration wird Viper verwendet, der spezifische Code wird weggelassen.

go
type TimeEncoder func(time.Time, PrimitiveArrayEncoder)

TimerEncoder ist im Wesentlichen eine Funktion. Wir können andere von der offiziellen Seite bereitgestellte Zeit-Encoder verwenden oder einen eigenen schreiben.

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

Der gesamte Teil sieht wie folgt aus:

go
func zapEncoder(config *ZapConfig) zapcore.Encoder {
   // Neue Konfiguration erstellen
   encoderConfig := zapcore.EncoderConfig{
      TimeKey:       "Time",
      LevelKey:      "Level",
      NameKey:       "Logger",
      CallerKey:     "Caller",
      MessageKey:    "Message",
      StacktraceKey: "StackTrace",
      LineEnding:    zapcore.DefaultLineEnding,
      FunctionKey:   zapcore.OmitKey,
   }
   // Benutzerdefiniertes Zeitformat
   encoderConfig.EncodeTime = CustomTimeFormatEncoder
   // Protokollebene in Großbuchstaben
   encoderConfig.EncodeLevel = zapcore.CapitalColorLevelEncoder
   // Sekundengenau Zeitintervall
   encoderConfig.EncodeDuration = zapcore.SecondsDurationEncoder
   // Kurze Aufrufer-Ausgabe
   encoderConfig.EncodeCaller = zapcore.ShortCallerEncoder
   // Vollständige Serialisierung des Logger-Namens
   encoderConfig.EncodeName = zapcore.FullNameEncoder
   // Endgültige Protokollkodierung json oder console
   switch config.Encode {
   case "json":
      {
         return zapcore.NewJSONEncoder(encoderConfig)
      }
   case "console":
      {
         return zapcore.NewConsoleEncoder(encoderConfig)
      }
   }
   // Standardmäßig console
   return zapcore.NewConsoleEncoder(encoderConfig)
}

Protokollausgabe

Die Protokollausgabe ist in Konsolenausgabe und Dateiausgabe unterteilt. Wir können dies dynamisch über die Konfigurationsdatei konfigurieren. Wenn wir Protokolldateien rotieren möchten, benötigen wir eine weitere Drittanbieter-Abhängigkeit.

go get -u github.com/natefinch/lumberjack

Der endgültige Code sieht wie folgt aus:

go
 func zapWriteSyncer(cfg *ZapConfig) zapcore.WriteSyncer {
   syncers := make([]zapcore.WriteSyncer, 0, 2)
   // Wenn Konsolenausgabe aktiviert ist, füge Konsolen-Writer hinzu
   if cfg.Writer == config.WriteBoth || cfg.Writer == config.WriteConsole {
      syncers = append(syncers, zapcore.AddSync(os.Stdout))
   }

   // Wenn Dateispeicherung aktiviert ist, füge Writer basierend auf Dateipfaden hinzu
   if cfg.Writer == config.WriteBoth || cfg.Writer == config.WriteFile {
      // Protokollausgabe-Handler hinzufügen
      for _, path := range cfg.LogFile.Output {
         logger := &lumberjack.Logger{
            Filename:   path, //Dateipfad
            MaxSize:    cfg.LogFile.MaxSize, //Größe für Dateiaufteilung
            MaxBackups: cfg.LogFile.BackUps, //Anzahl Backups
            Compress:   cfg.LogFile.Compress, // Komprimierung
            LocalTime:  true, //Lokale Zeit verwenden
         }
         syncers = append(syncers, zapcore.Lock(zapcore.AddSync(logger)))
      }
   }
   return zap.CombineWriteSyncers(syncers...)
}

Protokollebene

Die offizielle Seite bietet Aufzählungswerte für Protokollebenen, die direkt verwendet werden können.

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
   }
   // Standardmäßig Debug-Ebene
   return zap.DebugLevel
}

Endgültige Erstellung

go
func InitZap(config *ZapConfig) *zap.Logger {
   // Encoder erstellen
   encoder := zapEncoder(config)
   // Protokollebene erstellen
   levelEnabler := zapLevelEnabler(config)
   // Core und Options abrufen
   subCore, options := tee(config, encoder, levelEnabler)
    // Logger erstellen
   return zap.New(subCore, options...)
}

// Alles zusammenführen
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)
}

// Option erstellen
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
}

Endgültiges Ergebnis:

ZapLogTest      2023/01/09 - 19:44:00.91076     INFO    demo/zap.go:49     Protokollinitialisierung abgeschlossen

Golang by www.golangdev.cn edit