【Go】slog を触ってみる
はじめに
Go1.21 から新しく log/slog
が追加されました。
今回は slog
に関するブログとドキュメントを読んで学んだことを備忘録として残します。
slog の主な構成要素
slog パッケージに含まれる3つのタイプ
- Logger
Info
やLogAttrs
のように出力を担当する。 - Record
ログイベントに関する情報を保持する - Handler
ロガーによって生成されたログレコードを処理する
slog の LogLevel
以下の4 つがあり、整数が割り当てられています。
レベル | 整数 |
---|---|
Debug | -4 |
Info | 0 |
Warn | 4 |
Error | 8 |
https://github.com/golang/go/blob/go1.21.0/src/log/slog/level.go#L43-L48
トップレベルのロガー
サンプルで紹介されているような、トップレベルの slog はデフォルトのロガーを使用しています
"slog.Info" のコードを読んでみる
slog.Info
のコードを読んでみます。
どうやら Default().log
が呼ばれているようです。
https://github.com/golang/go/blob/go1.21.0/src/log/slog/logger.go#L259-L261
さらに読み進めてみます。
slog パッケージをインポートしたとき init
関数によって defaultLogger
が初期化されており、Default
関数は Logger
構造体のインスタンスを返しているようです。
つまり Default().log
は Logger
構造体のメソッドである log()
が呼ばれている、ということですね。
https://github.com/golang/go/blob/go1.21.0/src/log/slog/logger.go#L17-L24
ちなみに Logger
構造体は以下のフィールドを持っています。
https://github.com/golang/go/blob/go1.21.0/src/log/slog/logger.go#L79-L81
Logger.log()
を見つけました。 slog.Info
や slog.Debug
などの関数は、以下の共通したメソッドが呼ばれていることがわかりました。
ちなみに Logger.log
メソッドの1行目(コードだと210行目)にある if !l.Enabled(ctx, level) {
で、ログレベルによる出力の制御をしていることがわかります。
https://github.com/golang/go/blob/go1.21.0/src/log/slog/logger.go#L209-L226
この Enabled
は Handler
インターフェイスを実装するために必要なメソッドです。
https://github.com/golang/go/blob/master/src/log/slog/handler.go#L31-L41
デフォルトロガーのログレベル
デフォルトロガーのログレベルは Info
なので、 slog.Debug()
は出力されません。
https://github.com/golang/go/blob/go1.21.0/src/log/slog/handler.go#L101-L103
デフォルトハンドラーは標準エラー出力
以下のコードを実行してみます。
標準エラー出力を捨てると、何も出力されません。
つまり、デフォルトのロガーを使用すると、ログが標準エラーとして出力されることがわかります。
ここで疑問が湧いてきます。
どこで stderr
を定義しているのか?
疑問を解決すべくコードを読んでいきます。
先ほど、 slog.Info
のコードを読んでみた結果、src/log/slog/logger.go
で以下のコードが呼ばれていることがわかりました。
この関数を見てみると、Logger.log
メソッドが直接出力しているようではなさそうです。
https://github.com/golang/go/blob/go1.21.0/src/log/slog/logger.go#L209-L226
プロポーザルによると、ハンドラーが何かをしていそうです。
A typical handler may print log records to standard error, or write them to a file, database or network service, or perhaps augment them with additional attributes and pass them on to another handler.
引用;https://go.googlesource.com/proposal/+/master/design/56345-structured-logging.md#handlers
Logger.log
メソッドの最後に _ = l.Handler().Handle(ctx, r)
が実行されています。 Handle
メソッドを読んでいきます。
Handle
メソッドでは、buf.WriteString
も関係していそうですが、最後の行 return h.output(r.PC, *buf)
が何者が確認してみます。
https://github.com/golang/go/blob/go1.21.0/src/log/slog/handler.go#L108-L117
h.output
は、defaultHandler
構造体のフィールドであり、 newDefaultHandler
関数の第一引数で初期化されています。
https://github.com/golang/go/blob/go1.21.0/src/log/slog/handler.go#L88-L99
そんな newDefaultHandler
関数は、src/log/slog/logger.go
の init
関数で呼ばれています。
第一引数をみると、loginternal.DefaultOutput
という値が渡されています。
このファイルのインポート文をみると、loginternal "log/internal"
と書いてありますね。
どうやら output するメソッドの実態は log
パッケージから来ていることがわかりました。
https://github.com/golang/go/blob/go1.21.0/src/log/slog/logger.go#L7-L21
go/src/log/internal/internal.go
に DefaultOutput
がありました。
どうやら変数のようです。しかし、何も定義されていません。
https://github.com/golang/go/blob/go1.21.0/src/log/internal/internal.go#L12
internal.DefaultOutput
を初期化している init
関数を見つけました。
何やら return std.output(pc, 0, func(buf []byte) []byte {
の行が怪しそうです。
std.output
が何者か探してみます。
https://github.com/golang/go/blob/go1.21.0/src/log/log.go#L249-L255
slog ではなく、src/log/log.go
に定義されている log
パッケージに std
変数があり、パッケージ内のグローバル変数に定義されています。
やっと見つけました! ここで os.Stderr
を指定していました。
https://github.com/golang/go/blob/go1.21.0/src/log/log.go#L87
New
関数は、同じファイル内にあり、log
パッケージの Logger
構造体を戻り値にしています。
https://github.com/golang/go/blob/go1.21.0/src/log/log.go#L57-L77
この Logger
構造体に output
メソッドがあります。これが、先ほどの std.output
正体です。
_, err := l.out.Write(*buf)
の行で、stderr 出力していることがわかりました。
https://github.com/golang/go/blob/go1.21.0/src/log/log.go#L202-L247
組み込みHnadler
TextHandler
と JSONHandler
2つの組み込み Handler があります。
TextHandler
https://github.com/golang/go/blob/go1.21.0/src/log/slog/text_handler.go#L28
JSONHandler
https://github.com/golang/go/blob/go1.21.0/src/log/slog/json_handler.go#L30
HandlerOptions
HandlerOptions
には以下の設定が可能です。
type HandlerOptions - pkg.go.dev
https://github.com/golang/go/blob/go1.21.0/src/log/slog/handler.go#L129-L167
AddSource
HandlerOptions に AddSource
を追加してみます。
実行すると、 ログの中に source
が追加されました。
Level
HandlerOptions の Level にログレベルを設定すると、ログレベルの出力を制御できます。
ReplaceAttr
ReplaceAttr
がどんな値を渡してくるのかイメージができなかったので、とりあえず引数を出力してみました。
どんな値が引数に入っているのかなんとなくイメージができました。
HandlerOptions
の Example にもありますが、特定の key を削除することが可能です。
LogAttr
log.Info
を実行するよりも Attr
型を使用した方がメモリ割り当てが最小になるように動作するので、効率が良いそうです。
frequently executed log statements it may be more efficient to use the Attr type and call the LogAttrs method. These work together to minimize memory allocations.
Attr 型は以下の通りです。
- String
- Int64
- Int
- Uint64
- Float64
- Bool
- Time
- Duration
- Group
- Any
引用:https://github.com/golang/go/blob/go1.21.0/src/log/slog/attr.go
Logger.With
Logger のメソッドである、With
を使うと、With の引数に指定した属性を含めたロガーを新規に作成できます。
トレース ID のように共通のデータを log の呼び出しごとではなく、属性を含む新しいロガーを使うことで繰り返しを避けることができます。
With なし
With あり
感想
slog のパッケージはそれほど大きくないので、コードリーディングしやすいと感じました。
InfoContext や LogAttrs のように、第一引数に Context をとるメソッドや関数がありますが、2023年9月現在、Context は利用されていないようです。
なので、context を渡す必要があるときは、 context.Background()
でいいと思います。
https://github.com/golang/go/blob/go1.21.0/src/log/slog/json_handler.go#L90-L92 https://github.com/golang/go/blob/go1.21.0/src/log/slog/json_handler.go#L46-L48
https://github.com/golang/go/blob/go1.21.0/src/log/slog/text_handler.go#L94-L96 https://github.com/golang/go/blob/go1.21.0/src/log/slog/text_handler.go#L44-L46
slog のブログ には、 context.Context からトレース ID のように値を取り出すことができる旨が書いてあります。 しかし、それをしたいのならば自分で実装しましょう。ということなのだと思っています。
As the call to LogAttrs shows, you can pass a context.Context to some log functions so a handler can extract context information like trace IDs. (Canceling the context does not prevent the log entry from being written.)