Skip to content

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 입니다.

go
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 입니다.

go
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 의 구성도 구성 파일을 통해 역직렬화를 지원하지만 기본 구성만 지원합니다. 고급 구성의 경우 공식 예제도 매우 간단하여 실제 사용에는 부족하므로 세부 구성에 대해 자세히 설명하겠습니다.

먼저 전체 구성 구조체를 살펴보아야 하며 각 필드의 의미를 이해해야 합니다.

go
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"`
}

다음은 인코딩 구성에 대한 세부 사항입니다.

go
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 은 일부 구성의 스위치 및 적용에 관한 것으로 많은 구현이 있습니다.

go
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
  })
}

이는 가장 일반적으로 사용되는 로그 코어로 내부 필드는 기본적으로 구성 단계를 나타내며 공식에서 역직렬화 구성 시의 단계를 참고하면 대략 동일합니다.

go
type ioCore struct {
   // 로그 레벨
   LevelEnabler
   // 로그 인코딩
   enc Encoder
   // 로그 기록
   out WriteSyncer
}

zap.Encoder 는 로그 포맷팅, 인코딩을 담당합니다.

zap.WriteSyncer 는 로그 출력을 담당하며 주로 파일과 콘솔로 출력합니다.

zap.LevelEnabler 는 최소 로그 레벨로 해당 레벨 이하의 로그는 syncer 를 통해 출력되지 않습니다.

로그 인코딩

로그 인코딩은 주로 로그의 세부 사항 포맷팅과 관련이 있으며 먼저 원본 로그 출력을 직접 사용하는 것을 살펴보겠습니다.

go
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 이라는 구성 파일을 생성합니다.

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"

매핑된 구조체

go
// 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 를 사용하며 구체적인 코드는 생략합니다.

go
type TimeEncoder func(time.Time, PrimitiveArrayEncoder)

TimeEncoder 는 본질적으로 함수이며 공식에서 제공하는 다른 시간 인코더를 사용하거나 직접 작성할 수 있습니다.

go
func CustomTimeFormatEncoder(t time.Time, encoder zapcore.PrimitiveArrayEncoder) {
   encoder.AppendString(global.Config.ZapConfig.Prefix + "\t" + t.Format(global.Config.ZapConfig.TimeFormat))
}

전체 부분은 다음과 같습니다.

go
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

최종 코드는 다음과 같습니다.

go
 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...)
}

로그 레벨

공식에는 로그 레벨에 대한 열거형 항목이 있으며 직접 사용하면 됩니다.

go
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
}

최종 구축

go
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     로그 초기화 완료

Golang by www.golangdev.cn edit