Skip to content

Commit eea682a

Browse files
committed
Use gRPC metadata to pass trace context
1 parent 3732a29 commit eea682a

17 files changed

+180
-169
lines changed

cli/azd/.vscode/cspell.yaml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -246,6 +246,9 @@ overrides:
246246
- filename: cli/azd/pkg/azdext/trace_context.go
247247
words:
248248
- traceparent
249+
- filename: internal/grpcserver/server.go
250+
words:
251+
- traceparents
249252
ignorePaths:
250253
- "**/*_test.go"
251254
- "**/mock*.go"

cli/azd/cmd/middleware/extensions.go

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ import (
1515

1616
"github.com/azure/azure-dev/cli/azd/cmd/actions"
1717
"github.com/azure/azure-dev/cli/azd/internal/grpcserver"
18+
"github.com/azure/azure-dev/cli/azd/internal/tracing"
1819
"github.com/azure/azure-dev/cli/azd/pkg/extensions"
1920
"github.com/azure/azure-dev/cli/azd/pkg/input"
2021
"github.com/azure/azure-dev/cli/azd/pkg/ioc"
@@ -132,6 +133,11 @@ func (m *ExtensionsMiddleware) Run(ctx context.Context, next NextFn) (*actions.A
132133
allEnv = append(allEnv, "FORCE_COLOR=1")
133134
}
134135

136+
// Propagate trace context to the extension process
137+
if traceEnv := tracing.Environ(ctx); len(traceEnv) > 0 {
138+
allEnv = append(allEnv, traceEnv...)
139+
}
140+
135141
args := []string{"listen"}
136142
if debugEnabled, _ := m.options.Flags.GetBool("debug"); debugEnabled {
137143
args = append(args, "--debug")

cli/azd/docs/extension-framework.md

Lines changed: 18 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -336,40 +336,32 @@ The build process automatically creates binaries for multiple platforms and arch
336336
337337
### Distributed Tracing
338338

339-
`azd` uses OpenTelemetry and W3C Trace Context for distributed tracing. To ensure your extension's operations are correctly correlated with the parent `azd` process, you must hydrate the context in your extension's entry point.
339+
`azd` uses OpenTelemetry and W3C Trace Context for distributed tracing. Trace context is propagated through gRPC metadata, and `azd` sets `TRACEPARENT` in the environment when it launches an extension.
340340

341-
**Update `main.go`:**
341+
Use `azdext.NewContext()` to hydrate the root context with trace context:
342342

343343
```go
344-
import (
345-
"context"
346-
"os"
347-
"github.com/azure/azure-dev/cli/azd/pkg/azdext"
348-
)
349-
350344
func main() {
351-
ctx := context.Background()
345+
ctx := azdext.NewContext()
346+
rootCmd := cmd.NewRootCommand()
347+
if err := rootCmd.ExecuteContext(ctx); err != nil {
348+
// Handle error
349+
}
350+
}
351+
```
352352

353-
// Hydrate context with traceparent from environment if present
354-
// This ensures the extension process participates in the active trace
355-
if traceparent := os.Getenv("TRACEPARENT"); traceparent != "" {
356-
ctx = azdext.ContextFromTraceParent(ctx, traceparent)
357-
}
353+
To correlate Azure SDK calls with the parent trace, add the correlation policy to your client options:
358354

359-
rootCmd := cmd.NewRootCommand()
355+
```go
356+
import "github.com/azure/azure-dev/cli/azd/pkg/azsdk"
360357

361-
if err := rootCmd.ExecuteContext(ctx); err != nil {
362-
// Handle error
363-
}
358+
clientOptions := &policy.ClientOptions{
359+
PerCallPolicies: []policy.Policy{
360+
azsdk.NewMsCorrelationPolicy(),
361+
},
364362
}
365363
```
366364

367-
The `ExtensionHost` automatically handles trace propagation for all gRPC communication:
368-
1. **Outgoing**: Traces are injected into messages sent back to `azd` core.
369-
2. **Incoming**: Traces from `azd` core are extracted and injected into the `context.Context` passed to your handlers.
370-
371-
This ensures that calls to Azure SDKs (which support `TRACEPARENT`) within your handlers will automatically be correlated.
372-
373365
### Developer Extension
374366

375367
The easiest way to get started building extensions is to install the `azd` Developer extension.
@@ -1931,7 +1923,7 @@ func (r *RustFrameworkProvider) Package(ctx context.Context, serviceConfig *azde
19311923

19321924
// Register the framework provider
19331925
func main() {
1934-
ctx := azdext.WithAccessToken(context.Background())
1926+
ctx := azdext.WithAccessToken(azdext.NewContext())
19351927
azdClient, err := azdext.NewAzdClient()
19361928
if err != nil {
19371929
log.Fatal(err)
@@ -2047,7 +2039,7 @@ func (v *VMServiceTargetProvider) Endpoints(ctx context.Context, serviceConfig *
20472039

20482040
// Register the service target provider
20492041
func main() {
2050-
ctx := azdext.WithAccessToken(context.Background())
2042+
ctx := azdext.WithAccessToken(azdext.NewContext())
20512043
azdClient, err := azdext.NewAzdClient()
20522044
if err != nil {
20532045
log.Fatal(err)

cli/azd/grpc/proto/event.proto

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -25,10 +25,6 @@ message EventMessage {
2525
InvokeServiceHandler invoke_service_handler = 5;
2626
ServiceHandlerStatus service_handler_status = 6;
2727
}
28-
29-
// W3C traceparent format for distributed tracing propagation.
30-
// Format: "00-{traceId}-{spanId}-{flags}"
31-
string trace_parent = 98;
3228
}
3329

3430
// Client subscribes to project-related events

cli/azd/grpc/proto/framework_service.proto

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -17,10 +17,6 @@ service FrameworkService {
1717
// Envelope for all possible framework service messages (requests and responses)
1818
message FrameworkServiceMessage {
1919
string request_id = 1;
20-
// W3C Trace Context traceparent header value for distributed tracing.
21-
// Format: "00-{trace-id}-{span-id}-{trace-flags}"
22-
// See: https://www.w3.org/TR/trace-context/
23-
string trace_parent = 98;
2420
FrameworkServiceErrorMessage error = 99;
2521
oneof message_type {
2622
RegisterFrameworkServiceRequest register_framework_service_request = 2;

cli/azd/grpc/proto/service_target.proto

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -17,10 +17,6 @@ service ServiceTargetService {
1717
// Envelope for all possible service target messages (requests and responses)
1818
message ServiceTargetMessage {
1919
string request_id = 1;
20-
// W3C Trace Context traceparent header value for distributed tracing.
21-
// Format: "00-{trace-id}-{span-id}-{trace-flags}"
22-
// See: https://www.w3.org/TR/trace-context/
23-
string trace_parent = 98;
2420
ServiceTargetErrorMessage error = 99;
2521
oneof message_type {
2622
RegisterServiceTargetRequest register_service_target_request = 2;

cli/azd/internal/grpcserver/server.go

Lines changed: 72 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import (
1111
"net"
1212

1313
"github.com/azure/azure-dev/cli/azd/pkg/azdext"
14+
"go.opentelemetry.io/otel/propagation"
1415
"google.golang.org/grpc"
1516
"google.golang.org/grpc/codes"
1617
"google.golang.org/grpc/metadata"
@@ -81,7 +82,13 @@ func (s *Server) Start() (*ServerInfo, error) {
8182
var serverInfo ServerInfo
8283

8384
s.grpcServer = grpc.NewServer(
84-
grpc.UnaryInterceptor(s.tokenAuthInterceptor(&serverInfo)),
85+
grpc.ChainUnaryInterceptor(
86+
s.tokenAuthInterceptor(&serverInfo),
87+
traceContextUnaryInterceptor(),
88+
),
89+
grpc.ChainStreamInterceptor(
90+
traceContextStreamInterceptor(),
91+
),
8592
)
8693

8794
// Use ":0" to let the system assign an available random port
@@ -174,3 +181,67 @@ func generateSigningKey() ([]byte, error) {
174181
}
175182
return bytes, nil
176183
}
184+
185+
// traceContextUnaryInterceptor extracts W3C traceparent from incoming gRPC metadata
186+
// and injects it into the context for distributed tracing correlation.
187+
func traceContextUnaryInterceptor() grpc.UnaryServerInterceptor {
188+
return func(
189+
ctx context.Context,
190+
req interface{},
191+
info *grpc.UnaryServerInfo,
192+
handler grpc.UnaryHandler,
193+
) (interface{}, error) {
194+
ctx = extractTraceContextFromMetadata(ctx)
195+
return handler(ctx, req)
196+
}
197+
}
198+
199+
// traceContextStreamInterceptor extracts W3C traceparent from incoming gRPC metadata
200+
// and injects it into the context for distributed tracing correlation.
201+
func traceContextStreamInterceptor() grpc.StreamServerInterceptor {
202+
return func(
203+
srv interface{},
204+
ss grpc.ServerStream,
205+
info *grpc.StreamServerInfo,
206+
handler grpc.StreamHandler,
207+
) error {
208+
ctx := extractTraceContextFromMetadata(ss.Context())
209+
if ctx != ss.Context() {
210+
// Wrap the stream with the new context so downstream handlers see the trace span.
211+
ss = &wrappedServerStream{ServerStream: ss, ctx: ctx}
212+
}
213+
214+
return handler(srv, ss)
215+
}
216+
}
217+
218+
func extractTraceContextFromMetadata(ctx context.Context) context.Context {
219+
md, ok := metadata.FromIncomingContext(ctx)
220+
if !ok {
221+
return ctx
222+
}
223+
224+
traceparents := md.Get(azdext.TraceparentKey)
225+
if len(traceparents) == 0 {
226+
return ctx
227+
}
228+
229+
carrier := propagation.MapCarrier{azdext.TraceparentKey: traceparents[0]}
230+
231+
tracestates := md.Get(azdext.TracestateKey)
232+
if len(tracestates) > 0 && tracestates[0] != "" {
233+
carrier[azdext.TracestateKey] = tracestates[0]
234+
}
235+
236+
return propagation.TraceContext{}.Extract(ctx, carrier)
237+
}
238+
239+
// wrappedServerStream wraps a grpc.ServerStream with a custom context.
240+
type wrappedServerStream struct {
241+
grpc.ServerStream
242+
ctx context.Context
243+
}
244+
245+
func (w *wrappedServerStream) Context() context.Context {
246+
return w.ctx
247+
}

cli/azd/pkg/azdext/azd_client.go

Lines changed: 18 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,13 @@ type AzdClient struct {
3535
// WithAddress sets the address of the `azd` gRPC server.
3636
func WithAddress(address string) AzdClientOption {
3737
return func(c *AzdClient) error {
38-
connection, err := grpc.NewClient(address, grpc.WithTransportCredentials(insecure.NewCredentials()))
38+
opts := []grpc.DialOption{
39+
grpc.WithTransportCredentials(insecure.NewCredentials()),
40+
grpc.WithChainUnaryInterceptor(tracingUnaryInterceptor),
41+
grpc.WithChainStreamInterceptor(tracingStreamInterceptor),
42+
}
43+
44+
connection, err := grpc.NewClient(address, opts...)
3945
if err != nil {
4046
return err
4147
}
@@ -123,7 +129,7 @@ func (c *AzdClient) Deployment() DeploymentServiceClient {
123129
return c.deploymentClient
124130
}
125131

126-
// Deployment returns the deployment service client.
132+
// Events returns the event service client.
127133
func (c *AzdClient) Events() EventServiceClient {
128134
if c.eventsClient == nil {
129135
c.eventsClient = NewEventServiceClient(c.connection)
@@ -189,3 +195,13 @@ func (c *AzdClient) Account() AccountServiceClient {
189195

190196
return c.accountClient
191197
}
198+
199+
func tracingUnaryInterceptor(ctx context.Context, method string, req, reply interface{}, cc *grpc.ClientConn, invoker grpc.UnaryInvoker, opts ...grpc.CallOption) error {
200+
ctx = WithTracing(ctx)
201+
return invoker(ctx, method, req, reply, cc, opts...)
202+
}
203+
204+
func tracingStreamInterceptor(ctx context.Context, desc *grpc.StreamDesc, cc *grpc.ClientConn, method string, streamer grpc.Streamer, opts ...grpc.CallOption) (grpc.ClientStream, error) {
205+
ctx = WithTracing(ctx)
206+
return streamer(ctx, desc, cc, method, opts...)
207+
}

cli/azd/pkg/azdext/context.go

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
// Copyright (c) Microsoft Corporation. All rights reserved.
2+
// Licensed under the MIT License.
3+
4+
package azdext
5+
6+
import (
7+
"context"
8+
"os"
9+
10+
"go.opentelemetry.io/otel/propagation"
11+
)
12+
13+
// NewContext initializes a new context with tracing information extracted from environment variables.
14+
func NewContext() context.Context {
15+
ctx := context.Background()
16+
parent := os.Getenv(TraceparentEnv)
17+
state := os.Getenv(TracestateEnv)
18+
19+
if parent != "" {
20+
tc := propagation.TraceContext{}
21+
return tc.Extract(ctx, propagation.MapCarrier{
22+
TraceparentKey: parent,
23+
TracestateKey: state,
24+
})
25+
}
26+
27+
return ctx
28+
}

cli/azd/pkg/azdext/event.pb.go

Lines changed: 3 additions & 14 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)