My External Storage

May 24, 2018 - 4 minute read - Comments - go

[Go]go.uber.org/zap.loggerの出力をbytes.Bufferに変更する #golang

go.uber.org/zapzap.loggerは構造化されたログを高速に出力できるとしてGolangのLoggerの中で有名だ。

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

gRPCを用いたMicroservicesを構成する際にも利用されることが多い。

https://github.com/grpc-ecosystem/go-grpc-middleware/tree/master/logging/zap

go.uber.org/zapzap.loggerの利用方法としてよく見るのは zao.Config構造体を生成し、Build()メソッドからzap.Loggerインスタンスを生成するやり方だ。
この生成方法だと、文字列で出力先を指定するため、一見標準(エラー)出力もしくはファイルにしか出力できないように読める。

https://godoc.org/go.uber.org/zap#Config

    // OutputPaths is a list of paths to write logging output to. See Open for
    // details.
    OutputPaths []string `json:"outputPaths" yaml:"outputPaths"`

このログ出力先をbytes.Buffer(io.Writer)に変更する。

TL;DR

  • zap.ConfigBuild()メソッドでzap.Loggerを生成するとログ出力先を標準(エラー)出力orファイルにしかできない
  • zap.New関数でzap.Loggerを生成すると任意のio.Writerにログを出力できる

    // 引数のbytes.Bufferにログを出力するzap.Loggerを生成する
    func getDummyLogger(buf *bytes.Buffer) *zap.Logger {
        encoder := zapcore.NewConsoleEncoder(...)
        core := zapcore.NewCore(encoder, zapcore.AddSync(buf), zapcore.InfoLevel)
        return zap.New(core)
    }

zapとは

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

go.uber.org/zapは構造化したメッセージを高速にロギングできるライブラリ。 ただ、標準ライブラリのlogパッケージのインターフェースとzap.logger構造体は大きく異なる。

なぜやりたかったのか

一定の処理を加えたあとにログ出力をするlogging用のMiddlewareやInterceptorを作ろうと思ったときに、 Loggerに渡すログ出力自体を検証したいと考えることがある。Goの場合、出力先がio.Writerインターフェースならば、テストをするときだけbytes.Bufferなどに置き換えて検証することが出来る。
しかし、zap.Configにあるzap.Loggerを出力先を指定する設定方法は文字列指定であり、stdout(stderr)もしくは出力先のファイルパスしか設定出来ない。

https://godoc.org/go.uber.org/zap#Config

    // OutputPaths is a list of paths to write logging output to. See Open for
    // details.
    OutputPaths []string `json:"outputPaths" yaml:"outputPaths"`

なのでzapcoreパッケージを使う方法でio.Writerへログ出力をするzap.Loggerインスタンスを取得する。

zap.New、zapcore.NewCoreを使ったzap.Loggerの生成

zap.Loggerのインスタンスを生成する方法はConfigインスタンスのBuild()メソッドを利用する他に、zap.New()関数を利用する方法がある。

https://godoc.org/go.uber.org/zap#New

func New(core zapcore.Core, options ...Option) *Logger

New()関数のzapcore.Corezapcore.NewCore関数から生成できる。

https://godoc.org/go.uber.org/zap/zapcore#NewCore

func NewCore(enc Encoder, ws WriteSyncer, enab LevelEnabler) Core

この第二引数のzapcore.WriteSyncerインターフェースがzap.New()関数から生成したときのzap.Loggerの出力先になる。
zapcore.WriteSyncerインターフェースはio.WriterSync()メソッドを満たしていればよい。

type WriteSyncer interface {
    io.Writer
    Sync() error
}

そしてひとまずインターフェースを満たすだけで良いならばio.Writerを引数にzapcore.WriteSyncerを返す便利な関数がzapcoreパッケージに存在する。

https://godoc.org/go.uber.org/zap/zapcore#AddSync

func AddSync(w io.Writer) WriteSyncer

よって、以下のような手順を踏めば、zapcore.NewCore関数、zap.New関数を経由すればbytes.Buffer(io.Writer)にログ出力をするzap.Loggerを取得できる。

func getDummyLogger(buf *bytes.Buffer) *zap.Logger {
        // ログ出力のフォーマットを指定できる
        encoder := zapcore.NewConsoleEncoder(zapcore.EncoderConfig{
                // TimeKey: "time", // 時刻情報は期待結果に含めにくいので省略
                NameKey:        "name",
                EncodeLevel:    zapcore.CapitalLevelEncoder,
                EncodeTime:     zapcore.ISO8601TimeEncoder,
                EncodeDuration: zapcore.StringDurationEncoder,
                EncodeCaller:   zapcore.ShortCallerEncoder,
        })
        core := zapcore.NewCore(encoder, zapcore.AddSync(buf), zapcore.InfoLevel)
        return zap.New(core)
}

終わりに

Goはシンプルな標準インターフェースとダックタイピングを提供している。
そのため、疎な設計になっていれば標準使用のみで容易にモックを使うことが出来る。

関連記事