diff --git a/go.mod b/go.mod index 1e337ec..3a6e2cb 100644 --- a/go.mod +++ b/go.mod @@ -1,10 +1,10 @@ module github.com/ihippik/slog-sentry -go 1.21.0 +go 1.21 -require github.com/getsentry/sentry-go v0.23.0 +require github.com/getsentry/sentry-go v0.26.0 require ( - golang.org/x/sys v0.6.0 // indirect - golang.org/x/text v0.8.0 // indirect + golang.org/x/sys v0.16.0 // indirect + golang.org/x/text v0.14.0 // indirect ) diff --git a/go.sum b/go.sum index d3b2fec..59707a0 100644 --- a/go.sum +++ b/go.sum @@ -1,7 +1,7 @@ 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/getsentry/sentry-go v0.23.0 h1:dn+QRCeJv4pPt9OjVXiMcGIBIefaTJPw/h0bZWO05nE= -github.com/getsentry/sentry-go v0.23.0/go.mod h1:lc76E2QywIyW8WuBnwl8Lc4bkmQH4+w1gwTf25trprY= +github.com/getsentry/sentry-go v0.26.0 h1:IX3++sF6/4B5JcevhdZfdKIHfyvMmAq/UnqcyT2H6mA= +github.com/getsentry/sentry-go v0.26.0/go.mod h1:lc76E2QywIyW8WuBnwl8Lc4bkmQH4+w1gwTf25trprY= github.com/go-errors/errors v1.4.2 h1:J6MZopCL4uSllY1OfXM374weqZFFItUbrImctkmUxIA= github.com/go-errors/errors v1.4.2/go.mod h1:sIVyrIiJhuEF+Pj9Ebtd6P/rEYROXFi3BopGUQ5a5Og= github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38= @@ -14,9 +14,9 @@ github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZb github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/stretchr/testify v1.8.2 h1:+h33VjcLVPDHtOdpUCuF+7gSuG3yGIftsP1YvFihtJ8= github.com/stretchr/testify v1.8.2/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= -golang.org/x/sys v0.6.0 h1:MVltZSvRTcU2ljQOhs94SXPftV6DCNnZViHeQps87pQ= -golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/text v0.8.0 h1:57P1ETyNKtuIjB4SRd15iJxuhj8Gc416Y78H3qgMh68= -golang.org/x/text v0.8.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= +golang.org/x/sys v0.16.0 h1:xWw16ngr6ZMtmxDyKyIgsE93KNKz5HKmMa3b8ALHidU= +golang.org/x/sys v0.16.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ= +golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= 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/handler.go b/handler.go index 1b038a8..9c934b3 100644 --- a/handler.go +++ b/handler.go @@ -2,12 +2,42 @@ package slogsentry import ( "context" + "fmt" "log/slog" "slices" "github.com/getsentry/sentry-go" ) +const ( + shortErrKey = "err" + longErrKey = "error" +) + +var slogDefaultKeys = []string{slog.TimeKey, slog.LevelKey, slog.SourceKey, slog.MessageKey, shortErrKey, longErrKey} + +// SlogEror contains both the slog msg and the actual error. +type SlogError struct { + msg string + err error +} + +// Error appends both the msg and err from the SlogError. +func (e SlogError) Error() string { + msg := e.msg + if e.err != nil { + if len(msg) > 0 { + msg += ": " + } + msg += e.err.Error() + } + return msg +} + +func (e SlogError) Unwrap() error { + return e.err +} + // SentryHandler is a Handler that writes log records to the Sentry. type SentryHandler struct { slog.Handler @@ -34,26 +64,38 @@ func (s *SentryHandler) Enabled(ctx context.Context, level slog.Level) bool { // Handle intercepts and processes logger messages. // In our case, send a message to the Sentry. func (s *SentryHandler) Handle(ctx context.Context, record slog.Record) error { - const ( - shortErrKey = "err" - longErrKey = "error" - ) - if slices.Contains(s.levels, record.Level) { - switch record.Level { - case slog.LevelError: - record.Attrs(func(attr slog.Attr) bool { - if attr.Key == shortErrKey || attr.Key == longErrKey { - if err, ok := attr.Value.Any().(error); ok { - sentry.CaptureException(err) - } - } - - return true - }) - case slog.LevelDebug, slog.LevelInfo, slog.LevelWarn: - sentry.CaptureMessage(record.Message) + hub := sentry.GetHubFromContext(ctx) + if hub == nil { + hub = sentry.CurrentHub() } + if hub == nil { + return fmt.Errorf("sentry: hub is nil") + } + var err error + slogContext := map[string]any{} + record.Attrs(func(attr slog.Attr) bool { + if !slices.Contains(slogDefaultKeys, attr.Key) { + slogContext[attr.Key] = attr.Value.String() + } else if attr.Key == shortErrKey || attr.Key == longErrKey { + err = attr.Value.Any().(error) + } + return true + }) + + hub.WithScope(func(scope *sentry.Scope) { + if len(slogContext) > 0 { + scope.SetContext("slog", slogContext) + } + + switch record.Level { + case slog.LevelError: + sentry.CaptureException(SlogError{msg: record.Message, err: err}) + case slog.LevelDebug, slog.LevelInfo, slog.LevelWarn: + sentry.CaptureMessage(record.Message) + + } + }) } return s.Handler.Handle(ctx, record) diff --git a/handler_test.go b/handler_test.go new file mode 100644 index 0000000..72b694b --- /dev/null +++ b/handler_test.go @@ -0,0 +1,24 @@ +package slogsentry + +import ( + "errors" + "testing" +) + +func TestSlogErrorErrorMethod(t *testing.T) { + tests := []struct { + input SlogError + expectOutput string + }{ + {SlogError{msg: "the message", err: errors.New("the error")}, "the message: the error"}, + {SlogError{err: errors.New("the error")}, "the error"}, + {SlogError{msg: "the message"}, "the message"}, + } + + for i, test := range tests { + output := test.input.Error() + if output != test.expectOutput { + t.Errorf("test %d: expect: %q, got: %q", i, test.expectOutput, output) + } + } +}