Go 1.21 slogで構造化ログを実装する
Go 1.21 slogで構造化ログを実装する
はじめに
前回の記事で、Go言語でURL監視ツール「CloudOps Monitor」を作った話を書きました。
その記事の「今後の展望」で触れていた構造化ログ(JSON形式) を実装しました。
slogを使ってみて気づいたことをこの記事では書いていきます。
slogを使うと、「楽に、安全に、読みやすく」構造化されたログを出力できます。
fmt.Fprintfで頑張るとどうなるか
まず、slogを使わずにJSON形式のログを出力してみます。
package main
import (
"fmt"
"os"
"time"
)
func main() {
url := "https://example.com"
status := 200
fmt.Fprintf(os.Stdout, `{"time":"%s","level":"INFO","msg":"health check","url":"%s","statuscode":%d}`+"\n",
time.Now().Format(time.RFC3339),
url,
status)
}
出力:
{"time":"2024-12-21T10:30:00+09:00","level":"INFO","msg":"health check","url":"https://example.com","statuscode":200}
ちゃんとJSON形式で、時刻もログレベルも入ってます。できてる。
でもこのコード、どう思いますか?
fmt.Fprintf(os.Stdout, `{"time":"%s","level":"INFO","msg":"health check","url":"%s","statuscode":%d}`+"\n",
問題点を挙げます:
- 読みにくい - カンマ、コロン、括弧、エスケープが入り乱れてる
-
ミスしやすい -
"の閉じ忘れ、カンマの位置ズレ、気づきにくい - フィールド追加が面倒 - 新しい項目を増やすたびにフォーマット文字列を慎重に編集
- レベル切り替えが面倒 - ERRORにしたい時、文字列を手で書き換える
slogで書くとこうなる
package main
import (
"log/slog"
"os"
)
func main() {
logger := slog.New(slog.NewJSONHandler(os.Stdout, nil))
slog.SetDefault(logger)
url := "https://example.com"
status := 200
slog.Info("health check",
"url", url,
"statuscode", status)
}
出力:
{"time":"2024-12-21T10:30:00.000000+09:00","level":"INFO","msg":"health check","url":"https://example.com","statuscode":200}
比較してみる
fmt.Fprintf版:
fmt.Fprintf(os.Stdout, `{"time":"%s","level":"INFO","msg":"health check","url":"%s","statuscode":%d}`+"\n",
time.Now().Format(time.RFC3339),
url,
status)
slog版:
slog.Info("health check",
"url", url,
"statuscode", status)
出力は同じですが、コードの読みやすさが改善されています。
slogの「楽ポイント」を整理する
1. 時刻は自動で入る
// fmt.Fprintf - 手動で書く
fmt.Fprintf(os.Stdout, `{"time":"%s",...}`, time.Now().Format(time.RFC3339), ...)
// slog - 何も書かなくていい
slog.Info("message")
2. ログレベルは関数で分かれてる
// fmt.Fprintf - 文字列を手で書き換える
fmt.Fprintf(os.Stdout, `{..."level":"INFO"...}`)
fmt.Fprintf(os.Stdout, `{..."level":"ERROR"...}`)
// slog - 関数を変えるだけ
slog.Info(...)
slog.Error(...)
3. フィールド追加が楽
// fmt.Fprintf - フォーマット文字列を慎重に編集
fmt.Fprintf(os.Stdout, `{"url":"%s","statuscode":%d,"concurrency":%d}`, url, status, concurrency)
// slog - キーバリューを追加するだけ
slog.Info("health check",
"url", url,
"statuscode", status,
"concurrency", concurrency) // ← 追加
4. レベルでフィルタリングできる
// 本番ではWARN以上だけ出力したい
logger := slog.New(slog.NewJSONHandler(os.Stdout, &slog.HandlerOptions{
Level: slog.LevelWarn,
}))
これだけで、slog.Info()は出力されなくなり、slog.Warn()とslog.Error()だけ出力されます。
fmt.Fprintfでこれをやろうとすると、自分でif文を書く必要があります。
CloudOps Monitorでの実装
実際にCloudOps Monitorで実装したコードを紹介します。
セットアップ
func logconfig() {
logger := slog.New(slog.NewJSONHandler(os.Stdout, &slog.HandlerOptions{
Level: slog.LevelInfo,
}))
slog.SetDefault(logger)
}
main()の最初で呼び出すだけ。以降はslog.Info()などがJSON形式で出力されます。
成功時
slog.Info("http check completed",
"line_number", idx+1,
"url", orig,
"statuscode", resp.StatusCode,
"status", resp.Status)
エラー時
slog.Error("http request failed",
"line_number", idx+1,
"url", orig,
"error", err)
監視完了時
slog.Info("monitoring completed",
"total_urls", len(lines),
"timeout", *timeout,
"concurrency", *concurrency,
"success", success,
"failed", failed,
"duration", time.Since(startTime).String(),
"timestamp", time.Now().Format(time.RFC3339))
どのログも、読めばすぐ何を出力してるかわかります。
なぜJSON形式のログが必要なのか
ここまで「slogで楽にJSONが出せる」と書いてきましたが、そもそもなぜJSON形式が必要なのでしょうか。
機械が解析できる
// テキストログ
2024-12-21 10:30:00 health check completed: https://example.com status: 200
// JSONログ
{"time":"2024-12-21T10:30:00Z","msg":"health check completed","url":"https://example.com","statuscode":200}
テキストログは人間には読みやすいですが、例えば「statusが500以上のログだけ抽出」しようとすると、パーサーを自分で書く必要があります。
また、構造化ログのデファクトスタンダードであるJSON形式で出力することで、ログアグリゲータへの連携もしやすくなります。
まとめ
| 観点 | fmt.Fprintf | slog |
|---|---|---|
| JSON出力 | 手動 | 自動 |
| 時刻付与 | 手動 | 自動 |
| ログレベル | 手動 | 関数で分離 |
| レベルフィルタ | 自前実装 | オプション一行 |
| 出力形式切り替え | 全部書き直し | ハンドラ変更だけ |
| コードの可読性 | 😰 | 😊 |
slogは「構造化されたログを楽に読みやすく実装できる」ツールでした。
CloudOps Monitorのコード全体はこちら:
GitHub: https://github.com/YukihiroKimuraJ/cloudops-monitor
最後まで読んでいただき、ありがとうございました。
Discussion