From dde0fb2da94b62f628e132a58bec532362b0f1c9 Mon Sep 17 00:00:00 2001 From: Peter Date: Mon, 26 Jun 2023 10:19:00 +0200 Subject: [PATCH] feat: add FrameMatcher config option (#23) 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. --- config.go | 4 ++ core.go | 99 +++++++++++++++++++++++++++++++++--------- core_test.go | 93 +++++++++++++++++++++++++++++++++++++++ example_logger_test.go | 15 ++++--- 4 files changed, 185 insertions(+), 26 deletions(-) create mode 100644 core_test.go diff --git a/config.go b/config.go index c589f7e..0730170 100644 --- a/config.go +++ b/config.go @@ -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 } diff --git a/core.go b/core.go index 8aef10e..229c1de 100644 --- a/core.go +++ b/core.go @@ -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() @@ -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, @@ -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}} } } @@ -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 } } @@ -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)) +} diff --git a/core_test.go b/core_test.go new file mode 100644 index 0000000..9bac375 --- /dev/null +++ b/core_test.go @@ -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) + } + }) + } +} diff --git a/example_logger_test.go b/example_logger_test.go index 57094ed..842a362 100644 --- a/example_logger_test.go +++ b/example_logger_test.go @@ -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) @@ -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 }