Skip to content

Commit 7aa0180

Browse files
authored
Suppress nested SDK method spans (Azure#21251)
* Suppress nested SDK method spans The inner SDK span must be omitted. However, any REST span it creates must be a child of the outer SDK method span. * only suppress client and internal spans
1 parent 1c474ab commit 7aa0180

File tree

3 files changed

+71
-1
lines changed

3 files changed

+71
-1
lines changed

sdk/azcore/CHANGELOG.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,8 @@
88

99
### Bugs Fixed
1010

11+
* Suppress creating spans for nested SDK API calls. The HTTP span will be a child of the outer API span.
12+
1113
### Other Changes
1214

1315
## 1.8.0-beta.2 (2023-08-14)

sdk/azcore/runtime/policy_http_trace.go

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -103,10 +103,23 @@ func StartSpan(ctx context.Context, name string, tracer tracing.Tracer, options
103103
if !tracer.Enabled() {
104104
return ctx, func(err error) {}
105105
}
106+
const newSpanKind = tracing.SpanKindInternal
107+
if activeSpan := ctx.Value(ctxActiveSpan{}); activeSpan != nil {
108+
// per the design guidelines, if a SDK method Foo() calls SDK method Bar(),
109+
// then the span for Bar() must be suppressed. however, if Bar() makes a REST
110+
// call, then Bar's HTTP span must be a child of Foo's span.
111+
// however, there is an exception to this rule. if the SDK method Foo() is a
112+
// messaging producer/consumer, and it takes a callback that's a SDK method
113+
// Bar(), then the span for Bar() must _not_ be suppressed.
114+
if kind := activeSpan.(tracing.SpanKind); kind == tracing.SpanKindClient || kind == tracing.SpanKindInternal {
115+
return ctx, func(err error) {}
116+
}
117+
}
106118
ctx, span := tracer.Start(ctx, name, &tracing.SpanOptions{
107-
Kind: tracing.SpanKindInternal,
119+
Kind: newSpanKind,
108120
})
109121
ctx = context.WithValue(ctx, shared.CtxWithTracingTracer{}, tracer)
122+
ctx = context.WithValue(ctx, ctxActiveSpan{}, newSpanKind)
110123
return ctx, func(err error) {
111124
if err != nil {
112125
errType := strings.Replace(fmt.Sprintf("%T", err), "*exported.", "*azcore.", 1)
@@ -115,3 +128,6 @@ func StartSpan(ctx context.Context, name string, tracer tracing.Tracer, options
115128
span.End()
116129
}
117130
}
131+
132+
// ctxActiveSpan is used as a context key for indicating a SDK client span is in progress.
133+
type ctxActiveSpan struct{}

sdk/azcore/runtime/policy_http_trace_test.go

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -161,3 +161,55 @@ func TestStartSpan(t *testing.T) {
161161
require.Contains(t, errStr, "*azcore.ResponseError")
162162
require.Contains(t, errStr, "ERROR CODE: ErrorItFailed")
163163
}
164+
165+
func TestStartSpansDontNest(t *testing.T) {
166+
srv, close := mock.NewServer()
167+
srv.SetResponse() // always return http.StatusOK
168+
defer close()
169+
170+
pl := exported.NewPipeline(srv, newHTTPTracePolicy(nil))
171+
172+
apiSpanCount := 0
173+
httpSpanCount := 0
174+
endCalled := 0
175+
tr := tracing.NewTracer(func(ctx context.Context, spanName string, options *tracing.SpanOptions) (context.Context, tracing.Span) {
176+
if spanName == "HTTP GET" {
177+
httpSpanCount++
178+
} else if spanName == "FooMethod" {
179+
apiSpanCount++
180+
} else {
181+
t.Fatalf("unexpected span name %s", spanName)
182+
}
183+
spanImpl := tracing.SpanImpl{
184+
End: func() { endCalled++ },
185+
}
186+
return ctx, tracing.NewSpan(spanImpl)
187+
}, nil)
188+
189+
barMethod := func(ctx context.Context) {
190+
ourCtx, endSpan := StartSpan(ctx, "BarMethod", tr, nil)
191+
require.Same(t, ctx, ourCtx)
192+
defer endSpan(nil)
193+
req, err := exported.NewRequest(ourCtx, http.MethodGet, srv.URL()+"/bar")
194+
require.NoError(t, err)
195+
_, err = pl.Do(req)
196+
require.NoError(t, err)
197+
}
198+
199+
fooMethod := func(ctx context.Context) {
200+
ctx, endSpan := StartSpan(ctx, "FooMethod", tr, nil)
201+
defer endSpan(nil)
202+
barMethod(ctx)
203+
req, err := exported.NewRequest(ctx, http.MethodGet, srv.URL()+"/foo")
204+
require.NoError(t, err)
205+
_, err = pl.Do(req)
206+
require.NoError(t, err)
207+
}
208+
209+
fooMethod(context.Background())
210+
211+
// there should be a total of three spans. one for FooMethod, and two HTTP spans
212+
require.EqualValues(t, 1, apiSpanCount)
213+
require.EqualValues(t, 2, httpSpanCount)
214+
require.EqualValues(t, 3, endCalled)
215+
}

0 commit comments

Comments
 (0)