1つの例で見るslog
log/slog(Go 1.21)は、メッセージ、レベル、キーと値の属性を持つ構造化されたレコードを書きます。
各行は time=... level=INFO msg="user logged in" user=ada attempts=1 のように出力されます。すべての値が別々のフィールドなので、ログの収集システム(Loki、Elasticsearch、CloudWatch、Datadog)は自由なテキストに正規表現をかけることなく、user=ada や level=ERROR でフィルタできます。
このページの例は、出力が順番どおりに表示されるよう os.Stdout に書いています。実際のサービスでは、ログはたいてい os.Stderr に出します。デフォルトのロガーが書き込むのもそこです。
レベル
| レベル | 値 | 用途 |
|---|---|---|
slog.LevelDebug | -4 | 開発者向けの詳細。本番ではオフ |
slog.LevelInfo | 0 | 通常の出来事:起動した、リクエストを処理した、ジョブが終わった |
slog.LevelWarn | 4 | プログラムが対処した想定外のこと |
slog.LevelError | 8 | 操作が失敗した |
ハンドラは最低レベルより低いレコードを捨て、デフォルトの最低レベルはInfoです。Level: slog.LevelDebug でハンドラを設定するまで slog.Debug(...) が何も表示しないのはこのためです。値の間の隙間は、slog.Level(2) のような独自のレベルのための余地です。
実行時に(フラグ、管理用のエンドポイント、シグナルから)レベルを変えるには、オプションに slog.LevelVar を入れ、後でその Set を呼びます。
var level slog.LevelVar // zero value: Info
logger := slog.New(slog.NewJSONHandler(os.Stderr, &slog.HandlerOptions{Level: &level}))
level.Set(slog.LevelDebug) // from now on, debug records are written
テキストかJSONか
slog.NewTextHandler は key=value のペアを書き、ターミナルで読みやすくなります。slog.NewJSONHandler は1行に1つのJSONオブジェクトを書き、多くのログのパイプラインが期待する形式です。ログを出す呼び出しは同じままで、ハンドラだけが変わります。
値は型を保ちます。JSONの出力では status は数値、retry は真偽値で、time.Duration はテキストでは 42ms、JSONではナノ秒として表示され、error はメッセージを表示します。ReplaceAttr は属性を書き換えたり取り除いたりするためのフックで、ここではタイムスタンプを落とすのに使っています。実際には、キーの名前を変えたり(msg を message に)、値を伏せたりするのに使います。
属性
型の緩い形では、キーと値を交互に並べます:"user", "ada", "attempts", 3。短く書けますが、失敗のしかたが1つあります。引数の数が奇数になることです。余った値は !BADKEY というキーで記録されます。go vet がそれを検出します。
./main.go:14:2: call to slog.Info missing a final value
型安全性と少しのメモリ確保の削減のためには、属性のコンストラクタを使い、ホットな経路でログを出すときは LogAttrs を使います。
logger.Info("order placed",
slog.Int("order_id", 1017),
slog.String("currency", "EUR"),
slog.Float64("total", 59.90),
slog.Duration("took", elapsed),
)
logger.LogAttrs(ctx, slog.LevelInfo, "order placed", slog.Int("order_id", 1017))
キーにはコードベース全体で1つの命名規則を使います(あるパッケージでは userID、別のパッケージでは uid ではなく、どこでも user_id)。ログシステムでのクエリがそれに依存します。
With:文脈を運ぶロガー
logger.With(attrs...) は、すべてのレコードにそれらの属性を加える新しいロガーを返します。リクエストやジョブごとに1つ作れば、それが書くすべての行を結びつけられます。
どの行も、呼び出しのたびに繰り返すことなく service、version、request_id、user を運びます。slog.Group は属性を入れ子にし、JSONのハンドラはそれを入れ子のオブジェクト("payment":{"amount":25,"currency":"USD"})として、テキストのハンドラはドット区切りのキー(payment.amount=25)として書きます。logger.WithGroup("db") は、そのロガーの以降のすべての属性をグループの下に置きます。
リクエストスコープのロガーは、引数か構造体のフィールドとして下に渡します。context.Context に保存することもできますが、依存関係が隠れてしまいます。slogの InfoContext(ctx, ...) メソッドはコンテキストをハンドラに渡し、独自のハンドラはそこからトレースIDを取り出せます。
LogValuerで秘密情報を隠す
型は slog.LogValuer を実装することで、自分がどうログに出るかを制御できます。これにより、誰がその値をログに出しても、パスワードやトークンがログに残りません。
User はIDとメールアドレスだけをログに出し、単独でログに出した Token は REDACTED と表示されます。ハンドラはレコードが実際に書き込まれるときにだけ LogValue を呼ぶので、計算が高くつく値にも使えます。
昔ながらのlogパッケージ
log は slog より前からあり、小さなプログラムやスクリプトには今でも十分です。日付と時刻の接頭辞付きで、標準エラー出力に行を書きます。
| フラグ | 加えるもの |
|---|---|
log.LstdFlags(デフォルト) | 2009/11/10 23:00:00 という日付と時刻 |
log.Lmicroseconds | 時刻にマイクロ秒 |
log.LUTC | UTCの時刻 |
log.Lshortfile / log.Llongfile | main.go:14 / 完全なパス |
log.Lmsgprefix | 接頭辞を行の先頭ではなくメッセージの前に置く |
終了したりpanicしたりする関数が3種類あり、その違いが重要です。
log.Fatal、log.Fatalf、log.Fatallnは表示してからos.Exit(1)を呼びます。deferした呼び出しは実行されません。mainでの起動時の失敗に使い、ライブラリのコードやリクエストのハンドラでは決して使いません。log.Panicとその仲間は表示してからpanicするので、deferした呼び出しは実行され、panicは回復できます。- それ以外の関数はすべて、単に1行を書きます。
ファイルにログを出すには、ファイルを開いて log.New か log.SetOutput に渡します。io.MultiWriter(os.Stderr, f) は両方に書き込みます。
logとslogを一緒に使う
slog.SetDefault(logger) は、logger をトップレベルの slog.Info などの関数のデフォルトにし、さらに log パッケージの出力もそれに流します。すると、自分のコードや依存パッケージにある既存の log.Printf の呼び出しが、Infoレベルの構造化されたレコードとして出てきます。
slog.SetDefault(slog.New(slog.NewJSONHandler(os.Stderr, nil)))
log.Printf("legacy message") // {"time":"...","level":"INFO","msg":"legacy message"}
SetDefault の前は、デフォルトのslogのロガーは log パッケージを通して書きます。単独の slog.Info("hi") が 2026/09/23 14:30:00 INFO hi と表示されるのはこのためです。
実践的なルール
- エラーはログに出すか返すかのどちらか。両方はしない。 エラーをログに出して返す関数があると、同じ失敗が呼び出しスタックのすべての層でログに出ます。エラーは文脈を付けて上に返し、処理する場所で一度だけログに出します。
- 変わるデータはメッセージではなく属性に入れる。
logger.Info("user created", "user_id", id)はログシステムでうまくまとまります。logger.Info(fmt.Sprintf("user %d created", id))はユーザーごとに別のメッセージを作ります。 - 秘密情報やリクエストのボディ全体を決してログに出さない。
LogValuerかReplaceAttrで伏せます。 - 本番ではJSON、開発ではテキスト。 起動時にフラグか環境変数からハンドラを選びます。
- レベルは意図して選ぶ。 すべてをErrorで出すと、エラーのアラートが雑音になります。
よくある質問
Goのslogとは何ですか?
log/slog は、Go 1.21で標準ライブラリに追加された構造化ログのパッケージです。整形した文字列の代わりに、各レコードがメッセージ、レベル(Debug、Info、Warn、Error)、キーと値の属性を持ち、ハンドラがそれを key=value のテキストかJSONとして書き出します:slog.Info("login", "user", "ada", "attempts", 3)。
slogでデバッグログを有効にするには?
デフォルトの最低レベルはInfoなので、slog.Debug は何も表示しません。より低いレベルのハンドラを作り、それをデフォルトにします:slog.SetDefault(slog.New(slog.NewTextHandler(os.Stderr, &slog.HandlerOptions{Level: slog.LevelDebug})))。プログラムの実行中にレベルを変えたいなら、定数の代わりに slog.LevelVar を使います。
Goのlogとslogの違いは何ですか?
log は、省略可能なタイムスタンプの接頭辞付きで自由な形式の行を書き、レベルを持ちません。slog は、レベルと型付きのキーと値の属性を持つレコードを書き、ログの収集システムがパースしてフィルタできます。どちらも標準ライブラリにあり、slog.SetDefault は log パッケージの出力もslogのハンドラに流します。
log.Fatalはdeferした関数を実行しますか?
実行しません。log.Fatal と log.Fatalf はメッセージを表示して os.Exit(1) を呼び、deferした呼び出しはすべて飛ばされます。後始末するものがない main や準備処理のコードでだけ使います。log.Panic は代わりにpanicするので、deferした呼び出しは実行されます。