Golangの高速なロガーzapとlumberjackでログを出力してrotateさせる

golang

https://github.com/uber-go/zap

$ go get -u go.uber.org/zap
$ go get -u gopkg.in/natefinch/lumberjack.v2

速さの秘訣

Go言語のLogger「zap」は何故高速に構造化されたログを出力する事が出来るのか|株式会社CAリワード

reflectionとallocationの回避。

一度allocateしたBufferやEncoderは sync.Poolで使い回している。 このPoolはまさにallocateされたアイテムを再利用するためのもので、GCの負担を緩和させることができる。 Poolのアイテムは勝手に削除されることがあり、もし参照しか持っていなかったらそのままdeallocateされる。

https://github.com/uber-go/zap/blob/v1.4.0/buffer/pool.go#L34

func NewPool() Pool {
	return Pool{p: &sync.Pool{
		New: func() interface{} {
			return &Buffer{bs: make([]byte, 0, _size)}
		},
	}}
}

使い方

現状ドキュメントが乏しいのでコードから探っていく必要がある。 まずはQuick Startから。

zap.NewProduction() は NewProductionConfig().Build(options…)ショートカット。 ConfigをBuildしてLoggerを取得し、InfoやErrorで書く流れ。

logger, _ := zap.NewProduction()
defer logger.Sync()
logger.Info("Hoge",
  // Structured context as strongly-typed Field values.
  zap.Int("attempt", 3),
  zap.Duration("backoff", time.Second),
)
{"level":"info","ts":1495870212.3378785,"caller":"zap-log/main.go:36","msg":"Hoge","attempt":3,"backoff":1}

NewProductionConfig() の内容はこんな感じ。ここからOutputPathを書き換えるとファイルに出力されるようにできる。

config := zap.Config{
    Level:       zap.NewAtomicLevelAt(zap.ErrorLevel),
    Development: false,
    Sampling: &zap.SamplingConfig{
        Initial:    100,
        Thereafter: 100,
    },
    Encoding: "json",
    EncoderConfig: zapcore.EncoderConfig{
        TimeKey:        "ts",
        LevelKey:       "level",
        NameKey:        "logger",
        CallerKey:      "caller",
        MessageKey:     "msg",
        StacktraceKey:  "stacktrace",
        LineEnding:     zapcore.DefaultLineEnding,
        EncodeLevel:    zapcore.LowercaseLevelEncoder,
        EncodeTime:     zapcore.EpochTimeEncoder,
        EncodeDuration: zapcore.SecondsDurationEncoder,
        EncodeCaller:   zapcore.ShortCallerEncoder,
    },
    OutputPaths:      []string{"stderr"},
    ErrorOutputPaths: []string{"stderr"},
}
config.OutputPaths = []string{"./aaaa.log"}
logger, _ = config.Build()
defer logger.Sync()

Buildの引数にも渡せる Option というのは apply(logger) でloggerを操作するインタフェース。 zap.Fieldsは フィールドを追加するもの。

logger = logger.WithOptions(zap.Fields(zap.String("hoge", "fuga")))
defer logger.Sync()
logger.Error("aaa",
    zap.String("eee", "eee"),
)

Buildの実装をみると、 中では zapcore.NewCore(enc, sink, cfg.Level) とOptionを引数として取る New() でloggerを生成している。

enc, err := cfg.buildEncoder()
if err != nil {
    return nil, err
}

sink, errSink, err := cfg.openSinks()
if err != nil {
    return nil, err
}

log := New(
    zapcore.NewCore(enc, sink, cfg.Level),
    cfg.buildOptions(errSink)...,
)

このsinkは io.Writer をwrapしたWriteSyncer で、 zapcore.AddSync(w io.Writer) で変換できる。 これにlumberjackを渡してやるとrotateできる。

config := zap.NewProductionConfig()
enc := zapcore.NewJSONEncoder(config.EncoderConfig)
sink := zapcore.AddSync(
    &lumberjack.Logger{
        Filename:   "./aaaa.log",
        MaxSize:    500, // megabytes
        MaxBackups: 3,
        MaxAge:     28, //days
    },
)
logger := zap.New(
    zapcore.NewCore(enc, sink, config.Level),
)
defer logger.Sync()
logger.Error("aaa",
    zap.String("eeef", "eefe"),
)