フューチャー技術ブログ

Go 1.25 リリース連載 log/slog

はじめに

Go 1.25 リリース連載 の 2 本目です。

本記事では log/slog パッケージ のアップデートについて紹介します。
Go の log/slog パッケージは、Go 1.21 で導入された構造化ロギングをサポートする標準ライブラリです。

本記事では slog についての基本的は割愛しますが、slog の概要やこれまでのアップデート経緯をつかみたい方は、過去のリリース連載記事を参照してください。

アップデートの概要

リリースノート を参照してみましょう。

GroupAttrs creates a group Attr from a slice of Attr values.

Record now has a Source method, returning its source location or nil if unavailable.

  • slog.GroupAttrs の追加
    複数の属性 ([]slog.Attr) を簡潔にグルーピングできるようになりました。
  • Record.Source() の公開
    ログエントリの発生元(ファイル・行番号・関数名)を取得できるようになりました。

それぞれ詳細な内容を見ていきましょう。

アップデートの詳細

slog.GroupAttrs による属性のグルーピング(#66365

slog.GroupAttrs を使用して構造化ログの属性(要素)をグルーピングできるようになりました。

g := slog.GroupAttrs("user",
slog.String("id", "00001"),
slog.String("name", "Bob"),
)
// {"level":"INFO","msg":"GroupAttrs","user":{"id":"00001","name":"Bob"}}
logger.Info("GroupAttrs", g)

属性のグルーピング自体はもともと slog.Group を使用して実現できましたが、属性を動的に生成する場合にいくつか使用上の問題がありました。

属性が静的な場合

g := slog.Group("user",
slog.String("id", "00001"),
slog.String("name", "Bob"),
)
// {"level":"INFO","msg":"Group","user":{"id":"00001","name":"Bob"}}
logger.Info("Group", g)

属性が動的な場合

条件に応じて属性を追加する場合などは slog.Group がうまく機能しません。

attrs := []slog.Attr{
slog.String("id", "00001"),
slog.String("name", "Bob"),
}

// 条件に応じて属性を追加
if showAge {
attrs = append(attrs, slog.Int("age", 36))
}

// []slog.Attr doesn't match []any となり動かない
g := slog.Group("user", attrs...)

そのためこれまでは slog.Any を利用したり、[]slog.Attr[]any に変換したりして対応してきた背景があります。

// slog.Any を使用する
g := slog.Any("user", slog.GroupValue(attrs...))

// ヘルパーファンクションを用意して []any に変換する
g := slog.Group("key", attr2any(attrs)...)

// []any を使用する
var attrs []any
...
g := slog.Group("key", attrs...)

このアプローチは any の使用が避けられず、適切なコードサジェストがなされないなど直感的ではないということで []slog.Attr を引数として渡せる slog.GroupAttrs が生まれました。

record.Source によるソース情報の取得(#70280

Go の log/slog では、1 件のログエントリを内部では slog.Record という構造体で表現しています。
Go1.25 で追加された record.Source はレコードからログの発生元(ファイル名・行番号・関数名)を返します。
ソースの Diff を見るとはもともとパッケージ内限定(unexported)だったものが公開された(export)された形になります。

これにより、自作のカスタムハンドラなどからもログのソース情報にアクセスできます。
サンプルソースは次の通りです。

func main() {
logger := slog.New(NewSourceHandler(slog.NewJSONHandler(os.Stdout, nil)))
g := slog.Group("user",
slog.String("id", "00001"),
slog.String("name", "Bob"),
)
// LOG from /xxx/main.go:61 (main)
logger.Info("Group", g)
}


type SourceHandler struct {
h slog.Handler
}

func NewSourceHandler(h slog.Handler) *SourceHandler {
return &SourceHandler{
h: h,
}
}

func (sh SourceHandler) Enabled(ctx context.Context, level slog.Level) bool {
return sh.h.Enabled(ctx, level)
}

func (sh SourceHandler) Handle(ctx context.Context, r slog.Record) error {
// ソース情報を出力
if src := r.Source(); src != nil {
fmt.Printf("LOG from %s:%d (%s)\n", src.File, src.Line, src.Function)
}
return sh.h.Handle(ctx, r)
}

func (sh SourceHandler) WithAttrs(attrs []slog.Attr) slog.Handler {
return SourceHandler{h: sh.h.WithAttrs(attrs)}
}

func (sh SourceHandler) WithGroup(name string) slog.Handler {
return SourceHandler{h: sh.h.WithGroup(name)}
}

おわりに

Go 1.25 の log/slog では、構造化ログの柔軟性と拡張性がさらに向上しました。

サンプルのソースコードは こちらのリポジトリ で公開しています。

次回は sync のアップデートについてです。