Zap
Zap — это быстрый, структурированный, уровневый компонент логирования, созданный на Go.
Официальный репозиторий: uber-go/zap: Blazing fast, structured, leveled logging in Go. (github.com)
Официальная документация: zap package - go.uber.org/zap - Go Packages
Установка
go get -u go.uber.org/zapБыстрый старт
Официальная документация предоставляет два примера быстрого старта. Оба являются производственными логгерами. Первый — Sugar, который поддерживает стиль printf, но с относительно более низкой производительностью.
logger, _ := zap.NewProduction()
defer logger.Sync() // Сброс буферизованных записей лога в файл при выходе программы
sugar := logger.Sugar()
sugar.Infow("failed to fetch URL",
"url", url,
"attempt", 3,
"backoff", time.Second,
)
sugar.Infof("Failed to fetch URL: %s", url)Второй — logger, который имеет лучшую производительность, но поддерживает только строго типизированный вывод.
logger, _ := zap.NewProduction()
defer logger.Sync()
logger.Info("failed to fetch URL",
// Структурированный контекст как строго типизированные значения Field.
zap.String("url", url),
zap.Int("attempt", 3),
zap.Duration("backoff", time.Second),
)TIP
Zap очень прост в использовании. Сложная часть — настройка логгера, подходящего для вашего проекта. Официальных примеров мало, поэтому читайте комментарии к исходному коду больше.
Конфигурация
Обычно конфигурация логгера пишется в файлах конфигурации. Конфигурация Zap также поддерживает десериализацию из файлов конфигурации, но поддерживает только базовую конфигурацию. Даже для расширенной конфигурации официальные примеры очень кратки и недостаточны для производственного использования, поэтому нам нужно обсудить детальную конфигурацию.
Сначала посмотрим на общую структуру конфигурации. Нам нужно понять значение каждого поля.
type Config struct {
// Минимальный уровень логирования
Level AtomicLevel `json:"level" yaml:"level"`
// Режим разработки, в основном влияет на трассировку стека
Development bool `json:"development" yaml:"development"`
// Отслеживание вызывающего
DisableCaller bool `json:"disableCaller" yaml:"disableCaller"`
// Трассировка стека
DisableStacktrace bool `json:"disableStacktrace" yaml:"disableStacktrace"`
// Выборка, запись только представительных логов при ограничении влияния на производительность логов
Sampling *SamplingConfig `json:"sampling" yaml:"sampling"`
// Кодирование, либо json, либо режим консоли
Encoding string `json:"encoding" yaml:"encoding"`
// Конфигурация кодировщика, в основном конфигурация форматирования вывода
EncoderConfig zapcore.EncoderConfig `json:"encoderConfig" yaml:"encoderConfig"`
// Пути вывода файлов логов
OutputPaths []string `json:"outputPaths" yaml:"outputPaths"`
// Пути вывода файлов ошибок
ErrorOutputPaths []string `json:"errorOutputPaths" yaml:"errorOutputPaths"`
// Добавление некоторого содержимого вывода по умолчанию в логи
InitialFields map[string]interface{} `json:"initialFields" yaml:"initialFields"`
}Ниже приведены детали конфигурации кодировщика
type EncoderConfig struct {
// Значение ключа, если ключ пустой, соответствующее свойство не будет выведено
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"`
// Некоторые пользовательские кодировщики
EncodeLevel LevelEncoder `json:"levelEncoder" yaml:"levelEncoder"`
EncodeTime TimeEncoder `json:"timeEncoder" yaml:"timeEncoder"`
EncodeDuration DurationEncoder `json:"durationEncoder" yaml:"durationEncoder"`
EncodeCaller CallerEncoder `json:"callerEncoder" yaml:"callerEncoder"`
// Кодировщик имени логгера
EncodeName NameEncoder `json:"nameEncoder" yaml:"nameEncoder"`
// Кодировщик рефлексии, в основном для типов interface{}, если нет jsonencoder по умолчанию
NewReflectedEncoder func(io.Writer) ReflectedEncoder `json:"-" yaml:"-"`
// Строка разделителя вывода консоли
ConsoleSeparator string `json:"consoleSeparator" yaml:"consoleSeparator"`
}Option — это переключатели конфигурации и приложения, с множеством реализаций.
type Option interface {
apply(*Logger)
}
// Реализация Option
type optionFunc func(*Logger)
func (f optionFunc) apply(log *Logger) {
f(log)
}
// Приложение
func Development() Option {
return optionFunc(func(log *Logger) {
log.development = true
})
}Это наиболее часто используемое ядро логгера. Его внутренние поля в основном представляют наши шаги конфигурации. Вы также можете обратиться к официальным шагам для десериализации конфигурации, которые примерно одинаковы.
type ioCore struct {
// Уровень логирования
LevelEnabler
// Кодирование лога
enc Encoder
// Запись лога
out WriteSyncer
}zap.Encoder отвечает за форматирование и кодирование лога
zap.WriteSyncer отвечает за вывод лога, в основном в файлы и консоль
zap.LevelEnabler — это минимальный уровень логирования; логи ниже этого уровня больше не выводятся через syncer.
Кодирование лога
Кодирование лога в основном включает детали форматирования логов. Сначала посмотрим на вывод при использовании оригинального логгера напрямую.
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"}Вы обнаружите, что у этого лога есть несколько проблем:
- Нет временной метки
- Нет информации о вызывающем, не знаете, откуда был выведен этот лог, что затрудняет устранение ошибок
- Нет трассировки стека
Далее решим эти проблемы пошагово, в основном изменяя zapcore.EncoderConfig. Сначала нам нужно написать собственный файл конфигурации вместо использования официальной прямой десериализации. Создайте файл конфигурации config.yml
# Конфигурация логгера Zap
zap:
prefix: ZapLogTest
timeFormat: 2006/01/02 - 15:04:05.00000
level: debug
caller: true
stackTrace: false
encode: console
# Куда выводить логи: file | console | both
writer: both
logFile:
maxSize: 20
backups: 5
compress: true
output:
- "./log/output.log"Сопоставленная структура
// ZapConfig
// @Date: 2023-01-09 16:37:05
// @Description: структура конфигурации логгера 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: структура конфигурации файла лога
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
Используйте Viper для чтения конфигурации; код здесь опущен.
type TimeEncoder func(time.Time, PrimitiveArrayEncoder)TimeEncoder на самом деле является функцией. Мы можем использовать другие кодировщики времени, предоставленные официальными, или написать собственный.
func CustomTimeFormatEncoder(t time.Time, encoder zapcore.PrimitiveArrayEncoder) {
encoder.AppendString(global.Config.ZapConfig.Prefix + "\t" + t.Format(global.Config.ZapConfig.TimeFormat))
}Общая часть следующая
func zapEncoder(config *ZapConfig) zapcore.Encoder {
// Создать новую конфигурацию
encoderConfig := zapcore.EncoderConfig{
TimeKey: "Time",
LevelKey: "Level",
NameKey: "Logger",
CallerKey: "Caller",
MessageKey: "Message",
StacktraceKey: "StackTrace",
LineEnding: zapcore.DefaultLineEnding,
FunctionKey: zapcore.OmitKey,
}
// Пользовательский формат времени
encoderConfig.EncodeTime = CustomTimeFormatEncoder
// Уровень лога в верхнем регистре
encoderConfig.EncodeLevel = zapcore.CapitalColorLevelEncoder
// Интервал времени в секундах
encoderConfig.EncodeDuration = zapcore.SecondsDurationEncoder
// Краткий вывод вызывающего
encoderConfig.EncodeCaller = zapcore.ShortCallerEncoder
// Полная сериализация имени логгера
encoderConfig.EncodeName = zapcore.FullNameEncoder
// Окончательное кодирование лога: json или console
switch config.Encode {
case "json":
{
return zapcore.NewJSONEncoder(encoderConfig)
}
case "console":
{
return zapcore.NewConsoleEncoder(encoderConfig)
}
}
// Консоль по умолчанию
return zapcore.NewConsoleEncoder(encoderConfig)
}Вывод лога
Вывод лога делится на вывод консоли и вывод файла. Мы можем настроить динамически на основе файла конфигурации. Если нам нужна ротация файлов логов, нужно использовать другую стороннюю зависимость.
go get -u github.com/natefinch/lumberjackОкончательный код следующий
func zapWriteSyncer(cfg *ZapConfig) zapcore.WriteSyncer {
syncers := make([]zapcore.WriteSyncer, 0, 2)
// Если включен вывод консоли, добавить writer консоли
if cfg.Writer == config.WriteBoth || cfg.Writer == config.WriteConsole {
syncers = append(syncers, zapcore.AddSync(os.Stdout))
}
// Если включено логирование в файл, добавить writers на основе путей файлов
if cfg.Writer == config.WriteBoth || cfg.Writer == config.WriteFile {
// Добавить вывод лога
for _, path := range cfg.LogFile.Output {
logger := &lumberjack.Logger{
Filename: path, // Путь файла
MaxSize: cfg.LogFile.MaxSize, // Размер разделённых файлов
MaxBackups: cfg.LogFile.BackUps, // Количество резервных копий
Compress: cfg.LogFile.Compress, // Сжимать ли
LocalTime: true, // Использовать локальное время
}
syncers = append(syncers, zapcore.Lock(zapcore.AddSync(logger)))
}
}
return zap.CombineWriteSyncers(syncers...)
}Уровень лога
В официальной документации есть элементы enum для уровней логов; просто используйте их напрямую.
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
}
// Уровень Debug по умолчанию
return zap.DebugLevel
}Окончательная сборка
func InitZap(config *ZapConfig) *zap.Logger {
// Собрать кодировщик
encoder := zapEncoder(config)
// Собрать уровень лога
levelEnabler := zapLevelEnabler(config)
// Наконец получить Core и Options
subCore, options := tee(config, encoder, levelEnabler)
// Создать Logger
return zap.New(subCore, options...)
}
// Объединить всё
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)
}
// Собрать Options
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
}Окончательный результат
ZapLogTest 2023/01/09 - 19:44:00.91076 INFO demo/zap.go:49 Logger initialization complete