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/zapGuida Rapida
Gli autori forniscono due esempi di livello production, il primo è uno Sugar che supporta lo stile printf ma con prestazioni relativamente inferiori.
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
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.
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
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.
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.
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.
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
# 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
// 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.
type TimeEncoder func(time.Time, PrimitiveArrayEncoder)TimeEncoder è essenzialmente una funzione, possiamo usare altri encoder di tempo forniti dagli autori o scriverne uno personalizzato.
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
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/lumberjackIl codice finale è il seguente
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.
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
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