Zap
Zap é um componente de logs rápido, estruturado e nivelado construído em Go.
Repositório oficial: uber-go/zap: Blazing fast, structured, leveled logging in Go. (github.com)
Documentação oficial: zap package - go.uber.org/zap - Go Packages
Instalação
go get -u go.uber.org/zapInício Rápido
A equipe oficial forneceu dois exemplos de logs de nível de produção. O primeiro é um Sugar que suporta o estilo printf mas com desempenho relativamente menor.
logger, _ := zap.NewProduction()
defer logger.Sync() // Sincroniza o buffer para o arquivo no final do programa
sugar := logger.Sugar()
sugar.Infow("failed to fetch URL",
"url", url,
"attempt", 3,
"backoff", time.Second,
)
sugar.Infof("Failed to fetch URL: %s", url)O segundo é um logger com melhor desempenho, mas suporta apenas saída fortemente tipada.
logger, _ := zap.NewProduction()
defer logger.Sync()
logger.Info("failed to fetch URL",
// Contexto estruturado como valores de campo fortemente tipados.
zap.String("url", url),
zap.Int("attempt", 3),
zap.Duration("backoff", time.Second),
)TIP
O uso do Zap é muito simples, o ponto problemático é configurar logs adequados para seu projeto. Os exemplos oficiais são poucos, então é necessário ler mais os comentários do código-fonte.
Configuração
Geralmente, a configuração de logs é escrita em arquivos de configuração. A configuração do Zap também suporta desserialização através de arquivos de configuração, mas suporta apenas configurações básicas. Mesmo as configurações avançadas nos exemplos oficiais são muito simples, insuficientes para uso em produção, então vamos detalhar as configurações.
Primeiro, vamos ver a estrutura geral de configuração, precisamos entender o significado de cada campo.
type Config struct {
// Nível mínimo de log
Level AtomicLevel `json:"level" yaml:"level"`
// Modo de desenvolvimento, afeta principalmente o rastreamento de pilha
Development bool `json:"development" yaml:"development"`
// Rastreamento de chamador
DisableCaller bool `json:"disableCaller" yaml:"disableCaller"`
// Rastreamento de pilha
DisableStacktrace bool `json:"disableStacktrace" yaml:"disableStacktrace"`
// Amostragem, registra apenas alguns logs mais representativos enquanto limita o uso de desempenho
Sampling *SamplingConfig `json:"sampling" yaml:"sampling"`
// Codificação,分为 json e console
Encoding string `json:"encoding" yaml:"encoding"`
// Configuração de codificação, principalmente formatação de saída
EncoderConfig zapcore.EncoderConfig `json:"encoderConfig" yaml:"encoderConfig"`
// Caminho de saída do arquivo de log
OutputPaths []string `json:"outputPaths" yaml:"outputPaths"`
// Caminho de saída do arquivo de erro
ErrorOutputPaths []string `json:"errorOutputPaths" yaml:"errorOutputPaths"`
// Adiciona alguns campos de saída padrão ao log
InitialFields map[string]interface{} `json:"initialFields" yaml:"initialFields"`
}Abaixo estão os detalhes sobre a configuração de codificação.
type EncoderConfig struct {
// Chave de valor, se a chave estiver vazia, a propriedade correspondente não será exibida
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"`
// Alguns codificadores personalizados
EncodeLevel LevelEncoder `json:"levelEncoder" yaml:"levelEncoder"`
EncodeTime TimeEncoder `json:"timeEncoder" yaml:"timeEncoder"`
EncodeDuration DurationEncoder `json:"durationEncoder" yaml:"durationEncoder"`
EncodeCaller CallerEncoder `json:"callerEncoder" yaml:"callerEncoder"`
// Codificador de nome do logger
EncodeName NameEncoder `json:"nameEncoder" yaml:"nameEncoder"`
// Codificador de reflexão, principalmente para tipo interface{}, se não houver jsonencoder padrão
NewReflectedEncoder func(io.Writer) ReflectedEncoder `json:"-" yaml:"-"`
// String de separação de saída do console
ConsoleSeparator string `json:"consoleSeparator" yaml:"consoleSeparator"`
}Option é sobre switches e aplicações de algumas configurações, há muitas implementações.
type Option interface {
apply(*Logger)
}
// Implementação de Option
type optionFunc func(*Logger)
func (f optionFunc) apply(log *Logger) {
f(log)
}
// Aplicação
func Development() Option {
return optionFunc(func(log *Logger) {
log.development = true
})
}Este é o núcleo de log mais comumente usado, seus campos internos basicamente representam nossas etapas de configuração, também podemos nos referir às etapas da equipe oficial ao desserializar configurações, são aproximadamente as mesmas.
type ioCore struct {
// Nível de log
LevelEnabler
// Codificação de log
enc Encoder
// Escrita de log
out WriteSyncer
}zap.Encoder é responsável pela formatação e codificação de logs
zap.WriteSyncer é responsável pela saída de logs, principalmente para arquivo e console
zap.LevelEnabler é o nível mínimo de log, logs abaixo deste nível não serão mais exibidos através de syncer.
Codificação de Logs
A codificação de logs envolve principalmente a formatação de detalhes de logs. Primeiro, vamos ver a saída do log original.
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"}Você notará que este log tem alguns problemas:
- Sem tempo
- Sem informações do chamador, não sabemos de onde este log foi gerado, o que dificulta a depuração em caso de erro
- Sem informações de pilha
Agora vamos resolver os problemas passo a passo, principalmente modificando zapcore.EncoderConfig. Primeiro, precisamos escrever nosso próprio arquivo de configuração, não usar a desserialização direta oficial. Primeiro, crie um arquivo de configuração config.yml
# Configuração de Log Zap
zap:
prefix: ZapLogTest
timeFormat: 2006/01/02 - 15:04:05.00000
level: debug
caller: true
stackTrace: false
encode: console
# Para onde o log é enviado file | console | both
writer: both
logFile:
maxSize: 20
backups: 5
compress: true
output:
- "./log/output.log"Mapeado para a estrutura
// ZapConfig
// @Date: 2023-01-09 16:37:05
// @Description: Estrutura de configuração de 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: Estrutura de configuração de arquivo 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
A leitura de configuração usa Viper, o código foi omitido.
type TimeEncoder func(time.Time, PrimitiveArrayEncoder)TimeEncoder é essencialmente uma função, podemos usar outros codificadores de tempo fornecidos oficialmente ou escrever os nossos próprios.
func CustomTimeFormatEncoder(t time.Time, encoder zapcore.PrimitiveArrayEncoder) {
encoder.AppendString(global.Config.ZapConfig.Prefix + "\t" + t.Format(global.Config.ZapConfig.TimeFormat))
}A parte geral é a seguinte
func zapEncoder(config *ZapConfig) zapcore.Encoder {
// Cria uma nova configuração
encoderConfig := zapcore.EncoderConfig{
TimeKey: "Time",
LevelKey: "Level",
NameKey: "Logger",
CallerKey: "Caller",
MessageKey: "Message",
StacktraceKey: "StackTrace",
LineEnding: zapcore.DefaultLineEnding,
FunctionKey: zapcore.OmitKey,
}
// Formato de tempo personalizado
encoderConfig.EncodeTime = CustomTimeFormatEncoder
// Nível de log em maiúsculas
encoderConfig.EncodeLevel = zapcore.CapitalColorLevelEncoder
// Intervalo de tempo em segundos
encoderConfig.EncodeDuration = zapcore.SecondsDurationEncoder
// Saída de chamador abreviada
encoderConfig.EncodeCaller = zapcore.ShortCallerEncoder
// Serialização completa do nome do logger
encoderConfig.EncodeName = zapcore.FullNameEncoder
// Codificação final de log json ou console
switch config.Encode {
case "json":
{
return zapcore.NewJSONEncoder(encoderConfig)
}
case "console":
{
return zapcore.NewConsoleEncoder(encoderConfig)
}
}
// Console padrão
return zapcore.NewConsoleEncoder(encoderConfig)
}Saída de Logs
A saída de logs é dividida entre console e arquivo. Podemos configurar dinamicamente com base no arquivo de configuração e, se quisermos realizar rotação de arquivos de log, precisamos usar outra dependência de terceiros.
go get -u github.com/natefinch/lumberjackO código final é o seguinte
func zapWriteSyncer(cfg *ZapConfig) zapcore.WriteSyncer {
syncers := make([]zapcore.WriteSyncer, 0, 2)
// Se a saída do console de log estiver habilitada, adiciona o gravador do console
if cfg.Writer == config.WriteBoth || cfg.Writer == config.WriteConsole {
syncers = append(syncers, zapcore.AddSync(os.Stdout))
}
// Se o armazenamento de arquivo de log estiver habilitado, adiciona gravadores com base nos caminhos dos arquivos
if cfg.Writer == config.WriteBoth || cfg.Writer == config.WriteFile {
// Adiciona saída de log
for _, path := range cfg.LogFile.Output {
logger := &lumberjack.Logger{
Filename: path, // Caminho do arquivo
MaxSize: cfg.LogFile.MaxSize, // Tamanho do arquivo para divisão
MaxBackups: cfg.LogFile.BackUps, // Número de backups
Compress: cfg.LogFile.Compress, // Comprimir ou não
LocalTime: true, // Usar hora local
}
syncers = append(syncers, zapcore.Lock(zapcore.AddSync(logger)))
}
}
return zap.CombineWriteSyncers(syncers...)
}Nível de Log
A equipe oficial tem enumerações sobre níveis de log, basta usá-las.
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
}
// Padrão Debug
return zap.DebugLevel
}Construção Final
func InitZap(config *ZapConfig) *zap.Logger {
// Constrói o codificador
encoder := zapEncoder(config)
// Constrói o nível de log
levelEnabler := zapLevelEnabler(config)
// Finalmente obtém Core e Options
subCore, options := tee(config, encoder, levelEnabler)
// Cria Logger
return zap.New(subCore, options...)
}
// Combina tudo
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)
}
// Constrói 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
}Resultado final
ZapLogTest 2023/01/09 - 19:44:00.91076 INFO demo/zap.go:49 日志初始化完成