😊

Go 1.21 slogで構造化ログを実装する

に公開

Go 1.21 slogで構造化ログを実装する

はじめに

前回の記事で、Go言語でURL監視ツール「CloudOps Monitor」を作った話を書きました。

https://zenn.dev/yukihirokimuraj/articles/13ee8236b029d6

その記事の「今後の展望」で触れていた構造化ログ(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",

問題点を挙げます:

  1. 読みにくい - カンマ、コロン、括弧、エスケープが入り乱れてる
  2. ミスしやすい - "の閉じ忘れ、カンマの位置ズレ、気づきにくい
  3. フィールド追加が面倒 - 新しい項目を増やすたびにフォーマット文字列を慎重に編集
  4. レベル切り替えが面倒 - 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