The Go Blog
slog を用いた構造化ロギング
Go 1.21 の新しい log/slog パッケージは、標準ライブラリに構造化ロギングをもたらします。構造化ログはキーと値のペアを使用するため、迅速かつ確実に解析、フィルタリング、検索、分析が可能です。サーバーの場合、ロギングは開発者がシステムの詳細な動作を把握するための重要な手段であり、多くの場合、デバッグを行う最初の場所となります。そのため、ログは膨大になりがちであり、迅速に検索およびフィルタリングできる機能は不可欠です。
標準ライブラリには、Go の最初のリリースから10年以上前からロギングパッケージ log がありました。時間の経過とともに、構造化ロギングが Go プログラマーにとって重要であることがわかってきました。これは毎年恒例のアンケートで常に上位にランクインしており、Go エコシステムの多くのパッケージがそれを提供しています。これらのいくつかは非常に人気があります。Go 用の最初の構造化ロギングパッケージの1つである logrus は、10万以上の他のパッケージで使用されています。
構造化ロギングパッケージが多数あるため、大規模なプログラムでは、依存関係を介して複数のパッケージが含まれることがよくあります。メインプログラムは、ログ出力が一貫するように(すべて同じ場所に、同じ形式で出力されるように)これらのロギングパッケージのそれぞれを構成する必要があるかもしれません。標準ライブラリに構造化ロギングを含めることで、他のすべての構造化ロギングパッケージが共有できる共通のフレームワークを提供できます。
slog のツアー
slog を使用する最もシンプルなプログラムを次に示します。
package main
import "log/slog"
func main() {
slog.Info("hello, world")
}
この記事の執筆時点では、次のように出力されます。
2023/08/04 16:09:19 INFO hello, world
Info 関数は、デフォルトのロガーを使用して Info ログレベルでメッセージを出力します。この場合、それは log パッケージのデフォルトのロガーであり、log.Printf を書くときに取得するのと同じロガーです。これが出力が非常によく似ている理由を説明しています。「INFO」だけが新しいです。すぐに使用できる slog と元の log パッケージは連携して、簡単に開始できるようにします。
Info の他に、他の3つのレベル(Debug、Warn、Error)の関数と、レベルを引数として受け取るより一般的な Log 関数があります。slog では、レベルは単なる整数なので、4つの名前付きレベルに限定されません。たとえば、Info はゼロで Warn は4なので、ロギングシステムにそれらの中間のレベルがある場合、それに2を使用できます。
log パッケージとは異なり、メッセージの後にキーと値のペアを記述することで、出力に簡単にキーと値のペアを追加できます。
slog.Info("hello, world", "user", os.Getenv("USER"))
出力は次のようになります。
2023/08/04 16:27:19 INFO hello, world user=jba
前述のとおり、slog のトップレベル関数はデフォルトのロガーを使用します。このロガーを明示的に取得し、そのメソッドを呼び出すことができます。
logger := slog.Default()
logger.Info("hello, world", "user", os.Getenv("USER"))
すべてのトップレベル関数は slog.Logger のメソッドに対応しています。出力は以前と同じです。
当初、slog の出力はデフォルトの log.Logger を経由し、上記で見た出力を生成します。ロガーが使用する _ハンドラー_ を変更することで、出力を変更できます。slog には2つの組み込みハンドラーが付属しています。TextHandler は、すべてのログ情報を key=value の形式で出力します。このプログラムは、TextHandler を使用して新しいロガーを作成し、Info メソッドに同じ呼び出しを行います。
logger := slog.New(slog.NewTextHandler(os.Stdout, nil))
logger.Info("hello, world", "user", os.Getenv("USER"))
これで出力は次のようになります。
time=2023-08-04T16:56:03.786-04:00 level=INFO msg="hello, world" user=jba
すべての情報はキーと値のペアに変換され、構造を維持するために必要に応じて文字列が引用符で囲まれています。
JSON 出力の場合は、代わりに組み込みの JSONHandler をインストールします。
logger := slog.New(slog.NewJSONHandler(os.Stdout, nil))
logger.Info("hello, world", "user", os.Getenv("USER"))
これで、出力はログ呼び出しごとに1つの JSON オブジェクトのシーケンスになります。
{"time":"2023-08-04T16:58:02.939245411-04:00","level":"INFO","msg":"hello, world","user":"jba"}
組み込みのハンドラーに限定されるわけではありません。slog.Handler インターフェースを実装することで、誰でもハンドラーを作成できます。ハンドラーは特定の形式で出力を生成したり、他のハンドラーをラップして機能を追加したりできます。slog のドキュメントの 例 の1つは、ログメッセージが表示される最小レベルを変更するラッピングハンドラーの作成方法を示しています。
これまで使用してきた属性のキーと値の交互構文は便利ですが、頻繁に実行されるログステートメントでは、Attr 型を使用し、LogAttrs メソッドを呼び出す方が効率的な場合があります。これらは連携してメモリ割り当てを最小限に抑えます。文字列、数値、およびその他の一般的な型から Attr を構築するための関数があります。この LogAttrs の呼び出しは上記と同じ出力を生成しますが、より高速に実行されます。
slog.LogAttrs(context.Background(), slog.LevelInfo, "hello, world",
slog.String("user", os.Getenv("USER")))
slog にはさらに多くの機能があります
-
LogAttrsの呼び出しが示すように、一部のログ関数にcontext.Contextを渡すことができるため、ハンドラーはトレース ID のようなコンテキスト情報を抽出できます。(コンテキストをキャンセルしても、ログエントリが書き込まれるのを妨げません。) -
Logger.Withを呼び出すと、すべての出力に表示される属性をロガーに追加できます。これにより、いくつかのログステートメントの共通部分を効率的に除外できます。これは便利なだけでなく、後述するようにパフォーマンスの向上にも役立ちます。 -
属性はグループに結合できます。これにより、ログ出力にさらに構造を追加し、そうでなければ同一であった可能性のあるキーの曖昧さを解消するのに役立ちます。
-
LogValueメソッドでその型を提供することで、ログに値が表示される方法を制御できます。これは、構造体のフィールドをグループとしてログに記録する、または 機密データを編集する などに使用できます。
slog のすべてについて学ぶのに最適な場所は、パッケージドキュメント です。
パフォーマンス
slog を高速化したいと考えていました。大規模なパフォーマンス向上のため、最適化の機会を提供するために Handler インターフェース を設計しました。Enabled メソッドはすべてのログイベントの開始時に呼び出され、ハンドラーに不要なログイベントを迅速にドロップする機会を与えます。WithAttrs および WithGroup メソッドにより、ハンドラーは Logger.With によって追加された属性を、各ロギング呼び出しではなく一度だけフォーマットできます。この事前フォーマットは、http.Request のような大きな属性が Logger に追加され、その後多くのロギング呼び出しで使用される場合に、大幅な高速化を提供できます。
パフォーマンス最適化の作業を裏付けるため、既存のオープンソースプロジェクトにおけるロギングの典型的なパターンを調査しました。ロギングメソッドへの呼び出しの95%以上が5つ以下の属性を渡していることがわかりました。また、属性のタイプを分類し、少数の一般的なタイプが大部分を占めていることも判明しました。次に、一般的なケースを捕捉するベンチマークを作成し、それらを時間の使われ方を確認するためのガイドとして使用しました。最大の成果は、メモリ割り当てに細心の注意を払うことから得られました。
設計プロセス
slog パッケージは、2012年に Go 1 がリリースされて以来、標準ライブラリに追加された最大のものの一つです。私たちはその設計に時間をかけたいと考え、コミュニティからのフィードバックが不可欠であることを知っていました。
2022年4月までに、構造化ロギングが Go コミュニティにとって重要であることを示すのに十分なデータを収集しました。Go チームは、これを標準ライブラリに追加することを検討することにしました。
私たちはまず、既存の構造化ロギングパッケージがどのように設計されているかを調べました。また、Go モジュールプロキシに保存されているオープンソース Go コードの大量のコレクションを利用して、これらのパッケージが実際にどのように使用されているかを学びました。私たちの最初の設計は、この調査と Go のシンプルさの精神に基づいていました。パフォーマンスを犠牲にすることなく、ページ上で軽く、理解しやすい API を求めていました。
既存のサードパーティ製ロギングパッケージを置き換えることは、当初からの目標ではありませんでした。それらはすべてそれぞれの役割で優れており、うまく機能している既存のコードを置き換えることは、開発者の時間の良い使い方とはめったに言えません。API をフロントエンド (Logger) とバックエンドインターフェース (Handler) に分割しました。これにより、既存のロギングパッケージが共通のバックエンドと連携できるため、それらを使用するパッケージは書き換えなしで相互運用できます。Zap、logr、hclog など、多くの一般的なロギングパッケージ向けにハンドラーが作成されているか、作成中です。
Go チーム内および広範なロギング経験を持つ他の開発者と最初の設計を共有しました。彼らのフィードバックに基づいて変更を行い、2022年8月には実用的な設計ができたと感じました。8月29日、実験的な実装を公開し、コミュニティの意見を聞くために GitHub の議論を開始しました。反応は熱狂的で、概ね肯定的でした。他の構造化ロギングパッケージの設計者やユーザーからの洞察に富んだコメントのおかげで、いくつかの変更を行い、グループや LogValuer インターフェースなどのいくつかの機能を追加しました。ログレベルから整数へのマッピングを2回変更しました。
2か月と約300件のコメントの後、私たちは実際の 提案 とそれに付随する 設計ドキュメント を出す準備ができたと感じました。提案のイシューは800件以上のコメントを集め、APIと実装に多くの改善をもたらしました。ここに、context.Context に関するAPI変更の2つの例があります。
-
当初、API はロガーをコンテキストに追加することをサポートしていました。多くの人が、これはロガーを気にしないコードのレベルを介して簡単にロガーを配線する便利な方法だと感じていました。しかし、他の人はそれが暗黙的な依存関係を密輸していると見なし、コードを理解しにくくしていると感じていました。最終的に、あまりにも物議を醸す機能であるとして、この機能は削除されました。
-
また、ロギングメソッドにコンテキストを渡すという関連する問題にも取り組み、いくつかの設計を試しました。すべてのロギング呼び出しでコンテキストが必要になることを望まなかったため、当初はコンテキストを最初の引数として渡すという標準的なパターンに抵抗しましたが、最終的に、コンテキストがあるものとないものの2組のロギングメソッドを作成しました。
私たちが変更しなかったことの1つは、属性を表現するためのキーと値の交互構文に関するものでした。
slog.Info("message", "k1", v1, "k2", v2)
多くの人が、これは悪い考えだと強く感じていました。読みにくく、キーや値を省略することで間違えやすいと感じていました。彼らは構造を表現するために明示的な属性を好んでいました。
slog.Info("message", slog.Int("k1", v1), slog.String("k2", v2))
しかし、私たちは、特に新しい Go プログラマーにとって、Go を使いやすく、楽しく保つためには、より軽い構文が重要だと感じました。また、logr、go-kit/log、zap (その SugaredLogger とともに) など、いくつかの Go ロギングパッケージがキーと値の交互構文をうまく使用していることも知っていました。一般的な間違いを捕捉するために vet チェック を追加しましたが、設計は変更しませんでした。
2023年3月15日、提案は承認されましたが、まだ解決されていないいくつかの小さな問題がありました。その後の数週間で、さらに10件の変更が提案され、解決されました。7月初旬までに、log/slog パッケージの実装は、ハンドラーを検証するための testing/slogtest パッケージと、キーと値の交互使用の正確性をチェックする vet チェックとともに完了しました。
そして8月8日、Go 1.21 がリリースされ、それに伴い slog もリリースされました。皆様にとって便利で、作成するのと同じくらい楽しく使えることを願っています。
そして、議論と提案プロセスに参加してくださった皆様に深く感謝いたします。皆様の貢献により、slog は計り知れないほど改善されました。
リソース
log/slog パッケージの ドキュメント は、その使用方法を説明し、いくつかの例を提供します。
Wiki ページ には、さまざまなハンドラーを含む、Go コミュニティが提供する追加のリソースがあります。
ハンドラーを作成したい場合は、ハンドラー作成ガイド を参照してください。