diff --git a/CHANGELOG.md b/CHANGELOG.md index 9b62de10..a9843f97 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,7 @@ - fix: Scope values should not override Event values (#446) - feat: Extend User inteface by adding Data, Name and Segment (#483) +- feat: Make maximum amount of spans configurable (#460) ## 0.14.0 diff --git a/client.go b/client.go index d8898894..be8d2668 100644 --- a/client.go +++ b/client.go @@ -28,6 +28,11 @@ import ( // stack trace is often the most useful information. const maxErrorDepth = 10 +// defaultMaxSpans limits the default number of recorded spans per transaction. The limit is +// meant to bound memory usage and prevent too large transaction events that +// would be rejected by Sentry. +const defaultMaxSpans = 1000 + // hostname is the host name reported by the kernel. It is precomputed once to // avoid syscalls when capturing events. // @@ -175,6 +180,11 @@ type ClientOptions struct { // Maximum number of breadcrumbs // when MaxBreadcrumbs is negative then ignore breadcrumbs. MaxBreadcrumbs int + // Maximum number of spans. + // + // See https://develop.sentry.dev/sdk/envelopes/#size-limits for size limits + // applied during event ingestion. Events that exceed these limits might get dropped. + MaxSpans int // An optional pointer to http.Client that will be used with a default // HTTPTransport. Using your own client will make HTTPTransport, HTTPProxy, // HTTPSProxy and CaCerts options ignored. @@ -250,6 +260,10 @@ func NewClient(options ClientOptions) (*Client, error) { options.MaxErrorDepth = maxErrorDepth } + if options.MaxSpans == 0 { + options.MaxSpans = defaultMaxSpans + } + // SENTRYGODEBUG is a comma-separated list of key=value pairs (similar // to GODEBUG). It is not a supported feature: recognized debug options // may change any time. diff --git a/client_test.go b/client_test.go index 2388ecac..88a98824 100644 --- a/client_test.go +++ b/client_test.go @@ -520,3 +520,17 @@ func TestRecover(t *testing.T) { }) } } + +func TestCustomMaxSpansProperty(t *testing.T) { + client, _, _ := setupClientTest() + assertEqual(t, client.Options().MaxSpans, defaultMaxSpans) + + client.options.MaxSpans = 2000 + assertEqual(t, client.Options().MaxSpans, 2000) + + properClient, _ := NewClient(ClientOptions{ + MaxSpans: 3000, + }) + + assertEqual(t, properClient.Options().MaxSpans, 3000) +} diff --git a/span_recorder.go b/span_recorder.go index 9e611ca6..137aa233 100644 --- a/span_recorder.go +++ b/span_recorder.go @@ -4,11 +4,6 @@ import ( "sync" ) -// maxSpans limits the number of recorded spans per transaction. The limit is -// meant to bound memory usage and prevent too large transaction events that -// would be rejected by Sentry. -const maxSpans = 1000 - // A spanRecorder stores a span tree that makes up a transaction. Safe for // concurrent use. It is okay to add child spans from multiple goroutines. type spanRecorder struct { @@ -20,6 +15,10 @@ type spanRecorder struct { // record stores a span. The first stored span is assumed to be the root of a // span tree. func (r *spanRecorder) record(s *Span) { + maxSpans := defaultMaxSpans + if client := CurrentHub().Client(); client != nil { + maxSpans = client.Options().MaxSpans + } r.mu.Lock() defer r.mu.Unlock() if len(r.spans) >= maxSpans { diff --git a/span_recorder_test.go b/span_recorder_test.go new file mode 100644 index 00000000..70c46437 --- /dev/null +++ b/span_recorder_test.go @@ -0,0 +1,63 @@ +package sentry + +import ( + "bytes" + "context" + "fmt" + "io" + "testing" +) + +func Test_spanRecorder_record(t *testing.T) { + testRootSpan := StartSpan(context.Background(), "test", TransactionName("test transaction")) + + for _, tt := range []struct { + name string + maxSpans int + toRecordSpans int + expectOverflow bool + }{ + { + name: "record span without problems", + maxSpans: defaultMaxSpans, + toRecordSpans: 1, + expectOverflow: false, + }, + { + name: "record span with overflow", + maxSpans: 2, + toRecordSpans: 4, + expectOverflow: true, + }, + } { + t.Run(tt.name, func(t *testing.T) { + logBuffer := bytes.Buffer{} + Logger.SetOutput(&logBuffer) + defer Logger.SetOutput(io.Discard) + spanRecorder := spanRecorder{} + + currentHub.BindClient(&Client{ + options: ClientOptions{ + MaxSpans: tt.maxSpans, + }, + }) + // Unbind the client afterwards, to not affect other tests + defer currentHub.stackTop().SetClient(nil) + + for i := 0; i < tt.toRecordSpans; i++ { + child := testRootSpan.StartChild(fmt.Sprintf("test %d", i)) + spanRecorder.record(child) + } + + if tt.expectOverflow { + assertNotEqual(t, len(spanRecorder.spans), tt.toRecordSpans, "expected overflow") + } else { + assertEqual(t, len(spanRecorder.spans), tt.toRecordSpans, "expected no overflow") + } + // check if Logger was called for overflow messages + if bytes.Contains(logBuffer.Bytes(), []byte("Too many spans")) && !tt.expectOverflow { + t.Error("unexpected overflow log") + } + }) + } +}