slog の基本
Go1.21 から公式の標準ライブラリに構造化ログを出力可能な slog が追加されました。
呼び出し方
デフォルトで時刻やログレベルが付与されます。 比較のため、fmt でも出力します
fmt.Println("Hello, World!")
slog.Info("Hello, World!")
slog.InfoContext(ctx, "Hello, World!")
出力
Hello, World!
2024/04/13 13:30:48 INFO Hello, World!
2024/04/13 13:30:48 INFO Hello, World!
コード全文
package main
import (
"context"
"fmt"
"log/slog"
)
func main() {
fmt.Println("Hello, World!")
slog.Info("Hello, World!")
ctx := context.TODO()
slog.InfoContext(ctx, "Hello, World!")
}
追加の情報を付与
key: value
pair をログに付与することができます
slog.Info("Hello, World!", slog.Int("line", 9), slog.String("file", "main.go"))
slog.InfoContext(ctx, "Hello, World!", slog.Int("line", 11), slog.String("file", "main.go"))
出力
2024/04/13 13:39:06 INFO Hello, World! line=9 file=main.go
2024/04/13 13:39:06 INFO Hello, World! line=11 file=main.go
コード全体
package main
import (
"context"
"log/slog"
)
func main() {
slog.Info("Hello, World!", slog.Int("line", 9), slog.String("file", "main.go"))
ctx := context.TODO()
slog.InfoContext(ctx, "Hello, World!", slog.Int("line", 11), slog.String("file", "main.go"))
}
出力の形式は handller で調整可能で、デフォルトでは TextHandler が使われます。
実用上便利なことが多いので、実際の開発では JSON Handler はよく使われます
func main() {
jsonHandler := slog.NewJSONHandler(os.Stdout, nil)
logger := slog.New(jsonHandler)
ctx := context.TODO()
logger.Info("Hello, World!", slog.Int("line", 15), slog.String("file", "main.go"))
logger.InfoContext(ctx, "Hello, World!", slog.Int("line", 16), slog.String("file", "main.go"))
}
出力
{"time":"2024-04-13T13:53:07.414249+09:00","level":"INFO","msg":"Hello, World!","line":15,"file":"main.go"}
{"time":"2024-04-13T13:53:07.414599+09:00","level":"INFO","msg":"Hello, World!","line":16,"file":"main.go"}
コード全体
package main
import (
"context"
"os"
"log/slog"
)
func main() {
jsonHandler := slog.NewJSONHandler(os.Stdout, nil)
logger := slog.New(jsonHandler)
ctx := context.TODO()
logger.Info("Hello, World!", slog.Int("line", 15), slog.String("file", "main.go"))
logger.InfoContext(ctx, "Hello, World!", slog.Int("line", 16), slog.String("file", "main.go"))
}
余談ではありますが、比較すると、人間的には(個人的には) Text の方が読みやすいと思いました
# TextHandler
2024/04/13 13:39:06 INFO Hello, World! line=11 file=main.go
# JSONHandler
{"time":"2024-04-13T13:50:21.753606+09:00","level":"INFO","msg":"Hello, World!","line":15,"file":"main.go"}
slog の AddSource Option を探索
slog.NewJSONHandler
の第2引数には Option を渡せます。
AddSource Option は Stacktrace まではいきませんが、コードの場所は教えてくれます。
...
jsonHandler := slog.NewJSONHandler(os.Stdout, &slog.HandlerOptions{
AddSource: true,
})
logger := slog.New(jsonHandler)
...
出力
{"time":"2024-04-13T13:56:09.692728+09:00","level":"INFO","source":{"function":"main.main","file":"/Users/atsuhiro.uchida/dev/playground/go-slog-playground/main.go","line":15},"msg":"Hello, World!"}
コード全体
package main
import (
"os"
"log/slog"
)
func main() {
jsonHandler := slog.NewJSONHandler(os.Stdout, &slog.HandlerOptions{
AddSource: true,
})
logger := slog.New(jsonHandler)
logger.Info("Hello, World!")
}
どのファイルのどの関数で、何行目か、出力されてます。
ちなみに、 build しても source map の情報は維持されているようです
% go build -o main
% ./main
{"time":"2024-04-13T14:04:38.670391+09:00","level":"INFO","source":{"function":"main.main","file":"/Users/atsuhiro.uchida/dev/playground/go-slog-playground/main.go","line":15},"msg":"Hello, World!"}
struct の場合
構造体を出力したい場合は、json タグをつければ出力されます
package main
import (
"os"
"log/slog"
)
type Address struct {
City string `json:"city"`
State string `json:"state"`
}
type User struct {
Name string `json:"name"`
Age int `json:"age"`
Address *Address `json:"address,omitempty"`
}
func main() {
jsonHandler := slog.NewJSONHandler(os.Stdout, &slog.HandlerOptions{
AddSource: true,
})
logger := slog.New(jsonHandler)
u := &User{
Name: "Alice",
Age: 25,
Address: &Address{
City: "San Francisco",
State: "CA",
},
}
logger.Info("Hello, World!", "user", u)
}
出力
{"time":"2024-04-13T14:17:13.399365+09:00","level":"INFO","source":{"function":"main.main","file":"/Users/atsuhiro.uchida/dev/playground/go-slog-playground/main.go","line":34},"msg":"Hello, World!","user":{"name":"Alice","age":25,"address":{"city":"San Francisco","state":"CA"}}}
見づらいので整えると、以下のように出力が得られていることがわかります
{
"time": "2024-04-13T14:17:13.399365+09:00",
"level": "INFO",
"source": {
"function": "main.main",
"file": "/Users/atsuhiro.uchida/dev/playground/go-slog-playground/main.go",
"line": 34
},
"msg": "Hello, World!",
"user": {
"name": "Alice",
"age": 25,
"address": { "city": "San Francisco", "state": "CA" }
}
}
ちなみに json タグがない場合、struct のフィールド名が key として使われます
// タグをなくす
type Address struct {
City string
State string
}
func main() {
...
logger.Info("Hello, World!", "user", u)
}
出力
{"time":"2024-04-13T14:36:16.765509+09:00","level":"INFO","source":{"function":"main.main","file":"/Users/atsuhiro.uchida/dev/playground/go-slog-playground/main.go","line":34},"msg":"Hello, World!","user":{"name":"Alice","age":25,"address":{"City":"San Francisco","State":"CA"}}}
コード全体
package main
import (
"os"
"log/slog"
)
type Address struct {
City string
State string
}
type User struct {
Name string `json:"name"`
Age int `json:"age"`
Address *Address `json:"address,omitempty"`
}
func main() {
jsonHandler := slog.NewJSONHandler(os.Stdout, &slog.HandlerOptions{
AddSource: true,
})
logger := slog.New(jsonHandler)
u := &User{
Name: "Alice",
Age: 25,
Address: &Address{
City: "San Francisco",
State: "CA",
},
}
logger.Info("Hello, World!", "user", u)
}
*"City", "State" と、先頭が大文字になっている(フィールド名が使われている)
group で出力する
同じ出力を slog.Group を使って再現できます。実際は struct なら group は使わず直接渡した方が楽です。
package main
import (
"os"
"log/slog"
)
func main() {
jsonHandler := slog.NewJSONHandler(os.Stdout, &slog.HandlerOptions{
AddSource: true,
})
logger := slog.New(jsonHandler)
logger.Info(
"Hello, World!",
slog.Group(
"user",
slog.String("name", "Alice"),
slog.Int("age", 25),
slog.Group(
"address",
slog.String("city", "San Francisco"),
slog.String("state", "CA"),
),
),
)
}
{"time":"2024-04-13T14:26:48.645613+09:00","level":"INFO","source":{"function":"main.main","file":"/Users/atsuhiro.uchida/dev/playground/go-slog-playground/main.go","line":15},"msg":"Hello, World!","user":{"name":"Alice","age":25,"address":{"city":"San Francisco","state":"CA"}}}
余談
公式 GoDoc 曰く、
logger.LogAttrs(ctx, slog.LevelInfo, "hello", slog.Int("count", 3))
と呼ぶことが最も効率がいいようです。
LogLevel の指定は面倒なので、プロジェクトの中でこれで統一するなら、snippet を用意したいなと思いました。