go.uber.org/zapでコンソールとファイルのそれぞれに別々のフォーマットでログを出力する

夏休みの自由課題です。

今作っているサーバにログ出力の機能をつけたいと思っていて、ログライブラリを検討していました。

必要な要件は以下です。

  • ファイルには、構造化ログを出力したい(JSONでもLTSVでもなんでもいい)
  • コンソール(STDOUT)には、人間にある程度見やすいログを出力したい(色がつく必要はない)
    • ファイルログとコンソールログの情報は一緒で良い。見え方を変えたいだけ

複数の io.Writer に同時に出力するというところでは io.MultiWriter が思いつくのですが、 io.MultiWriter では io.Stdout とファイルに同時に出力することはできても、別のフォーマットにはできません。

実は、最初に採用しようと考えていたログライブラリでもいろいろ試行錯誤したのですが、そのライブラリでの解決方法は見つけられませんでした。

で、結局、zap というLoggerで綺麗に解決できたので紹介します。

zap

github.com

Goの世界ではとても有名なLoggerなのですね。高速というのも魅力です。

ちなみにzapの使い方や設定の説明は以下のエントリが最高です。

qiita.com

io.Stdoutとファイルのそれぞれに別々のフォーマットでログを出力する

上のエントリにもあるように普通のzapの使い方だと zap.Config を作成-> Build() して zap.Logger を作るのですが、今回は面倒なことをするので、zap.New() を利用して zap.Logger を作成します。

zap.New()zapcore.Core を引数に必要としますので、まずはzapcore.Core を作成します。

zapcore.Corezapcore.NewCore() を使って作成します。

zapcore.NewCore() は、zapcore.Encoderzapcore.WriteSyncerzapcore.LevelEnabler の3つの引数を必要とします

それぞれの引数の概要は以下のとおりです。

引数 概要
zapcore.Encoder ログフォーマット形式
zapcore.WriteSyncer 出力先
zapcore.LevelEnabler ログレベル

ようは、ログフォーマットと出力先の異なる複数の zapcore.NewCore() を持つ zapcore.Logger を作れればいいのですが、zapのソースコードを読んでいたらありました

というわけで具体的なコードです。

package logger

import (
    "log"
    "os"

    "go.uber.org/zap"
    "go.uber.org/zap/zapcore"
)

// NewLogger returns logger
func NewLogger() *zap.Logger {
    encoderConfig := zapcore.EncoderConfig{
        TimeKey:        "time",
        LevelKey:       "level",
        NameKey:        "name",
        CallerKey:      "caller",
        MessageKey:     "msg",
        StacktraceKey:  "stacktrace",
        EncodeLevel:    zapcore.LowercaseLevelEncoder,
        EncodeTime:     zapcore.ISO8601TimeEncoder,
        EncodeDuration: zapcore.StringDurationEncoder,
        EncodeCaller:   zapcore.ShortCallerEncoder,
    }

    file, _ := os.Create("app.log")

    consoleCore := zapcore.NewCore(
        zapcore.NewConsoleEncoder(encoderConfig), # コンソールで見やすい形式
        zapcore.AddSync(os.Stdout), # io.Writerをzapcore.WriteSyncerに変換
        zapcore.DebugLevel,
    )

    logCore := zapcore.NewCore(
        zapcore.NewJSONEncoder(encoderConfig), # 構造化ログ(JSON)
        zapcore.AddSync(file),
        zapcore.InfoLevel,
    )

    logger := zap.New(zapcore.NewTee(
        consoleCore,
        logCore,
    ))

    return logger
}

zapcore.NewTee() を使って zapcore.Core をまとめているのがポイントです。

というわけで

zapでやりたかったログ出力の形を実現できました。

構造化ログのフォーマットもltsvのエンコーダもあることを確認していますので、良さそうです。