Skip to content

Commit

Permalink
feat: add FrameMatcher config option (#23)
Browse files Browse the repository at this point in the history
This allows to remove any kind of frame from the stacktrace sent to Sentry e.g. if zap is used in combination with go-logr.
  • Loading branch information
prskr authored Jun 26, 2023
1 parent f656d73 commit dde0fb2
Show file tree
Hide file tree
Showing 4 changed files with 185 additions and 26 deletions.
4 changes: 4 additions & 0 deletions config.go
Original file line number Diff line number Diff line change
Expand Up @@ -45,4 +45,8 @@ type Configuration struct {
// Hub overrides the sentry.CurrentHub value.
// See sentry.Hub docs for more detail.
Hub *sentry.Hub

// FrameMatcher allows to ignore some frames of the stack trace.
// this is particularly useful when you want to ignore for instances frames from convenience wrappers
FrameMatcher FrameMatcher
}
99 changes: 78 additions & 21 deletions core.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,49 @@ const (
zapSentryScopeKey = "_zapsentry_scope_"
)

var ErrInvalidBreadcrumbLevel = errors.New("breadcrumb level must be lower than or equal to error level")
var (
ErrInvalidBreadcrumbLevel = errors.New("breadcrumb level must be lower than or equal to error level")

defaultFrameMatchers = FrameMatchers{
SkipModulePrefixFrameMatcher("github.com/TheZeroSlave/zapsentry"),
SkipFunctionPrefixFrameMatcher("go.uber.org/zap"),
}
)

type (
FrameMatcher interface {
Matches(f sentry.Frame) bool
}
FrameMatchers []FrameMatcher
FrameMatcherFunc func(f sentry.Frame) bool
SkipModulePrefixFrameMatcher string
SkipFunctionPrefixFrameMatcher string
)

func (f FrameMatcherFunc) Matches(frame sentry.Frame) bool {
return f(frame)
}

func (f SkipModulePrefixFrameMatcher) Matches(frame sentry.Frame) bool {
return strings.HasPrefix(frame.Module, string(f))
}

func (f SkipFunctionPrefixFrameMatcher) Matches(frame sentry.Frame) bool {
return strings.HasPrefix(frame.Function, string(f))
}

func (ff FrameMatchers) Matches(frame sentry.Frame) bool {
for i := range ff {
if ff[i].Matches(frame) {
return true
}
}
return false
}

func CombineFrameMatchers(matcher ...FrameMatcher) FrameMatcher {
return FrameMatchers(matcher)
}

func NewScopeFromScope(scope *sentry.Scope) zapcore.Field {
f := zap.Skip()
Expand Down Expand Up @@ -46,6 +88,20 @@ func NewCore(cfg Configuration, factory SentryClientFactory) (zapcore.Core, erro
cfg.MaxBreadcrumbs = defaultMaxBreadcrumbs
}

// copy default values to avoid accidental modification
matchers := make(FrameMatchers, len(defaultFrameMatchers), len(defaultFrameMatchers)+1)
copy(matchers, defaultFrameMatchers)

switch m := cfg.FrameMatcher.(type) {
case nil:
cfg.FrameMatcher = matchers
case FrameMatchers:
// in case the configured matcher was already a collection, append the default ones to avoid nested looping
cfg.FrameMatcher = append(matchers, m...)
default:
cfg.FrameMatcher = append(matchers, cfg.FrameMatcher)
}

core := core{
client: client,
cfg: &cfg,
Expand Down Expand Up @@ -125,7 +181,7 @@ func (c *core) Write(ent zapcore.Entry, fs []zapcore.Field) error {
if event.Exception == nil && !c.cfg.DisableStacktrace && c.client.Options().AttachStacktrace {
stacktrace := sentry.NewStacktrace()
if stacktrace != nil {
stacktrace.Frames = filterFrames(stacktrace.Frames)
stacktrace.Frames = c.filterFrames(stacktrace.Frames)
event.Threads = []sentry.Thread{{Stacktrace: stacktrace, Current: true}}
}
}
Expand Down Expand Up @@ -166,7 +222,7 @@ func (c *core) createExceptions() []sentry.Exception {
if !c.cfg.DisableStacktrace && exceptions[0].Stacktrace == nil {
stacktrace := sentry.NewStacktrace()
if stacktrace != nil {
stacktrace.Frames = filterFrames(stacktrace.Frames)
stacktrace.Frames = c.filterFrames(stacktrace.Frames)
exceptions[0].Stacktrace = stacktrace
}
}
Expand Down Expand Up @@ -314,33 +370,34 @@ type core struct {
fields map[string]interface{}
}

type LevelEnabler struct {
zapcore.LevelEnabler
enableBreadcrumbs bool
breadcrumbsLevel zapcore.LevelEnabler
}

func (l *LevelEnabler) Enabled(lvl zapcore.Level) bool {
return l.LevelEnabler.Enabled(lvl) || (l.enableBreadcrumbs && l.breadcrumbsLevel.Enabled(lvl))
}

// follow same logic with sentry-go to filter unnecessary frames
// ref:
// https://github.com/getsentry/sentry-go/blob/362a80dcc41f9ad11c8df556104db3efa27a419e/stacktrace.go#L256-L280
func filterFrames(frames []sentry.Frame) []sentry.Frame {
func (c *core) filterFrames(frames []sentry.Frame) []sentry.Frame {
if len(frames) == 0 {
return nil
}

for i := range frames {
// Skip zapsentry and zap internal frames, except for frames in _test packages (for
// testing).
if (strings.HasPrefix(frames[i].Module, "github.com/TheZeroSlave/zapsentry") ||
strings.HasPrefix(frames[i].Function, "go.uber.org/zap")) &&
!strings.HasSuffix(frames[i].Module, "_test") {
return frames[0:i]
for i := 0; i < len(frames); {
if c.cfg.FrameMatcher.Matches(frames[i]) {
if i < len(frames)-1 {
copy(frames[i:], frames[i+1:])
}
frames = frames[:len(frames)-1]
continue
}
i++
}

return frames
}

type LevelEnabler struct {
zapcore.LevelEnabler
enableBreadcrumbs bool
breadcrumbsLevel zapcore.LevelEnabler
}

func (l *LevelEnabler) Enabled(lvl zapcore.Level) bool {
return l.LevelEnabler.Enabled(lvl) || (l.enableBreadcrumbs && l.breadcrumbsLevel.Enabled(lvl))
}
93 changes: 93 additions & 0 deletions core_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
package zapsentry

import (
"strings"
"testing"

"github.com/getsentry/sentry-go"
)

func Test_core_filterFrames(t *testing.T) {
t.Parallel()
type args struct {
frames []sentry.Frame
}
tests := []struct {
name string
matcher FrameMatcher
args args
wantRemainingFrames int
}{
{
name: "Empty filter set - do not filter anything at all",
matcher: FrameMatchers{},
args: args{
[]sentry.Frame{
{
Module: "github.com/TheZeroSlave/zapsentry",
},
},
},
wantRemainingFrames: 1,
},
{
name: "Default filter set - filter frames from zapsentry",
matcher: defaultFrameMatchers,
args: args{
[]sentry.Frame{
{
Module: "github.com/TheZeroSlave/zapsentry",
},
{
Module: "github.com/TheZeroSlave/zapsentry/someinternal",
},
},
},
wantRemainingFrames: 0,
},
{
name: "Default filter set - filter frames from zap",
matcher: defaultFrameMatchers,
args: args{
[]sentry.Frame{
{
Function: "go.uber.org/zap/String",
},
},
},
wantRemainingFrames: 0,
},
{
name: "Custom filter - ignore if test file",
matcher: FrameMatcherFunc(func(f sentry.Frame) bool {
return strings.HasSuffix(f.Filename, "_test.go")
}),
args: args{
[]sentry.Frame{
{
Filename: "core_test.go",
},
{
Filename: "core.go",
},
},
},
wantRemainingFrames: 1,
},
}
for _, tt := range tests {
tt := tt
t.Run(tt.name, func(t *testing.T) {
t.Parallel()
c := &core{
cfg: &Configuration{
FrameMatcher: tt.matcher,
},
}
got := c.filterFrames(tt.args.frames)
if len(got) != tt.wantRemainingFrames {
t.Errorf("filterFrames() = %v, want %v", got, tt.wantRemainingFrames)
}
})
}
}
15 changes: 10 additions & 5 deletions example_logger_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -29,12 +29,17 @@ func ExampleAttachCoreToLogger() {

// Setup zapsentry
core, err := zapsentry.NewCore(zapsentry.Configuration{
Level: zapcore.ErrorLevel, // when to send message to sentry
EnableBreadcrumbs: true, // enable sending breadcrumbs to Sentry
BreadcrumbLevel: zapcore.InfoLevel, // at what level should we sent breadcrumbs to sentry
Level: zapcore.ErrorLevel, // when to send message to sentry
EnableBreadcrumbs: true, // enable sending breadcrumbs to Sentry
BreadcrumbLevel: zapcore.InfoLevel, // at what level should we sent breadcrumbs to sentry
Tags: map[string]string{
"component": "system",
},
FrameMatcher: zapsentry.CombineFrameMatchers(
// skip all frames having a prefix of 'go.uber.org/zap'
// this can be used to exclude e.g. logging adapters from the stacktrace
zapsentry.SkipFunctionPrefixFrameMatcher("go.uber.org/zap"),
),
}, zapsentry.NewSentryClientFromClient(sentryClient))
if err != nil {
log.Fatal(err)
Expand All @@ -59,8 +64,8 @@ func ExampleAttachCoreToLogger() {

func mockSentryClient(f func(event *sentry.Event)) *sentry.Client {
client, _ := sentry.NewClient(sentry.ClientOptions{
Dsn: "",
Transport: &transport{MockSendEvent: f},
Dsn: "",
Transport: &transport{MockSendEvent: f},
})
return client
}
Expand Down

0 comments on commit dde0fb2

Please sign in to comment.