Generic slog.LogValuer helper

· 170 words · 1 minute read

I like to use log/slog in Go. And I use custom LogValue() functions to have more control over how my structs get logged. But sometimes getting the right format is tedious.

Logging pointers leaves a bit to be desired.

// LogValue implements the slog.LogValuer interface.
func (s *Struct) LogValue() slog.Value {
	attrs := []slog.Attr{}

	if s.Pint == nil {
		attrs = append(attrs, slog.Any("s.Pint", s.Pint))
	} else {
		attrs = append(attrs, slog.Int("s.PInt", *s.Pint))
	}

	return slog.GroupValue(
		slog.Int("s.Int", s.Int),     // logged as 123
		slog.Any("s.PInt", s.PInt),   // pointer, logged as 0x123456789012
		slog.Any("s.Pint", s.Pint),   // nil logged as <nil>
		slog.Int("*s.Pint", *s.Pint), // deref, logged as 123, panic on nil
        attrs...
	)
}

Generics can be employed to avoid writing long nil checks.

func nilSlogAttr[T comparable](s string, x any, f func(string, T) slog.Attr) slog.Attr {
	switch v := x.(type) {
	case *T:
		if v == nil {
			return slog.Any(s, v)
		}

		return f(s, *v)
	default:
		return slog.Any(s, v)
	}
}

func (s *Struct) LogValue() slog.Value {
	return slog.GroupValue(
		slog.Int("s.Int", s.Int),
		nilSlogAttr[int]("s.Pint", s.Pint, slog.Int),
	}
}