From 8d8766b1d105d83f1a231aef8b845f60ccc58f46 Mon Sep 17 00:00:00 2001 From: pgallo Date: Fri, 29 Dec 2023 09:11:32 -0800 Subject: [PATCH] Add slog adapter Adding adapter for golang's stadard lib log/slog logger. --- logadapter/slogadapter/README.md | 14 ++++ logadapter/slogadapter/go.mod | 15 ++++ logadapter/slogadapter/go.sum | 28 +++++++ logadapter/slogadapter/logger.go | 44 ++++++++++ logadapter/slogadapter/logger_test.go | 114 ++++++++++++++++++++++++++ 5 files changed, 215 insertions(+) create mode 100644 logadapter/slogadapter/README.md create mode 100644 logadapter/slogadapter/go.mod create mode 100644 logadapter/slogadapter/go.sum create mode 100644 logadapter/slogadapter/logger.go create mode 100644 logadapter/slogadapter/logger_test.go diff --git a/logadapter/slogadapter/README.md b/logadapter/slogadapter/README.md new file mode 100644 index 0000000..65f4f70 --- /dev/null +++ b/logadapter/slogadapter/README.md @@ -0,0 +1,14 @@ +## SQLDB-LOGGER log/slog ADAPTER + +sqldb-logger log adapter for go's standard lib's [log/slog](https://pkg.go.dev/log/slog) + +```go +logger, _ := slog.Default() +// populate log pre-fields here before set to OpenDriver +db := sqldblogger.OpenDriver( + dsn, + &mysql.MySQLDriver{}, + slogadapter.New(logger), + // optional config... +) +``` diff --git a/logadapter/slogadapter/go.mod b/logadapter/slogadapter/go.mod new file mode 100644 index 0000000..4d7de33 --- /dev/null +++ b/logadapter/slogadapter/go.mod @@ -0,0 +1,15 @@ +module github.com/simukti/sqldb-logger/logadapter/slogadapter + +go 1.17 + +require ( + github.com/simukti/sqldb-logger v0.0.0-20230108154142-840120f68bea + github.com/stretchr/testify v1.8.1 +) + +require ( + github.com/davecgh/go-spew v1.1.1 // indirect + github.com/kr/text v0.2.0 // indirect + github.com/pmezard/go-difflib v1.0.0 // indirect + gopkg.in/yaml.v3 v3.0.1 // indirect +) diff --git a/logadapter/slogadapter/go.sum b/logadapter/slogadapter/go.sum new file mode 100644 index 0000000..e44e5c7 --- /dev/null +++ b/logadapter/slogadapter/go.sum @@ -0,0 +1,28 @@ +github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI= +github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= +github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= +github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= +github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= +github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/simukti/sqldb-logger v0.0.0-20230108154142-840120f68bea h1:MygiYxbZHQAGOsZmrIiytjLhPLwww1xcdXzPORrOrLM= +github.com/simukti/sqldb-logger v0.0.0-20230108154142-840120f68bea/go.mod h1:ztTX0ctjRZ1wn9OXrzhonvNmv43yjFUXJYJR95JQAJE= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= +github.com/stretchr/objx v0.5.0 h1:1zr/of2m5FGMsad5YfcqgdqdWrIhu+EBEJRhR1U7z/c= +github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= +github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= +github.com/stretchr/testify v1.8.1 h1:w7B6lhMri9wdJUVmEZPGGhZzrYTPvgJArz7wNPgYKsk= +github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 h1:qIbj1fsPNlZgppZ+VLlY7N33q108Sa+fhmuc+sWQYwY= +gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/logadapter/slogadapter/logger.go b/logadapter/slogadapter/logger.go new file mode 100644 index 0000000..5545fe3 --- /dev/null +++ b/logadapter/slogadapter/logger.go @@ -0,0 +1,44 @@ +// Package slogadapter provides a log adapter for go standard lib's +// "log/slog" package - https://pkg.go.dev/log/slog +package slogadapter + +import ( + "context" + "log/slog" + + sqldblogger "github.com/simukti/sqldb-logger" +) + +type slogAdapter struct { + logger *slog.Logger +} + +// New creates a log adapter from sqldblogger.Logger to an slog.Logger one. +func New(logger *slog.Logger) sqldblogger.Logger { + return &slogAdapter{logger: logger} +} + +// Log implement sqldblogger.Logger and converts its levels to corresponding +// log/slog ones. +func (a *slogAdapter) Log(ctx context.Context, sqldbLevel sqldblogger.Level, msg string, data map[string]interface{}) { + + attrs := make([]slog.Attr, 0, len(data)) + for k, v := range data { + attrs = append(attrs, slog.Any(k, v)) + } + + var level slog.Level + switch sqldbLevel { + case sqldblogger.LevelError: + level = slog.LevelError + case sqldblogger.LevelInfo: + level = slog.LevelInfo + case sqldblogger.LevelDebug: + level = slog.LevelDebug + default: + // trace will use slog debug + level = slog.LevelDebug + } + + a.logger.LogAttrs(ctx, level, msg, attrs...) +} diff --git a/logadapter/slogadapter/logger_test.go b/logadapter/slogadapter/logger_test.go new file mode 100644 index 0000000..ac4c2ca --- /dev/null +++ b/logadapter/slogadapter/logger_test.go @@ -0,0 +1,114 @@ +package slogadapter + +import ( + "context" + "fmt" + "log/slog" + "testing" + "time" + + "github.com/stretchr/testify/assert" + + sqldblogger "github.com/simukti/sqldb-logger" +) + +// A TestHandler is an slog.Handler that simply records the latest record, +// which is used to verify the expected values provided by the sqldblogger.Logger. +type TestHandler struct { + latestRecord slog.Record +} + +func NewTestHandler() *TestHandler { + return &TestHandler{} +} + +// Enabled implements slog.Handler. +func (h *TestHandler) Enabled(_ context.Context, level slog.Level) bool { + // All levels are always enabled. + return true +} + +// Handle implements slog.Handler. +func (h *TestHandler) Handle(_ context.Context, r slog.Record) error { + // Simply store the latest record. We'll use it to verify expected + // values. + h.latestRecord = r + return nil +} + +// WithAttrs implements slog.Handler. +func (h *TestHandler) WithAttrs(attrs []slog.Attr) slog.Handler { + // Not needed by the adapter. + return h +} + +// WithGroup implements slog.Handler. +func (h *TestHandler) WithGroup(name string) slog.Handler { + // Not needed by the adapter. + return h +} + +// Handler implements slog.Handler. +func (h *TestHandler) Handler() slog.Handler { + // Not needed by the adapter. + return h +} + +func TestSlogAdapter_Log(t *testing.T) { + testHandler := NewTestHandler() + logger := New(slog.New(testHandler)) + + levelMap := map[sqldblogger.Level]slog.Level{ + sqldblogger.LevelError: slog.LevelError, + sqldblogger.LevelInfo: slog.LevelInfo, + sqldblogger.LevelDebug: slog.LevelDebug, + sqldblogger.LevelTrace: slog.LevelDebug, + sqldblogger.Level(99): slog.LevelDebug, // unknown + } + + now := time.Now() + const queryStr = "SELECT at.* FROM a_table AS at WHERE a.id = ? LIMIT 1" + + for sqldbLevel, slogLevel := range levelMap { + + data := map[string]interface{}{ + "time": now.Unix(), + "duration": time.Since(now).Nanoseconds(), + "query": queryStr, + "args": []interface{}{1}, + } + + if sqldbLevel == sqldblogger.LevelError { + data["error"] = fmt.Errorf("some error").Error() + } + + // Log the message with associated data + logger.Log(context.TODO(), sqldbLevel, "query msg", data) + + // Check expected values by inspecting the latest record + // stored in the test handler. + record := testHandler.latestRecord + + assert.Equal(t, "query msg", record.Message) + assert.Equal(t, slogLevel, record.Level) + assert.Equal(t, len(data), record.NumAttrs()) + + record.Attrs(func(a slog.Attr) bool { + switch a.Key { + case "time": + assert.Equal(t, now.Unix(), a.Value.Int64()) + case "query": + assert.Equal(t, queryStr, a.Value.String()) + case "duration": + assert.True(t, a.Value.Int64() > 0) + case "args": + assert.Equal(t, []interface{}{1}, a.Value.Any()) + case "error": + assert.Equal(t, sqldblogger.LevelError, sqldbLevel) + assert.Equal(t, "some error", a.Value.String()) + } + + return true + }) + } +}