zapでログレベルでログの出力先を振り分ける方法

# 経緯
Golangの構造化ロギングライブラリ『[zap](https://github.com/uber-go/zap)』を使ってて、ログレベル毎に出力先を分けたかったんだけど標準のCoreの実装ではできなさそう。
出力先を複数指定することはできるんだけど、同じログを複数の出力先に書き出すってだけで、InfoはこっちでErrorはこっちみたいなことはできなかった。
ログレベル毎にzapのLogger作ってそれらをラップした独自のLoggerみたいのを作ればできそうだけど、それだとログレベル毎にLoggerは独立しちゃうからWithとかNamedの恩恵が受けられない。
ということで、ログレベル毎に利用するCoreを分けるようなCoreを自作したら割りとうまくいったという話。

* zapの基本的な使い方は[以前に書いた](http://qiita.com/emonuh/items/28dbee9bf2fe51d28153)ので興味があればご参考まで。


# やったこと
とりあえず今回はErrorレベル未満のログ（Debug/Info/Warn）の場合は普通の出力先（標準出力とログファイル）に出力して、Errorレベル以上のログ（Error/DPanic/Panic/Fatal）の場合はエラー用の出力先（標準エラーとエラー用のファイル）に出力するようにしたい。
なので、

* 独自のCore(`errorDispatcher`)を作成
* ConfigからBuildできた方がいいかなということでzap.Configを拡張した`ErrorDispatcherConfig`を作成

というところまでやった。

# Coreをつくる
```go:core/error_dispatcher.go
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をつくる
```go:config/error_dispatcher.go~抜粋~
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を作ってるところ。

# 試してみる
それでは確認してみます。

```go:main.go
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と同じように扱えるのはうれしいことだと思う。

```yaml:config.yaml
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には標準エラーとエラー用のファイルパスを設定してる。

```text:yaml_out.log
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"}
```

```text:yaml_err.log
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でやったけど、より柔軟にするなら、

```go
type levelDispatcher struct {
	zapcore.Core
	cores map[zapcore.Level]zapcore.Core
}
```
とかにして、レベル毎にCoreをセットできるようにすればどんなログレベルでも対応できるはず。

zapはCoreレベルまで弄れば柔軟にやりたいことができるのがいいですね。
~~とはいえ、これくらい標準でできないのかなーとは思えてならない。~~

今回のサンプルは[github](https://github.com/emonuh/zap-error-dispatcher)に置いてあるので参考まで。