🔖

slog.LogValuer の実装を自動生成するツールを作った

2023/08/27に公開

Go 1.21 で新たに log/slog という構造化ロギングのための標準パッケージが追加されました。パッケージの内容や使い方は Go Doc や先日公開された Go Blog などで確認できます。

slog には slog.LogValuer というインターフェースが定義されており、この実装を満たすことでログに出力する内容をカスタマイズできます。Go Doc に例がありますが、構造体のフィールドをグループ化したり、ログに出力したくない場合に代わりの文字を出力したりすることができます。

type Name struct {
	First, Last string
}

func (n Name) LogValue() slog.Value {
	return slog.GroupValue(
		slog.String("first", n.First),
		slog.String("last", n.Last))
}

n := Name{"Perry", "Platypus"}
logger := slog.New(slog.NewTextHandler(os.Stdout, &slog.HandlerOptions{ReplaceAttr: func(groups []string, a slog.Attr) slog.Attr {
	if a.Key == slog.TimeKey && len(groups) == 0 {
		return slog.Attr{}
	}
	return a
}}))
logger.Info("mission accomplished", "agent", n)

// Output:
//
// level=INFO msg="mission accomplished" agent.first=Perry agent.last=Platypus

// JSON Output would look in part like:
// {
//     ...
//     "msg": "mission accomplished",
//     "agent": {
//         "first": "Perry",
//         "last": "Platypus"
//     }
// }
type Token string

func (Token) LogValue() slog.Value {
	return slog.StringValue("REDACTED_TOKEN")
}

t := Token("shhhh!")
logger := slog.New(slog.NewTextHandler(os.Stdout, &slog.HandlerOptions{ReplaceAttr: func(groups []string, a slog.Attr) slog.Attr {
	if a.Key == slog.TimeKey && len(groups) == 0 {
		return slog.Attr{}
	}
	return a
}}))
logger.Info("permission granted", "user", "Perry", "token", t)

// Output:
//
// level=INFO msg="permission granted" user=Perry token=REDACTED_TOKEN

構造体のフィールドをグループ化するためには、LogValue メソッド内でグルーピングしたいフィールドを列挙する必要があります。これについて、slog.LogValuer の実装を自動生成する簡易的なツール Logvaluer を作ってみました。コマンドライン引数で指定された型を探して LogValue メソッドを実装します。現状は構造体タグによってフィールドを出力対象外にしたり、ログに値を出力したくないフィールドで代替文字を出力したりできます。

例えば以下の構造体については

type Foo struct {
	Str       string
	pstr      string
	IgnoreStr string `ignored:"true"`
	Passwd    string `mask:"true"`
	Flo       float64
	M         int
	N         int64
	L         uint64
	Flag      bool
	Date      time.Time
	Dur       time.Duration
}

このようなコードを生成します。

func (f Foo) LogValue() slog.Value {
	return slog.GroupValue(
		slog.String("Str", f.Str),
		slog.String("pstr", f.pstr),
		slog.String("Passwd", "MASK"),
		slog.Float64("Flo", f.Flo),
		slog.Int("M", f.M),
		slog.Int64("N", f.N),
		slog.Any("L", f.L),
		slog.Bool("Flag", f.Flag),
		slog.Time("Date", f.Date),
		slog.Duration("Dur", f.Dur),
	)
}

他の構造体タグを使ってログ出力をカスタマイズするとより便利かもしれません。json タグを見るようにしたりするとか

Discussion