diff --git a/CHANGELOG.md b/CHANGELOG.md index 25f6b723..03f73f5b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -13,6 +13,26 @@ Add ability to override `hub` in `context` for integrations that use custom context ([#931](https://github.com/getsentry/sentry-go/pull/931)) +- Add `HubProvider` Hook for `sentrylogrus`, enabling dynamic Sentry hub allocation for each log entry or goroutine. ([#936](https://github.com/getsentry/sentry-go/pull/936)) + +This change enhances compatibility with Sentry's recommendation of using separate hubs per goroutine. To ensure a separate Sentry hub for each goroutine, configure the `HubProvider` like this: + +```go +hook, err := sentrylogrus.New(nil, sentry.ClientOptions{}) +if err != nil { + log.Fatalf("Failed to initialize Sentry hook: %v", err) +} + +// Set a custom HubProvider to generate a new hub for each goroutine or log entry +hook.SetHubProvider(func() *sentry.Hub { + client, _ := sentry.NewClient(sentry.ClientOptions{}) + return sentry.NewHub(client, sentry.NewScope()) +}) + +logrus.AddHook(hook) +``` + + ## 0.30.0 The Sentry SDK team is happy to announce the immediate availability of Sentry Go SDK v0.30.0. diff --git a/logrus/logrusentry.go b/logrus/logrusentry.go index 94385243..e6739950 100644 --- a/logrus/logrusentry.go +++ b/logrus/logrusentry.go @@ -46,10 +46,10 @@ const ( // It is not safe to configure the hook while logging is happening. Please // perform all configuration before using it. type Hook struct { - hub *sentry.Hub - fallback FallbackFunc - keys map[string]string - levels []logrus.Level + hubProvider func() *sentry.Hub + fallback FallbackFunc + keys map[string]string + levels []logrus.Level } var _ logrus.Hook = &Hook{} @@ -70,17 +70,26 @@ func New(levels []logrus.Level, opts sentry.ClientOptions) (*Hook, error) { // NewFromClient initializes a new Logrus hook which sends logs to the provided // sentry client. func NewFromClient(levels []logrus.Level, client *sentry.Client) *Hook { - h := &Hook{ + defaultHub := sentry.NewHub(client, sentry.NewScope()) + return &Hook{ levels: levels, - hub: sentry.NewHub(client, sentry.NewScope()), - keys: make(map[string]string), + hubProvider: func() *sentry.Hub { + // Default to using the same hub if no specific provider is set + return defaultHub + }, + keys: make(map[string]string), } - return h +} + +// SetHubProvider sets a function to provide a hub for each log entry. +// This can be used to ensure separate hubs per goroutine if needed. +func (h *Hook) SetHubProvider(provider func() *sentry.Hub) { + h.hubProvider = provider } // AddTags adds tags to the hook's scope. func (h *Hook) AddTags(tags map[string]string) { - h.hub.Scope().SetTags(tags) + h.hubProvider().Scope().SetTags(tags) } // A FallbackFunc can be used to attempt to handle any errors in logging, before @@ -128,8 +137,9 @@ func (h *Hook) Levels() []logrus.Level { // Fire sends entry to Sentry. func (h *Hook) Fire(entry *logrus.Entry) error { + hub := h.hubProvider() // Use the hub provided by the HubProvider event := h.entryToEvent(entry) - if id := h.hub.CaptureEvent(event); id == nil { + if id := hub.CaptureEvent(event); id == nil { if h.fallback != nil { return h.fallback(entry) } @@ -160,34 +170,40 @@ func (h *Hook) entryToEvent(l *logrus.Entry) *sentry.Event { Timestamp: l.Time, Logger: name, } + key := h.key(FieldRequest) if req, ok := s.Extra[key].(*http.Request); ok { delete(s.Extra, key) s.Request = sentry.NewRequest(req) } + if err, ok := s.Extra[logrus.ErrorKey].(error); ok { delete(s.Extra, logrus.ErrorKey) s.SetException(err, -1) } + key = h.key(FieldUser) - if user, ok := s.Extra[key].(sentry.User); ok { + switch user := s.Extra[key].(type) { + case sentry.User: delete(s.Extra, key) s.User = user - } - if user, ok := s.Extra[key].(*sentry.User); ok { + case *sentry.User: delete(s.Extra, key) s.User = *user } + key = h.key(FieldTransaction) if txn, ok := s.Extra[key].(string); ok { delete(s.Extra, key) s.Transaction = txn } + key = h.key(FieldFingerprint) if fp, ok := s.Extra[key].([]string); ok { delete(s.Extra, key) s.Fingerprint = fp } + delete(s.Extra, FieldGoVersion) delete(s.Extra, FieldMaxProcs) return s @@ -197,5 +213,5 @@ func (h *Hook) entryToEvent(l *logrus.Entry) *sentry.Event { // blocking for at most the given timeout. It returns false if the timeout was // reached, in which case some events may not have been sent. func (h *Hook) Flush(timeout time.Duration) bool { - return h.hub.Client().Flush(timeout) + return h.hubProvider().Client().Flush(timeout) } diff --git a/logrus/logrusentry_test.go b/logrus/logrusentry_test.go index de68e16d..d44af1bc 100644 --- a/logrus/logrusentry_test.go +++ b/logrus/logrusentry_test.go @@ -8,9 +8,10 @@ import ( "testing" "time" + pkgerr "github.com/pkg/errors" + "github.com/google/go-cmp/cmp" "github.com/google/go-cmp/cmp/cmpopts" - pkgerr "github.com/pkg/errors" "github.com/sirupsen/logrus" "github.com/getsentry/sentry-go" @@ -33,15 +34,39 @@ func TestNew(t *testing.T) { if err != nil { t.Fatal(err) } - if id := h.hub.CaptureEvent(&sentry.Event{}); id == nil { + if id := h.hubProvider().CaptureEvent(&sentry.Event{}); id == nil { t.Error("CaptureEvent failed") } - if !h.Flush(testutils.FlushTimeout()) { + if !h.hubProvider().Client().Flush(testutils.FlushTimeout()) { t.Error("flush failed") } }) } +func TestSetHubProvider(t *testing.T) { + t.Parallel() + + h, err := New(nil, sentry.ClientOptions{}) + if err != nil { + t.Fatal(err) + } + + // Custom HubProvider to ensure separate hubs for each test + h.SetHubProvider(func() *sentry.Hub { + client, _ := sentry.NewClient(sentry.ClientOptions{}) + return sentry.NewHub(client, sentry.NewScope()) + }) + + entry := &logrus.Entry{Level: logrus.ErrorLevel} + if err := h.Fire(entry); err != nil { + t.Fatal(err) + } + + if !h.hubProvider().Client().Flush(testutils.FlushTimeout()) { + t.Error("flush failed") + } +} + func TestFire(t *testing.T) { t.Parallel() @@ -54,12 +79,13 @@ func TestFire(t *testing.T) { if err != nil { t.Fatal(err) } + err = hook.Fire(entry) if err != nil { t.Fatal(err) } - if !hook.Flush(testutils.FlushTimeout()) { + if !hook.hubProvider().Client().Flush(testutils.FlushTimeout()) { t.Error("flush failed") } } @@ -262,6 +288,12 @@ func Test_entryToEvent(t *testing.T) { t.Fatal(err) } + // Custom HubProvider for test environment + h.SetHubProvider(func() *sentry.Hub { + client, _ := sentry.NewClient(sentry.ClientOptions{}) + return sentry.NewHub(client, sentry.NewScope()) + }) + for name, tt := range tests { tt := tt t.Run(name, func(t *testing.T) {