経緯
Golangの構造化ロギングライブラリ『zap』を使ってて、ログレベル毎に出力先を分けたかったんだけど標準のCoreの実装ではできなさそう。
出力先を複数指定することはできるんだけど、同じログを複数の出力先に書き出すってだけで、InfoはこっちでErrorはこっちみたいなことはできなかった。
ログレベル毎にzapのLogger作ってそれらをラップした独自のLoggerみたいのを作ればできそうだけど、それだとログレベル毎にLoggerは独立しちゃうからWithとかNamedの恩恵が受けられない。
ということで、ログレベル毎に利用するCoreを分けるようなCoreを自作したら割りとうまくいったという話。
- zapの基本的な使い方は以前に書いたので興味があればご参考まで。
やったこと
とりあえず今回はErrorレベル未満のログ(Debug/Info/Warn)の場合は普通の出力先(標準出力とログファイル)に出力して、Errorレベル以上のログ(Error/DPanic/Panic/Fatal)の場合はエラー用の出力先(標準エラーとエラー用のファイル)に出力するようにしたい。
なので、
- 独自のCore(
errorDispatcher
)を作成 - ConfigからBuildできた方がいいかなということでzap.Configを拡張した
ErrorDispatcherConfig
を作成
というところまでやった。
Coreをつくる
import "go.uber.org/zap/zapcore"
func NewErrorDispatcher(base zapcore.Core, err zapcore.Core) zapcore.Core {
return &errorDispatcher{
Core: base,
err: err,
}
}
type errorDispatcher struct {
zapcore.Core
err zapcore.Core
}
func (e *errorDispatcher) With(fields []zapcore.Field) zapcore.Core {
clone := e.clone()
clone.Core = e.Core.With(fields)
clone.err = e.err.With(fields)
return clone
}
func (e *errorDispatcher) Check(ent zapcore.Entry, ce *zapcore.CheckedEntry) *zapcore.CheckedEntry {
if ent.Level >= zapcore.ErrorLevel {
return e.err.Check(ent, ce)
}
return e.Core.Check(ent, ce)
}
func (e *errorDispatcher) Sync() error {
if err := e.err.Sync(); err != nil {
return err
}
return e.Core.Sync()
}
func (e *errorDispatcher) clone() *errorDispatcher {
return &errorDispatcher{
Core: e.Core,
err: e.err,
}
}
通常用のCoreをembeddedして、エラー用のCoreをerrメンバに持つerrorDispatcher
を作って、zapcore.Core
インターフェースを満たすようにメソッドを実装した。
ポイント
- 通常用のCoreをembeddedする
embeddedすることでEnabledメソッドやWriteメソッドを定義しなくて済む。
ioCoreと同じだしね。
-
With
メソッドではクローンして新しいerrorDispatcherを返す
クローンしないとWithの呼び出し元のCoreが変わってしまうのでzapの挙動としてはおかしい。Withの呼び出し元のCoreとWithから返されるCoreは別物でないといけない。
-
With
メソッドでは保持している全てのCoreに対してWith
メソッドを呼び出す
今回のように複数のCoreを保持している場合、その全てに対してWith
メソッドを呼び出してやる。でないとFieldの引き継ぎに不整合が出てきてしまう。
-
Check
でログレベル毎に使うCoreを分ける
ここが本命。
Check
はロギングメソッドから呼び出され、有効なログレベルなどを満たしていればcheckしたCoreを格納したCheckedEntryが返される。CheckedEntryから格納されている全てのCoreのWriteメソッドが呼ばれ、ロギングする。
なので、このCheck
メソッドをどのCoreでやるかというのがポイント。
今回はErrorレベル以上はエラー用のCore(errorDispatcher.err
)でやりたいので条件を設定して振り分けをしている。
Configをつくる
type ErrorDispatcherConfig struct {
zap.Config `json:",inline" yaml:",inline"`
ErrorDispatcherPaths []string `json:"errorDispatcherPaths" yaml:"errorDispatcherPaths"`
}
func (c *ErrorDispatcherConfig) Build(opts ...zap.Option) (*zap.Logger, error) {
enc, err := c.buildEncoder()
if err != nil {
return nil, err
}
sink, errDispSink, errSink, err := c.openSinks()
if err != nil {
return nil, err
}
baseCore := zapcore.NewCore(enc, sink, c.Level)
errCore := zapcore.NewCore(enc, errDispSink, c.Level)
errorDispatcher := core.NewErrorDispatcher(baseCore, errCore)
log := zap.New(
errorDispatcher,
c.buildOptions(errSink)...,
)
if len(opts) > 0 {
log = log.WithOptions(opts...)
}
return log, nil
}
基本的にはzap.Config
を参考に作ってる。
ErrorDispatcherConfigを作って、メンバにembeddedしたzap.ConfigとError以上のログの出力先を指定するErrorDispatcherPathsを定義する。
それぞれjson/yaml用にタグを設定してあげればファイルからの読み込みもできるようになりますね。
Buildメソッドもほぼほぼzap.Config
を参考。
違うのはSinksでエラー用のSinkも作って返すようにしてるところと、自作したErrorDispatcherを作って、それをベースにLoggerを作ってるところ。
試してみる
それでは確認してみます。
func main() {
configYaml, err := ioutil.ReadFile("example/config.yaml")
if err != nil {
panic(err)
}
var myConfig config.ErrorDispatcherConfig
if err := yaml.Unmarshal(configYaml, &myConfig); err != nil {
panic(err)
}
logger, err := myConfig.Build()
if err != nil {
panic(err)
}
logger.Debug("debug log")
logger.Error("error log")
logger = logger.With(zap.String("with", "with_value"))
logger.Info("debug log")
logger.DPanic("dpanic log")
logger = logger.Named("named")
logger.Warn("warn log")
logger.Fatal("fatal log")
}
Yamlファイルから読み込んでErrorDispatcherConfigにUnmarshal。
BuildメソッドからLoggerを生成してロギング。
標準のzapと同じように扱えるのはうれしいことだと思う。
level: "debug"
development: false
disableCaller: true
disableStacktrace: true
encoding: "console"
encoderConfig:
messageKey: "Msg"
levelKey: "Level"
timeKey: "Time"
nameKey: "Name"
callerKey: "Caller"
stacktraceKey: "St"
levelEncoder: "capital"
timeEncoder: "iso8601"
durationEncoder: "string"
callerEncoder: "short"
outputPaths:
- "stdout"
- "example/yaml_out.log"
errorDispatcherPaths:
- "stderr"
- "example/yaml_err.log"
errorOutputPaths:
- "stderr"
Yamlファイルはこんな感じ。
今回追加したerrorDispatcherPathsには標準エラーとエラー用のファイルパスを設定してる。
2017-03-29T13:38:19.847+0900 DEBUG debug log
2017-03-29T13:38:19.847+0900 INFO debug log {"with": "with_value"}
2017-03-29T13:38:19.847+0900 WARN named warn log {"with": "with_value"}
2017-03-29T13:38:19.847+0900 ERROR error log
2017-03-29T13:38:19.847+0900 DPANIC dpanic log {"with": "with_value"}
2017-03-29T13:38:19.847+0900 FATAL named fatal log {"with": "with_value"}
結果はこんな感じで、Errorレベル未満のログとそれ以上のログで分かれて出力されるようになった。
With
で繋いだところも大丈夫そう。
所感
比較的簡単にできた。
今回やったのは単純にする為に2つのCoreでやったけど、より柔軟にするなら、
type levelDispatcher struct {
zapcore.Core
cores map[zapcore.Level]zapcore.Core
}
とかにして、レベル毎にCoreをセットできるようにすればどんなログレベルでも対応できるはず。
zapはCoreレベルまで弄れば柔軟にやりたいことができるのがいいですね。
とはいえ、これくらい標準でできないのかなーとは思えてならない。
今回のサンプルはgithubに置いてあるので参考まで。