はじめに
昨今ログ集計用に構造化ログは必要不可欠となっており、logrusとはGO用の構造化ロガーで多くのプロジェクトで使われていました。
しかし現状はメンテナンスモードとなっており、今後新機能は追加される予定はないようです。(※以下原文)
README原文から抜粋
Logrus is in maintenance-mode. We will not be introducing new features.
This does not mean Logrus is dead. Logrus will continue to be maintained for security, (backwards compatible) bug fixes, and performance (where we are limited by the interface).
REAEME原文から抜粋
Logrus would look like those, had it been re-designed with what we know about structured logging in Go today. Check out, for example, Zerolog, Zap, and Apex.
(和訳)
Logrusは、現状のGoの構造化ロギングについての知見のもとに再設計されていれば、そのようになったでしょう。
たとえば、Zerolog、Zap、Apex をチェックしてみてください。
一方でセキュリティ、バグ修正などのメンテナンスは今後も行っていくようなので、既に使っているプロジェクトは急いで他のライブラリに切り替える必要はありません。
しかし昨今構造化ログが広く使われている中でより良いロガーはありそうです。
そこで、今回は代替ロガーとして、logrusのREADMEにも紹介があったZerologを使ってみたので紹介します。
なぜZerolog?
特徴
zerologの特徴はを箇条書すると以下になります。
- JSON出力似特化
- シンプル
- 高速
zerologの特徴は、JSON出力に特化したロガーで、非常にシンプルでかつ高速です。
機能を豊富にした結果、複雑になり使いこなすまでに時間がかかるというライブラリもあるかもしれませんが、こちらのライブラリは非常にシンプルで、すぐに使いこなせます。そのため特殊な要件がなければ、こちらのロガーで十分だと思いました。
またzerologはスピードも売りの一つですが、これは内部的にはリフレクションを回避していて、ログイベントを書き込むことができるためで、ロギングにパフォーマンスを求める場合もお勧めです。
使ってみる
ざっとですが、できることを以下に書き出してみました。
1. 基本出力
デフォルト設定でログレベルを変更するだけなら以下でOK。
出力FMTはデフォルトでJSONとなっています。
import "github.com/rs/zerolog"
func main() {
log.Info().Msg("hello world")
log.Debug().Msg("hello world")
log.Warn().Msg("hello world")
log.Error().Msg("hello world")
log.Fatal().Msg("hello world")
log.Panic().Msg("hello world")
// {"level":"info","time":"2022-04-22T19:25:52+09:00","message":"hello world"}
// {"level":"debug","time":"2022-04-22T19:25:52+09:00","message":"hello world"}
// {"level":"warn","time":"2022-04-22T19:25:52+09:00","message":"hello world"}
// {"level":"error","time":"2022-04-22T19:25:52+09:00","message":"hello world"}
// {"level":"fatal","time":"2022-04-22T19:25:52+09:00","message":"hello world"}
}
ログレベルの定義
出力するログレベルのグローバル定義です。以下の例の場合INFO未満は出力されません。
func main() {
zerolog.SetGlobalLevel(zerolog.InfoLevel)
log.Info().Msg("info log")
log.Debug().Msg("debug log")
}
// debugは出力されない
// {"level":"info","time":"2022-04-22T19:20:31+09:00","message":"info log"}
個別に指定したい場合は以下のように設定できます。
実際のアプリケーションでは、アクセスログ、アプリケーションログ、セキュリティログなどいくつものロガーを用意するため、以下のように設定することが多いかもしれません。
func main() {
log := zerolog.New(os.Stderr).Level(zerolog.InfoLevel)
log.Info().Msg("info log")
log.Debug().Msg("debug log")
}
// debugは出力されない
// Output: {"level":"info","message":"info log"}
日付FMTを変更
以下のようにグローバル定義を変更すると、ログ出力する日付のFMTを変更できます。
func main() {
zerolog.TimeFieldFormat = time.RFC3339
log.Info().Msg("info log. RFC3339")
zerolog.TimeFieldFormat = time.RFC3339Nano
log.Info().Msg("info log. RFC3339Nano")
zerolog.TimeFieldFormat = zerolog.TimeFormatUnix
log.Info().Msg("info log. TimeFormatUnix")
}
// 指定した日付FMTで出力
// {"level":"info","time":"2022-04-22T19:24:34+09:00","message":"info log. RFC3339"}
// {"level":"info","time":"2022-04-22T19:24:34.40185+09:00","message":"info log. RFC3339Nano"}
// {"level":"info","time":1650623074,"message":"info log. TimeFormatUnix"}
出力ログにフィールドを追加
分析用に出力するログにフィールドを追加できます。以下はstring型,int型,bool型のフィールドを追加しています。
func main() {
log.Info().
Str("type", "app").
Int("counter", 10).
Bool("flag", false).
Msg("hello world")
}
// 追加したフィールドが出力される
// {"level":"info","type":"app","counter":10,"flag":false,"time":"2022-04-22T19:29:46+09:00","message":"hello world"}
出力ログに共通フィールドを追加
前述の例だと、毎回log呼び出しのたびにフィールド定義する必要がある為面倒ですね。
共通で定義したい場合は、以下のように With()
関数を使ってContext
に対して、個別に設定することも可能です。
var securityLog = securityLogger()
func securityLogger() *zerolog.Logger {
logger := zerolog.New(os.Stderr).With().Timestamp().Str("type", "security").Logger()
return &logger
}
func main() {
securityLog.Info().Msg("hello")
}
// 追加したtypeフィールドが出力される
// {"level":"info","type":"security","time":"2022-04-22T19:39:50+09:00","message":"hello"}
ログ内容をstructで定義
例えば、ユーザ単位で処理を行う場合、毎回ユーザー情報をInt()
やStr()
で定義するのは面倒ですね。
そういった場合は、以下のようにEmbedObject()
を使うと構造体のUser情報をそのまま出力できます。
その際、構造体UserにMarshalZerologObject()
を実装することを忘れずに。
またUser情報を一つ下の階層に定義したい場合は、Object()
を使うとフィールドのキーを定義できます。
type User struct {
ID int
Name string
}
// MarshalZerologObjectをstructに対してメソッド定義
func (u *User) MarshalZerologObject(e *zerolog.Event) {
e.Int("userId", u.ID).Str("name", u.Name)
}
func main() {
u := User{ID: 100, Name: "Tom"}
log.Info().EmbedObject(&u).Msg("hello")
log.Info().Object("user", &u).Msg("hello")
}
// 構造体Userの情報が出力される
//{"level":"info","userId":100,"name":"Tom","time":"2022-04-22T19:53:45+09:00","message":"hello"}
// 構造体Userの情報を, userというキーに対する値として出力する
//{"level":"info","user":{"userId":100,"name":"Tom"},"time":"2022-04-22T19:53:45+09:00","message":"hello"}
Prettyロギング
ローカル環境ではJSONは見辛いよという場合、以下のように出力できます。
以下はコメントアウトに記載しているため、色がありませんがターミナル等で確認すると色付きで出力されます。
func main() {
log.Logger = log.Output(zerolog.ConsoleWriter{Out: os.Stderr})
log.Info().
Str("type", "app").
Int("counter", 10).
Bool("flag", false).
Msg("hello world")
}
// 7:58PM INF hello world counter=10 flag=false type=app
Pretty,JSONロギングの切り替え
例えばローカル環境においてはPrettyログ,dev/prd環境ではJSONにしたい時は以下のように定義できます.
以下の例ではアプリケーションログを想定しているため, type: app
を定義しています.
// log用config
type LogConfig struct {
Level Level `yaml:"level"` // LogLevelの指定
UseStructuredLog bool `yaml:"useStructuredLog"` // JSONログの使用可否
}
var Log *zerolog.Logger
func SetLogger(cfg *LogConfig) {
var zl zerolog.Logger
if cfg.UseStructuredLog {
zl = zerolog.New(os.Stderr)
} else {
zl = zerolog.New(zerolog.ConsoleWriter{Out: os.Stderr})
}
Log = zl.Level(cfg.Level.ZeroLogLevel()).
With().
Str("type", "app").
Timestamp().
Logger()
}
おわりに
いかがだったでしょうか。非常にシンプルなため,すぐに使うイメージができたかと思います。
個人的には軽量アプリケーションならこちらで十分だと思いました。みなさんもぜひ使ってみてください。