ええっ!? 文字列で書くの!? ログレベル付きロガーhashicorp/logutilsのご紹介


全世界100万人のコンパイルエラーラバーの皆さんこんばんは、この記事はGo Advent Calendar 2017の18日目の記事です。

標準パッケージのlog

Goで作るものはミドルウェアだったり、Webアプリケーションだったり、CLIツールだったり様々だと思いますが、ちょっと凝ったことをするとついてくるものといえばロギングですよね。Goにはlogという標準のログライブラリがありますが、

  • 標準パッケージなのでimport "log"だけで使える
  • インターフェイスがシンプル。だいたいfmt
  • 複数goroutineから扱っても混ざらない
  • そのまま使っても時間が出る

などなど、最低限の実用性があります。ただ、他の言語のロガーと比べると、

  • ログレベル
  • 構造付きログ

などの機能が不足していると言えます。

よく使われているロガー

そもそもこの記事を書くきっかけとなったのが、Go Conference 2017 Autumnでの、Goのロギング周りの考察という発表を見たことだったのですが、このトークでは様々なロガーが上げられています。

私が使ったことがあるものだと、github.com/sirusupen/logrusgo.uber.org/zapが挙げられていました。この2つは提供する機能も似通っていますが、私がほしい「ログレベル」と「構造付きログ」の機能は搭載されています。

ちなみに、WebSocketを扱うミドルウェアkuiperbeltでは、以前logrusからzapへの移行を行いました。理由はベンチマーク上の速さなどからです。

ミニマリスト向けロガーlogutils

先ほどの発表スライドの中で気になったのがgithub.com/hashicorp/logutilsです。このライブラリは、標準パッケージのlogと同じインターフェイスを持ちながらログレベルの機能を実現しています。

え??? 思うのですが、こんな感じです。

log.Printf("[DEBUG] debug log")
log.Printf("[WARN] warning log")
log.Printf("[ERROR] error log")

ええっ!? 文字列で書くの!? というわけで先頭に書いた[***]をログレベルの指定とみなして出力するかどうかを決めているわけです。

さらに言うとこのlogというのは標準パッケージのlogです。使い始めるときには


import (
    "log"
    "github.com/hashicorp/logutils"
)

func main() {
    filter := &logutils.LevelFilter{
        Levels: []logutils.LogLevel{"DEBUG", "WARN", "ERROR"},
        MinLevel: logutils.LogLevel("WARN"),
        Writer: os.Stderr,
    }
    log.SetOutput(filter)

    // do something
}

と標準パッケージのlogの出力先にlogutilsのフィルタを指定することで、出力するかどうかを決めているわけです。一番初めのエントリーポイントでこのように書いておけば、他のロガーライブラリのように書き方自体を全部変更して回るみたいなこともしなくてよいわけです。

開発初期はlogとかfmtでログ出していたけれど、後からログレベルがほしいなってときには便利ですね。

しかし文字列だとtypoするやろ

静的型付け世界にいる我々としては、出来るだけ文字列の中の何かで挙動を変えることはせず、型や変数などで指定したくなります。何故なら、typoしたときにコンパイラが怒ってくれるからです。しかしlogutilsは、それと逆行するようなライブラリです。いくら互換性と言われても。

私もはじめはそう思ったのですが、よく考えると発展性があるなと思いました。自分で関数書けば良いわけです。

func debug(msg string) {
    log.Print("[DEBUG] " + msg)
}

func warn(msg string) {
    log.Print("[WARN] " + msg)
}

// errorであってもerrであっても被ることが多いのでここだけはerrorlogにする
func errorlog(msg string) {
    log.Print("[ERROR] " + msg)
}

とまあ、自分でこういう簡単なラッパーを作ってあげれば、typoに対する備えも出来ます。

構造化されたログ出力が欲しい

これもラッパー作りましょう

func debugJSON(msg interface{}) error {
    bs, err := json.Marshal(msg)
    if err != nil {
        return err
    }
    debug(string(bs))
}

[]byteからstringにキャストし直しているのとかちょっとダサいですが概観はこんな感じです。あとログ吐くときにエラーもらってもどうするねん問題とかありますね。それはまあさておき。

まとめ

  • github.com/hashicorp/logutilsはパッと見ちょっとあれ?ってなるけれど、使い始めるのが楽で良いよ
  • 元がシンプルすぎて機能が足りなかったら簡単なラッパー書けばいいよ
  • typoもラッパー書けば防げるよ

という話でした。