Skip to content

Add slog adapter #76

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 1 commit into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 14 additions & 0 deletions logadapter/slogadapter/README.md
Original file line number Diff line number Diff line change
@@ -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...
)
```
15 changes: 15 additions & 0 deletions logadapter/slogadapter/go.mod
Original file line number Diff line number Diff line change
@@ -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
)
28 changes: 28 additions & 0 deletions logadapter/slogadapter/go.sum
Original file line number Diff line number Diff line change
@@ -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=
44 changes: 44 additions & 0 deletions logadapter/slogadapter/logger.go
Original file line number Diff line number Diff line change
@@ -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...)
}
114 changes: 114 additions & 0 deletions logadapter/slogadapter/logger_test.go
Original file line number Diff line number Diff line change
@@ -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
})
}
}