diff --git a/CHANGELOG.md b/CHANGELOG.md index 8261f72ca3c..750d9addde1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,11 @@ This project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.htm ## [Unreleased] +### Added + +- Add the `trace.WithStatus` option for `span.RecordError`. (#5762) +- Add the`trace.WithStatusOnPanic` option for `span.End`. (#5762) + ### Changed - Enable exemplars by default in `go.opentelemetry.io/otel/sdk/metric`. Exemplars can be disabled by setting `OTEL_METRICS_EXEMPLAR_FILTER=always_off` (#5778) diff --git a/sdk/trace/span.go b/sdk/trace/span.go index 4945f508303..75e3becef4b 100644 --- a/sdk/trace/span.go +++ b/sdk/trace/span.go @@ -396,10 +396,12 @@ func (s *recordingSpan) End(options ...trace.SpanEndOption) { if recovered := recover(); recovered != nil { // Record but don't stop the panic. defer panic(recovered) + recoveredStr := fmt.Sprint(recovered) + opts := []trace.EventOption{ trace.WithAttributes( semconv.ExceptionType(typeStr(recovered)), - semconv.ExceptionMessage(fmt.Sprint(recovered)), + semconv.ExceptionMessage(recoveredStr), ), } @@ -409,6 +411,10 @@ func (s *recordingSpan) End(options ...trace.SpanEndOption) { )) } + if config.ErrorStatusOnPanic() { + s.SetStatus(codes.Error, recoveredStr) + } + s.addEvent(semconv.ExceptionEventName, opts...) } @@ -466,6 +472,10 @@ func (s *recordingSpan) RecordError(err error, opts ...trace.EventOption) { )) } + if c.ErrorStatus() { + s.SetStatus(codes.Error, err.Error()) + } + s.addEvent(semconv.ExceptionEventName, opts...) } diff --git a/sdk/trace/trace_test.go b/sdk/trace/trace_test.go index 87247d1f167..ba44b1ebc5a 100644 --- a/sdk/trace/trace_test.go +++ b/sdk/trace/trace_test.go @@ -1299,6 +1299,54 @@ func TestRecordErrorWithStackTrace(t *testing.T) { 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]) } +func TestRecordErrorWithErrorStatus(t *testing.T) { + err := ottest.NewTestError("test error") + typ := "go.opentelemetry.io/otel/sdk/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.WithStatus()) + + 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.Error, Description: msg}, + 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, 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()) +} + func TestRecordErrorNil(t *testing.T) { te := NewTestExporter() tp := NewTracerProvider(WithSyncer(te), WithResource(resource.Empty())) @@ -1532,6 +1580,32 @@ func TestSpanCapturesPanicWithStackTrace(t *testing.T) { 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 TestSpanCapturesPanicWithErrorStatus(t *testing.T) { + err := errors.New("error message") + typ := "*errors.errorString" + msg := "error message" + + te := NewTestExporter() + tp := NewTracerProvider(WithSyncer(te), WithResource(resource.Empty())) + _, span := tp.Tracer("CatchPanic").Start( + context.Background(), + "span", + ) + + f := func() { + defer span.End(trace.WithStatusOnPanic()) + panic(err) + } + require.PanicsWithError(t, msg, f) + spans := te.Spans() + require.Len(t, spans, 1) + require.Equal(t, Status{Code: codes.Error, Description: msg}, spans[0].Status()) + 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(), typ) + assert.Equal(t, spans[0].Events()[0].Attributes[1].Value.AsString(), msg) +} + func TestReadOnlySpan(t *testing.T) { kv := attribute.String("foo", "bar") diff --git a/trace/config.go b/trace/config.go index 273d58e0014..2413f503a9f 100644 --- a/trace/config.go +++ b/trace/config.go @@ -55,12 +55,13 @@ func (fn tracerOptionFunc) apply(cfg TracerConfig) TracerConfig { // SpanConfig is a group of options for a Span. type SpanConfig struct { - attributes []attribute.KeyValue - timestamp time.Time - links []Link - newRoot bool - spanKind SpanKind - stackTrace bool + attributes []attribute.KeyValue + timestamp time.Time + links []Link + newRoot bool + spanKind SpanKind + stackTrace bool + errorStatusOnPanic bool } // Attributes describe the associated qualities of a Span. @@ -95,6 +96,11 @@ func (cfg *SpanConfig) SpanKind() SpanKind { return cfg.spanKind } +// ErrorStatusOnPanic checks whether setting error status on panic is enabled. +func (cfg *SpanConfig) ErrorStatusOnPanic() bool { + return cfg.errorStatusOnPanic +} + // NewSpanStartConfig applies all the options to a returned SpanConfig. // No validation is performed on the returned SpanConfig (e.g. no uniqueness // checking or bounding of data), it is left to the SDK to perform this @@ -125,9 +131,9 @@ type SpanStartOption interface { applySpanStart(SpanConfig) SpanConfig } -type spanOptionFunc func(SpanConfig) SpanConfig +type spanStartOptionFunc func(SpanConfig) SpanConfig -func (fn spanOptionFunc) applySpanStart(cfg SpanConfig) SpanConfig { +func (fn spanStartOptionFunc) applySpanStart(cfg SpanConfig) SpanConfig { return fn(cfg) } @@ -137,11 +143,18 @@ type SpanEndOption interface { applySpanEnd(SpanConfig) SpanConfig } +type spanEndOptionFunc func(config SpanConfig) SpanConfig + +func (fn spanEndOptionFunc) applySpanEnd(cfg SpanConfig) SpanConfig { + return fn(cfg) +} + // EventConfig is a group of options for an Event. type EventConfig struct { - attributes []attribute.KeyValue - timestamp time.Time - stackTrace bool + attributes []attribute.KeyValue + timestamp time.Time + stackTrace bool + errorStatus bool } // Attributes describe the associated qualities of an Event. @@ -159,6 +172,11 @@ func (cfg *EventConfig) StackTrace() bool { return cfg.stackTrace } +// ErrorStatus checks whether setting error status is enabled. +func (cfg *EventConfig) ErrorStatus() bool { + return cfg.errorStatus +} + // 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 @@ -179,6 +197,12 @@ type EventOption interface { applyEvent(EventConfig) EventConfig } +type eventOptionFunc func(EventConfig) EventConfig + +func (fn eventOptionFunc) applyEvent(cfg EventConfig) EventConfig { + return fn(cfg) +} + // SpanOption are options that can be used at both the beginning and end of a span. type SpanOption interface { SpanStartOption @@ -269,10 +293,26 @@ func WithStackTrace(b bool) SpanEndEventOption { return stackTraceOption(b) } +// WithStatus sets the flag to set span's status to error. +func WithStatus() EventOption { + return eventOptionFunc(func(cfg EventConfig) EventConfig { + cfg.errorStatus = true + return cfg + }) +} + +// WithStatusOnPanic sets the flag to set span's status to error if panic is occurred. +func WithStatusOnPanic() SpanEndOption { + return spanEndOptionFunc(func(cfg SpanConfig) SpanConfig { + cfg.errorStatusOnPanic = true + return cfg + }) +} + // WithLinks adds links to a Span. The links are added to the existing Span // links, i.e. this does not overwrite. Links with invalid span context are ignored. func WithLinks(links ...Link) SpanStartOption { - return spanOptionFunc(func(cfg SpanConfig) SpanConfig { + return spanStartOptionFunc(func(cfg SpanConfig) SpanConfig { cfg.links = append(cfg.links, links...) return cfg }) @@ -282,7 +322,7 @@ func WithLinks(links ...Link) SpanStartOption { // existing parent span context will be ignored when defining the Span's trace // identifiers. func WithNewRoot() SpanStartOption { - return spanOptionFunc(func(cfg SpanConfig) SpanConfig { + return spanStartOptionFunc(func(cfg SpanConfig) SpanConfig { cfg.newRoot = true return cfg }) @@ -290,7 +330,7 @@ func WithNewRoot() SpanStartOption { // WithSpanKind sets the SpanKind of a Span. func WithSpanKind(kind SpanKind) SpanStartOption { - return spanOptionFunc(func(cfg SpanConfig) SpanConfig { + return spanStartOptionFunc(func(cfg SpanConfig) SpanConfig { cfg.spanKind = kind return cfg }) diff --git a/trace/config_test.go b/trace/config_test.go index 9a613ace2c9..aaccff24628 100644 --- a/trace/config_test.go +++ b/trace/config_test.go @@ -203,12 +203,82 @@ func TestEndSpanConfig(t *testing.T) { timestamp: timestamp, }, }, + { + []SpanEndOption{ + WithStatusOnPanic(), + }, + SpanConfig{ + errorStatusOnPanic: true, + }, + }, } for _, test := range tests { assert.Equal(t, test.expected, NewSpanEndConfig(test.options...)) } } +func TestEventConfig(t *testing.T) { + kv := attribute.String("key", "value") + timestamp := time.Unix(0, 0) + + tests := []struct { + options []EventOption + expected EventConfig + }{ + { + []EventOption{ + WithTimestamp(timestamp), + }, + EventConfig{ + timestamp: timestamp, + }, + }, + { + []EventOption{ + WithTimestamp(timestamp), + WithStackTrace(true), + }, + EventConfig{ + timestamp: timestamp, + stackTrace: true, + }, + }, + { + []EventOption{ + WithTimestamp(timestamp), + WithStackTrace(true), + }, + EventConfig{ + timestamp: timestamp, + stackTrace: true, + }, + }, + { + []EventOption{ + WithTimestamp(timestamp), + WithAttributes(kv), + }, + EventConfig{ + timestamp: timestamp, + attributes: []attribute.KeyValue{kv}, + }, + }, + { + []EventOption{ + WithTimestamp(timestamp), + WithStatus(), + }, + EventConfig{ + timestamp: timestamp, + errorStatus: true, + }, + }, + } + for _, test := range tests { + assert.Equal(t, test.expected, NewEventConfig(test.options...)) + } +} + func TestTracerConfig(t *testing.T) { v1 := "semver:0.0.1" v2 := "semver:1.0.0"