diff --git a/CHANGELOG.md b/CHANGELOG.md index d8b9979cb..492e0227e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,6 +11,7 @@ This project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.htm ### Added - Added `ErrorHandlerFunc` to use a function as an `"go.opentelemetry.io/otel".ErrorHandler`. (#2149) +- Added `"go.opentelemetry.io/otel/trace".WithStackTrace` option to add a stack trace when using `span.RecordError` or when panic is handled in `span.End`. (#2163) - Added typed slice attribute types and functionality to the `go.opentelemetry.io/otel/attribute` package to replace the existing array type and functions. (#2162) - `BoolSlice`, `IntSlice`, `Int64Slice`, `Float64Slice`, and `StringSlice` replace the use of the `Array` function in the package. diff --git a/sdk/trace/span.go b/sdk/trace/span.go index 5200fa59b..635044f0b 100644 --- a/sdk/trace/span.go +++ b/sdk/trace/span.go @@ -18,6 +18,7 @@ import ( "context" "fmt" "reflect" + "runtime" "sync" "time" @@ -235,24 +236,30 @@ func (s *span) End(options ...trace.SpanEndOption) { return } + config := trace.NewSpanEndConfig(options...) if recovered := recover(); recovered != nil { // Record but don't stop the panic. defer panic(recovered) - s.addEvent( - semconv.ExceptionEventName, + opts := []trace.EventOption{ trace.WithAttributes( semconv.ExceptionTypeKey.String(typeStr(recovered)), semconv.ExceptionMessageKey.String(fmt.Sprint(recovered)), ), - ) + } + + if config.StackTrace() { + opts = append(opts, trace.WithAttributes( + semconv.ExceptionStacktraceKey.String(recordStackTrace()), + )) + } + + s.addEvent(semconv.ExceptionEventName, opts...) } if s.executionTracerTaskEnd != nil { s.executionTracerTaskEnd() } - config := trace.NewSpanEndConfig(options...) - s.mu.Lock() // Setting endTime to non-zero marks the span as ended and not recording. if config.Timestamp().IsZero() { @@ -286,6 +293,14 @@ func (s *span) RecordError(err error, opts ...trace.EventOption) { semconv.ExceptionTypeKey.String(typeStr(err)), semconv.ExceptionMessageKey.String(err.Error()), )) + + c := trace.NewEventConfig(opts...) + if c.StackTrace() { + opts = append(opts, trace.WithAttributes( + semconv.ExceptionStacktraceKey.String(recordStackTrace()), + )) + } + s.addEvent(semconv.ExceptionEventName, opts...) } @@ -298,6 +313,13 @@ func typeStr(i interface{}) string { return fmt.Sprintf("%s.%s", t.PkgPath(), t.Name()) } +func recordStackTrace() string { + stackTrace := make([]byte, 2048) + n := runtime.Stack(stackTrace, false) + + return string(stackTrace[0:n]) +} + // AddEvent adds an event with the provided name and options. If this span is // not being recorded than this method does nothing. func (s *span) AddEvent(name string, o ...trace.EventOption) { diff --git a/sdk/trace/trace_test.go b/sdk/trace/trace_test.go index 01007f2ce..8dcc7b5f4 100644 --- a/sdk/trace/trace_test.go +++ b/sdk/trace/trace_test.go @@ -1154,6 +1154,58 @@ func TestRecordError(t *testing.T) { } } +func TestRecordErrorWithStackTrace(t *testing.T) { + err := ottest.NewTestError("test error") + typ := "go.opentelemetry.io/otel/internal/internaltest.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.ExceptionTypeKey.String(typ), + semconv.ExceptionMessageKey.String(msg), + }, + }, + }, + instrumentationLibrary: instrumentation.Library{Name: "RecordError"}, + } + + assert.Equal(t, got.spanContext, want.spanContext) + assert.Equal(t, got.parent, want.parent) + assert.Equal(t, got.name, want.name) + assert.Equal(t, got.status, want.status) + assert.Equal(t, got.spanKind, want.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") + + assert.True(t, strings.HasPrefix(gotStackTraceFunctionName[1], "go.opentelemetry.io/otel/sdk/trace.recordStackTrace")) + assert.True(t, strings.HasPrefix(gotStackTraceFunctionName[3], "go.opentelemetry.io/otel/sdk/trace.(*span).RecordError")) +} + func TestRecordErrorNil(t *testing.T) { te := NewTestExporter() tp := NewTracerProvider(WithSyncer(te), WithResource(resource.Empty())) @@ -1361,6 +1413,32 @@ func TestSpanCapturesPanic(t *testing.T) { }) } +func TestSpanCapturesPanicWithStackTrace(t *testing.T) { + te := NewTestExporter() + tp := NewTracerProvider(WithSyncer(te), WithResource(resource.Empty())) + _, span := tp.Tracer("CatchPanic").Start( + context.Background(), + "span", + ) + + f := func() { + defer span.End(trace.WithStackTrace(true)) + 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, spans[0].Events()[0].Name, semconv.ExceptionEventName) + assert.Equal(t, spans[0].Events()[0].Attributes[0].Value.AsString(), "*errors.errorString") + assert.Equal(t, spans[0].Events()[0].Attributes[1].Value.AsString(), "error message") + + gotStackTraceFunctionName := strings.Split(spans[0].Events()[0].Attributes[2].Value.AsString(), "\n") + fmt.Println(strings.Split(gotStackTraceFunctionName[1], "(0x")[0]) + assert.True(t, strings.HasPrefix(gotStackTraceFunctionName[1], "go.opentelemetry.io/otel/sdk/trace.recordStackTrace")) + assert.True(t, strings.HasPrefix(gotStackTraceFunctionName[3], "go.opentelemetry.io/otel/sdk/trace.(*span).End")) +} + func TestReadOnlySpan(t *testing.T) { kv := attribute.String("foo", "bar") diff --git a/trace/config.go b/trace/config.go index 87fb48393..3bfb2fff6 100644 --- a/trace/config.go +++ b/trace/config.go @@ -64,6 +64,7 @@ type SpanConfig struct { links []Link newRoot bool spanKind SpanKind + stackTrace bool } // Attributes describe the associated qualities of a Span. @@ -76,6 +77,11 @@ func (cfg *SpanConfig) Timestamp() time.Time { return cfg.timestamp } +// StackTrace checks whether stack trace capturing is enabled. +func (cfg *SpanConfig) StackTrace() bool { + return cfg.stackTrace +} + // Links are the associations a Span has with other Spans. func (cfg *SpanConfig) Links() []Link { return cfg.links @@ -139,6 +145,7 @@ type SpanEndOption interface { type EventConfig struct { attributes []attribute.KeyValue timestamp time.Time + stackTrace bool } // Attributes describe the associated qualities of an Event. @@ -151,6 +158,11 @@ func (cfg *EventConfig) Timestamp() time.Time { return cfg.timestamp } +// StackTrace checks whether stack trace capturing is enabled. +func (cfg *EventConfig) StackTrace() bool { + return cfg.stackTrace +} + // NewEventConfig applies all the EventOptions to a returned EventConfig. If no // timestamp option is passed, the returned EventConfig will have a Timestamp // set to the call time, otherwise no validation is performed on the returned @@ -183,6 +195,12 @@ type SpanStartEventOption interface { EventOption } +// SpanEndEventOption are options that can be used at the end of a span, or with an event. +type SpanEndEventOption interface { + SpanEndOption + EventOption +} + type attributeOption []attribute.KeyValue func (o attributeOption) applySpan(c *SpanConfig) { @@ -229,6 +247,17 @@ func WithTimestamp(t time.Time) SpanEventOption { return timestampOption(t) } +type stackTraceOption bool + +func (o stackTraceOption) applyEvent(c *EventConfig) { c.stackTrace = bool(o) } +func (o stackTraceOption) applySpan(c *SpanConfig) { c.stackTrace = bool(o) } +func (o stackTraceOption) applySpanEnd(c *SpanConfig) { o.applySpan(c) } + +// WithStackTrace sets the flag to capture the error with stack trace (e.g. true, false). +func WithStackTrace(b bool) SpanEndEventOption { + return stackTraceOption(b) +} + // WithLinks adds links to a Span. The links are added to the existing Span // links, i.e. this does not overwrite. func WithLinks(links ...Link) SpanStartOption { diff --git a/trace/config_test.go b/trace/config_test.go index 3b8c9b18e..2b4ab965e 100644 --- a/trace/config_test.go +++ b/trace/config_test.go @@ -174,6 +174,39 @@ func TestNewSpanConfig(t *testing.T) { } } +func TestEndSpanConfig(t *testing.T) { + timestamp := time.Unix(0, 0) + + tests := []struct { + options []SpanEndOption + expected *SpanConfig + }{ + { + []SpanEndOption{}, + new(SpanConfig), + }, + { + []SpanEndOption{ + WithStackTrace(true), + }, + &SpanConfig{ + stackTrace: true, + }, + }, + { + []SpanEndOption{ + WithTimestamp(timestamp), + }, + &SpanConfig{ + timestamp: timestamp, + }, + }, + } + for _, test := range tests { + assert.Equal(t, test.expected, NewSpanEndConfig(test.options...)) + } +} + func TestTracerConfig(t *testing.T) { v1 := "semver:0.0.1" v2 := "semver:1.0.0"