【Go×ログ】logrusの使い方を簡単に分かりやすくまとめてみた

# はじめに
Goの標準パッケージのlogはシンプルで使いやすいのですが、機能が貧弱としばしば言われます。例えば、他言語では当たり前なログレベルなどの機能はあえて組み込まれていません。
本記事では、乱立するlogの外部パッケージの中で最も有名なパッケージの1つである`logrus`の使い方をまとめます。

![zozo.go](https://gyazo.com/edf4b24a694f36f83e1a4e6a7f865367.png)

# インストール
```console
$ go get github.com/sirupsen/logrus
```

# 機能
## メッセージと変数の分離
logrusでは、 `WithFields` を使って「メッセージ」と「変数」を分離して実装と表示させる方法が推奨されています。こちらの方が長い文字列を延々と書くよりも生産的だろ？という思想とのことです。

```:ログ例
↓msgがログメッセージ、それ以降が変数です

time="2015-03-26T01:27:38-04:00" level=debug msg="Started observing beach" animal=walrus number=8
```

```go:従来
log.Fatalf("Failed to send event %s to topic %s with key %d")
```

```go:WithFields
log.WithFields(log.Fields{
  "event": event,
  "topic": topic,
  "key": key,
}).Fatal("Failed to send event")
```

WithFieldsを使うかどうかは任意です。
従来にあるような文字列の中に変数を埋め込むこともできます。

## デフォルト変数
全てのログに共通的に出力するような変数は `Fields ` で一度だけの宣言にまとめることができます。

```go
func main() {
	requestID := 1
	userIP := "192.168.10.1"
	requestLogger := log.WithFields(log.Fields{"request_id": requestID, "user_ip": userIP})
	requestLogger.Info("something happened on that request")
	requestLogger.Warn("something not great happened")
}
```
```console:実行結果
INFO[0000] something happened on that request            request_id=1 user_ip=192.168.10.1
WARN[0000] something not great happened                  request_id=1 user_ip=192.168.10.1
```

## ログレベル
Trace, Debug, Info, Warning, Error, Fatal, Panicの7つのレベルが用意されています。

```go
log.Trace("Something very low level.")
log.Debug("Useful debugging information.")
log.Info("Something noteworthy happened!")
log.Warn("You should probably take a look at this.")
log.Error("Something failed but I'm not quitting.")
log.Fatal("Bye.") // Calls os.Exit(1) after logging
log.Panic("I'm bailing.") // Calls panic() after logging
```

`SetLevel`で、セットしたレベル以上のレベルを出力するようにします。

```go
log.SetLevel(log.InfoLevel)
```

ちなみに、TTYが有効になっているとログレベルに合わせてこんな風にカラフルに表示されます。
<img width="633" alt="logrus画像.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/221948/2e6954d6-d495-70af-e3d1-13420fa7e1c0.png">

## Fatalハンドラー
`RegisterExitHandler`で、Fatal以上のレベルのメッセージを出力した際に、logrusがos.Exit(1)してしまう前に自動で実行される1つ以上の関数を登録しておくことができます。

```go
handler := func() {
  // gracefully shutdown something...
}
logrus.RegisterExitHandler(handler)
```

## フォーマット
logrusの標準ではテキストフォーマットとJSONフォーマットが用意されています。

### テキストフォーマット
デフォルトのフォーマットです。
何も指定しなかった場合はテキストフォーマットになります。

```go
package main

import (
  log "github.com/sirupsen/logrus"
)

func main() {
  log.WithFields(log.Fields{
    "animal": "walrus",
  }).Info("A walrus appears")
}
```
```console:実行結果
INFO[0000] A walrus appears                              animal=walrus number=1 size=10
```

### JSONフォーマット
`SetFormatter`で`&log.JSONFormatter{}`を指定すれば、JSONフォーマットになります。
例えばdatadogやLogstashなどでログを監視する場合は、JSONフォーマットを使うことが多いと思います。

```go
func main() {
	log.SetFormatter(&log.JSONFormatter{})
	log.WithFields(log.Fields{
		"animal": "walrus",
	}).Info("A walrus appears")
}
```
```console:実行結果
{"animal":"walrus","level":"info","msg":"A walrus appears","time":"2019-06-16T09:27:28+09:00"}
```

## 環境
環境毎にログの設定を変える機能はlogrusには用意されていません。
プロダクション環境とその他の環境でログ設定を変更したい場合は、パッケージ変数などを活用して自分でハンドリングする必要があります。

```go
init() {
  // do something here to set environment depending on an environment variable
  // or command-line flag
  if Environment == "production" {
    log.SetFormatter(&log.JSONFormatter{})
  } else {
    // The TextFormatter is default, you don't actually have to do this.
    log.SetFormatter(&log.TextFormatter{})
  }
}
```

## ログローテーション
ログローテーション機能はlogrusには用意されていません。

# 具体例
## 具体例1(init関数で共通設定）
```go
package main

import (
  "os"
  log "github.com/sirupsen/logrus"
)

func init() {
  // JSONフォーマット
  log.SetFormatter(&log.JSONFormatter{})

  // 標準エラー出力でなく標準出力とする
  log.SetOutput(os.Stdout)

  // Warningレベル以上を出力
  log.SetLevel(log.WarnLevel)
}

func main() {
  // ログ例1（出力されない）
  log.WithFields(log.Fields{
    "animal": "walrus",
    "size":   10,
  }).Info("A group of walrus emerges from the ocean")

  // ログ例2（出力される）
  log.WithFields(log.Fields{
    "omg":    true,
    "number": 122,
  }).Warn("The group's number increased tremendously!")

  // ログ例3（出力される）
  log.WithFields(log.Fields{
    "omg":    true,
    "number": 100,
  }).Fatal("The ice breaks!")

  // 共通的に使用する変数の設定
  contextLogger := log.WithFields(log.Fields{
    "common": "this is a common field",
    "other": "I also should be logged always",
  })

  contextLogger.Info("I'll be logged with common and other field")
  contextLogger.Info("Me too")
}
```
```console:実行結果
{"level":"warning","msg":"The group's number increased tremendously!","number":122,"omg":true,"time":"2019-06-17T07:18:09+09:00"}
{"level":"fatal","msg":"The ice breaks!","number":100,"omg":true,"time":"2019-06-17T07:18:09+09:00"}
exit status 1
```

## 具体例2（ログインスタンスで共通設定）
```go
package main

import (
  "os"
  "github.com/sirupsen/logrus"
)

// ログインスタンス生成
var log = logrus.New()

func main() {
  // 標準出力に設定
  log.Out = os.Stdout

  // ファイル出力の場合の例
  // file, err := os.OpenFile("logrus.log", os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0666)
  // if err == nil {
  //  log.Out = file
  // } else {
  //  log.Info("Failed to log to file, using default stderr")
  // }

  // ログ出力例
  log.WithFields(logrus.Fields{
    "animal": "walrus",
    "size":   10,
  }).Info("A group of walrus emerges from the ocean")
}
```
```console:実行結果
INFO[0000] A group of walrus emerges from the ocean      animal=walrus size=10
```

# 最後に
logrusは簡単に導入できる反面、`zap`などの他の外部パッケージに比べてると速度が遅いという短所もあります。
私が導入したプロジェクトではとりあえずJSONで出力できるようにしたかっただけで、それほどリクエスト数も多くなく、ログの出力数も多くはなかったので`logrus`を導入しました。ケースを見定めてパッケージを選定する必要がありそうです。