go.uber.org/zapのzap.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/zapのzap.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.ConfigのBuild()メソッドで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.Coreはzapcore.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.WriterとSync()メソッドを満たしていればよい。
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はシンプルな標準インターフェースとダックタイピングを提供している。
そのため、疎な設計になっていれば標準使用のみで容易にモックを使うことが出来る。