Golang アプリケーションでログを収集・集約する方法
この記事は、How to collect, standardize, and centralize Golang logs | Datadog を読んだまとめだ。
ログを扱う際、通常いくつかの注意点(アプローチ)がある:
- ログを引数として渡し、ログが必要になったときに引数経由で渡す
- context に一元的にカプセル化し、必要なときに context から取り出す
- ロガーをパッケージとしてラップし、他のパッケージが使えるように変数を公開する
明らかに、最初のアプローチはすぐに問題に直面する。それは引数がかなり不格好になることだ。例えば:
func (p *Post) CreatePost(name string, content string, logger *log.Logger) {
///
if err != nil {
logger.Fatal("error!")
}
}
CreatePost のロジックにわざわざロガーを渡さなければならず、引数が汚くなりやすいし、階層を越えてロガーをバケツリレーする状況も生じやすい。このアプローチの最大のメリットは依存関係が透明なことで、関数シグネチャを見れば内部でロガーが使われることが明確にわかり、テスト時にもロガーを簡単にモックできる点だ。
僕たちは2番目のアプローチを採用することもできる。main の中で明示的に context を渡し、その中にロガーを詰めておくのだ。必要になったら log.Logger.FromContext(ctx) のように取り出す。今のところ特に目立ったデメリットは思い浮かばない。
const (
ContextKeyLogger = "logger"
)
func main() {
ctx := context.Background()
loggerCtx := context.WithValue(ctx, ContextKeyLogger, log.Logger)
}
3番目のアプローチは、僕が現在採用している方法だ。ロガーをパッケージにラップし、他のパッケージ向けに変数を公開するというものだ。このアプローチの最大のメリットは手軽さで、プラグアンドプレイで使える点にある。デメリットは、テストを書く際があまり容易ではないことだ。引数としてパラメータ化されていないため、ロガーをモック化するのが難しい。(もしかしたら他にも良い解決策があるかもしれないが。)
package logging
var Logger *log.Logger
func init() {
Logger = &log.Logger{
// your setting
}
}
続いて、元記事で挙げられている推奨事項を見ていこう:
- logrus の使用を推奨する。logrus は非常に使い勝手の良いロギングパッケージであり、標準の
logも完全にサポートしているため、痛みなく移行できる。hook の仕組みを利用すれば、Datadog や Sentry などのサードパーティ製レポーティングプラットフォームとの連携も容易だ。 - ログファイルのフォーマットに JSON を使用する:JSON は非常にパースしやすく、主要なプログラミング言語がすべてサポートしており、サードパーティのプラットフォームにとっても分析しやすい。非常におすすめだ。logrus では直接
SetFormatter(&logrus.JSONFormatter{})で出力を設定できる。 - 統一されたインターフェースを使用する。
- goroutine の中ではできるだけロガーを呼び出さないようにする。並行処理(concurrency)の問題を考慮しなければならないだけでなく、ロガーの内部実装でも goroutine が動いている可能性があるため、全体の挙動を把握しづらくなるからだ。
- 外部プラットフォームを使用している場合でも、ログをローカルファイルとして保存しておく。こうすることでネットワーク障害によるログの消失を防ぎ、いつでもログファイルを確認できるように担保できる。
- クラスターのようなアーキテクチャを採用している場合、
syslogなどの方式を用いて専用のログサーバーに一元集約する必要があるかもしれない。 - Docker などのコンテナサービスを利用している場合は、Docker の STDOUT を監視・収集する必要があるかもしれない。
その他
log.Fatal() と log.Panic() についてだが、Fatal は裏でこっそり os.Exit(1) を呼び出す。ドキュメントに書いてあるのは知っているけれど、見ていない人も多いかもしれない。一方 Panic は裏で panic を呼び出すので、使用する際は十分に注意が必要だ。
関連記事
- 測定が目標になるとき:窓税からPull Request数まで かつて僕は小さなツールを自作し、四半期で自分がどれだけPRに貢献したか、レビューコメントをどれだけ残したか、チケットをどれだけ消化したかを集計して、上司にアウトプットを証明しようとしたことがある。上司は淡々と、評価はアウトプットだけで見るものではないと言った。数年後、僕はようやく理解した――測定が目標になるとき、それはもはや良い測定ではなくなるのだ。英国の窓税、ハノイのネズミ駆除の報奨金から、現代のPR数による開発者評価に至るまで、そのメカニズムはまったく同じだ。
- Cloudflare Images を画像ストレージ・変換ソリューションとして使う ウェブページに画像を1枚置くのはフロントエンドにとって最も簡単なことだが、リサイズや各種フォーマットの生成、さらにはトラフィックの負荷に耐えることまで完璧にやろうとすると、実際には一つの包括的なソリューションが必要になる。僕はその後、すべて Cloudflare Images に任せるようになり、オリジナル画像1枚だけを渡すようにしている。
- もう AWS Access Key を使うのはやめよう Access Key は AWS において見落とされがちなセキュリティリスクだ。OIDC と IAM Role を組み合わせることで、GitHub Actions にシークレットを一切保持させることなく、安全に AWS リソースを操作できるようにする。
- データベース主キー:AUTO_INCREMENT、UUID、そしてUUIDv7 バックエンド開発で度々直面する主キーの決定。auto incrementを使うべきか、それともUUIDか?衝突への懸念は?UUIDv7とcreated_at + インデックスの性能差はどれほどか?実際に2,000万件のデータで検証したベンチマークと設計上の意思決定を解説する。