構造化エラーをログ出力する

Go 1.13 のリリースに合わせて spiegel-im-spiegel/errs パッケージを公開したのだが,このパッケージで構成した構造化エラーをログ出力することを考える。

まぁ標準の log パッケージでエラーメッセージを出力してもいいのだが,せっかく JSON 形式で出力できるようにしたんだから,ログ出力も JSON 形式にしたいよね。

ちうわけで,今回はこれを使います。 てってれー

まずは準備として以下の関数を考える。

import (
    "os"

    "github.com/spiegel-im-spiegel/errs"
)

func checkFileOpen(path string) error {
    file, err := os.Open(path)
    if err != nil {
        return errs.Wrap(
            err,
            errs.WithContext("path", path),
        )
    }
    defer file.Close()

    return nil
}

ファイルをオープンするだけの関数で,オープンに失敗すると error を返す。 spiegel-im-spiegel/errs パッケージの使い方は

を参考にしてね。

この関数を使った main() 関数を書いてみよう。 まずは標準出力に対して書式 %+v を指定して普通に

func main() {
    if err := checkFileOpen("not-exist.txt"); err != nil {
        fmt.Printf("%+v\n", err)
    }
}

と出力する。 これの実行結果は

$ go run sample1.go | jq .
{
  "Type": "*errs.Error",
  "Err": {
    "Type": "*os.PathError",
    "Msg": "open not-exist.txt: no such file or directory",
    "Cause": {
      "Type": "syscall.Errno",
      "Msg": "no such file or directory"
    }
  },
  "Context": {
    "function": "main.checkFileOpen",
    "path": "not-exist.txt"
  }
}

てな感じになる。 うむうむ。

で,ここからが本題。

fmt.Printf() の部分を rs/zerolog によるログ出力に置き換えてみよう。 とりあえず logger インスタンスの生成はこんな感じかな。

logger := zerolog.New(os.Stdout).Level(zerolog.DebugLevel).With().
    Timestamp().
    Str("role", "logger-sample").
    Logger()

まずは普通に error インスタンスをログ出力してみる。

func main() {
    logger := zerolog.New(os.Stdout).Level(zerolog.DebugLevel).With().
        Timestamp().
        Str("role", "logger-sample").
        Logger()

    if err := checkFileOpen("not-exist.txt"); err != nil {
        logger.Error().Err(err).Send()
    }
}

これの実行結果は以下の通り。

$ go run sample2.go
{"level":"error","role":"logger-sample","error":"open not-exist.txt: no such file or directory","time":"2009-11-10T23:00:00Z"}

更に jq コマンドを噛ませるとこんな感じになる。

$ go run sample.go | jq .
{
  "level": "error",
  "role": "logger-sample",
  "error": "open not-exist.txt: no such file or directory",
  "time": "2009-11-10T23:00:00Z"
}

見ての通り zerolog.Event.Err() メソッドでは単純なエラーメッセージしか出力されない(当たり前だが)。 通常の error ならこれで十分だが errs.Wrap() 関数でラップした error では不十分である。

そこで zerolog.Event.Interface() メソッドのほうを使ってみる。

func main() {
    logger := zerolog.New(os.Stdout).Level(zerolog.DebugLevel).With().
        Timestamp().
        Str("role", "logger-sample").
        Logger()

    if err := checkFileOpen("not-exist.txt"); err != nil {
        logger.Error().Interface("error", err).Send()
    }
}

これでログ出力は

$ go run sample3.go | jq .
{
  "level": "error",
  "role": "logger-sample",
  "error": {
    "Type": "*errs.Error",
    "Err": {
      "Type": "*os.PathError",
      "Msg": "open not-exist.txt: no such file or directory",
      "Cause": {
        "Type": "syscall.Errno",
        "Msg": "no such file or directory"
      }
    },
    "Context": {
      "function": "main.checkFileOpen",
      "path": "not-exist.txt"
    }
  },
  "time": "2009-11-10T23:00:00Z"
}

てな感じになった。 よーし,うむうむ,よーし。

rs/zerolog はパフォーマンスもよく使い勝手のいいパッケージで,しかも JSON 出力できるので加工しやすいだろう。 オススメである。

ブックマーク

参考図書

photo
プログラミング言語Go (ADDISON-WESLEY PROFESSIONAL COMPUTING SERIES)
Alan A.A. Donovan (著), Brian W. Kernighan (著), 柴田 芳樹 (翻訳)
丸善出版 2016-06-20
単行本(ソフトカバー)
4621300253 (ASIN), 9784621300251 (EAN), 4621300253 (ISBN), 9784621300251 (ISBN)
評価     

著者のひとりは(あの「バイブル」とも呼ばれる)通称 “K&R” の K のほうである。この本は Go 言語の教科書と言ってもいいだろう。

reviewed by Spiegel on 2016-07-13 (powered by PA-APIv5)