pgx icon indicating copy to clipboard operation
pgx copied to clipboard

slog compatible structured logging

Open jacksgt opened this issue 1 year ago • 9 comments

Hello & thanks for the awesome project!

I have recently been playing with slog, which is a new (currently experimental) structured logging interface for the Go programming language. In one of the upcoming Go releases it will be merged into the standard library.

Recommended resources:

  • https://pkg.go.dev/golang.org/x/exp/slog
  • https://www.youtube.com/watch?v=gd_Vyb5vEw0
  • https://github.com/golang/go/issues/56345

The function signatures for tracelog.QueryTracer.Log and slog.Log are almost compatible:

Log(ctx context.Context, level LogLevel, msg string, data map[string]any)

https://godocs.io/github.com/jackc/pgx/v5/tracelog#Logger

func (l *Logger) Log(ctx context.Context, level Level, msg string, args ...any)

https://pkg.go.dev/golang.org/x/exp/slog#Logger.Log

It would be awesome if they were fully compatible, since then a slog Logger can be passed as-is to pgx/tracelog -- though I'm not sure if anything can be done about that at this point.

Alternatively, I came up with the following adapter for tracelog / slog:

package adapter

import (
	"context"

	"github.com/jackc/pgx/v5"
	"github.com/jackc/pgx/v5/tracelog"
	"golang.org/x/exp/slog"
)

// This type implements the tracelog.Logger interface by wrapping a slog.Logger
// https://godocs.io/github.com/jackc/pgx/v5/tracelog
// https://godocs.io/github.com/jackc/pgx/v5#QueryTracer
// https://godocs.io/golang.org/x/exp/slog
type Logger struct {
	slogger *slog.Logger
}

func NewTracerLogger(l *slog.Logger) pgx.QueryTracer {
	return &tracelog.TraceLog{
		Logger:   &Logger{slogger: l},
		LogLevel: tracelog.LogLevelTrace,
	}
}

func (l *Logger) Log(ctx context.Context, level tracelog.LogLevel, msg string, data map[string]any) {
	var attrs []slog.Attr
	for k, v := range data {
		attrs = append(attrs, slog.Any(k, v))
	}
	l.slogger.LogAttrs(ctx, translateLevel(level), msg, attrs...)
}

func translateLevel(level tracelog.LogLevel) slog.Level {
	switch level {
	case tracelog.LogLevelTrace:
		return slog.LevelDebug
	case tracelog.LogLevelDebug:
		return slog.LevelDebug
	case tracelog.LogLevelInfo:
		return slog.LevelInfo
	case tracelog.LogLevelWarn:
		return slog.LevelWarn
	case tracelog.LogLevelError:
		return slog.LevelError
	case tracelog.LogLevelNone:
		return slog.LevelError
	default:
		return slog.LevelError
	}
}

Since I'm not aware of all the best-practices regarding Go module structures, I'm not sure if this adapter should go into a separate repo, be part of the tracelog package, have it's own module but be part of the pgx repo etc.

jacksgt avatar Apr 25 '23 14:04 jacksgt

Long term, I think the best case scenario would be to add a traceslog package that directly supported the new slog package instead of an adapter for tracelog. Internally, tracelog uses maps for args so we lose the presumed performance advantages of the new logging interface if we use an adapter.

This package could live outside of pgx until slog was finally released in Go.

jackc avatar Apr 28 '23 01:04 jackc

Hi Jack!

Long term, I think the best case scenario would be to add a traceslog package

Yes, I think that totally makes sense.

Let's leave the issue open until then.

jacksgt avatar Apr 28 '23 08:04 jacksgt

slog is now in go 1.21 also https://pkg.go.dev/github.com/mcosta74/pgx-slog

bjartek avatar Aug 26 '23 11:08 bjartek

Long term, I think the best case scenario would be to add a traceslog package that directly supported the new slog package

@jackc If someone contributes such an adapter to this repo, would you be willing to merge it? Given that slog is part of the stdlib in Go 1.21 now (and therefore has the associated stability guarantees), the slog.Logger interface should be stable and safe to use.

jacksgt avatar Sep 25 '23 14:09 jacksgt

@jackc If someone contributes such an adapter to this repo, would you be willing to merge it? Given that slog is part of the stdlib in Go 1.21 now (and therefore has the associated stability guarantees), the slog.Logger interface should be stable and safe to use.

Yes, but I'm not sure if it can be done yet. pgx supports the latest 2 versions of Go. If it can be done in a way that works on Go 1.20 then it can happen now. If not, it would need to wait until Go 1.22 is released.

jackc avatar Sep 25 '23 22:09 jackc

Yes, but I'm not sure if it can be done yet. pgx supports the latest 2 versions of Go. [...] If not, it would need to wait until Go 1.22 is released.

That's great news and means we'll have enough time to come up with a solid implementation :-)

jacksgt avatar Sep 26 '23 07:09 jacksgt

@jacksgt FYI, regarding your implementation of Log(..., data map[string]any): a downside of this implementation is that the attrs are passed to slog in an ever-changing order, because order of a map[string]any is not guaranteed. This makes it difficult to find information in the logs when scanning through them manually. I would suggest the following, for now, until this is fixed in pgx:

func (l *Logger) Log(ctx context.Context, level pgx.LogLevel, msg string, data map[string]any) {
	var keys []string
	for k := range data {
		keys = append(keys, k)
	}
	sort.Strings(keys)
	var attrs []slog.Attr
	for _, k := range keys {
		attrs = append(attrs, slog.Any(k, data[k]))
	}
	l.logger.LogAttrs(ctx, translateLevel(level), msg, attrs...)
}

sgielen avatar Oct 07 '23 16:10 sgielen