Skip to content

Commit ca3f74d

Browse files
Aneurysm9krnowak
andauthored
Add Span#RecordError method to simplify adding error events to spans (#473)
* Add `Span#Error` method to simplify setting an error status and message. * `Span#Error` should no-op on nil errors * Record errors as a span event rather than status/attributes. The implementation in the SDK package now relies on existing API methods. * Add WithErrorStatus() ErrorOption to allow setting span status on error. * Apply suggestions from code review Co-Authored-By: Krzesimir Nowak <qdlacz@gmail.com> * Address code review feedback * Clean up RecordError tests * Ensure complete and unique error type is recorded for defined types * Avoid duplicating logic under test in tests * Move TestError to internal/testing package, improve RecordError test scenarios Co-authored-by: Krzesimir Nowak <qdlacz@gmail.com>
1 parent 8ebc7bb commit ca3f74d

File tree

10 files changed

+407
-0
lines changed

10 files changed

+407
-0
lines changed

api/trace/api.go

+26
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,29 @@ func WithEndTime(t time.Time) EndOption {
6060
}
6161
}
6262

63+
// ErrorConfig provides options to set properties of an error event at the time it is recorded.
64+
type ErrorConfig struct {
65+
Timestamp time.Time
66+
Status codes.Code
67+
}
68+
69+
// ErrorOption applies changes to ErrorConfig that sets options when an error event is recorded.
70+
type ErrorOption func(*ErrorConfig)
71+
72+
// WithErrorTime sets the time at which the error event should be recorded.
73+
func WithErrorTime(t time.Time) ErrorOption {
74+
return func(c *ErrorConfig) {
75+
c.Timestamp = t
76+
}
77+
}
78+
79+
// WithErrorStatus indicates the span status that should be set when recording an error event.
80+
func WithErrorStatus(s codes.Code) ErrorOption {
81+
return func(c *ErrorConfig) {
82+
c.Status = s
83+
}
84+
}
85+
6386
type Span interface {
6487
// Tracer returns tracer used to create this span. Tracer cannot be nil.
6588
Tracer() Tracer
@@ -77,6 +100,9 @@ type Span interface {
77100
// IsRecording returns true if the span is active and recording events is enabled.
78101
IsRecording() bool
79102

103+
// RecordError records an error as a span event.
104+
RecordError(ctx context.Context, err error, opts ...ErrorOption)
105+
80106
// SpanContext returns span context of the span. Returned SpanContext is usable
81107
// even after the span ends.
82108
SpanContext() core.SpanContext

api/trace/context_test.go

+4
Original file line numberDiff line numberDiff line change
@@ -87,6 +87,10 @@ func (mockSpan) SetAttributes(attributes ...core.KeyValue) {
8787
func (mockSpan) End(options ...trace.EndOption) {
8888
}
8989

90+
// RecordError does nothing.
91+
func (mockSpan) RecordError(ctx context.Context, err error, opts ...trace.ErrorOption) {
92+
}
93+
9094
// Tracer returns noop implementation of Tracer.
9195
func (mockSpan) Tracer() trace.Tracer {
9296
return trace.NoopTracer{}

api/trace/noop_span.go

+4
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,10 @@ func (NoopSpan) SetAttributes(attributes ...core.KeyValue) {
5454
func (NoopSpan) End(options ...EndOption) {
5555
}
5656

57+
// RecordError does nothing.
58+
func (NoopSpan) RecordError(ctx context.Context, err error, opts ...ErrorOption) {
59+
}
60+
5761
// Tracer returns noop implementation of Tracer.
5862
func (NoopSpan) Tracer() Tracer {
5963
return NoopTracer{}

api/trace/testtrace/span.go

+38
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,8 @@ package testtrace
1616

1717
import (
1818
"context"
19+
"fmt"
20+
"reflect"
1921
"sync"
2022
"time"
2123

@@ -25,6 +27,12 @@ import (
2527
"go.opentelemetry.io/otel/api/trace"
2628
)
2729

30+
const (
31+
errorTypeKey = core.Key("error.type")
32+
errorMessageKey = core.Key("error.message")
33+
errorEventName = "error"
34+
)
35+
2836
var _ trace.Span = (*Span)(nil)
2937

3038
type Span struct {
@@ -69,6 +77,36 @@ func (s *Span) End(opts ...trace.EndOption) {
6977
s.ended = true
7078
}
7179

80+
func (s *Span) RecordError(ctx context.Context, err error, opts ...trace.ErrorOption) {
81+
if err == nil || s.ended {
82+
return
83+
}
84+
85+
cfg := trace.ErrorConfig{}
86+
for _, o := range opts {
87+
o(&cfg)
88+
}
89+
90+
if cfg.Timestamp.IsZero() {
91+
cfg.Timestamp = time.Now()
92+
}
93+
94+
if cfg.Status != codes.OK {
95+
s.SetStatus(cfg.Status)
96+
}
97+
98+
errType := reflect.TypeOf(err)
99+
errTypeString := fmt.Sprintf("%s.%s", errType.PkgPath(), errType.Name())
100+
if errTypeString == "." {
101+
errTypeString = errType.String()
102+
}
103+
104+
s.AddEventWithTimestamp(ctx, cfg.Timestamp, errorEventName,
105+
errorTypeKey.String(errTypeString),
106+
errorMessageKey.String(err.Error()),
107+
)
108+
}
109+
72110
func (s *Span) AddEvent(ctx context.Context, name string, attrs ...core.KeyValue) {
73111
s.AddEventWithTimestamp(ctx, time.Now(), name, attrs...)
74112
}

api/trace/testtrace/span_test.go

+111
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ package testtrace_test
1616

1717
import (
1818
"context"
19+
"errors"
1920
"sync"
2021
"testing"
2122
"time"
@@ -26,6 +27,7 @@ import (
2627
"go.opentelemetry.io/otel/api/trace"
2728
"go.opentelemetry.io/otel/api/trace/testtrace"
2829
"go.opentelemetry.io/otel/internal/matchers"
30+
ottest "go.opentelemetry.io/otel/internal/testing"
2931
)
3032

3133
func TestSpan(t *testing.T) {
@@ -120,6 +122,115 @@ func TestSpan(t *testing.T) {
120122
})
121123
})
122124

125+
t.Run("#RecordError", func(t *testing.T) {
126+
t.Run("records an error", func(t *testing.T) {
127+
t.Parallel()
128+
129+
scenarios := []struct {
130+
err error
131+
typ string
132+
msg string
133+
}{
134+
{
135+
err: ottest.NewTestError("test error"),
136+
typ: "go.opentelemetry.io/otel/internal/testing.TestError",
137+
msg: "test error",
138+
},
139+
{
140+
err: errors.New("test error 2"),
141+
typ: "*errors.errorString",
142+
msg: "test error 2",
143+
},
144+
}
145+
146+
for _, s := range scenarios {
147+
e := matchers.NewExpecter(t)
148+
149+
tracer := testtrace.NewTracer()
150+
ctx, span := tracer.Start(context.Background(), "test")
151+
152+
subject, ok := span.(*testtrace.Span)
153+
e.Expect(ok).ToBeTrue()
154+
155+
testTime := time.Now()
156+
subject.RecordError(ctx, s.err, trace.WithErrorTime(testTime))
157+
158+
expectedEvents := []testtrace.Event{{
159+
Timestamp: testTime,
160+
Name: "error",
161+
Attributes: map[core.Key]core.Value{
162+
core.Key("error.type"): core.String(s.typ),
163+
core.Key("error.message"): core.String(s.msg),
164+
},
165+
}}
166+
e.Expect(subject.Events()).ToEqual(expectedEvents)
167+
e.Expect(subject.Status()).ToEqual(codes.OK)
168+
}
169+
})
170+
171+
t.Run("sets span status if provided", func(t *testing.T) {
172+
t.Parallel()
173+
174+
e := matchers.NewExpecter(t)
175+
176+
tracer := testtrace.NewTracer()
177+
ctx, span := tracer.Start(context.Background(), "test")
178+
179+
subject, ok := span.(*testtrace.Span)
180+
e.Expect(ok).ToBeTrue()
181+
182+
errMsg := "test error message"
183+
testErr := ottest.NewTestError(errMsg)
184+
testTime := time.Now()
185+
expStatus := codes.Unknown
186+
subject.RecordError(ctx, testErr, trace.WithErrorTime(testTime), trace.WithErrorStatus(expStatus))
187+
188+
expectedEvents := []testtrace.Event{{
189+
Timestamp: testTime,
190+
Name: "error",
191+
Attributes: map[core.Key]core.Value{
192+
core.Key("error.type"): core.String("go.opentelemetry.io/otel/internal/testing.TestError"),
193+
core.Key("error.message"): core.String(errMsg),
194+
},
195+
}}
196+
e.Expect(subject.Events()).ToEqual(expectedEvents)
197+
e.Expect(subject.Status()).ToEqual(expStatus)
198+
})
199+
200+
t.Run("cannot be set after the span has ended", func(t *testing.T) {
201+
t.Parallel()
202+
203+
e := matchers.NewExpecter(t)
204+
205+
tracer := testtrace.NewTracer()
206+
ctx, span := tracer.Start(context.Background(), "test")
207+
208+
subject, ok := span.(*testtrace.Span)
209+
e.Expect(ok).ToBeTrue()
210+
211+
subject.End()
212+
subject.RecordError(ctx, errors.New("ignored error"))
213+
214+
e.Expect(len(subject.Events())).ToEqual(0)
215+
})
216+
217+
t.Run("has no effect with nil error", func(t *testing.T) {
218+
t.Parallel()
219+
220+
e := matchers.NewExpecter(t)
221+
222+
tracer := testtrace.NewTracer()
223+
ctx, span := tracer.Start(context.Background(), "test")
224+
225+
subject, ok := span.(*testtrace.Span)
226+
e.Expect(ok).ToBeTrue()
227+
228+
subject.RecordError(ctx, nil)
229+
230+
e.Expect(len(subject.Events())).ToEqual(0)
231+
})
232+
})
233+
123234
t.Run("#IsRecording", func(t *testing.T) {
124235
t.Run("returns true", func(t *testing.T) {
125236
t.Parallel()

bridge/opentracing/internal/mock.go

+30
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ package internal
1717
import (
1818
"context"
1919
"math/rand"
20+
"reflect"
2021
"sync"
2122
"time"
2223

@@ -259,6 +260,35 @@ func (s *MockSpan) End(options ...oteltrace.EndOption) {
259260
s.mockTracer.FinishedSpans = append(s.mockTracer.FinishedSpans, s)
260261
}
261262

263+
func (s *MockSpan) RecordError(ctx context.Context, err error, opts ...oteltrace.ErrorOption) {
264+
if err == nil {
265+
return // no-op on nil error
266+
}
267+
268+
if !s.EndTime.IsZero() {
269+
return // already finished
270+
}
271+
272+
cfg := oteltrace.ErrorConfig{}
273+
274+
for _, o := range opts {
275+
o(&cfg)
276+
}
277+
278+
if cfg.Timestamp.IsZero() {
279+
cfg.Timestamp = time.Now()
280+
}
281+
282+
if cfg.Status != codes.OK {
283+
s.SetStatus(cfg.Status)
284+
}
285+
286+
s.AddEventWithTimestamp(ctx, cfg.Timestamp, "error",
287+
otelcore.Key("error.type").String(reflect.TypeOf(err).String()),
288+
otelcore.Key("error.message").String(err.Error()),
289+
)
290+
}
291+
262292
func (s *MockSpan) Tracer() oteltrace.Tracer {
263293
return s.officialTracer
264294
}

internal/testing/errors.go

+13
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
package testing
2+
3+
type TestError string
4+
5+
var _ error = TestError("")
6+
7+
func NewTestError(s string) error {
8+
return TestError(s)
9+
}
10+
11+
func (e TestError) Error() string {
12+
return string(e)
13+
}

internal/trace/mock_span.go

+4
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,10 @@ func (ms *MockSpan) SetAttributes(attributes ...core.KeyValue) {
6262
func (ms *MockSpan) End(options ...apitrace.EndOption) {
6363
}
6464

65+
// RecordError does nothing.
66+
func (ms *MockSpan) RecordError(ctx context.Context, err error, opts ...apitrace.ErrorOption) {
67+
}
68+
6569
// SetName does nothing.
6670
func (ms *MockSpan) SetName(name string) {
6771
}

sdk/trace/span.go

+44
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,8 @@ package trace
1616

1717
import (
1818
"context"
19+
"fmt"
20+
"reflect"
1921
"sync"
2022
"time"
2123

@@ -27,6 +29,12 @@ import (
2729
"go.opentelemetry.io/otel/sdk/internal"
2830
)
2931

32+
const (
33+
errorTypeKey = core.Key("error.type")
34+
errorMessageKey = core.Key("error.message")
35+
errorEventName = "error"
36+
)
37+
3038
// span implements apitrace.Span interface.
3139
type span struct {
3240
// data contains information recorded about the span.
@@ -123,6 +131,42 @@ func (s *span) End(options ...apitrace.EndOption) {
123131
})
124132
}
125133

134+
func (s *span) RecordError(ctx context.Context, err error, opts ...apitrace.ErrorOption) {
135+
if s == nil || err == nil {
136+
return
137+
}
138+
139+
if !s.IsRecording() {
140+
return
141+
}
142+
143+
cfg := apitrace.ErrorConfig{}
144+
145+
for _, o := range opts {
146+
o(&cfg)
147+
}
148+
149+
if cfg.Timestamp.IsZero() {
150+
cfg.Timestamp = time.Now()
151+
}
152+
153+
if cfg.Status != codes.OK {
154+
s.SetStatus(cfg.Status)
155+
}
156+
157+
errType := reflect.TypeOf(err)
158+
errTypeString := fmt.Sprintf("%s.%s", errType.PkgPath(), errType.Name())
159+
if errTypeString == "." {
160+
// PkgPath() and Name() may be empty for builtin Types
161+
errTypeString = errType.String()
162+
}
163+
164+
s.AddEventWithTimestamp(ctx, cfg.Timestamp, errorEventName,
165+
errorTypeKey.String(errTypeString),
166+
errorMessageKey.String(err.Error()),
167+
)
168+
}
169+
126170
func (s *span) Tracer() apitrace.Tracer {
127171
return s.tracer
128172
}

0 commit comments

Comments
 (0)