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/zapSchnellstart
Die offizielle Dokumentation zeigt zwei Schnellstart-Beispiele, beide sind produktionsreife Logger. Der erste ist Sugar, der printf-Stil unterstützt, aber relativ langsam ist.
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.
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.
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:
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.
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.
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.
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.
# 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:
// 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.
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.
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:
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/lumberjackDer endgültige Code sieht wie folgt aus:
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.
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
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