From a68c6a8d006f60d11ee37920e08c345060fad400 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Emir=20Ribi=C4=87?= Date: Mon, 23 Dec 2024 11:18:49 +0100 Subject: [PATCH 01/10] initial zap work --- zap/client.go | 66 +++++++++++ zap/core.go | 225 ++++++++++++++++++++++++++++++++++++++ zap/field.go | 29 +++++ zap/frame_matcher.go | 49 +++++++++ zap/frame_matcher_test.go | 93 ++++++++++++++++ zap/go.mod | 16 +++ zap/go.sum | 26 +++++ zap/helpers.go | 63 +++++++++++ zap/sentryzap.go | 95 ++++++++++++++++ 9 files changed, 662 insertions(+) create mode 100644 zap/client.go create mode 100644 zap/core.go create mode 100644 zap/field.go create mode 100644 zap/frame_matcher.go create mode 100644 zap/frame_matcher_test.go create mode 100644 zap/go.mod create mode 100644 zap/go.sum create mode 100644 zap/helpers.go create mode 100644 zap/sentryzap.go diff --git a/zap/client.go b/zap/client.go new file mode 100644 index 000000000..708cb02e8 --- /dev/null +++ b/zap/client.go @@ -0,0 +1,66 @@ +package sentryzap + +import ( + "time" + + "github.com/getsentry/sentry-go" + "go.uber.org/zap/zapcore" +) + +// Configuration is a minimal set of parameters for Sentry integration. +type Configuration struct { + // Tags are passed as is to the corresponding sentry.Event field. + Tags map[string]string + + // LoggerNameKey is the key for zap logger name. + // Leave LoggerNameKey empty to disable the feature. + LoggerNameKey string + + // DisableStacktrace disables adding stacktrace to sentry.Event, if set. + DisableStacktrace bool + + // Level is the minimal level of sentry.Event(s). + Level zapcore.LevelEnabler + + // EnableBreadcrumbs enables use of sentry.Breadcrumb(s). + // This feature works only when you explicitly passed new scope. + EnableBreadcrumbs bool + + // BreadcrumbLevel is the minimal level of sentry.Breadcrumb(s). + // Breadcrumb specifies an application event that occurred before a Sentry event. + // NewCore fails if BreadcrumbLevel is greater than Level. + // The field is ignored, if EnableBreadcrumbs is not set. + BreadcrumbLevel zapcore.LevelEnabler + + // MaxBreadcrumbs is the maximum number of breadcrumb events to keep. + // Leave it zero or set to negative for a reasonable default value. + // The field is ignored, if EnableBreadcrumbs is not set. + MaxBreadcrumbs int + + // FlushTimeout is the timeout for flushing events to Sentry. + FlushTimeout time.Duration + + // 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 +} + +func NewSentryClientFromDSN(DSN string) SentryClientFactory { + return func() (*sentry.Client, error) { + return sentry.NewClient(sentry.ClientOptions{ + Dsn: DSN, + }) + } +} + +func NewSentryClientFromClient(client *sentry.Client) SentryClientFactory { + return func() (*sentry.Client, error) { + return client, nil + } +} + +type SentryClientFactory func() (*sentry.Client, error) diff --git a/zap/core.go b/zap/core.go new file mode 100644 index 000000000..00ff7caac --- /dev/null +++ b/zap/core.go @@ -0,0 +1,225 @@ +package sentryzap + +import ( + "slices" + "time" + + "github.com/getsentry/sentry-go" + "go.uber.org/zap" + "go.uber.org/zap/zapcore" +) + +type core struct { + client *sentry.Client + cfg *Configuration + zapcore.LevelEnabler + flushTimeout time.Duration + + sentryScope *sentry.Scope + errs []error + fields map[string]any +} + +func (c *core) With(fs []zapcore.Field) zapcore.Core { + return c.with(fs) +} + +func (c *core) with(fs []zapcore.Field) *core { + fields := make(map[string]interface{}, len(c.fields)+len(fs)) + for k, v := range c.fields { + fields[k] = v + } + + errs := append([]error{}, c.errs...) + var sentryScope *sentry.Scope + + enc := zapcore.NewMapObjectEncoder() + for _, f := range fs { + f.AddTo(enc) + switch f.Type { + case zapcore.ErrorType: + errs = append(errs, f.Interface.(error)) + case zapcore.SkipType: + if scope := getScope(f); scope != nil { + sentryScope = scope + } + } + } + + for k, v := range enc.Fields { + fields[k] = v + } + + return &core{ + client: c.client, + cfg: c.cfg, + LevelEnabler: c.LevelEnabler, + flushTimeout: c.flushTimeout, + sentryScope: sentryScope, + errs: errs, + fields: fields, + } +} + +func (c *core) Check(ent zapcore.Entry, ce *zapcore.CheckedEntry) *zapcore.CheckedEntry { + if c.cfg.EnableBreadcrumbs && c.cfg.BreadcrumbLevel.Enabled(ent.Level) { + return ce.AddCore(ent, c) + } + if c.cfg.Level.Enabled(ent.Level) { + return ce.AddCore(ent, c) + } + return ce +} + +func (c *core) Write(ent zapcore.Entry, fs []zapcore.Field) error { + clone := c.with(c.addSpecialFields(ent, fs)) + + if c.shouldAddBreadcrumb(ent.Level) { + c.addBreadcrumb(ent, clone.fields) + } + + if c.shouldLogEvent(ent.Level) { + c.logEvent(ent, fs, clone) + } + + if ent.Level > zapcore.ErrorLevel { + return c.Sync() + } + return nil +} + +func (c *core) Sync() error { + c.client.Flush(c.flushTimeout) + return nil +} + +func (c *core) shouldAddBreadcrumb(level zapcore.Level) bool { + return c.cfg.EnableBreadcrumbs && c.cfg.BreadcrumbLevel.Enabled(level) +} + +func (c *core) shouldLogEvent(level zapcore.Level) bool { + return c.cfg.Level.Enabled(level) +} + +func (c *core) addBreadcrumb(ent zapcore.Entry, fields map[string]interface{}) { + breadcrumb := sentry.Breadcrumb{ + Message: ent.Message, + Data: fields, + Level: levelMap[ent.Level], + Timestamp: ent.Time, + } + c.scope().AddBreadcrumb(&breadcrumb, c.cfg.MaxBreadcrumbs) +} + +func (c *core) logEvent(ent zapcore.Entry, fs []zapcore.Field, clone *core) { + event := sentry.NewEvent() + event.Message = ent.Message + event.Timestamp = ent.Time + event.Level = levelMap[ent.Level] + event.Tags = c.collectTags(fs) + event.Extra = clone.fields + event.Exception = clone.createExceptions() + + if event.Exception == nil && !c.cfg.DisableStacktrace && c.client.Options().AttachStacktrace { + stacktrace := sentry.NewStacktrace() + if stacktrace != nil { + stacktrace.Frames = c.filterFrames(stacktrace.Frames) + event.Threads = []sentry.Thread{{Stacktrace: stacktrace, Current: true}} + } + } + + hint := c.getEventHint(fs) + c.client.CaptureEvent(event, hint, c.scope()) +} + +func (c *core) addSpecialFields(ent zapcore.Entry, fs []zapcore.Field) []zapcore.Field { + if c.cfg.LoggerNameKey != "" && ent.LoggerName != "" { + fs = append(fs, zap.String(c.cfg.LoggerNameKey, ent.LoggerName)) + } + return fs +} + +func (c *core) createExceptions() []sentry.Exception { + if len(c.errs) == 0 { + return nil + } + + processedErrors := make(map[string]struct{}) + exceptions := []sentry.Exception{} + + for i := len(c.errs) - 1; i >= 0; i-- { + exceptions = c.addExceptionsFromError(exceptions, processedErrors, c.errs[i]) + } + + slices.Reverse(exceptions) + return exceptions +} + +func (c *core) collectTags(fs []zapcore.Field) map[string]string { + tags := make(map[string]string, len(c.cfg.Tags)) + for k, v := range c.cfg.Tags { + tags[k] = v + } + for _, f := range fs { + if f.Type == zapcore.SkipType { + if tag, ok := f.Interface.(tagField); ok { + tags[tag.Key] = tag.Value + } + } + } + return tags +} + +func (c *core) addExceptionsFromError( + exceptions []sentry.Exception, + processedErrors map[string]struct{}, + err error, +) []sentry.Exception { + for i := 0; i < maxErrorDepth && err != nil; i++ { + key := getTypeOf(err) + if _, seen := processedErrors[key]; seen { + break + } + processedErrors[key] = struct{}{} + + exception := sentry.Exception{Value: err.Error(), Type: getTypeName(err)} + if !c.cfg.DisableStacktrace { + stacktrace := sentry.ExtractStacktrace(err) + if stacktrace != nil { + stacktrace.Frames = c.filterFrames(stacktrace.Frames) + exception.Stacktrace = stacktrace + } + } + exceptions = append(exceptions, exception) + + err = unwrapError(err) + } + return exceptions +} + +func (c *core) getEventHint(fs []zapcore.Field) *sentry.EventHint { + for _, f := range fs { + if f.Type == zapcore.SkipType { + if ctxField, ok := f.Interface.(ctxField); ok { + return &sentry.EventHint{Context: ctxField.Value} + } + } + } + return nil +} + +func (c *core) hub() *sentry.Hub { + if c.cfg.Hub != nil { + return c.cfg.Hub + } + + return sentry.CurrentHub() +} + +func (c *core) scope() *sentry.Scope { + if c.sentryScope != nil { + return c.sentryScope + } + + return c.hub().Scope() +} diff --git a/zap/field.go b/zap/field.go new file mode 100644 index 000000000..6b0f6d317 --- /dev/null +++ b/zap/field.go @@ -0,0 +1,29 @@ +package sentryzap + +import ( + "context" + + "go.uber.org/zap" + "go.uber.org/zap/zapcore" +) + +type tagField struct { + Key string + Value string +} + +func Tag(key string, value string) zap.Field { + return zap.Field{Key: key, Type: zapcore.SkipType, Interface: tagField{key, value}} +} + +type ctxField struct { + Value context.Context +} + +// Context adds a context to the logger. +// This can be used e.g. to pass trace information to sentry and allow linking events to their respective traces. +// +// See also https://docs.sentry.io/platforms/go/performance/instrumentation/opentelemetry/#linking-errors-to-transactions +func Context(ctx context.Context) zap.Field { + return zap.Field{Key: "context", Type: zapcore.SkipType, Interface: ctxField{ctx}} +} diff --git a/zap/frame_matcher.go b/zap/frame_matcher.go new file mode 100644 index 000000000..4ee36a544 --- /dev/null +++ b/zap/frame_matcher.go @@ -0,0 +1,49 @@ +package sentryzap + +import ( + "strings" + + "github.com/getsentry/sentry-go" +) + +type ( + FrameMatchers []FrameMatcher + FrameMatcherFunc func(f sentry.Frame) bool + SkipModulePrefixFrameMatcher string + SkipFunctionPrefixFrameMatcher string +) + +type FrameMatcher interface { + Matches(f sentry.Frame) bool +} + +var ( + defaultFrameMatchers = FrameMatchers{ + SkipModulePrefixFrameMatcher("go.uber.org/zap"), + } +) + +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) +} diff --git a/zap/frame_matcher_test.go b/zap/frame_matcher_test.go new file mode 100644 index 000000000..e7f470b92 --- /dev/null +++ b/zap/frame_matcher_test.go @@ -0,0 +1,93 @@ +package sentryzap + +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{ + { + Module: "go.uber.org/zap", + }, + }, + }, + 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/zap/go.mod b/zap/go.mod new file mode 100644 index 000000000..70b36d9c5 --- /dev/null +++ b/zap/go.mod @@ -0,0 +1,16 @@ +module github.com/getsentry/sentry-go/zap + +go 1.21 + +replace github.com/getsentry/sentry-go => ../ + +require ( + github.com/getsentry/sentry-go v0.0.0-00010101000000-000000000000 + go.uber.org/zap v1.27.0 +) + +require ( + go.uber.org/multierr v1.10.0 // indirect + golang.org/x/sys v0.18.0 // indirect + golang.org/x/text v0.14.0 // indirect +) diff --git a/zap/go.sum b/zap/go.sum new file mode 100644 index 000000000..1e9dff681 --- /dev/null +++ b/zap/go.sum @@ -0,0 +1,26 @@ +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/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= +github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= +github.com/pingcap/errors v0.11.4 h1:lFuQV/oaUMGcD2tqt+01ROSmJs75VG1ToEOkZIZ4nE4= +github.com/pingcap/errors v0.11.4/go.mod h1:Oi8TUi2kEtXXLMJk9l1cGmz20kV3TaQ0usTwv5KuLY8= +github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= +github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +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= +go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto= +go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE= +go.uber.org/multierr v1.10.0 h1:S0h4aNzvfcFsC3dRF1jLoaov7oRaKqRGC/pUEJ2yvPQ= +go.uber.org/multierr v1.10.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y= +go.uber.org/zap v1.27.0 h1:aJMhYGrd5QSmlpLMr2MftRKl7t8J8PTZPA732ud/XR8= +go.uber.org/zap v1.27.0/go.mod h1:GB2qFLM7cTU87MWRP2mPIjqfIDnGu+VIO4V/SdhGo2E= +golang.org/x/sys v0.18.0 h1:DBdB3niSjOA/O0blCZBqDefyWNYveAYMNF1Wum0DYQ4= +golang.org/x/sys v0.18.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/zap/helpers.go b/zap/helpers.go new file mode 100644 index 000000000..66a12af41 --- /dev/null +++ b/zap/helpers.go @@ -0,0 +1,63 @@ +package sentryzap + +import ( + "fmt" + "reflect" + "time" + + "github.com/getsentry/sentry-go" + "go.uber.org/zap/zapcore" +) + +func setDefaultConfig(cfg *Configuration) { + if cfg.MaxBreadcrumbs <= 0 { + cfg.MaxBreadcrumbs = defaultMaxBreadcrumbs + } + if cfg.FlushTimeout <= 0 { + cfg.FlushTimeout = 3 * time.Second + } + if cfg.FrameMatcher == nil { + cfg.FrameMatcher = defaultFrameMatchers + } +} + +func unwrapError(err error) error { + switch t := err.(type) { + case interface{ Unwrap() error }: + return t.Unwrap() + case interface{ Cause() error }: + return t.Cause() + default: + return nil + } +} + +func getTypeName(err error) string { + if t, ok := err.(interface{ TypeName() string }); ok { + return t.TypeName() + } + return reflect.TypeOf(err).String() +} + +func getTypeOf(err error) string { + return fmt.Sprintf("%s:%s", err.Error(), reflect.TypeOf(err).String()) +} + +func getScope(field zapcore.Field) *sentry.Scope { + if field.Type == zapcore.SkipType { + if scope, ok := field.Interface.(*sentry.Scope); ok && field.Key == zapSentryScopeKey { + return scope + } + } + return nil +} + +func (c *core) filterFrames(frames []sentry.Frame) []sentry.Frame { + filtered := frames[:0] + for _, frame := range frames { + if !c.cfg.FrameMatcher.Matches(frame) { + filtered = append(filtered, frame) + } + } + return filtered +} diff --git a/zap/sentryzap.go b/zap/sentryzap.go new file mode 100644 index 000000000..f694f090d --- /dev/null +++ b/zap/sentryzap.go @@ -0,0 +1,95 @@ +package sentryzap + +import ( + "errors" + "fmt" + "time" + + "github.com/getsentry/sentry-go" + "go.uber.org/zap" + "go.uber.org/zap/zapcore" +) + +const ( + defaultMaxBreadcrumbs = 100 + maxErrorDepth = 10 + zapSentryScopeKey = "_zapsentry_scope_" +) + +var ( + ErrInvalidBreadcrumbLevel = errors.New("breadcrumb level must be lower than or equal to error level") +) + +type ClientGetter interface { + GetClient() *sentry.Client +} + +type SentryClient interface { + CaptureEvent(event *sentry.Event, hint *sentry.EventHint, scope *sentry.Scope) string + Flush(timeout time.Duration) bool +} + +func NewScopeFromScope(scope *sentry.Scope) zapcore.Field { + return zapcore.Field{ + Key: zapSentryScopeKey, + Type: zapcore.SkipType, + Interface: scope, + } +} + +func NewScope() zapcore.Field { + return NewScopeFromScope(sentry.NewScope()) +} + +func NewCore(cfg Configuration, factory SentryClientFactory) (zapcore.Core, error) { + client, err := factory() + if err != nil { + return zapcore.NewNopCore(), fmt.Errorf("failed to create Sentry client: %w", err) + } + + setDefaultConfig(&cfg) + + if cfg.EnableBreadcrumbs && zapcore.LevelOf(cfg.BreadcrumbLevel) > zapcore.LevelOf(cfg.Level) { + return zapcore.NewNopCore(), fmt.Errorf("invalid configuration: %w", ErrInvalidBreadcrumbLevel) + } + + core := &core{ + client: client, + cfg: &cfg, + LevelEnabler: &LevelEnabler{ + LevelEnabler: cfg.Level, + breadcrumbsLevel: cfg.BreadcrumbLevel, + enableBreadcrumbs: cfg.EnableBreadcrumbs, + }, + flushTimeout: cfg.FlushTimeout, + fields: make(map[string]any), + } + + return core, nil +} + +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)) +} + +var levelMap = map[zapcore.Level]sentry.Level{ + zapcore.DebugLevel: sentry.LevelDebug, + zapcore.InfoLevel: sentry.LevelInfo, + zapcore.WarnLevel: sentry.LevelWarning, + zapcore.ErrorLevel: sentry.LevelError, + zapcore.DPanicLevel: sentry.LevelFatal, + zapcore.PanicLevel: sentry.LevelFatal, + zapcore.FatalLevel: sentry.LevelFatal, +} + +func AttachCoreToLogger(sentryCore zapcore.Core, l *zap.Logger) *zap.Logger { + return l.WithOptions(zap.WrapCore(func(core zapcore.Core) zapcore.Core { + return zapcore.NewTee(core, sentryCore) + })) +} From e3649ad34c03302e773d83ee08ffc6d4b70c4fd7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Emir=20Ribi=C4=87?= Date: Sat, 25 Jan 2025 20:16:51 +0100 Subject: [PATCH 02/10] add zap integration --- .craft.yml | 3 ++ zap/README.MD | 90 +++++++++++++++++++++++++++++++++++++++ zap/client.go | 66 ---------------------------- zap/frame_matcher_test.go | 17 +------- zap/go.mod | 4 ++ zap/go.sum | 16 +++++++ zap/helpers.go | 2 +- zap/sentryzap.go | 73 ++++++++++++++++++++++++++++++- zap/sentryzap_test.go | 87 +++++++++++++++++++++++++++++++++++++ 9 files changed, 273 insertions(+), 85 deletions(-) create mode 100644 zap/README.MD delete mode 100644 zap/client.go create mode 100644 zap/sentryzap_test.go diff --git a/.craft.yml b/.craft.yml index 5786bba22..865064701 100644 --- a/.craft.yml +++ b/.craft.yml @@ -35,6 +35,9 @@ targets: - name: github tagPrefix: zerolog/v tagOnly: true + - name: github + tagPrefix: zap/v + tagOnly: true - name: registry sdks: github:getsentry/sentry-go: diff --git a/zap/README.MD b/zap/README.MD new file mode 100644 index 000000000..718f975bb --- /dev/null +++ b/zap/README.MD @@ -0,0 +1,90 @@ +

+ + + +
+

+ +# Official Sentry Zap Core for Sentry-Go SDK + +**Go.dev Documentation:** [https://pkg.go.dev/github.com/getsentry/sentryzap](https://pkg.go.dev/github.com/getsentry/sentryzap) +**Example Usage:** [https://github.com/getsentry/sentry-go/tree/master/_examples/zap](https://github.com/getsentry/sentry-go/tree/master/_examples/zap) + +## Overview + +This package provides a core for the [Zap](https://github.com/uber-go/zap) logger, enabling seamless integration with [Sentry](https://sentry.io). With this core, logs at specific levels can be captured as Sentry events, while others can be added as breadcrumbs for enhanced context. + +## Installation + +```sh +go get github.com/getsentry/sentry-go/zap +``` + +## Usage + +```go +package main + +import ( + "time" + + "github.com/getsentry/sentry-go" + "github.com/getsentry/sentry-go/zap" + "go.uber.org/zap" + "go.uber.org/zap/zapcore" +) + +func main() { + // Initialize Sentry + client, err := sentry.NewClient(sentry.ClientOptions{ + Dsn: "your-public-dsn", + }) + if err != nil { + panic(err) + } + defer sentry.Flush(2 * time.Second) + + // Configure Sentry Zap Core + sentryCore, err := sentryzap.NewCore( + sentryzap.Configuration{ + Level: zapcore.ErrorLevel, + BreadcrumbLevel: zapcore.InfoLevel, + EnableBreadcrumbs: true, + FlushTimeout: 3 * time.Second, + }, + sentryzap.NewSentryClientFromClient(client), + ) + if err != nil { + panic(err) + } + + // Create a logger with Sentry Core + logger := sentryzap.AttachCoreToLogger(sentryCore, zap.NewExample()) + + // Example Logs + logger.Info("This is an info message") // Breadcrumb + logger.Error("This is an error message") // Captured as an event + logger.Fatal("This is a fatal message") // Captured as an event and flushes +} +``` + +## Configuration + +The `sentryzap.NewCore` function accepts a `sentryzap.Configuration` struct, which allows for the following configuration options: + +- Tags: A map of key-value pairs to add as tags to every event sent to Sentry. +- LoggerNameKey: Specifies the field key used to represent the logger name in the Sentry event. If empty, this feature is disabled. +- DisableStacktrace: If true, stack traces are not included in events sent to Sentry. +- Level: The minimum severity level for logs to be sent to Sentry. +- EnableBreadcrumbs: If true, logs below the event level are added as breadcrumbs to Sentry. +- BreadcrumbLevel: The minimum severity level for breadcrumbs to be recorded. +- Note: BreadcrumbLevel must be lower than or equal to Level. +- MaxBreadcrumbs: The maximum number of breadcrumbs to retain (default: 100). +- FlushTimeout: The maximum duration to wait for events to flush when calling Sync() (default: 3 seconds). +- Hub: Overrides the default Sentry hub. +- FrameMatcher: A function to filter stack trace frames. + +## Notes + +- Always call sentry.Flush to ensure all events are sent to Sentry before program termination. +- Use sentryzap.AttachCoreToLogger to attach the Sentry core to your existing logger seamlessly. \ No newline at end of file diff --git a/zap/client.go b/zap/client.go deleted file mode 100644 index 708cb02e8..000000000 --- a/zap/client.go +++ /dev/null @@ -1,66 +0,0 @@ -package sentryzap - -import ( - "time" - - "github.com/getsentry/sentry-go" - "go.uber.org/zap/zapcore" -) - -// Configuration is a minimal set of parameters for Sentry integration. -type Configuration struct { - // Tags are passed as is to the corresponding sentry.Event field. - Tags map[string]string - - // LoggerNameKey is the key for zap logger name. - // Leave LoggerNameKey empty to disable the feature. - LoggerNameKey string - - // DisableStacktrace disables adding stacktrace to sentry.Event, if set. - DisableStacktrace bool - - // Level is the minimal level of sentry.Event(s). - Level zapcore.LevelEnabler - - // EnableBreadcrumbs enables use of sentry.Breadcrumb(s). - // This feature works only when you explicitly passed new scope. - EnableBreadcrumbs bool - - // BreadcrumbLevel is the minimal level of sentry.Breadcrumb(s). - // Breadcrumb specifies an application event that occurred before a Sentry event. - // NewCore fails if BreadcrumbLevel is greater than Level. - // The field is ignored, if EnableBreadcrumbs is not set. - BreadcrumbLevel zapcore.LevelEnabler - - // MaxBreadcrumbs is the maximum number of breadcrumb events to keep. - // Leave it zero or set to negative for a reasonable default value. - // The field is ignored, if EnableBreadcrumbs is not set. - MaxBreadcrumbs int - - // FlushTimeout is the timeout for flushing events to Sentry. - FlushTimeout time.Duration - - // 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 -} - -func NewSentryClientFromDSN(DSN string) SentryClientFactory { - return func() (*sentry.Client, error) { - return sentry.NewClient(sentry.ClientOptions{ - Dsn: DSN, - }) - } -} - -func NewSentryClientFromClient(client *sentry.Client) SentryClientFactory { - return func() (*sentry.Client, error) { - return client, nil - } -} - -type SentryClientFactory func() (*sentry.Client, error) diff --git a/zap/frame_matcher_test.go b/zap/frame_matcher_test.go index e7f470b92..8e46719c2 100644 --- a/zap/frame_matcher_test.go +++ b/zap/frame_matcher_test.go @@ -24,27 +24,12 @@ func Test_core_filterFrames(t *testing.T) { args: args{ []sentry.Frame{ { - Module: "github.com/TheZeroSlave/zapsentry", + Module: "github.com/getsentry/sentry-go/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, diff --git a/zap/go.mod b/zap/go.mod index 70b36d9c5..b5d4cc9ae 100644 --- a/zap/go.mod +++ b/zap/go.mod @@ -6,11 +6,15 @@ replace github.com/getsentry/sentry-go => ../ require ( github.com/getsentry/sentry-go v0.0.0-00010101000000-000000000000 + github.com/stretchr/testify v1.8.2 go.uber.org/zap v1.27.0 ) require ( + github.com/davecgh/go-spew v1.1.1 // indirect + github.com/pmezard/go-difflib v1.0.0 // indirect go.uber.org/multierr v1.10.0 // indirect golang.org/x/sys v0.18.0 // indirect golang.org/x/text v0.14.0 // indirect + gopkg.in/yaml.v3 v3.0.1 // indirect ) diff --git a/zap/go.sum b/zap/go.sum index 1e9dff681..2bf1b70db 100644 --- a/zap/go.sum +++ b/zap/go.sum @@ -1,15 +1,27 @@ +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 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/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= github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= +github.com/kr/pretty v0.3.0 h1:WgNl7dwNpEZ6jJ9k1snq4pZsg7DOEN8hP9Xw0Tsjwk0= +github.com/kr/pretty v0.3.0/go.mod h1:640gp4NfQd8pI5XOwp5fnNeVWj67G7CFk/SaSQn7NBk= +github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= +github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= github.com/pingcap/errors v0.11.4 h1:lFuQV/oaUMGcD2tqt+01ROSmJs75VG1ToEOkZIZ4nE4= github.com/pingcap/errors v0.11.4/go.mod h1:Oi8TUi2kEtXXLMJk9l1cGmz20kV3TaQ0usTwv5KuLY8= github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/rogpeppe/go-internal v1.8.0 h1:FCbCCtXNOY3UtUuHUYaghJg4y7Fd14rXifAYUAtL9R8= +github.com/rogpeppe/go-internal v1.8.0/go.mod h1:WmiCO8CzOY8rg0OYDC4/i/2WRWAB6poM+XZ2dLUbcbE= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +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.2 h1:+h33VjcLVPDHtOdpUCuF+7gSuG3yGIftsP1YvFihtJ8= github.com/stretchr/testify v1.8.2/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto= @@ -22,5 +34,9 @@ golang.org/x/sys v0.18.0 h1:DBdB3niSjOA/O0blCZBqDefyWNYveAYMNF1Wum0DYQ4= golang.org/x/sys v0.18.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/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/zap/helpers.go b/zap/helpers.go index 66a12af41..00fc35940 100644 --- a/zap/helpers.go +++ b/zap/helpers.go @@ -45,7 +45,7 @@ func getTypeOf(err error) string { func getScope(field zapcore.Field) *sentry.Scope { if field.Type == zapcore.SkipType { - if scope, ok := field.Interface.(*sentry.Scope); ok && field.Key == zapSentryScopeKey { + if scope, ok := field.Interface.(*sentry.Scope); ok && field.Key == sentryZapScopeKey { return scope } } diff --git a/zap/sentryzap.go b/zap/sentryzap.go index f694f090d..2f3ebcf96 100644 --- a/zap/sentryzap.go +++ b/zap/sentryzap.go @@ -13,7 +13,7 @@ import ( const ( defaultMaxBreadcrumbs = 100 maxErrorDepth = 10 - zapSentryScopeKey = "_zapsentry_scope_" + sentryZapScopeKey = "_sentryzap_scope_" ) var ( @@ -24,23 +24,46 @@ type ClientGetter interface { GetClient() *sentry.Client } +// SentryClient is an interface that represents a Sentry client. type SentryClient interface { CaptureEvent(event *sentry.Event, hint *sentry.EventHint, scope *sentry.Scope) string Flush(timeout time.Duration) bool } +// NewSentryClientFromDSN creates a new Sentry client factory that uses the provided DSN. +func NewSentryClientFromDSN(dsn string) SentryClientFactory { + return func() (*sentry.Client, error) { + return sentry.NewClient(sentry.ClientOptions{ + Dsn: dsn, + }) + } +} + +// NewSentryClientFromClient creates a new Sentry client factory that returns the provided client. +func NewSentryClientFromClient(client *sentry.Client) SentryClientFactory { + return func() (*sentry.Client, error) { + return client, nil + } +} + +// SentryClientFactory is a function that creates a new Sentry client. +type SentryClientFactory func() (*sentry.Client, error) + +// NewScopeFromScope creates a new zapcore.Field that holds the provided Sentry scope. func NewScopeFromScope(scope *sentry.Scope) zapcore.Field { return zapcore.Field{ - Key: zapSentryScopeKey, + Key: sentryZapScopeKey, Type: zapcore.SkipType, Interface: scope, } } +// NewScope creates a new zapcore.Field that holds a new Sentry scope. func NewScope() zapcore.Field { return NewScopeFromScope(sentry.NewScope()) } +// NewCore creates a new zapcore.Core that sends logs to Sentry. func NewCore(cfg Configuration, factory SentryClientFactory) (zapcore.Core, error) { client, err := factory() if err != nil { @@ -68,12 +91,14 @@ func NewCore(cfg Configuration, factory SentryClientFactory) (zapcore.Core, erro return core, nil } +// LevelEnabler is a zapcore.LevelEnabler that also enables breadcrumbs. type LevelEnabler struct { zapcore.LevelEnabler enableBreadcrumbs bool breadcrumbsLevel zapcore.LevelEnabler } +// Enabled returns true if the given level is at or above the configured level. func (l *LevelEnabler) Enabled(lvl zapcore.Level) bool { return l.LevelEnabler.Enabled(lvl) || (l.enableBreadcrumbs && l.breadcrumbsLevel.Enabled(lvl)) } @@ -88,8 +113,52 @@ var levelMap = map[zapcore.Level]sentry.Level{ zapcore.FatalLevel: sentry.LevelFatal, } +// AttachCoreToLogger attaches the Sentry core to the provided logger. func AttachCoreToLogger(sentryCore zapcore.Core, l *zap.Logger) *zap.Logger { return l.WithOptions(zap.WrapCore(func(core zapcore.Core) zapcore.Core { return zapcore.NewTee(core, sentryCore) })) } + +// Configuration is a minimal set of parameters for Sentry integration. +type Configuration struct { + // Tags is a map of key-value pairs that will be added directly to the sentry.Event tags. + Tags map[string]string + + // LoggerNameKey specifies the key used to represent the zap logger name in the sentry.Event. + // If left empty, this feature is disabled. + LoggerNameKey string + + // DisableStacktrace, when set to true, prevents the addition of stack traces to sentry.Event instances. + DisableStacktrace bool + + // Level defines the minimum severity level required for events to be sent to Sentry. + Level zapcore.LevelEnabler + + // EnableBreadcrumbs enables the usage of sentry.Breadcrumb instances, which log application events + // leading up to a captured Sentry event. This feature requires an explicitly passed new scope. + EnableBreadcrumbs bool + + // BreadcrumbLevel defines the minimum severity level for breadcrumbs to be recorded. + // Breadcrumbs represent events that occurred prior to a Sentry event. + // This field is ignored if EnableBreadcrumbs is false. + // Note: NewCore will fail if BreadcrumbLevel is greater than Level. + BreadcrumbLevel zapcore.LevelEnabler + + // MaxBreadcrumbs specifies the maximum number of breadcrumb events to retain. + // Set to zero or a negative value to use the default limit. + // This field is ignored if EnableBreadcrumbs is false. + MaxBreadcrumbs int + + // FlushTimeout defines the maximum duration allowed for flushing events to Sentry. + FlushTimeout time.Duration + + // Hub overrides the default sentry.CurrentHub. + // For more information, see the sentry.Hub documentation. + Hub *sentry.Hub + + // FrameMatcher allows certain frames in stack traces to be ignored. + // This is particularly useful for excluding frames from utility or wrapper functions + // that do not provide meaningful context for error analysis. + FrameMatcher FrameMatcher +} diff --git a/zap/sentryzap_test.go b/zap/sentryzap_test.go new file mode 100644 index 000000000..4810a785a --- /dev/null +++ b/zap/sentryzap_test.go @@ -0,0 +1,87 @@ +package sentryzap_test + +import ( + "errors" + "testing" + "time" + + "github.com/getsentry/sentry-go" + sentryzap "github.com/getsentry/sentry-go/zap" + "go.uber.org/zap" + "go.uber.org/zap/zaptest/observer" +) + +func mockSentryClient(f func(event *sentry.Event)) *sentry.Client { + client, _ := sentry.NewClient(sentry.ClientOptions{ + Dsn: "", + Transport: &transport{MockSendEvent: f}, + }) + return client +} + +type transport struct { + MockSendEvent func(event *sentry.Event) +} + +// Flush waits until any buffered events are sent to the Sentry server, blocking +// for at most the given timeout. It returns false if the timeout was reached. +func (f *transport) Flush(_ time.Duration) bool { return true } + +// Configure is called by the Client itself, providing it it's own ClientOptions. +func (f *transport) Configure(_ sentry.ClientOptions) {} + +// SendEvent assembles a new packet out of Event and sends it to remote server. +// We use this method to capture the event for testing +func (f *transport) SendEvent(event *sentry.Event) { + f.MockSendEvent(event) +} + +func (f *transport) Close() {} + +func TestLevelEnabler(t *testing.T) { + lvl := zap.NewAtomicLevelAt(zap.PanicLevel) + core, recordedLogs := observer.New(lvl) + logger := zap.New(core) + + var recordedSentryEvent *sentry.Event + sentryClient := mockSentryClient(func(event *sentry.Event) { + recordedSentryEvent = event + }) + + core, err := sentryzap.NewCore( + sentryzap.Configuration{Level: lvl}, + sentryzap.NewSentryClientFromClient(sentryClient), + ) + if err != nil { + t.Fatal(err) + } + newLogger := sentryzap.AttachCoreToLogger(core, logger) + + newLogger.Error("foo") + if recordedLogs.Len() > 0 || recordedSentryEvent != nil { + t.Errorf("expected no logs before level change") + t.Logf("logs=%v", recordedLogs.All()) + t.Logf("events=%v", recordedSentryEvent) + } + + lvl.SetLevel(zap.ErrorLevel) + newLogger.Error("bar") + if recordedLogs.Len() != 1 || recordedSentryEvent == nil { + t.Errorf("expected exactly one log after level change") + t.Logf("logs=%v", recordedLogs.All()) + t.Logf("events=%v", recordedSentryEvent) + } +} + +func TestBreadcrumbLevelEnabler(t *testing.T) { + corelvl := zap.NewAtomicLevelAt(zap.ErrorLevel) + breadlvl := zap.NewAtomicLevelAt(zap.PanicLevel) + + _, err := sentryzap.NewCore( + sentryzap.Configuration{Level: corelvl, BreadcrumbLevel: breadlvl, EnableBreadcrumbs: true}, + sentryzap.NewSentryClientFromClient(mockSentryClient(func(event *sentry.Event) {})), + ) + if !errors.Is(err, sentryzap.ErrInvalidBreadcrumbLevel) { + t.Errorf("expected ErrInvalidBreadcrumbLevel, got %v", err) + } +} From ed92bed07ac98e16b20be330454ff70ff94bef64 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Emir=20Ribi=C4=87?= Date: Sat, 25 Jan 2025 20:17:12 +0100 Subject: [PATCH 03/10] add zap integration --- _examples/zap/main.go | 43 +++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 43 insertions(+) create mode 100644 _examples/zap/main.go diff --git a/_examples/zap/main.go b/_examples/zap/main.go new file mode 100644 index 000000000..10abbfa5f --- /dev/null +++ b/_examples/zap/main.go @@ -0,0 +1,43 @@ +package main + +import ( + "time" + + "github.com/getsentry/sentry-go" + sentryzap "github.com/getsentry/sentry-go/zap" + "go.uber.org/zap" + "go.uber.org/zap/zapcore" +) + +func main() { + // Initialize Sentry + client, err := sentry.NewClient(sentry.ClientOptions{ + Dsn: "your-public-dsn", + }) + if err != nil { + panic(err) + } + defer sentry.Flush(2 * time.Second) + + // Configure Sentry Zap Core + sentryCore, err := sentryzap.NewCore( + sentryzap.Configuration{ + Level: zapcore.ErrorLevel, + BreadcrumbLevel: zapcore.InfoLevel, + EnableBreadcrumbs: true, + FlushTimeout: 3 * time.Second, + }, + sentryzap.NewSentryClientFromClient(client), + ) + if err != nil { + panic(err) + } + + // Create a logger with Sentry Core + logger := sentryzap.AttachCoreToLogger(sentryCore, zap.NewExample()) + + // Example Logs + logger.Info("This is an info message") // Breadcrumb + logger.Error("This is an error message") // Captured as an event + logger.Fatal("This is a fatal message") // Captured as an event and flushes +} From 86fc8e8eee378e1132d0ed20d054abe9a7f73880 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Emir=20Ribi=C4=87?= Date: Sat, 25 Jan 2025 20:21:59 +0100 Subject: [PATCH 04/10] tidy go mod --- zap/go.mod | 4 ---- zap/go.sum | 16 ---------------- 2 files changed, 20 deletions(-) diff --git a/zap/go.mod b/zap/go.mod index b5d4cc9ae..70b36d9c5 100644 --- a/zap/go.mod +++ b/zap/go.mod @@ -6,15 +6,11 @@ replace github.com/getsentry/sentry-go => ../ require ( github.com/getsentry/sentry-go v0.0.0-00010101000000-000000000000 - github.com/stretchr/testify v1.8.2 go.uber.org/zap v1.27.0 ) require ( - github.com/davecgh/go-spew v1.1.1 // indirect - github.com/pmezard/go-difflib v1.0.0 // indirect go.uber.org/multierr v1.10.0 // indirect golang.org/x/sys v0.18.0 // indirect golang.org/x/text v0.14.0 // indirect - gopkg.in/yaml.v3 v3.0.1 // indirect ) diff --git a/zap/go.sum b/zap/go.sum index 2bf1b70db..1e9dff681 100644 --- a/zap/go.sum +++ b/zap/go.sum @@ -1,27 +1,15 @@ -github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 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/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= github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= -github.com/kr/pretty v0.3.0 h1:WgNl7dwNpEZ6jJ9k1snq4pZsg7DOEN8hP9Xw0Tsjwk0= -github.com/kr/pretty v0.3.0/go.mod h1:640gp4NfQd8pI5XOwp5fnNeVWj67G7CFk/SaSQn7NBk= -github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= -github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= github.com/pingcap/errors v0.11.4 h1:lFuQV/oaUMGcD2tqt+01ROSmJs75VG1ToEOkZIZ4nE4= github.com/pingcap/errors v0.11.4/go.mod h1:Oi8TUi2kEtXXLMJk9l1cGmz20kV3TaQ0usTwv5KuLY8= github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= -github.com/rogpeppe/go-internal v1.8.0 h1:FCbCCtXNOY3UtUuHUYaghJg4y7Fd14rXifAYUAtL9R8= -github.com/rogpeppe/go-internal v1.8.0/go.mod h1:WmiCO8CzOY8rg0OYDC4/i/2WRWAB6poM+XZ2dLUbcbE= -github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= -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.2 h1:+h33VjcLVPDHtOdpUCuF+7gSuG3yGIftsP1YvFihtJ8= github.com/stretchr/testify v1.8.2/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto= @@ -34,9 +22,5 @@ golang.org/x/sys v0.18.0 h1:DBdB3niSjOA/O0blCZBqDefyWNYveAYMNF1Wum0DYQ4= golang.org/x/sys v0.18.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/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= From a928aa783e7be76c012c52927935ecb766d9459c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Emir=20Ribi=C4=87?= Date: Sat, 25 Jan 2025 20:23:41 +0100 Subject: [PATCH 05/10] mention origin repo --- zap/sentryzap.go | 2 ++ 1 file changed, 2 insertions(+) diff --git a/zap/sentryzap.go b/zap/sentryzap.go index 2f3ebcf96..7606bb5be 100644 --- a/zap/sentryzap.go +++ b/zap/sentryzap.go @@ -10,6 +10,8 @@ import ( "go.uber.org/zap/zapcore" ) +// This implementation is largely adapted from https://github.com/TheZeroSlave/zapsentry + const ( defaultMaxBreadcrumbs = 100 maxErrorDepth = 10 From fc266312b52ab48cbf1a3fafced1d8734969f366 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Emir=20Ribi=C4=87?= Date: Sun, 26 Jan 2025 10:44:05 +0100 Subject: [PATCH 06/10] add tests for util package --- zap/go.mod | 4 ++++ zap/go.sum | 16 +++++++++++++ zap/helpers.go | 63 -------------------------------------------------- 3 files changed, 20 insertions(+), 63 deletions(-) delete mode 100644 zap/helpers.go diff --git a/zap/go.mod b/zap/go.mod index 70b36d9c5..b5d4cc9ae 100644 --- a/zap/go.mod +++ b/zap/go.mod @@ -6,11 +6,15 @@ replace github.com/getsentry/sentry-go => ../ require ( github.com/getsentry/sentry-go v0.0.0-00010101000000-000000000000 + github.com/stretchr/testify v1.8.2 go.uber.org/zap v1.27.0 ) require ( + github.com/davecgh/go-spew v1.1.1 // indirect + github.com/pmezard/go-difflib v1.0.0 // indirect go.uber.org/multierr v1.10.0 // indirect golang.org/x/sys v0.18.0 // indirect golang.org/x/text v0.14.0 // indirect + gopkg.in/yaml.v3 v3.0.1 // indirect ) diff --git a/zap/go.sum b/zap/go.sum index 1e9dff681..2bf1b70db 100644 --- a/zap/go.sum +++ b/zap/go.sum @@ -1,15 +1,27 @@ +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 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/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= github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= +github.com/kr/pretty v0.3.0 h1:WgNl7dwNpEZ6jJ9k1snq4pZsg7DOEN8hP9Xw0Tsjwk0= +github.com/kr/pretty v0.3.0/go.mod h1:640gp4NfQd8pI5XOwp5fnNeVWj67G7CFk/SaSQn7NBk= +github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= +github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= github.com/pingcap/errors v0.11.4 h1:lFuQV/oaUMGcD2tqt+01ROSmJs75VG1ToEOkZIZ4nE4= github.com/pingcap/errors v0.11.4/go.mod h1:Oi8TUi2kEtXXLMJk9l1cGmz20kV3TaQ0usTwv5KuLY8= github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/rogpeppe/go-internal v1.8.0 h1:FCbCCtXNOY3UtUuHUYaghJg4y7Fd14rXifAYUAtL9R8= +github.com/rogpeppe/go-internal v1.8.0/go.mod h1:WmiCO8CzOY8rg0OYDC4/i/2WRWAB6poM+XZ2dLUbcbE= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +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.2 h1:+h33VjcLVPDHtOdpUCuF+7gSuG3yGIftsP1YvFihtJ8= github.com/stretchr/testify v1.8.2/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto= @@ -22,5 +34,9 @@ golang.org/x/sys v0.18.0 h1:DBdB3niSjOA/O0blCZBqDefyWNYveAYMNF1Wum0DYQ4= golang.org/x/sys v0.18.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/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/zap/helpers.go b/zap/helpers.go deleted file mode 100644 index 00fc35940..000000000 --- a/zap/helpers.go +++ /dev/null @@ -1,63 +0,0 @@ -package sentryzap - -import ( - "fmt" - "reflect" - "time" - - "github.com/getsentry/sentry-go" - "go.uber.org/zap/zapcore" -) - -func setDefaultConfig(cfg *Configuration) { - if cfg.MaxBreadcrumbs <= 0 { - cfg.MaxBreadcrumbs = defaultMaxBreadcrumbs - } - if cfg.FlushTimeout <= 0 { - cfg.FlushTimeout = 3 * time.Second - } - if cfg.FrameMatcher == nil { - cfg.FrameMatcher = defaultFrameMatchers - } -} - -func unwrapError(err error) error { - switch t := err.(type) { - case interface{ Unwrap() error }: - return t.Unwrap() - case interface{ Cause() error }: - return t.Cause() - default: - return nil - } -} - -func getTypeName(err error) string { - if t, ok := err.(interface{ TypeName() string }); ok { - return t.TypeName() - } - return reflect.TypeOf(err).String() -} - -func getTypeOf(err error) string { - return fmt.Sprintf("%s:%s", err.Error(), reflect.TypeOf(err).String()) -} - -func getScope(field zapcore.Field) *sentry.Scope { - if field.Type == zapcore.SkipType { - if scope, ok := field.Interface.(*sentry.Scope); ok && field.Key == sentryZapScopeKey { - return scope - } - } - return nil -} - -func (c *core) filterFrames(frames []sentry.Frame) []sentry.Frame { - filtered := frames[:0] - for _, frame := range frames { - if !c.cfg.FrameMatcher.Matches(frame) { - filtered = append(filtered, frame) - } - } - return filtered -} From 8721fe989dcd3a914838ee7d97449f242f8be0f9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Emir=20Ribi=C4=87?= Date: Sun, 26 Jan 2025 10:44:13 +0100 Subject: [PATCH 07/10] add tests for util package --- zap/util.go | 63 +++++++++++++++ zap/util_test.go | 201 +++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 264 insertions(+) create mode 100644 zap/util.go create mode 100644 zap/util_test.go diff --git a/zap/util.go b/zap/util.go new file mode 100644 index 000000000..00fc35940 --- /dev/null +++ b/zap/util.go @@ -0,0 +1,63 @@ +package sentryzap + +import ( + "fmt" + "reflect" + "time" + + "github.com/getsentry/sentry-go" + "go.uber.org/zap/zapcore" +) + +func setDefaultConfig(cfg *Configuration) { + if cfg.MaxBreadcrumbs <= 0 { + cfg.MaxBreadcrumbs = defaultMaxBreadcrumbs + } + if cfg.FlushTimeout <= 0 { + cfg.FlushTimeout = 3 * time.Second + } + if cfg.FrameMatcher == nil { + cfg.FrameMatcher = defaultFrameMatchers + } +} + +func unwrapError(err error) error { + switch t := err.(type) { + case interface{ Unwrap() error }: + return t.Unwrap() + case interface{ Cause() error }: + return t.Cause() + default: + return nil + } +} + +func getTypeName(err error) string { + if t, ok := err.(interface{ TypeName() string }); ok { + return t.TypeName() + } + return reflect.TypeOf(err).String() +} + +func getTypeOf(err error) string { + return fmt.Sprintf("%s:%s", err.Error(), reflect.TypeOf(err).String()) +} + +func getScope(field zapcore.Field) *sentry.Scope { + if field.Type == zapcore.SkipType { + if scope, ok := field.Interface.(*sentry.Scope); ok && field.Key == sentryZapScopeKey { + return scope + } + } + return nil +} + +func (c *core) filterFrames(frames []sentry.Frame) []sentry.Frame { + filtered := frames[:0] + for _, frame := range frames { + if !c.cfg.FrameMatcher.Matches(frame) { + filtered = append(filtered, frame) + } + } + return filtered +} diff --git a/zap/util_test.go b/zap/util_test.go new file mode 100644 index 000000000..484bd76f1 --- /dev/null +++ b/zap/util_test.go @@ -0,0 +1,201 @@ +package sentryzap + +import ( + "errors" + "fmt" + "reflect" + "testing" + "time" + + "github.com/getsentry/sentry-go" + "github.com/stretchr/testify/assert" + "go.uber.org/zap/zapcore" +) + +type matcher struct { + matches func(sentry.Frame) bool +} + +func (m *matcher) Matches(s sentry.Frame) bool { + return m.matches(s) +} + +func TestSetDefaultConfig(t *testing.T) { + cfg := &Configuration{} + + // Test setting default values + setDefaultConfig(cfg) + assert.Equal(t, defaultMaxBreadcrumbs, cfg.MaxBreadcrumbs, "Expected MaxBreadcrumbs to be set to default") + assert.Equal(t, 3*time.Second, cfg.FlushTimeout, "Expected FlushTimeout to be set to default") + assert.NotNil(t, cfg.FrameMatcher, "Expected FrameMatcher to be set to default") + + // Test preserving non-default values + m := &matcher{} + cfg = &Configuration{ + MaxBreadcrumbs: 50, + FlushTimeout: 5 * time.Second, + FrameMatcher: m, + } + + setDefaultConfig(cfg) + + assert.Equal(t, 50, cfg.MaxBreadcrumbs, "Expected MaxBreadcrumbs to remain unchanged") + assert.Equal(t, 5*time.Second, cfg.FlushTimeout, "Expected FlushTimeout to remain unchanged") + assert.Equal(t, m, cfg.FrameMatcher, "Expected FrameMatcher to remain unchanged") + assert.Same(t, m, cfg.FrameMatcher, "Expected FrameMatcher to be the exact same instance") +} + +type customError struct { + message string +} + +func (e *customError) Error() string { + return e.message +} + +func (e *customError) Unwrap() error { + return errors.New("unwrapped error") +} + +// Test unwrapping an error with Cause method +type causeError struct{} + +func (causeError) Error() string { return "cause error" } +func (causeError) Cause() error { return errors.New("cause unwrapped error") } + +func TestUnwrapError(t *testing.T) { + // Test unwrapping an error with Unwrap method + err := &customError{message: "original error"} + unwrapped := unwrapError(err) + assert.EqualError(t, unwrapped, "unwrapped error", "Expected to unwrap to 'unwrapped error'") + + causeErr := causeError{} + + unwrapped = unwrapError(causeErr) + assert.EqualError(t, unwrapped, "cause unwrapped error", "Expected to unwrap to 'cause unwrapped error'") + + // Test with a regular error + newErr := errors.New("regular error") + unwrapped = unwrapError(newErr) + assert.Nil(t, unwrapped, "Expected no unwrapped error for regular error") +} + +type typeNameError struct{} + +func (typeNameError) Error() string { return "type name error" } +func (typeNameError) TypeName() string { + return "CustomTypeName" +} + +func TestGetTypeName(t *testing.T) { + // Test an error with TypeName method + err := typeNameError{} + typeName := getTypeName(err) + assert.Equal(t, "CustomTypeName", typeName, "Expected TypeName to return 'CustomTypeName'") + + // Test a regular error + newErr := errors.New("generic error") + typeName = getTypeName(newErr) + assert.Equal(t, "*errors.errorString", typeName, "Expected TypeName to return type string for generic error") +} + +func TestGetTypeOf(t *testing.T) { + err := errors.New("test error") + typeOf := getTypeOf(err) + expected := fmt.Sprintf("test error:%s", reflect.TypeOf(err).String()) + assert.Equal(t, expected, typeOf, "Expected getTypeOf to return 'error:reflect.Type'") +} + +func TestGetScope(t *testing.T) { + scope := sentry.NewScope() + field := zapcore.Field{ + Key: sentryZapScopeKey, + Type: zapcore.SkipType, + Interface: scope, + } + + // Test valid scope field + result := getScope(field) + assert.Equal(t, scope, result, "Expected getScope to return the provided scope") + + // Test field with incorrect key + field.Key = "wrong_key" + result = getScope(field) + assert.Nil(t, result, "Expected getScope to return nil for incorrect key") + + // Test field with incorrect type + field.Key = sentryZapScopeKey + field.Type = zapcore.StringType + result = getScope(field) + assert.Nil(t, result, "Expected getScope to return nil for incorrect type") + + // Test field with nil interface + field.Interface = nil + result = getScope(field) + assert.Nil(t, result, "Expected getScope to return nil for nil interface") +} + +func TestFilterFrames(t *testing.T) { + tests := map[string]struct { + frames []sentry.Frame + matcherFunc func(sentry.Frame) bool + expectedFrames []sentry.Frame + }{ + "No frames match": { + frames: []sentry.Frame{ + {Function: "func1", Module: "module1"}, + {Function: "func2", Module: "module2"}, + }, + matcherFunc: func(frame sentry.Frame) bool { + return false // No frames match the condition + }, + expectedFrames: []sentry.Frame{ + {Function: "func1", Module: "module1"}, + {Function: "func2", Module: "module2"}, + }, + }, + "All frames match": { + frames: []sentry.Frame{ + {Function: "func1", Module: "module1"}, + {Function: "func2", Module: "module2"}, + }, + matcherFunc: func(frame sentry.Frame) bool { + return true // All frames match the condition + }, + expectedFrames: []sentry.Frame{}, // All frames are filtered out + }, + "Some frames match": { + frames: []sentry.Frame{ + {Function: "func1", Module: "module1"}, + {Function: "func2", Module: "module2"}, + {Function: "func3", Module: "module3"}, + }, + matcherFunc: func(frame sentry.Frame) bool { + return frame.Function == "func2" // Only "func2" matches the condition + }, + expectedFrames: []sentry.Frame{ + {Function: "func1", Module: "module1"}, + {Function: "func3", Module: "module3"}, + }, + }, + } + + for name, tc := range tests { + t.Run(name, func(t *testing.T) { + // Create a mock matcher + mockMatcher := &matcher{ + matches: tc.matcherFunc, + } + + // Create a core instance with the mock matcher + cfg := &Configuration{FrameMatcher: mockMatcher} + core := &core{cfg: cfg} + + // Call filterFrames + filteredFrames := core.filterFrames(tc.frames) + + // Assert the result + assert.Equal(t, tc.expectedFrames, filteredFrames, "Filtered frames do not match expected frames") + }) + } +} From 6fc5447427414e2091679ec71713847185249cc3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Emir=20Ribi=C4=87?= Date: Sun, 26 Jan 2025 10:55:10 +0100 Subject: [PATCH 08/10] add tests for core --- zap/util_test.go | 12 ------------ 1 file changed, 12 deletions(-) diff --git a/zap/util_test.go b/zap/util_test.go index 484bd76f1..6573d789c 100644 --- a/zap/util_test.go +++ b/zap/util_test.go @@ -45,18 +45,6 @@ func TestSetDefaultConfig(t *testing.T) { assert.Same(t, m, cfg.FrameMatcher, "Expected FrameMatcher to be the exact same instance") } -type customError struct { - message string -} - -func (e *customError) Error() string { - return e.message -} - -func (e *customError) Unwrap() error { - return errors.New("unwrapped error") -} - // Test unwrapping an error with Cause method type causeError struct{} From 21b03232d13799df36d30fe2c85fc23cc4b671d4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Emir=20Ribi=C4=87?= Date: Sun, 26 Jan 2025 10:55:14 +0100 Subject: [PATCH 09/10] add tests for core --- zap/core_test.go | 125 +++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 125 insertions(+) create mode 100644 zap/core_test.go diff --git a/zap/core_test.go b/zap/core_test.go new file mode 100644 index 000000000..1770a221b --- /dev/null +++ b/zap/core_test.go @@ -0,0 +1,125 @@ +package sentryzap + +import ( + "errors" + "testing" + + "github.com/getsentry/sentry-go" + "github.com/stretchr/testify/assert" +) + +type customError struct { + message string +} + +func (e *customError) Error() string { + return e.message +} + +func (e *customError) Unwrap() error { + return errors.New("unwrapped error") +} + +type nestedError struct { + message string + cause error +} + +func (e *nestedError) Error() string { + return e.message +} + +func (e *nestedError) Unwrap() error { + return e.cause +} + +func TestAddExceptionsFromError(t *testing.T) { + tests := map[string]struct { + initialExceptions []sentry.Exception + initialProcessed map[string]struct{} + err error + disableStacktrace bool + expectedExceptions []sentry.Exception + expectedProcessed map[string]struct{} + }{ + "Single error with stacktrace": { + initialExceptions: []sentry.Exception{}, + initialProcessed: map[string]struct{}{}, + err: &customError{message: "test error"}, + disableStacktrace: false, + expectedExceptions: []sentry.Exception{ + {Value: "test error", Type: "*sentryzap.customError", Stacktrace: &sentry.Stacktrace{}}, + }, + expectedProcessed: map[string]struct{}{ + "test error:*sentryzap.customError": {}, + }, + }, + "Nested errors with stacktrace": { + initialExceptions: []sentry.Exception{}, + initialProcessed: map[string]struct{}{}, + err: &nestedError{ + message: "outer error", + cause: &customError{message: "inner error"}, + }, + disableStacktrace: false, + expectedExceptions: []sentry.Exception{ + {Value: "outer error", Type: "*sentryzap.nestedError", Stacktrace: &sentry.Stacktrace{}}, + {Value: "inner error", Type: "*sentryzap.customError", Stacktrace: &sentry.Stacktrace{}}, + }, + expectedProcessed: map[string]struct{}{ + "outer error:*sentryzap.nestedError": {}, + "inner error:*sentryzap.customError": {}, + }, + }, + "Duplicate error, skips processing": { + initialExceptions: []sentry.Exception{}, + initialProcessed: map[string]struct{}{ + "test error:*sentryzap.customError": {}, + }, + err: &customError{message: "test error"}, + disableStacktrace: false, + expectedExceptions: []sentry.Exception{}, + expectedProcessed: map[string]struct{}{ + "test error:*sentryzap.customError": {}, + }, + }, + "Error with disabled stacktrace": { + initialExceptions: []sentry.Exception{}, + initialProcessed: map[string]struct{}{}, + err: &customError{message: "test error"}, + disableStacktrace: true, + expectedExceptions: []sentry.Exception{ + {Value: "test error", Type: "*sentryzap.customError", Stacktrace: nil}, + }, + expectedProcessed: map[string]struct{}{ + "test error:*sentryzap.customError": {}, + }, + }, + } + + for name, tc := range tests { + t.Run(name, func(t *testing.T) { + // Mock configuration + cfg := &Configuration{DisableStacktrace: tc.disableStacktrace} + c := &core{cfg: cfg} + + // Call the method + result := c.addExceptionsFromError(tc.initialExceptions, tc.initialProcessed, tc.err) + + // Assert exceptions + assert.Equal(t, len(tc.expectedExceptions), len(result), "Unexpected number of exceptions") + for i, ex := range tc.expectedExceptions { + assert.Equal(t, ex.Value, result[i].Value, "Mismatch in exception Value") + assert.Equal(t, ex.Type, result[i].Type, "Mismatch in exception Type") + if !tc.disableStacktrace { + assert.NotNil(t, result[i].Stacktrace, "Expected Stacktrace to be set") + } else { + assert.Nil(t, result[i].Stacktrace, "Expected Stacktrace to be nil") + } + } + + // Assert processedErrors + assert.Equal(t, tc.expectedProcessed, tc.initialProcessed, "Mismatch in processed errors map") + }) + } +} From fede5d6a9f3cdbdcfa5a1c5a251a694988196307 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Emir=20Ribi=C4=87?= Date: Sun, 26 Jan 2025 11:40:13 +0100 Subject: [PATCH 10/10] fix tests --- zap/core_test.go | 50 ++++++++---------------------------------------- zap/util_test.go | 13 +++++-------- 2 files changed, 13 insertions(+), 50 deletions(-) diff --git a/zap/core_test.go b/zap/core_test.go index 1770a221b..935915c5f 100644 --- a/zap/core_test.go +++ b/zap/core_test.go @@ -1,7 +1,6 @@ package sentryzap import ( - "errors" "testing" "github.com/getsentry/sentry-go" @@ -16,10 +15,6 @@ func (e *customError) Error() string { return e.message } -func (e *customError) Unwrap() error { - return errors.New("unwrapped error") -} - type nestedError struct { message string cause error @@ -42,19 +37,19 @@ func TestAddExceptionsFromError(t *testing.T) { expectedExceptions []sentry.Exception expectedProcessed map[string]struct{} }{ - "Single error with stacktrace": { + "Single error": { initialExceptions: []sentry.Exception{}, initialProcessed: map[string]struct{}{}, err: &customError{message: "test error"}, disableStacktrace: false, expectedExceptions: []sentry.Exception{ - {Value: "test error", Type: "*sentryzap.customError", Stacktrace: &sentry.Stacktrace{}}, + {Value: "test error", Type: "*sentryzap.customError"}, }, expectedProcessed: map[string]struct{}{ "test error:*sentryzap.customError": {}, }, }, - "Nested errors with stacktrace": { + "Nested errors": { initialExceptions: []sentry.Exception{}, initialProcessed: map[string]struct{}{}, err: &nestedError{ @@ -63,62 +58,33 @@ func TestAddExceptionsFromError(t *testing.T) { }, disableStacktrace: false, expectedExceptions: []sentry.Exception{ - {Value: "outer error", Type: "*sentryzap.nestedError", Stacktrace: &sentry.Stacktrace{}}, - {Value: "inner error", Type: "*sentryzap.customError", Stacktrace: &sentry.Stacktrace{}}, + {Value: "outer error", Type: "*sentryzap.nestedError"}, + {Value: "inner error", Type: "*sentryzap.customError"}, }, expectedProcessed: map[string]struct{}{ "outer error:*sentryzap.nestedError": {}, "inner error:*sentryzap.customError": {}, }, }, - "Duplicate error, skips processing": { - initialExceptions: []sentry.Exception{}, - initialProcessed: map[string]struct{}{ - "test error:*sentryzap.customError": {}, - }, - err: &customError{message: "test error"}, - disableStacktrace: false, - expectedExceptions: []sentry.Exception{}, - expectedProcessed: map[string]struct{}{ - "test error:*sentryzap.customError": {}, - }, - }, - "Error with disabled stacktrace": { - initialExceptions: []sentry.Exception{}, - initialProcessed: map[string]struct{}{}, - err: &customError{message: "test error"}, - disableStacktrace: true, - expectedExceptions: []sentry.Exception{ - {Value: "test error", Type: "*sentryzap.customError", Stacktrace: nil}, - }, - expectedProcessed: map[string]struct{}{ - "test error:*sentryzap.customError": {}, - }, - }, } for name, tc := range tests { t.Run(name, func(t *testing.T) { // Mock configuration cfg := &Configuration{DisableStacktrace: tc.disableStacktrace} - c := &core{cfg: cfg} + core := &core{cfg: cfg} // Call the method - result := c.addExceptionsFromError(tc.initialExceptions, tc.initialProcessed, tc.err) + result := core.addExceptionsFromError(tc.initialExceptions, tc.initialProcessed, tc.err) // Assert exceptions assert.Equal(t, len(tc.expectedExceptions), len(result), "Unexpected number of exceptions") for i, ex := range tc.expectedExceptions { assert.Equal(t, ex.Value, result[i].Value, "Mismatch in exception Value") assert.Equal(t, ex.Type, result[i].Type, "Mismatch in exception Type") - if !tc.disableStacktrace { - assert.NotNil(t, result[i].Stacktrace, "Expected Stacktrace to be set") - } else { - assert.Nil(t, result[i].Stacktrace, "Expected Stacktrace to be nil") - } } - // Assert processedErrors + // Assert processed errors assert.Equal(t, tc.expectedProcessed, tc.initialProcessed, "Mismatch in processed errors map") }) } diff --git a/zap/util_test.go b/zap/util_test.go index 6573d789c..0e54f6290 100644 --- a/zap/util_test.go +++ b/zap/util_test.go @@ -45,19 +45,16 @@ func TestSetDefaultConfig(t *testing.T) { assert.Same(t, m, cfg.FrameMatcher, "Expected FrameMatcher to be the exact same instance") } -// Test unwrapping an error with Cause method -type causeError struct{} - -func (causeError) Error() string { return "cause error" } -func (causeError) Cause() error { return errors.New("cause unwrapped error") } - func TestUnwrapError(t *testing.T) { // Test unwrapping an error with Unwrap method err := &customError{message: "original error"} unwrapped := unwrapError(err) - assert.EqualError(t, unwrapped, "unwrapped error", "Expected to unwrap to 'unwrapped error'") + assert.Nil(t, unwrapped, "Expected no unwrapped error for customError without Unwrap") - causeErr := causeError{} + causeErr := &nestedError{ + message: "nested error", + cause: &customError{message: "cause unwrapped error"}, + } unwrapped = unwrapError(causeErr) assert.EqualError(t, unwrapped, "cause unwrapped error", "Expected to unwrap to 'cause unwrapped error'")