Skip to content

Commit 7c0ebcf

Browse files
authored
OpenTelemetry: Specify span kind and don't set error type for benign exception (#2048)
* Add specifying SpanKind for Server and Client * Don't set error for benign exceptions * Only check top level err for Benign, use To/FromHeader instead of new
1 parent 9efde41 commit 7c0ebcf

File tree

2 files changed

+129
-2
lines changed

2 files changed

+129
-2
lines changed

contrib/opentelemetry/tracing_interceptor.go

Lines changed: 19 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ import (
1414

1515
"go.temporal.io/sdk/interceptor"
1616
"go.temporal.io/sdk/log"
17+
"go.temporal.io/sdk/temporal"
1718
)
1819

1920
// DefaultTextMapPropagator is the default OpenTelemetry TextMapPropagator used
@@ -196,8 +197,17 @@ func (t *tracer) StartSpan(opts *interceptor.TracerStartSpanOptions) (intercepto
196197
}
197198
}
198199

200+
if opts.ToHeader && opts.FromHeader {
201+
return nil, fmt.Errorf("cannot set both ToHeader and FromHeader for span")
202+
}
203+
204+
spanKind := trace.SpanKindServer
205+
if opts.ToHeader {
206+
spanKind = trace.SpanKindClient
207+
}
208+
199209
// Create span
200-
span := t.options.SpanStarter(ctx, t.options.Tracer, opts.Operation+":"+opts.Name, trace.WithTimestamp(opts.Time))
210+
span := t.options.SpanStarter(ctx, t.options.Tracer, opts.Operation+":"+opts.Name, trace.WithTimestamp(opts.Time), trace.WithSpanKind(spanKind))
201211

202212
// Set tags
203213
if len(opts.Tags) > 0 {
@@ -241,12 +251,19 @@ type tracerSpan struct {
241251
}
242252

243253
func (t *tracerSpan) Finish(opts *interceptor.TracerFinishSpanOptions) {
244-
if opts.Error != nil {
254+
t.RecordError(opts.Error)
255+
256+
if opts.Error != nil && !isBenignApplicationError(opts.Error) {
245257
t.SetStatus(codes.Error, opts.Error.Error())
246258
}
247259
t.End()
248260
}
249261

262+
func isBenignApplicationError(err error) bool {
263+
appError, _ := err.(*temporal.ApplicationError)
264+
return appError != nil && appError.Category() == temporal.ApplicationErrorCategoryBenign
265+
}
266+
250267
type textMapCarrier map[string]string
251268

252269
func (t textMapCarrier) Get(key string) string { return t[key] }

contrib/opentelemetry/tracing_interceptor_test.go

Lines changed: 110 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,15 +2,19 @@ package opentelemetry_test
22

33
import (
44
"testing"
5+
"time"
56

7+
"github.com/stretchr/testify/assert"
68
"github.com/stretchr/testify/require"
9+
"go.opentelemetry.io/otel/codes"
710
sdktrace "go.opentelemetry.io/otel/sdk/trace"
811
"go.opentelemetry.io/otel/sdk/trace/tracetest"
912
"go.opentelemetry.io/otel/trace"
1013

1114
"go.temporal.io/sdk/contrib/opentelemetry"
1215
"go.temporal.io/sdk/interceptor"
1316
"go.temporal.io/sdk/internal/interceptortest"
17+
"go.temporal.io/sdk/temporal"
1418
)
1519

1620
func TestSpanPropagation(t *testing.T) {
@@ -42,3 +46,109 @@ func spanChildren(spans []sdktrace.ReadOnlySpan, parentID trace.SpanID) (ret []*
4246
}
4347
return
4448
}
49+
50+
func TestSpanKind(t *testing.T) {
51+
tests := []struct {
52+
operation string
53+
toHeader bool
54+
fromHeader bool
55+
expectedKind trace.SpanKind
56+
}{
57+
{
58+
operation: "StartWorkflow",
59+
toHeader: true,
60+
fromHeader: false,
61+
expectedKind: trace.SpanKindClient,
62+
},
63+
{
64+
operation: "RunWorkflow",
65+
toHeader: false,
66+
fromHeader: true,
67+
expectedKind: trace.SpanKindServer,
68+
},
69+
}
70+
71+
for _, tt := range tests {
72+
t.Run(tt.operation, func(t *testing.T) {
73+
rec := tracetest.NewSpanRecorder()
74+
tracer, err := opentelemetry.NewTracer(opentelemetry.TracerOptions{
75+
Tracer: sdktrace.NewTracerProvider(sdktrace.WithSpanProcessor(rec)).Tracer(""),
76+
})
77+
require.NoError(t, err)
78+
79+
span, err := tracer.StartSpan(&interceptor.TracerStartSpanOptions{
80+
Operation: tt.operation,
81+
Name: "test-span",
82+
ToHeader: tt.toHeader,
83+
FromHeader: tt.fromHeader,
84+
})
85+
require.NoError(t, err)
86+
87+
span.Finish(&interceptor.TracerFinishSpanOptions{})
88+
89+
spans := rec.Ended()
90+
require.Equal(t, len(spans), 1)
91+
92+
foundSpan := spans[0]
93+
assert.Equal(t, tt.expectedKind, foundSpan.SpanKind(),
94+
"Expected span kind %v but got %v for operation %s",
95+
tt.expectedKind, foundSpan.SpanKind(), tt.operation)
96+
})
97+
}
98+
}
99+
100+
func TestBenignErrorSpanStatus(t *testing.T) {
101+
tests := []struct {
102+
name string
103+
err error
104+
expectError bool
105+
expectStatus codes.Code
106+
}{
107+
{
108+
name: "benign application error should not set error status",
109+
err: temporal.NewApplicationErrorWithOptions("benign error", "TestType", temporal.ApplicationErrorOptions{Category: temporal.ApplicationErrorCategoryBenign}),
110+
expectError: false,
111+
expectStatus: codes.Unset,
112+
},
113+
{
114+
name: "regular application error should set error status",
115+
err: temporal.NewApplicationError("regular error", "TestType"),
116+
expectError: true,
117+
expectStatus: codes.Error,
118+
},
119+
}
120+
121+
for _, tt := range tests {
122+
t.Run(tt.name, func(t *testing.T) {
123+
rec := tracetest.NewSpanRecorder()
124+
tracer, err := opentelemetry.NewTracer(opentelemetry.TracerOptions{
125+
Tracer: sdktrace.NewTracerProvider(sdktrace.WithSpanProcessor(rec)).Tracer(""),
126+
})
127+
require.NoError(t, err)
128+
129+
span, err := tracer.StartSpan(&interceptor.TracerStartSpanOptions{
130+
Operation: "TestOperation",
131+
Name: "TestSpan",
132+
Time: time.Now(),
133+
})
134+
require.NoError(t, err)
135+
136+
span.Finish(&interceptor.TracerFinishSpanOptions{
137+
Error: tt.err,
138+
})
139+
140+
// Check recorded spans
141+
spans := rec.Ended()
142+
require.Len(t, spans, 1)
143+
144+
recordedSpan := spans[0]
145+
assert.Equal(t, tt.expectStatus, recordedSpan.Status().Code)
146+
147+
if tt.expectError {
148+
assert.NotEmpty(t, recordedSpan.Status().Description)
149+
} else {
150+
assert.Empty(t, recordedSpan.Status().Description)
151+
}
152+
})
153+
}
154+
}

0 commit comments

Comments
 (0)