Skip to content
Open
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
79 changes: 37 additions & 42 deletions core.go
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
package zapsentry

import (
"context"
"errors"
"maps"
"reflect"
"time"

Expand Down Expand Up @@ -76,6 +78,7 @@ func NewCore(cfg Configuration, factory SentryClientFactory) (zapcore.Core, erro
},
flushTimeout: flushTimeout,
fields: make(map[string]interface{}),
tags: maps.Clone(cfg.Tags),
}

return &core, nil
Expand Down Expand Up @@ -110,36 +113,17 @@ func (c *core) Write(ent zapcore.Entry, fs []zapcore.Field) error {
}

if c.cfg.Level.Enabled(ent.Level) {
tagsCount := len(c.cfg.Tags)
for _, f := range fs {
if f.Type == zapcore.SkipType {
if _, ok := f.Interface.(tagField); ok {
tagsCount++
}
}
}

var hint *sentry.EventHint
if clone.ctx != nil {
hint = &sentry.EventHint{Context: clone.ctx}
}

event := sentry.NewEvent()
event.Message = ent.Message
event.Timestamp = ent.Time
event.Level = sentrySeverity(ent.Level)
event.Contexts["Extra"] = clone.fields
event.Tags = make(map[string]string, tagsCount)
for k, v := range c.cfg.Tags {
event.Tags[k] = v
}
for _, f := range fs {
if f.Type == zapcore.SkipType {
switch t := f.Interface.(type) {
case tagField:
event.Tags[t.Key] = t.Value
case ctxField:
hint = &sentry.EventHint{Context: t.Value}
}
}
}
maps.Copy(event.Tags, clone.tags)
event.Exception = clone.createExceptions()

if event.Exception == nil && !c.cfg.DisableStacktrace && c.client.Options().AttachStacktrace {
Expand Down Expand Up @@ -266,16 +250,6 @@ func (c *core) scope() *sentry.Scope {
return c.hub().Scope()
}

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) Sync() error {
c.client.Flush(c.flushTimeout)

Expand All @@ -292,23 +266,40 @@ func (c *core) with(fs []zapcore.Field) *core {
copy(errs, c.errs)

fields := make(map[string]interface{}, len(c.fields)+len(fs))
maps.Copy(fields, c.fields)

for k, v := range c.fields {
fields[k] = v
}

// tags is shared with the parent until a tag field is actually added.
tags, tagsCloned := c.tags, false
ctx := c.ctx
sentryScope := c.sentryScope
enc := zapcore.NewMapObjectEncoder()

for _, f := range fs {
f.AddTo(enc)

if f.Type == zapcore.ErrorType {
switch f.Type {
case zapcore.ErrorType:
errs = append(errs, f.Interface.(error))
} else if errSlice, ok := f.Interface.([]error); ok {
errs = append(errs, errSlice...)
} else if scope := getScope(f); scope != nil {
sentryScope = scope
case zapcore.SkipType:
switch t := f.Interface.(type) {
case *sentry.Scope:
if f.Key == zapSentryScopeKey {
sentryScope = t
}
case tagField:
if !tagsCloned {
tags = make(map[string]string, len(c.tags)+1)
maps.Copy(tags, c.tags)
tagsCloned = true
}
tags[f.Key] = string(t)
case ctxField:
ctx = t.Value
}
default:
if errSlice, ok := f.Interface.([]error); ok {
errs = append(errs, errSlice...)
}
}
}

Expand All @@ -322,8 +313,10 @@ func (c *core) with(fs []zapcore.Field) *core {
LevelEnabler: c.LevelEnabler,
flushTimeout: c.flushTimeout,
sentryScope: sentryScope,
ctx: ctx,
errs: errs,
fields: fields,
tags: tags,
}
}

Expand All @@ -338,9 +331,11 @@ type core struct {
flushTimeout time.Duration

sentryScope *sentry.Scope
ctx context.Context

errs []error
fields map[string]interface{}
tags map[string]string
}

// follow same logic with sentry-go to filter unnecessary frames
Expand Down
75 changes: 75 additions & 0 deletions core_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
package zapsentry_test

import (
"context"
"testing"

"github.com/TheZeroSlave/zapsentry"
"github.com/getsentry/sentry-go"
"go.uber.org/zap"
"go.uber.org/zap/zapcore"
)

type captured struct {
events []*sentry.Event
hints []*sentry.EventHint
}

func newTestLogger(t *testing.T) (*zap.Logger, *captured) {
t.Helper()
c := &captured{}
client, err := sentry.NewClient(sentry.ClientOptions{
Transport: &transport{MockSendEvent: func(e *sentry.Event) { c.events = append(c.events, e) }},
BeforeSend: func(e *sentry.Event, hint *sentry.EventHint) *sentry.Event {
c.hints = append(c.hints, hint)
return e
},
})
if err != nil {
t.Fatal(err)
}
core, err := zapsentry.NewCore(
zapsentry.Configuration{Level: zapcore.ErrorLevel, DisableStacktrace: true, Tags: map[string]string{"from_cfg": "c"}},
zapsentry.NewSentryClientFromClient(client),
)
if err != nil {
t.Fatal(err)
}
return zap.New(core), c
}

func TestTagViaWith(t *testing.T) {
log, c := newTestLogger(t)

log.With(zapsentry.Tag("from_with", "a")).Error("boom", zapsentry.Tag("from_call", "b"))

if len(c.events) != 1 {
t.Fatalf("expected 1 event, got %d", len(c.events))
}
want := map[string]string{"from_cfg": "c", "from_with": "a", "from_call": "b"}
for k, v := range want {
if c.events[0].Tags[k] != v {
t.Errorf("tag %q: got %q, want %q (all: %v)", k, c.events[0].Tags[k], v, c.events[0].Tags)
}
}
}

func TestContextViaWith(t *testing.T) {
log, c := newTestLogger(t)

type ctxKey struct{}
ctx := context.WithValue(context.Background(), ctxKey{}, "marker")

log.With(zapsentry.Context(ctx)).Error("boom")

if len(c.hints) != 1 {
t.Fatalf("expected 1 hint, got %d", len(c.hints))
}
hint := c.hints[0]
if hint == nil || hint.Context == nil {
t.Fatalf("With() context missing from hint: %+v", hint)
}
if got := hint.Context.Value(ctxKey{}); got != "marker" {
t.Errorf("unexpected context value: %v", got)
}
}
12 changes: 7 additions & 5 deletions field.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,13 +7,12 @@ import (
"go.uber.org/zap/zapcore"
)

type tagField struct {
Key string
Value string
}
type tagField string

// Tag adds a Sentry tag to the event. Tags attached via [zap.Logger.With] are
// carried over to every event logged through the derived logger.
func Tag(key string, value string) zap.Field {
return zap.Field{Key: key, Type: zapcore.SkipType, Interface: tagField{key, value}}
return zap.Field{Key: key, Type: zapcore.SkipType, Interface: tagField(value)}
}

type ctxField struct {
Expand All @@ -23,6 +22,9 @@ type ctxField struct {
// 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.
//
// When attached via [zap.Logger.With], the context is retained for the lifetime of the
// derived logger, so prefer passing request-scoped contexts at the log call site.
//
// 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}}
Expand Down