From a9a238fd72e77d0fb8628e6438feea056b118f31 Mon Sep 17 00:00:00 2001 From: Dmitry Kotik <7944694+dkotik@users.noreply.github.com> Date: Mon, 25 Sep 2023 00:19:01 +0300 Subject: [PATCH] Add SlogLoggerAdapter (#355) --- go.mod | 2 +- go.sum | 14 +++++++---- slog.go | 70 ++++++++++++++++++++++++++++++++++++++++++++++++++++ slog_test.go | 53 +++++++++++++++++++++++++++++++++++++++ 4 files changed, 133 insertions(+), 6 deletions(-) create mode 100644 slog.go create mode 100644 slog_test.go diff --git a/go.mod b/go.mod index 92ec90ac5..45d9f6fda 100644 --- a/go.mod +++ b/go.mod @@ -1,6 +1,6 @@ module github.com/ThreeDotsLabs/watermill -go 1.20 +go 1.21.0 require ( github.com/cenkalti/backoff/v3 v3.2.2 diff --git a/go.sum b/go.sum index d771962f1..cf6170c6f 100644 --- a/go.sum +++ b/go.sum @@ -19,6 +19,7 @@ github.com/golang/protobuf v1.5.2 h1:ROPKBNFfQgOUMifHyP+KYbvpjbdoFNs+aK7DXlji0Tw github.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY= github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38= +github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= github.com/google/uuid v1.2.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/google/uuid v1.3.0 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I= github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= @@ -54,12 +55,13 @@ github.com/prometheus/procfs v0.9.0 h1:wzCHvIvM5SxWqYvwgVL7yJY8Lz3PKn49KQtpgMYJf github.com/prometheus/procfs v0.9.0/go.mod h1:+pB4zwohETzFnmlpe6yd2lSc+0/46IYZRB/chUwxUZY= github.com/rogpeppe/go-internal v1.9.0 h1:73kH8U+JUqXU8lRuOHeVHaa/SZPifC7BkcraZVejAe8= github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs= -github.com/sony/gobreaker v0.5.0 h1:dRCvqm0P490vZPmy7ppEk2qCnCieBooFJ+YoXGYB+yg= -github.com/sony/gobreaker v0.5.0/go.mod h1:ZKptC7FHNvhBz7dN2LGjPVBz2sZJmc0/PkyDJOjmxWY= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= -github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= -github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk= -github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= +github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= +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= github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= @@ -96,5 +98,7 @@ google.golang.org/protobuf v1.28.1 h1:d0NfwRgPtno5B1Wa6L2DAG+KivqkdutMf1UhdNx175 google.golang.org/protobuf v1.28.1/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= +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/slog.go b/slog.go new file mode 100644 index 000000000..a3082ef67 --- /dev/null +++ b/slog.go @@ -0,0 +1,70 @@ +package watermill + +import ( + "log/slog" +) + +// LevelTrace must be added, because [slog] package does not have one by default. Generate it by subtracting 4 levels from [slog.Debug] following the example of [slog.LevelWarn] and [slog.LevelError] which are set to 4 and 8. +const LevelTrace = slog.LevelDebug - 4 + +func slogAttrsFromFields(fields LogFields) []any { + result := make([]any, 0, len(fields)*2) + + for key, value := range fields { + // result = append(result, slog.Any(key, value)) + result = append(result, key, value) + } + + return result +} + +// SlogLoggerAdapter wraps [slog.Logger]. +type SlogLoggerAdapter struct { + slog *slog.Logger +} + +// Error logs a message to [slog.LevelError]. +func (s *SlogLoggerAdapter) Error(msg string, err error, fields LogFields) { + s.slog.Error(msg, append(slogAttrsFromFields(fields), "error", err)...) +} + +// Info logs a message to [slog.LevelInfo]. +func (s *SlogLoggerAdapter) Info(msg string, fields LogFields) { + s.slog.Info(msg, slogAttrsFromFields(fields)...) +} + +// Debug logs a message to [slog.LevelDebug]. +func (s *SlogLoggerAdapter) Debug(msg string, fields LogFields) { + s.slog.Debug(msg, slogAttrsFromFields(fields)...) +} + +// Trace logs a message to [LevelTrace]. +func (s *SlogLoggerAdapter) Trace(msg string, fields LogFields) { + s.slog.Log( + // Void context, following the slog example + // as it treats context slighly differently from + // normal usage, minding contextual + // values, but ignoring contextual deadline. + // See the [slog] package documentation + // for more details. + nil, + LevelTrace, + msg, + slogAttrsFromFields(fields)..., + ) +} + +// With return a [SlogLoggerAdapter] with a set of fields injected into all consequent logging messages. +func (s *SlogLoggerAdapter) With(fields LogFields) LoggerAdapter { + return &SlogLoggerAdapter{slog: s.slog.With(slogAttrsFromFields(fields)...)} +} + +// NewSlogLogger creates an adapter to the standard library's structured logging package. A `nil` logger is substituted for the result of [slog.Default]. +func NewSlogLogger(logger *slog.Logger) LoggerAdapter { + if logger == nil { + logger = slog.Default() + } + return &SlogLoggerAdapter{ + slog: logger, + } +} diff --git a/slog_test.go b/slog_test.go new file mode 100644 index 000000000..7d27b469c --- /dev/null +++ b/slog_test.go @@ -0,0 +1,53 @@ +package watermill + +import ( + "bytes" + "errors" + "strings" + "testing" + + "log/slog" + + "github.com/stretchr/testify/assert" +) + +func TestSlogLoggerAdapter(t *testing.T) { + b := &bytes.Buffer{} + + logger := NewSlogLogger(slog.New(slog.NewTextHandler( + b, // output + &slog.HandlerOptions{ + Level: LevelTrace, + ReplaceAttr: func(groups []string, a slog.Attr) slog.Attr { + if a.Key == "time" && len(groups) == 0 { + // omit time stamp to make the test idempotent + a.Value = slog.StringValue("[omit]") + } + return a + }, + }, + ))) + + logger = logger.With(LogFields{ + "common1": "commonvalue", + }) + logger.Trace("test trace", LogFields{ + "field1": "value1", + }) + logger.Error("test error", errors.New("error message"), LogFields{ + "field2": "value2", + }) + logger.Info("test info", LogFields{ + "field3": "value3", + }) + + assert.Equal(t, + strings.TrimSpace(b.String()), + strings.TrimSpace(` +time=[omit] level=DEBUG-4 msg="test trace" common1=commonvalue field1=value1 +time=[omit] level=ERROR msg="test error" common1=commonvalue field2=value2 error="error message" +time=[omit] level=INFO msg="test info" common1=commonvalue field3=value3 + `), + "Logging output does not match saved template.", + ) +}