Skip to content

Commit baaf470

Browse files
committed
Add supporting features to enable distributed tracing
This includes new internal pipeline policies and other supporting types. See the changelog for a full description. Added some missing doc comments.
1 parent 330be67 commit baaf470

18 files changed

+657
-31
lines changed

sdk/azcore/CHANGELOG.md

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,14 @@
44

55
### Features Added
66
* Add `Clone()` method for `arm/policy.ClientOptions`.
7+
* Added supporting features to enable distributed tracing.
8+
* Added func `runtime.StartSpan()` for use by SDKs to start spans.
9+
* Added method `WithContext()` to `runtime.Request` to support shallow cloning with a new context.
10+
* Added field `TracingNamespace` to `runtime.PipelineOptions`.
11+
* Added field `Tracer` to `runtime.NewPollerOptions` and `runtime.NewPollerFromResumeTokenOptions` types.
12+
* Added field `SpanFromContext` to `tracing.TracerOptions`.
13+
* Added methods `Enabled()`, `SetAttributes()`, and `SpanFromContext()` to `tracing.Tracer`.
14+
* Added supporting pipeline policies to include HTTP spans when creating clients.
715

816
### Breaking Changes
917

sdk/azcore/arm/runtime/pipeline.go

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ import (
1313
"github.com/Azure/azure-sdk-for-go/sdk/azcore"
1414
armpolicy "github.com/Azure/azure-sdk-for-go/sdk/azcore/arm/policy"
1515
"github.com/Azure/azure-sdk-for-go/sdk/azcore/cloud"
16+
"github.com/Azure/azure-sdk-for-go/sdk/azcore/internal/exported"
1617
azpolicy "github.com/Azure/azure-sdk-for-go/sdk/azcore/policy"
1718
azruntime "github.com/Azure/azure-sdk-for-go/sdk/azcore/runtime"
1819
)
@@ -31,7 +32,7 @@ func NewPipeline(module, version string, cred azcore.TokenCredential, plOpts azr
3132
authPolicy := NewBearerTokenPolicy(cred, &armpolicy.BearerTokenOptions{Scopes: []string{conf.Audience + "/.default"}})
3233
perRetry := make([]azpolicy.Policy, len(plOpts.PerRetry), len(plOpts.PerRetry)+1)
3334
copy(perRetry, plOpts.PerRetry)
34-
plOpts.PerRetry = append(perRetry, authPolicy)
35+
plOpts.PerRetry = append(perRetry, authPolicy, exported.PolicyFunc(httpTraceNamespacePolicy))
3536
if !options.DisableRPRegistration {
3637
regRPOpts := armpolicy.RegistrationOptions{ClientOptions: options.ClientOptions}
3738
regPolicy, err := NewRPRegistrationPolicy(cred, &regRPOpts)
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
//go:build go1.18
2+
// +build go1.18
3+
4+
// Copyright (c) Microsoft Corporation. All rights reserved.
5+
// Licensed under the MIT License.
6+
7+
package runtime
8+
9+
import (
10+
"net/http"
11+
12+
"github.com/Azure/azure-sdk-for-go/sdk/azcore/arm/internal/resource"
13+
"github.com/Azure/azure-sdk-for-go/sdk/azcore/internal/shared"
14+
"github.com/Azure/azure-sdk-for-go/sdk/azcore/policy"
15+
"github.com/Azure/azure-sdk-for-go/sdk/azcore/tracing"
16+
)
17+
18+
// httpTraceNamespacePolicy is a policy that adds the az.namespace attribute to the current Span
19+
func httpTraceNamespacePolicy(req *policy.Request) (resp *http.Response, err error) {
20+
rawTracer := req.Raw().Context().Value(shared.CtxWithTracingTracer{})
21+
if tracer, ok := rawTracer.(tracing.Tracer); ok {
22+
rt, err := resource.ParseResourceType(req.Raw().URL.Path)
23+
if err == nil {
24+
// add the namespace attribute to the current span
25+
if span, ok := tracer.SpanFromContext(req.Raw().Context()); ok {
26+
span.SetAttributes(tracing.Attribute{Key: "az.namespace", Value: rt.Namespace})
27+
}
28+
}
29+
}
30+
return req.Next()
31+
}
Lines changed: 97 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,97 @@
1+
//go:build go1.18
2+
// +build go1.18
3+
4+
// Copyright (c) Microsoft Corporation. All rights reserved.
5+
// Licensed under the MIT License.
6+
7+
package runtime
8+
9+
import (
10+
"context"
11+
"net/http"
12+
"testing"
13+
14+
"github.com/Azure/azure-sdk-for-go/sdk/azcore/internal/exported"
15+
"github.com/Azure/azure-sdk-for-go/sdk/azcore/internal/shared"
16+
"github.com/Azure/azure-sdk-for-go/sdk/azcore/tracing"
17+
"github.com/Azure/azure-sdk-for-go/sdk/internal/mock"
18+
"github.com/stretchr/testify/require"
19+
)
20+
21+
func TestHTTPTraceNamespacePolicy(t *testing.T) {
22+
srv, close := mock.NewServer()
23+
defer close()
24+
25+
pl := exported.NewPipeline(srv, exported.PolicyFunc(httpTraceNamespacePolicy))
26+
27+
// no tracer
28+
req, err := exported.NewRequest(context.Background(), http.MethodGet, srv.URL())
29+
require.NoError(t, err)
30+
srv.AppendResponse()
31+
_, err = pl.Do(req)
32+
require.NoError(t, err)
33+
34+
// wrong tracer type
35+
req, err = exported.NewRequest(context.WithValue(context.Background(), shared.CtxWithTracingTracer{}, 0), http.MethodGet, srv.URL())
36+
require.NoError(t, err)
37+
srv.AppendResponse()
38+
_, err = pl.Do(req)
39+
require.NoError(t, err)
40+
41+
// no SpanFromContext impl
42+
tr := tracing.NewTracer(func(ctx context.Context, spanName string, options *tracing.SpanOptions) (context.Context, tracing.Span) {
43+
return ctx, tracing.Span{}
44+
}, nil)
45+
req, err = exported.NewRequest(context.WithValue(context.Background(), shared.CtxWithTracingTracer{}, tr), http.MethodGet, srv.URL())
46+
require.NoError(t, err)
47+
srv.AppendResponse()
48+
_, err = pl.Do(req)
49+
require.NoError(t, err)
50+
51+
// failed to parse resource ID, shouldn't call SetAttributes
52+
var attrString string
53+
tr = tracing.NewTracer(func(ctx context.Context, spanName string, options *tracing.SpanOptions) (context.Context, tracing.Span) {
54+
return ctx, tracing.Span{}
55+
}, &tracing.TracerOptions{
56+
SpanFromContext: func(ctx context.Context) (tracing.Span, bool) {
57+
spanImpl := tracing.SpanImpl{
58+
SetAttributes: func(a ...tracing.Attribute) {
59+
require.Len(t, a, 1)
60+
v, ok := a[0].Value.(string)
61+
require.True(t, ok)
62+
attrString = a[0].Key + ":" + v
63+
},
64+
}
65+
return tracing.NewSpan(spanImpl), true
66+
},
67+
})
68+
req, err = exported.NewRequest(context.WithValue(context.Background(), shared.CtxWithTracingTracer{}, tr), http.MethodGet, srv.URL())
69+
require.NoError(t, err)
70+
srv.AppendResponse()
71+
_, err = pl.Do(req)
72+
require.NoError(t, err)
73+
require.Empty(t, attrString)
74+
75+
// success
76+
tr = tracing.NewTracer(func(ctx context.Context, spanName string, options *tracing.SpanOptions) (context.Context, tracing.Span) {
77+
return ctx, tracing.Span{}
78+
}, &tracing.TracerOptions{
79+
SpanFromContext: func(ctx context.Context) (tracing.Span, bool) {
80+
spanImpl := tracing.SpanImpl{
81+
SetAttributes: func(a ...tracing.Attribute) {
82+
require.Len(t, a, 1)
83+
v, ok := a[0].Value.(string)
84+
require.True(t, ok)
85+
attrString = a[0].Key + ":" + v
86+
},
87+
}
88+
return tracing.NewSpan(spanImpl), true
89+
},
90+
})
91+
req, err = exported.NewRequest(context.WithValue(context.Background(), shared.CtxWithTracingTracer{}, tr), http.MethodGet, srv.URL()+requestEndpoint)
92+
require.NoError(t, err)
93+
srv.AppendResponse()
94+
_, err = pl.Do(req)
95+
require.NoError(t, err)
96+
require.EqualValues(t, "az.namespace:Microsoft.Storage", attrString)
97+
}

sdk/azcore/core.go

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -99,6 +99,9 @@ func NewClient(clientName, moduleVersion string, plOpts runtime.PipelineOptions,
9999
pl := runtime.NewPipeline(pkg, moduleVersion, plOpts, options)
100100

101101
tr := options.TracingProvider.NewTracer(clientName, moduleVersion)
102+
if tr.Enabled() && plOpts.TracingNamespace != "" {
103+
tr.SetAttributes(tracing.Attribute{Key: "az.namespace", Value: plOpts.TracingNamespace})
104+
}
102105
return &Client{pl: pl, tr: tr}, nil
103106
}
104107

sdk/azcore/core_test.go

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,11 +7,17 @@
77
package azcore
88

99
import (
10+
"context"
11+
"net/http"
1012
"reflect"
1113
"testing"
1214

15+
"github.com/Azure/azure-sdk-for-go/sdk/azcore/internal/exported"
16+
"github.com/Azure/azure-sdk-for-go/sdk/azcore/internal/shared"
1317
"github.com/Azure/azure-sdk-for-go/sdk/azcore/policy"
1418
"github.com/Azure/azure-sdk-for-go/sdk/azcore/runtime"
19+
"github.com/Azure/azure-sdk-for-go/sdk/azcore/tracing"
20+
"github.com/Azure/azure-sdk-for-go/sdk/internal/mock"
1521
"github.com/stretchr/testify/require"
1622
)
1723

@@ -131,3 +137,37 @@ func TestNewClientError(t *testing.T) {
131137
require.Error(t, err)
132138
require.Nil(t, client)
133139
}
140+
141+
func TestNewClientTracingEnabled(t *testing.T) {
142+
srv, close := mock.NewServer()
143+
defer close()
144+
145+
var attrString string
146+
client, err := NewClient("package.Client", "v1.0.0", runtime.PipelineOptions{TracingNamespace: "Widget.Factory"}, &policy.ClientOptions{
147+
TracingProvider: tracing.NewProvider(func(name, version string) tracing.Tracer {
148+
return tracing.NewTracer(func(ctx context.Context, spanName string, options *tracing.SpanOptions) (context.Context, tracing.Span) {
149+
require.NotNil(t, options)
150+
for _, attr := range options.Attributes {
151+
if attr.Key == "az.namespace" {
152+
v, ok := attr.Value.(string)
153+
require.True(t, ok)
154+
attrString = attr.Key + ":" + v
155+
}
156+
}
157+
return ctx, tracing.Span{}
158+
}, nil)
159+
}, nil),
160+
Transport: srv,
161+
})
162+
require.NoError(t, err)
163+
require.NotNil(t, client)
164+
require.NotZero(t, client.Pipeline())
165+
require.NotZero(t, client.Tracer())
166+
167+
const requestEndpoint = "/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/fakeResourceGroupo/providers/Microsoft.Storage/storageAccounts/fakeAccountName"
168+
req, err := exported.NewRequest(context.WithValue(context.Background(), shared.CtxWithTracingTracer{}, client.Tracer()), http.MethodGet, srv.URL()+requestEndpoint)
169+
require.NoError(t, err)
170+
srv.AppendResponse()
171+
client.Pipeline().Do(req)
172+
require.EqualValues(t, "az.namespace:Widget.Factory", attrString)
173+
}

sdk/azcore/internal/exported/request.go

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -170,6 +170,14 @@ func (req *Request) Clone(ctx context.Context) *Request {
170170
return &r2
171171
}
172172

173+
// WithContext returns a shallow copy of the request with its context changed to ctx.
174+
func (req *Request) WithContext(ctx context.Context) *Request {
175+
r2 := new(Request)
176+
*r2 = *req
177+
r2.req = r2.req.WithContext(ctx)
178+
return r2
179+
}
180+
173181
// not exported but dependent on Request
174182

175183
// PolicyFunc is a type that implements the Policy interface.

sdk/azcore/internal/exported/request_test.go

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -194,3 +194,20 @@ func TestNewRequestFail(t *testing.T) {
194194
t.Fatal("unexpected request")
195195
}
196196
}
197+
198+
func TestRequestWithContext(t *testing.T) {
199+
type ctxKey1 struct{}
200+
type ctxKey2 struct{}
201+
202+
req1, err := NewRequest(context.WithValue(context.Background(), ctxKey1{}, 1), http.MethodPost, testURL)
203+
require.NoError(t, err)
204+
require.NotNil(t, req1.Raw().Context().Value(ctxKey1{}))
205+
206+
req2 := req1.WithContext(context.WithValue(context.Background(), ctxKey2{}, 1))
207+
require.Nil(t, req2.Raw().Context().Value(ctxKey1{}))
208+
require.NotNil(t, req2.Raw().Context().Value(ctxKey2{}))
209+
210+
// shallow copy, so changing req2 affects req1
211+
req2.Raw().Header.Add("added-req2", "value")
212+
require.EqualValues(t, "value", req1.Raw().Header.Get("added-req2"))
213+
}

sdk/azcore/internal/shared/constants.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ const (
2222
HeaderRetryAfter = "Retry-After"
2323
HeaderUserAgent = "User-Agent"
2424
HeaderXMSClientRequestID = "x-ms-client-request-id"
25+
HeaderXMSRequestID = "x-ms-request-id"
2526
)
2627

2728
const BearerTokenPrefix = "Bearer "

sdk/azcore/internal/shared/shared.go

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,9 @@ type CtxWithRetryOptionsKey struct{}
2828
// CtxIncludeResponseKey is used as a context key for retrieving the raw response.
2929
type CtxIncludeResponseKey struct{}
3030

31+
// CtxWithTracingTracer is used as a context key for adding/retrieving tracing.Tracer.
32+
type CtxWithTracingTracer struct{}
33+
3134
// Delay waits for the duration to elapse or the context to be cancelled.
3235
func Delay(ctx context.Context, delay time.Duration) error {
3336
select {

sdk/azcore/policy/policy.go

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,8 @@ type Request = exported.Request
2929
// ClientOptions contains optional settings for a client's pipeline.
3030
// All zero-value fields will be initialized with default values.
3131
type ClientOptions struct {
32-
// APIVersion overrides the default version requested of the service. Set with caution as this package version has not been tested with arbitrary service versions.
32+
// APIVersion overrides the default version requested of the service.
33+
// Set with caution as this package version has not been tested with arbitrary service versions.
3334
APIVersion string
3435

3536
// Cloud specifies a cloud for the client. The default is Azure Public Cloud.

sdk/azcore/runtime/pipeline.go

Lines changed: 24 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -13,9 +13,29 @@ import (
1313

1414
// PipelineOptions contains Pipeline options for SDK developers
1515
type PipelineOptions struct {
16-
AllowedHeaders, AllowedQueryParameters []string
17-
APIVersion APIVersionOptions
18-
PerCall, PerRetry []policy.Policy
16+
// AllowedHeaders is the slice of headers to log with their values intact.
17+
// All headers not in the slice will have their values REDACTED.
18+
// Applies to request and response headers.
19+
AllowedHeaders []string
20+
21+
// AllowedQueryParameters is the slice of query parameters to log with their values intact.
22+
// All query parameters not in the slice will have their values REDACTED.
23+
AllowedQueryParameters []string
24+
25+
// APIVersion overrides the default version requested of the service.
26+
// Set with caution as this package version has not been tested with arbitrary service versions.
27+
APIVersion APIVersionOptions
28+
29+
// PerCall contains custom policies to inject into the pipeline.
30+
// Each policy is executed once per request.
31+
PerCall []policy.Policy
32+
33+
// PerRetry contains custom policies to inject into the pipeline.
34+
// Each policy is executed once per request, and for each retry of that request.
35+
PerRetry []policy.Policy
36+
37+
// TracingNamespace contains the value to use for the az.namespace span attribute.
38+
TracingNamespace string
1939
}
2040

2141
// Pipeline represents a primitive for sending HTTP requests and receiving responses.
@@ -58,6 +78,7 @@ func NewPipeline(module, version string, plOpts PipelineOptions, options *policy
5878
policies = append(policies, cp.PerRetryPolicies...)
5979
policies = append(policies, NewLogPolicy(&cp.Logging))
6080
policies = append(policies, exported.PolicyFunc(httpHeaderPolicy), exported.PolicyFunc(bodyDownloadPolicy))
81+
policies = append(policies, newHTTPTracePolicy(cp.Logging.AllowedQueryParams))
6182
transport := cp.Transport
6283
if transport == nil {
6384
transport = defaultHTTPClient

0 commit comments

Comments
 (0)