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빠른 시작
공식에서는 두 가지 프로덕션 레벨 로그 예제를 제공합니다. 첫 번째는 printf 스타일을 지원하지만 상대적으로 성능이 낮은 Sugar 입니다.
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",
// Structured context as strongly typed Field values.
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 과 console 두 가지 모드
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 {
// 키 값, key 가 비어 있으면 해당 속성은 출력되지 않음
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
// 직렬화된 logger 이름 완성
encoderConfig.EncodeName = zapcore.FullNameEncoder
// 최종 로그 인코딩 json 또는 console
switch config.Encode {
case "json":
{
return zapcore.NewJSONEncoder(encoderConfig)
}
case "console":
{
return zapcore.NewConsoleEncoder(encoderConfig)
}
}
// 기본 console
return zapcore.NewConsoleEncoder(encoderConfig)
}로그 출력
로그 출력은 콘솔 출력과 파일 출력으로 나뉘며 구성 파일에 따라 동적으로 구성할 수 있으며 로그 파일 분할을 원하면 다른 서드파티 의존성을 사용해야 합니다.
go get -u github.com/natefinch/lumberjack최종 코드는 다음과 같습니다.
func zapWriteSyncer(cfg *ZapConfig) zapcore.WriteSyncer {
syncers := make([]zapcore.WriteSyncer, 0, 2)
// 로그 콘솔 출력이 활성화되어 있으면 콘솔 기록기 추가
if cfg.Writer == config.WriteBoth || cfg.Writer == config.WriteConsole {
syncers = append(syncers, zapcore.AddSync(os.Stdout))
}
// 로그 파일 저장이 활성화되어 있으면 파일 경로 슬라이스에 따라 기록기 추가
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...)
}로그 레벨
공식에는 로그 레벨에 대한 열거형 항목이 있으며 직접 사용하면 됩니다.
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)
}
// 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
}최종 결과
ZapLogTest 2023/01/09 - 19:44:00.91076 INFO demo/zap.go:49 로그 초기화 완료