1
0
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:
Bhautik Pipaliya
2021-08-13 14:44:18 -07:00
committed by GitHub
parent 41588fea26
commit dfc866bd03
5 changed files with 168 additions and 5 deletions

View File

@@ -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.

View File

@@ -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) {

View File

@@ -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")

View File

@@ -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 {

View File

@@ -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"