Skip to content

Zap

Zap est un composant de journalisation rapide, structuré et nivelé, construit en Go.

Dépôt officiel : uber-go/zap: Blazing fast, structured, leveled logging in Go. (github.com)

Documentation officielle : zap package - go.uber.org/zap - Go Packages

Installation

go get -u go.uber.org/zap

Démarrage rapide

La documentation officielle donne deux exemples de démarrage rapide, tous deux étant des logs de niveau production. Le premier est Sugar qui supporte le style printf mais avec des performances relativement faibles.

go
logger, _ := zap.NewProduction()
defer logger.Sync() // Synchronise le cache vers le fichier à la fin du programme
sugar := logger.Sugar()
sugar.Infow("failed to fetch URL",
  "url", url,
  "attempt", 3,
  "backoff", time.Second,
)
sugar.Infof("Failed to fetch URL: %s", url)

Le second est logger qui a de meilleures performances mais supporte uniquement la sortie fortement typée.

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

L'utilisation de Zap est très simple, la partie difficile consiste à configurer un logger adapté à votre projet. Les exemples officiels sont rares, il faut lire les commentaires du code source.

Configuration

Généralement, la configuration des logs est écrite dans un fichier de configuration. La configuration de Zap supporte également la désérialisation via fichier de configuration, mais elle ne supporte que les configurations de base. Même pour les configurations avancées, les exemples officiels sont très concis et insuffisants pour une utilisation en production, nous allons donc détailler la configuration.

D'abord, regardons la structure de configuration globale, il faut comprendre la signification de chaque champ.

go
type Config struct {
    // Niveau de log minimum
   Level AtomicLevel `json:"level" yaml:"level"`
    // Mode développement, affecte principalement le traçage de pile
   Development bool `json:"development" yaml:"development"`
    // Traçage de l'appelant
   DisableCaller bool `json:"disableCaller" yaml:"disableCaller"`
    // Traçage de pile
   DisableStacktrace bool `json:"disableStacktrace" yaml:"disableStacktrace"`
    // Échantillonnage, dans la limite de l'impact sur les performances, n'enregistre que certains logs représentatifs
   Sampling *SamplingConfig `json:"sampling" yaml:"sampling"`
    // Encodage, divisé en json et console
   Encoding string `json:"encoding" yaml:"encoding"`
    // Configuration d'encodage, principalement pour le formatage de sortie
   EncoderConfig zapcore.EncoderConfig `json:"encoderConfig" yaml:"encoderConfig"`
    // Chemin de sortie du fichier de log
   OutputPaths []string `json:"outputPaths" yaml:"outputPaths"`
    // Chemin de sortie du fichier d'erreur
   ErrorOutputPaths []string `json:"errorOutputPaths" yaml:"errorOutputPaths"`
    // Ajouter du contenu par défaut aux logs
   InitialFields map[string]interface{} `json:"initialFields" yaml:"initialFields"`
}

Voici les détails de la configuration d'encodage

go
type EncoderConfig struct {
   // Clé-valeur, si la clé est vide, l'attribut correspondant ne sera pas affiché
   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"`
   // Quelques encodeurs personnalisés
   EncodeLevel    LevelEncoder    `json:"levelEncoder" yaml:"levelEncoder"`
   EncodeTime     TimeEncoder     `json:"timeEncoder" yaml:"timeEncoder"`
   EncodeDuration DurationEncoder `json:"durationEncoder" yaml:"durationEncoder"`
   EncodeCaller   CallerEncoder   `json:"callerEncoder" yaml:"callerEncoder"`
   // Encodeur de nom de logger
   EncodeName NameEncoder `json:"nameEncoder" yaml:"nameEncoder"`
   // Encodeur de réflexion, principalement pour le type interface{}, par défaut jsonencoder
   NewReflectedEncoder func(io.Writer) ReflectedEncoder `json:"-" yaml:"-"`
   // Chaîne de séparation pour la sortie console
   ConsoleSeparator string `json:"consoleSeparator" yaml:"consoleSeparator"`
}

Option concerne les commutateurs et applications de certaines configurations, il y a beaucoup d'implémentations.

go
type Option interface {
   apply(*Logger)
}

// Implémentation de Option
type optionFunc func(*Logger)

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

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

C'est le cœur de log le plus couramment utilisé, ses champs internes représentent essentiellement les étapes de notre configuration, vous pouvez également vous référer aux étapes de la désérialisation officielle, qui sont globalement les mêmes.

go
type ioCore struct {
   // Niveau de log
   LevelEnabler
   // Encodage de log
   enc Encoder
   // Écriture de log
   out WriteSyncer
}

zap.Encoder est responsable du formatage et de l'encodage des logs

zap.WriteSyncer est responsable de la sortie des logs, principalement vers les fichiers et la console

zap.LevelEnabler est le niveau de log minimum, les logs en dessous de ce niveau ne seront pas sortis via syncer.

Encodage de log

L'encodage de log concerne principalement le formatage des détails des logs. D'abord, regardons la sortie en utilisant le log le plus basique.

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

On remarque plusieurs problèmes avec cette ligne de log :

  • Pas d'horodatage
  • Pas d'information sur l'appelant, on ne sait pas d'où vient cette ligne de log, ce qui rend le débogage impossible en cas d'erreur
  • Pas de trace de pile

Résolvons ces problèmes étape par étape, principalement en modifiant zapcore.EncoderConfig. D'abord, écrivons notre propre fichier de configuration, sans utiliser la désérialisation officielle. Créons un fichier de configuration config.yml.

yml
# Configuration des logs Zap
zap:
  prefix: ZapLogTest
  timeFormat: 2006/01/02 - 15:04:05.00000
  level: debug
  caller: true
  stackTrace: false
  encode: console
  # Où sortir les logs file | console | both
  writer: both
  logFile:
    maxSize: 20
    backups: 5
    compress: true
    output:
      - "./log/output.log"

Structure mappée

go
// ZapConfig
// @Date: 2023-01-09 16:37:05
// @Description: structure de configuration 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: structure de configuration du fichier 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

La lecture de la configuration utilise Viper, le code spécifique est omis.

go
type TimeEncoder func(time.Time, PrimitiveArrayEncoder)

TimerEncoder est essentiellement une fonction, nous pouvons utiliser les encodeurs de temps fournis officiellement ou écrire le nôtre.

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

La partie globale est la suivante

go
func zapEncoder(config *ZapConfig) zapcore.Encoder {
   // Créer une nouvelle configuration
   encoderConfig := zapcore.EncoderConfig{
      TimeKey:       "Time",
      LevelKey:      "Level",
      NameKey:       "Logger",
      CallerKey:     "Caller",
      MessageKey:    "Message",
      StacktraceKey: "StackTrace",
      LineEnding:    zapcore.DefaultLineEnding,
      FunctionKey:   zapcore.OmitKey,
   }
   // Format de temps personnalisé
   encoderConfig.EncodeTime = CustomTimeFormatEncoder
   // Niveau de log en majuscules
   encoderConfig.EncodeLevel = zapcore.CapitalColorLevelEncoder
   // Intervalle de temps en secondes
   encoderConfig.EncodeDuration = zapcore.SecondsDurationEncoder
   // Sortie d'appelant courte
   encoderConfig.EncodeCaller = zapcore.ShortCallerEncoder
   // Nom de logger sérialisé complet
   encoderConfig.EncodeName = zapcore.FullNameEncoder
   // Encodage final du log json ou console
   switch config.Encode {
   case "json":
      {
         return zapcore.NewJSONEncoder(encoderConfig)
      }
   case "console":
      {
         return zapcore.NewConsoleEncoder(encoderConfig)
      }
   }
   // Par défaut console
   return zapcore.NewConsoleEncoder(encoderConfig)
}

Sortie de log

La sortie de log est divisée en sortie console et sortie fichier. Nous pouvons configurer dynamiquement selon le fichier de configuration. Si nous voulons effectuer une rotation des fichiers de log, nous devons utiliser une autre dépendance tierce.

go get -u github.com/natefinch/lumberjack

Le code final est le suivant

go
 func zapWriteSyncer(cfg *ZapConfig) zapcore.WriteSyncer {
   syncers := make([]zapcore.WriteSyncer, 0, 2)
   // Si la sortie console est activée, ajouter le writer console
   if cfg.Writer == config.WriteBoth || cfg.Writer == config.WriteConsole {
      syncers = append(syncers, zapcore.AddSync(os.Stdout))
   }

   // Si le stockage de fichier de log est activé, ajouter le writer selon les chemins de fichiers
   if cfg.Writer == config.WriteBoth || cfg.Writer == config.WriteFile {
      // Ajouter les output de log
      for _, path := range cfg.LogFile.Output {
         logger := &lumberjack.Logger{
            Filename:   path, // Chemin du fichier
            MaxSize:    cfg.LogFile.MaxSize, // Taille de fichier pour la rotation
            MaxBackups: cfg.LogFile.BackUps, // Nombre de sauvegardes
            Compress:   cfg.LogFile.Compress, // Compresser ou non
            LocalTime:  true, // Utiliser l'heure locale
         }
         syncers = append(syncers, zapcore.Lock(zapcore.AddSync(logger)))
      }
   }
   return zap.CombineWriteSyncers(syncers...)
}

Niveau de log

La documentation officielle a des énumérations pour les niveaux de log, utilisez-les directement.

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
   }
   // Par défaut niveau Debug
   return zap.DebugLevel
}

Construction finale

go
func InitZap(config *ZapConfig) *zap.Logger {
   // Construire l'encodeur
   encoder := zapEncoder(config)
   // Construire le niveau de log
   levelEnabler := zapLevelEnabler(config)
   // Obtenir finalement Core et Options
   subCore, options := tee(config, encoder, levelEnabler)
    // Créer le Logger
   return zap.New(subCore, options...)
}

// Fusionner tous les éléments
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)
}

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

Résultat final

ZapLogTest      2023/01/09 - 19:44:00.91076     INFO    demo/zap.go:49     Initialisation du log terminée

Golang by www.golangdev.cn edit