Cool Go Slog.Logger Tricks

1 day ago 5

Intro

For years in Go, I’ve used many different logging libraries, from logrus to zap. After the release of slog into Go standard library, I immediately embraced it. It’s clever design made it so versatile and easy to use, and has already accumulated an enormous amount of libraries in the ecosystem.

Trick 1

Try have a dedicate package in your project for logging attributes.

  • you have broad view of what attributes are available, especially when you are setting up logging indexes and queries in your dashboards.
  • it enforces people using a consistent way of naming logging attributes, especially in large teams, you won’t end up with userID, user_id, UserID in your logs at the same time.
  • it provides a easy gateway of implementing something even greater, which I’ll show you in trick number 2.

e.g,

package logging func Error(err error) slog.Attr { return slog.String("error", err.Error()) } func UserID(id int) slog.Attr { return slog.Int("user_id", id) }

Trick 2

Combining logging attributes and error logs

package logging type ErrorWithAttrs struct { attrs []slog.Attr err error } func (e *ErrorWithAttrs) Error() string { return e.err.Error() } func (e *ErrorWithAttrs) Unwrap() error { return e.err } func Errorf(format string, args ...any) error { var attrs []slog.Attr for _, arg := range args { if attr, ok := arg.(slog.Attr); ok { attrs = append(attrs, attr) } } return &ErrorWithAttrs{ attrs: attrs, err: fmt.Errorf(format, args...), } } func Error(err error) slog.Attr { attrs := AttrsFromError(err) if len(attrs) == 0 { return slog.String("error", err.Error()) } attrs = append(attrs, slog.String("error", err.Error())) args := lo.Map(attrs, func(attr slog.Attr, _ int) any { return attr }) return slog.Group("", args...) }

This way, you will be able to do something like this:

func SearchDB(userID int) (*User, error) { return nil, fmt.Errorf("user %v not found in database due to: %w", logging.UserID(userID), err) } func main() { _, err := SearchDB(123) myLogger.ErrorContext(ctx, "failed to get user", logging.Error(err)) }

This will produce a log entry like this:

{"level":"error","msg":"failed to get user","user_id":123,"error":"user user_id=123 not found in database due to: <original error>"}}

You get the benefits of:

  • a. able to get all related details in error message
  • b. has a separate attribute field where you can index and query on in dashboards like kibana.

A less easy way of using loggers all over the places. Sometimes it’s just pain to pass loggers around, especially when you want to log as much as possible.

package logging import ( "context" "log/slog" "sync" ) var ( globalHandler slog.Handler pendingLogs []slog.Record pendingLogsMu sync.Mutex ) func Provide(handler slog.Handler) { pendingLogsMu.Lock() for _, record := range pendingLogs { _ = handler.Handle(context.Background(), record) } pendingLogs = nil pendingLogsMu.Unlock() globalHandler = handler } func New(name string) *slog.Logger { handler := &placeholderHandler{ attrs: []slog.Attr{ slog.String("instrument", name), }, } return slog.New(handler) } type placeholderHandler struct { attrs []slog.Attr groups []string once sync.Once handler slog.Handler } func (h *placeholderHandler) init() { h.once.Do(func() { handler := globalHandler for _, group := range h.groups { handler = handler.WithGroup(group) } h.handler = handler.WithAttrs(h.attrs) }) } func (h *placeholderHandler) Enabled(ctx context.Context, level slog.Level) bool { if globalHandler == nil { return true } return globalHandler.Enabled(ctx, level) } func (h *placeholderHandler) Handle(ctx context.Context, record slog.Record) error { if globalHandler == nil { pendingLogsMu.Lock() pendingLogs = append(pendingLogs, record) pendingLogsMu.Unlock() return nil } h.init() return h.handler.Handle(ctx, record) } func (h *placeholderHandler) WithAttrs(attrs []slog.Attr) slog.Handler { if globalHandler != nil { h.init() return h.handler.WithAttrs(attrs) } return &placeholderHandler{ attrs: append(append([]slog.Attr{}, attrs...), h.attrs...), groups: append([]string{}, h.groups...), } } func (h *placeholderHandler) WithGroup(name string) slog.Handler { if globalHandler != nil { h.init() return h.handler.WithGroup(name) } return &placeholderHandler{ attrs: append([]slog.Attr{}, h.attrs...), groups: append(append([]string{}, h.groups...), name), } }

with this, you can create a logger like this:

var logger = logging.New("my-special-library").WithAttrs([]slog.Attr{ slog.String("version", "1.0.0"), }) func DoSomething() { logger.Info("doing something") }

and just provide a handler at the entry point of your application once and for all:

logging.Provide(slog.NewJSONHandler(os.Stdout, nil))
Read Entire Article