You've already forked opentelemetry-go
							
							
				mirror of
				https://github.com/open-telemetry/opentelemetry-go.git
				synced 2025-10-31 00:07:40 +02:00 
			
		
		
		
	Support capturing stack trace (#2163)
* capturing stack trace support * added changelog entry * remove error package stack trace support * modified unnecessary changes to go.sum files * added EventOption to enable stack trace capturing * added tests * added runtime.Stack method and minor changes * minor changes * remove redundant line * fix gihub check on linter * fix tests * fix tests * Update sdk/trace/trace_test.go Co-authored-by: Anthony Mirabella <a9@aneurysm9.com> * Update sdk/trace/trace_test.go Co-authored-by: Anthony Mirabella <a9@aneurysm9.com> Co-authored-by: Anthony Mirabella <a9@aneurysm9.com>
This commit is contained in:
		| @@ -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. | ||||
|  | ||||
|   | ||||
| @@ -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) { | ||||
|   | ||||
| @@ -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") | ||||
|  | ||||
|   | ||||
| @@ -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 { | ||||
|   | ||||
| @@ -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" | ||||
|   | ||||
		Reference in New Issue
	
	Block a user