diff --git a/CHANGELOG.md b/CHANGELOG.md index 5b40e3a3904..e173fccdbe2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -18,6 +18,7 @@ This project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.htm - Add support for per-series start time tracking for cumulative metrics in `go.opentelemetry.io/otel/sdk/metric`. Set `OTEL_GO_X_PER_SERIES_START_TIMESTAMPS=true` to enable. (#8060) - Add `WithCardinalityLimitSelector` for metric reader for configuring cardinality limits specific to the instrument kind. (#7855) +- Add `WithStackTrace` option for TracerProvider to add stackTraces to all spans. (#8094) ### Changed diff --git a/sdk/trace/provider.go b/sdk/trace/provider.go index cd40d299d6c..4cc495a7776 100644 --- a/sdk/trace/provider.go +++ b/sdk/trace/provider.go @@ -22,6 +22,32 @@ import ( const defaultTracerName = "go.opentelemetry.io/otel/sdk/tracer" +// StackTraceMode configures how the TracerProvider adds stack traces to +// exception events for recorded errors and panics. +type StackTraceMode int + +const ( + // StackTraceModeDefault does not add stack traces unless the span or event + // uses trace.WithStackTrace(true). + StackTraceModeDefault StackTraceMode = iota + // StackTraceModeAlways adds stack traces for all recorded errors and panics. + StackTraceModeAlways + // StackTraceModeNever never adds stack traces, even when + // trace.WithStackTrace(true) is used. + StackTraceModeNever +) + +func (m StackTraceMode) String() string { + switch m { + case StackTraceModeAlways: + return "Always" + case StackTraceModeNever: + return "Never" + default: + return "Default" + } +} + // tracerProviderConfig. type tracerProviderConfig struct { // processors contains collection of SpanProcessors that are processing pipeline @@ -42,6 +68,9 @@ type tracerProviderConfig struct { // resource contains attributes representing an entity that produces telemetry. resource *resource.Resource + + // stackTraceMode configures stack trace capture for recorded errors and panics. + stackTraceMode StackTraceMode } // MarshalLog is the marshaling function used by the logging system to represent this Provider. @@ -52,12 +81,14 @@ func (cfg tracerProviderConfig) MarshalLog() any { IDGeneratorType string SpanLimits SpanLimits Resource *resource.Resource + StackTraceMode string }{ SpanProcessors: cfg.processors, SamplerType: fmt.Sprintf("%T", cfg.sampler), IDGeneratorType: fmt.Sprintf("%T", cfg.idGenerator), SpanLimits: cfg.spanLimits, Resource: cfg.resource, + StackTraceMode: cfg.stackTraceMode.String(), } } @@ -74,10 +105,11 @@ type TracerProvider struct { // These fields are not protected by the lock mu. They are assumed to be // immutable after creation of the TracerProvider. - sampler Sampler - idGenerator IDGenerator - spanLimits SpanLimits - resource *resource.Resource + sampler Sampler + idGenerator IDGenerator + spanLimits SpanLimits + resource *resource.Resource + stackTraceMode StackTraceMode } var _ trace.TracerProvider = &TracerProvider{} @@ -105,11 +137,12 @@ func NewTracerProvider(opts ...TracerProviderOption) *TracerProvider { o = ensureValidTracerProviderConfig(o) tp := &TracerProvider{ - namedTracer: make(map[instrumentation.Scope]*tracer), - sampler: o.sampler, - idGenerator: o.idGenerator, - spanLimits: o.spanLimits, - resource: o.resource, + namedTracer: make(map[instrumentation.Scope]*tracer), + sampler: o.sampler, + idGenerator: o.idGenerator, + spanLimits: o.spanLimits, + resource: o.resource, + stackTraceMode: o.stackTraceMode, } global.Info("TracerProvider created", "config", o) @@ -384,6 +417,36 @@ func WithIDGenerator(g IDGenerator) TracerProviderOption { }) } +// WithAlwaysStackTrace configures the TracerProvider to capture a stack trace +// for all recorded errors and panics. +func WithAlwaysStackTrace() TracerProviderOption { + return traceProviderOptionFunc(func(cfg tracerProviderConfig) tracerProviderConfig { + cfg.stackTraceMode = StackTraceModeAlways + return cfg + }) +} + +// WithNeverStackTrace configures the TracerProvider to never capture stack +// traces for recorded errors and panics, including when trace.WithStackTrace(true) +// is passed on a span or event. +func WithNeverStackTrace() TracerProviderOption { + return traceProviderOptionFunc(func(cfg tracerProviderConfig) tracerProviderConfig { + cfg.stackTraceMode = StackTraceModeNever + return cfg + }) +} + +func (p *TracerProvider) shouldRecordExceptionStackTrace(spanRequested bool) bool { + switch p.stackTraceMode { + case StackTraceModeNever: + return false + case StackTraceModeAlways: + return true + default: + return spanRequested + } +} + // WithSampler returns a TracerProviderOption that will configure the Sampler // s as a TracerProvider's Sampler. The configured Sampler is used by the // Tracers the TracerProvider creates to make their sampling decisions for the diff --git a/sdk/trace/span.go b/sdk/trace/span.go index 7d55ce1dc2e..5d41761348d 100644 --- a/sdk/trace/span.go +++ b/sdk/trace/span.go @@ -483,7 +483,7 @@ func (s *recordingSpan) End(options ...trace.SpanEndOption) { ), } - if config.StackTrace() { + if s.tracer.provider.shouldRecordExceptionStackTrace(config.StackTrace()) { opts = append(opts, trace.WithAttributes( semconv.ExceptionStacktrace(recordStackTrace()), )) @@ -558,7 +558,7 @@ func (s *recordingSpan) RecordError(err error, opts ...trace.EventOption) { )) c := trace.NewEventConfig(opts...) - if c.StackTrace() { + if s.tracer.provider.shouldRecordExceptionStackTrace(c.StackTrace()) { opts = append(opts, trace.WithAttributes( semconv.ExceptionStacktrace(recordStackTrace()), )) diff --git a/sdk/trace/trace_test.go b/sdk/trace/trace_test.go index a0af6172f85..55163c55f36 100644 --- a/sdk/trace/trace_test.go +++ b/sdk/trace/trace_test.go @@ -1286,64 +1286,113 @@ func TestRecordErrorWithStackTrace(t *testing.T) { typ := "go.opentelemetry.io/otel/sdk/trace.testError" msg := "test error" - te := NewTestExporter() - tp := NewTracerProvider(WithSyncer(te), WithResource(resource.Empty())) - span := startSpan(tp, "RecordError") - - errTime := time.Now() - span.RecordError(err, trace.WithTimestamp(errTime), trace.WithStackTrace(true)) - - got, err := endSpan(te, span) - if err != nil { - t.Fatal(err) - } - - want := &snapshot{ - spanContext: trace.NewSpanContext(trace.SpanContextConfig{ - TraceID: tid, - TraceFlags: 0x1, - }), - parent: sc.WithRemote(true), - name: "span0", - status: Status{Code: codes.Unset}, - spanKind: trace.SpanKindInternal, - events: []Event{ - { - Name: semconv.ExceptionEventName, - Time: errTime, - Attributes: []attribute.KeyValue{ - semconv.ExceptionType(typ), - semconv.ExceptionMessage(msg), - }, + tests := []struct { + name string + providerOpts []TracerProviderOption + recordOpts []trace.EventOption + wantStack bool + }{ + { + name: "span_option", + recordOpts: []trace.EventOption{trace.WithStackTrace(true)}, + wantStack: true, + }, + { + name: "provider_always", + providerOpts: []TracerProviderOption{WithAlwaysStackTrace()}, + wantStack: true, + }, + { + name: "provider_never_overrides_always", + providerOpts: []TracerProviderOption{ + WithAlwaysStackTrace(), + WithNeverStackTrace(), }, + wantStack: false, + }, + { + name: "provider_never_suppresses_span_option", + providerOpts: []TracerProviderOption{WithNeverStackTrace()}, + recordOpts: []trace.EventOption{trace.WithStackTrace(true)}, + wantStack: false, }, - instrumentationScope: instrumentation.Scope{Name: "RecordError"}, } - assert.Equal(t, want.spanContext, got.spanContext) - assert.Equal(t, want.parent, got.parent) - assert.Equal(t, want.name, got.name) - assert.Equal(t, want.status, got.status) - assert.Equal(t, want.spanKind, got.spanKind) - assert.Equal(t, got.events[0].Attributes[0].Value.AsString(), want.events[0].Attributes[0].Value.AsString()) - assert.Equal(t, got.events[0].Attributes[1].Value.AsString(), want.events[0].Attributes[1].Value.AsString()) - gotStackTraceFunctionName := strings.Split(got.events[0].Attributes[2].Value.AsString(), "\n") + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + te := NewTestExporter() + pOpts := append( + []TracerProviderOption{WithSyncer(te), WithResource(resource.Empty())}, + tt.providerOpts..., + ) + tp := NewTracerProvider(pOpts...) + span := startSpan(tp, "RecordError") + + errTime := time.Now() + recordOpts := append([]trace.EventOption{trace.WithTimestamp(errTime)}, tt.recordOpts...) + span.RecordError(err, recordOpts...) + + got, endErr := endSpan(te, span) + if endErr != nil { + t.Fatal(endErr) + } - assert.Truef( - t, - strings.HasPrefix(gotStackTraceFunctionName[1], "go.opentelemetry.io/otel/sdk/trace.recordStackTrace"), - "%q not prefixed with go.opentelemetry.io/otel/sdk/trace.recordStackTrace", - gotStackTraceFunctionName[1], - ) - assert.Truef( - t, - strings.HasPrefix( - gotStackTraceFunctionName[3], - "go.opentelemetry.io/otel/sdk/trace.(*recordingSpan).RecordError", - ), - "%q not prefixed with go.opentelemetry.io/otel/sdk/trace.(*recordingSpan).RecordError", - gotStackTraceFunctionName[3], - ) + want := &snapshot{ + spanContext: trace.NewSpanContext(trace.SpanContextConfig{ + TraceID: tid, + TraceFlags: 0x1, + }), + parent: sc.WithRemote(true), + name: "span0", + status: Status{Code: codes.Unset}, + spanKind: trace.SpanKindInternal, + events: []Event{ + { + Name: semconv.ExceptionEventName, + Time: errTime, + Attributes: []attribute.KeyValue{ + semconv.ExceptionType(typ), + semconv.ExceptionMessage(msg), + }, + }, + }, + instrumentationScope: instrumentation.Scope{Name: "RecordError"}, + } + + assert.Equal(t, want.spanContext, got.spanContext) + assert.Equal(t, want.parent, got.parent) + assert.Equal(t, want.name, got.name) + assert.Equal(t, want.status, got.status) + assert.Equal(t, want.spanKind, got.spanKind) + assert.Equal(t, typ, got.events[0].Attributes[0].Value.AsString()) + assert.Equal(t, msg, got.events[0].Attributes[1].Value.AsString()) + + if tt.wantStack { + require.Len(t, got.events[0].Attributes, 3) + gotStackTraceFunctionName := strings.Split(got.events[0].Attributes[2].Value.AsString(), "\n") + assert.Truef( + t, + strings.HasPrefix( + gotStackTraceFunctionName[1], + "go.opentelemetry.io/otel/sdk/trace.recordStackTrace", + ), + "%q not prefixed with go.opentelemetry.io/otel/sdk/trace.recordStackTrace", + gotStackTraceFunctionName[1], + ) + assert.Truef( + t, + strings.HasPrefix( + gotStackTraceFunctionName[3], + "go.opentelemetry.io/otel/sdk/trace.(*recordingSpan).RecordError", + ), + "%q not prefixed with go.opentelemetry.io/otel/sdk/trace.(*recordingSpan).RecordError", + gotStackTraceFunctionName[3], + ) + } else { + require.Len(t, got.events[0].Attributes, 2) + } + }) + } } func TestRecordErrorNil(t *testing.T) { @@ -1569,38 +1618,91 @@ func TestSpanCapturesPanic(t *testing.T) { } func TestSpanCapturesPanicWithStackTrace(t *testing.T) { - te := NewTestExporter() - tp := NewTracerProvider(WithSyncer(te), WithResource(resource.Empty())) - _, span := tp.Tracer("CatchPanic").Start( - t.Context(), - "span", - ) + tests := []struct { + name string + providerOpts []TracerProviderOption + endOpts []trace.SpanEndOption + wantStack bool + }{ + { + name: "span_option", + endOpts: []trace.SpanEndOption{trace.WithStackTrace(true)}, + wantStack: true, + }, + { + name: "provider_always", + providerOpts: []TracerProviderOption{WithAlwaysStackTrace()}, + wantStack: true, + }, + { + name: "provider_never_overrides_always", + providerOpts: []TracerProviderOption{ + WithAlwaysStackTrace(), + WithNeverStackTrace(), + }, + wantStack: false, + }, + { + name: "provider_never_suppresses_span_option", + providerOpts: []TracerProviderOption{WithNeverStackTrace()}, + endOpts: []trace.SpanEndOption{trace.WithStackTrace(true)}, + wantStack: false, + }, + } - f := func() { - defer span.End(trace.WithStackTrace(true)) - panic(errors.New("error message")) + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + te := NewTestExporter() + pOpts := append( + []TracerProviderOption{WithSyncer(te), WithResource(resource.Empty())}, + tt.providerOpts..., + ) + tp := NewTracerProvider(pOpts...) + _, span := tp.Tracer("CatchPanic").Start( + t.Context(), + "span", + ) + + f := func() { + defer span.End(tt.endOpts...) + panic(errors.New("error message")) + } + require.PanicsWithError(t, "error message", f) + spans := te.Spans() + require.Len(t, spans, 1) + require.Len(t, spans[0].Events(), 1) + assert.Equal(t, semconv.ExceptionEventName, spans[0].Events()[0].Name) + + if tt.wantStack { + assert.Equal(t, "*errors.errorString", spans[0].Events()[0].Attributes[0].Value.AsString()) + assert.Equal(t, "error message", spans[0].Events()[0].Attributes[1].Value.AsString()) + gotStackTraceFunctionName := strings.Split(spans[0].Events()[0].Attributes[2].Value.AsString(), "\n") + assert.Truef( + t, + strings.HasPrefix( + gotStackTraceFunctionName[1], + "go.opentelemetry.io/otel/sdk/trace.recordStackTrace", + ), + "%q not prefixed with go.opentelemetry.io/otel/sdk/trace.recordStackTrace", + gotStackTraceFunctionName[1], + ) + assert.Truef( + t, + strings.HasPrefix( + gotStackTraceFunctionName[3], + "go.opentelemetry.io/otel/sdk/trace.(*recordingSpan).End", + ), + "%q not prefixed with go.opentelemetry.io/otel/sdk/trace.(*recordingSpan).End", + gotStackTraceFunctionName[3], + ) + } else { + assert.Equal(t, []attribute.KeyValue{ + semconv.ExceptionType("*errors.errorString"), + semconv.ExceptionMessage("error message"), + }, spans[0].Events()[0].Attributes) + } + }) } - require.PanicsWithError(t, "error message", f) - spans := te.Spans() - require.Len(t, spans, 1) - require.Len(t, spans[0].Events(), 1) - assert.Equal(t, semconv.ExceptionEventName, spans[0].Events()[0].Name) - assert.Equal(t, "*errors.errorString", spans[0].Events()[0].Attributes[0].Value.AsString()) - assert.Equal(t, "error message", spans[0].Events()[0].Attributes[1].Value.AsString()) - - gotStackTraceFunctionName := strings.Split(spans[0].Events()[0].Attributes[2].Value.AsString(), "\n") - assert.Truef( - t, - strings.HasPrefix(gotStackTraceFunctionName[1], "go.opentelemetry.io/otel/sdk/trace.recordStackTrace"), - "%q not prefixed with go.opentelemetry.io/otel/sdk/trace.recordStackTrace", - gotStackTraceFunctionName[1], - ) - assert.Truef( - t, - strings.HasPrefix(gotStackTraceFunctionName[3], "go.opentelemetry.io/otel/sdk/trace.(*recordingSpan).End"), - "%q not prefixed with go.opentelemetry.io/otel/sdk/trace.(*recordingSpan).End", - gotStackTraceFunctionName[3], - ) } func TestReadOnlySpan(t *testing.T) {