Skip to content
Merged
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
33 changes: 21 additions & 12 deletions logging.go
Original file line number Diff line number Diff line change
Expand Up @@ -262,7 +262,7 @@ func getLogrusLogLevel(level string) logrus.Level {
return loglevel
}

func (s *sentryHook) Fire(entry *logrus.Entry) error {
func (s *sentryHook) buildEvent(entry *logrus.Entry) *sentry.Event {
var notifyErr error
var origErr error
switch err := entry.Data[logrus.ErrorKey].(type) {
Expand All @@ -284,17 +284,23 @@ func (s *sentryHook) Fire(entry *logrus.Entry) error {
event.Level = sentry.LevelFatal
}
event.Message = notifyErr.Error()
var stacktrace *sentry.Stacktrace
var errSt *errorWithStacktrace
if errors.As(notifyErr, &errSt) {
stacktrace = errSt.stacktrace
}

event.Exception = []sentry.Exception{{
Type: errorClass(origErr),
Value: notifyErr.Error(),
Stacktrace: stacktrace,
}}
// SetException walks the error chain, calls ExtractStacktrace on each
// link, and falls back to a fresh NewStacktrace() on the outermost
// exception when none is found — so events always carry frames.
event.SetException(notifyErr, -1)
if len(event.Exception) > 0 {
outermost := &event.Exception[len(event.Exception)-1]
// Group on the underlying error type, not the fmt.Errorf wrapper
// the hook itself introduced above.
outermost.Type = errorClass(origErr)
// Prefer a stacktrace captured at the error's creation site over
// the fallback captured here inside the hook.
var errSt *errorWithStacktrace
if errors.As(notifyErr, &errSt) && errSt.stacktrace != nil {
outermost.Stacktrace = errSt.stacktrace
}
}

extra := make(sentry.Context)
for key, val := range entry.Data {
Expand All @@ -305,8 +311,11 @@ func (s *sentryHook) Fire(entry *logrus.Entry) error {
if len(extra) > 0 {
event.Contexts["extra"] = extra
}
return event
}

sentry.CaptureEvent(event)
func (s *sentryHook) Fire(entry *logrus.Entry) error {
sentry.CaptureEvent(s.buildEvent(entry))

if entry.Level == logrus.FatalLevel || entry.Level == logrus.PanicLevel {
sentry.Flush(2 * time.Second)
Expand Down
92 changes: 92 additions & 0 deletions logging_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import (
"sync"
"testing"

"github.com/getsentry/sentry-go"
"github.com/nsqio/go-nsq"
"github.com/sirupsen/logrus"
"github.com/stretchr/testify/assert"
Expand Down Expand Up @@ -185,6 +186,97 @@ func TestSentryHookFire(t *testing.T) {
})
}

func TestSentryHookBuildEvent(t *testing.T) {
hook := &sentryHook{}

t.Run("plain error gets a fallback stacktrace", func(t *testing.T) {
event := hook.buildEvent(&logrus.Entry{
Level: logrus.ErrorLevel,
Message: "context",
Data: logrus.Fields{logrus.ErrorKey: fmt.Errorf("plain")},
})
assert.NotEmpty(t, event.Exception)
outer := event.Exception[len(event.Exception)-1]
assert.NotNil(t, outer.Stacktrace)
assert.NotEmpty(t, outer.Stacktrace.Frames)
})

t.Run("entry without error key still gets a stacktrace", func(t *testing.T) {
event := hook.buildEvent(&logrus.Entry{
Level: logrus.ErrorLevel,
Message: "no error attached",
Data: logrus.Fields{},
})
assert.NotEmpty(t, event.Exception)
outer := event.Exception[len(event.Exception)-1]
assert.NotNil(t, outer.Stacktrace)
assert.NotEmpty(t, outer.Stacktrace.Frames)
})

t.Run("ErrorWithStacktrace's captured frames win over fallback", func(t *testing.T) {
wrapped := ErrorWithStacktrace(fmt.Errorf("inner"))
var errSt *errorWithStacktrace
assert.True(t, errors.As(wrapped, &errSt))

event := hook.buildEvent(&logrus.Entry{
Level: logrus.ErrorLevel,
Message: "ctx",
Data: logrus.Fields{logrus.ErrorKey: wrapped},
})
outer := event.Exception[len(event.Exception)-1]
assert.Same(t, errSt.stacktrace, outer.Stacktrace)
})

t.Run("outermost exception type uses errorClass for grouping", func(t *testing.T) {
base := &customError{msg: "boom"}
event := hook.buildEvent(&logrus.Entry{
Level: logrus.ErrorLevel,
Message: "ctx",
Data: logrus.Fields{logrus.ErrorKey: base},
})
outer := event.Exception[len(event.Exception)-1]
assert.Equal(t, "*logging.customError", outer.Type)
})

t.Run("wrapped chain produces multiple exceptions", func(t *testing.T) {
base := &customError{msg: "boom"}
wrapped := fmt.Errorf("middle: %w", base)
event := hook.buildEvent(&logrus.Entry{
Level: logrus.ErrorLevel,
Message: "outer",
Data: logrus.Fields{logrus.ErrorKey: wrapped},
})
// hook's fmt.Errorf wrapping + middle wrap + base
assert.Len(t, event.Exception, 3)
assert.Equal(t, "*logging.customError", event.Exception[len(event.Exception)-1].Type)
})

t.Run("fatal level maps to sentry fatal", func(t *testing.T) {
event := hook.buildEvent(&logrus.Entry{
Level: logrus.FatalLevel,
Message: "dead",
Data: logrus.Fields{logrus.ErrorKey: fmt.Errorf("err")},
})
assert.Equal(t, sentry.LevelFatal, event.Level)
})

t.Run("non-error fields land in extra context", func(t *testing.T) {
event := hook.buildEvent(&logrus.Entry{
Level: logrus.ErrorLevel,
Message: "msg",
Data: logrus.Fields{
logrus.ErrorKey: fmt.Errorf("err"),
"user_id": 42,
},
})
extra, ok := event.Contexts["extra"]
assert.True(t, ok)
assert.Equal(t, 42, extra["user_id"])
_, hasErr := extra[logrus.ErrorKey]
assert.False(t, hasErr)
})
}

func TestSentryHookLevels(t *testing.T) {
hook := &sentryHook{}
levels := hook.Levels()
Expand Down