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/zapDé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.
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.
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.
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
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.
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.
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.
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.
# 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
// 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.
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.
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
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/lumberjackLe code final est le suivant
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.
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
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