From 3253c78ccf04df4fd384fab7dcdebb9964438902 Mon Sep 17 00:00:00 2001 From: Jeffrey Chen Date: Thu, 4 Dec 2025 23:28:13 +0000 Subject: [PATCH 01/10] Add distributed tracing and structured error handling for extensions --- cli/azd/.vscode/cspell.yaml | 3 + cli/azd/grpc/proto/event.proto | 4 + cli/azd/grpc/proto/framework_service.proto | 8 + cli/azd/grpc/proto/service_target.proto | 8 + cli/azd/internal/cmd/errors.go | 20 +++ cli/azd/internal/tracing/fields/domains.go | 3 + cli/azd/pkg/azdext/event.pb.go | 17 +- cli/azd/pkg/azdext/event_message_envelope.go | 10 ++ cli/azd/pkg/azdext/extension_error.go | 152 ++++++++++++++++++ cli/azd/pkg/azdext/framework_service.pb.go | 62 +++++-- .../pkg/azdext/framework_service_envelope.go | 32 ++-- cli/azd/pkg/azdext/service_target.pb.go | 62 +++++-- cli/azd/pkg/azdext/service_target_envelope.go | 32 ++-- cli/azd/pkg/azdext/trace_context.go | 20 +++ cli/azd/pkg/grpcbroker/message_broker.go | 44 +++++ 15 files changed, 426 insertions(+), 51 deletions(-) create mode 100644 cli/azd/pkg/azdext/extension_error.go create mode 100644 cli/azd/pkg/azdext/trace_context.go diff --git a/cli/azd/.vscode/cspell.yaml b/cli/azd/.vscode/cspell.yaml index 4721aa0864e..00bd7a48f7d 100644 --- a/cli/azd/.vscode/cspell.yaml +++ b/cli/azd/.vscode/cspell.yaml @@ -243,6 +243,9 @@ overrides: - userosscache - docstates - dylib + - filename: cli/azd/pkg/azdext/trace_context.go + words: + - traceparent ignorePaths: - "**/*_test.go" - "**/mock*.go" diff --git a/cli/azd/grpc/proto/event.proto b/cli/azd/grpc/proto/event.proto index dba5f76e877..4d4cf35a55b 100644 --- a/cli/azd/grpc/proto/event.proto +++ b/cli/azd/grpc/proto/event.proto @@ -25,6 +25,10 @@ message EventMessage { InvokeServiceHandler invoke_service_handler = 5; ServiceHandlerStatus service_handler_status = 6; } + + // W3C traceparent format for distributed tracing propagation. + // Format: "00-{traceId}-{spanId}-{flags}" + string trace_parent = 98; } // Client subscribes to project-related events diff --git a/cli/azd/grpc/proto/framework_service.proto b/cli/azd/grpc/proto/framework_service.proto index 15f1e610d4e..6e1a5c1e538 100644 --- a/cli/azd/grpc/proto/framework_service.proto +++ b/cli/azd/grpc/proto/framework_service.proto @@ -17,6 +17,10 @@ service FrameworkService { // Envelope for all possible framework service messages (requests and responses) message FrameworkServiceMessage { string request_id = 1; + // W3C Trace Context traceparent header value for distributed tracing. + // Format: "00-{trace-id}-{span-id}-{trace-flags}" + // See: https://www.w3.org/TR/trace-context/ + string trace_parent = 98; FrameworkServiceErrorMessage error = 99; oneof message_type { RegisterFrameworkServiceRequest register_framework_service_request = 2; @@ -41,6 +45,10 @@ message FrameworkServiceMessage { message FrameworkServiceErrorMessage { string message = 1; string details = 2; + // Structured error information for telemetry + string error_code = 3; // Error code from the service (e.g., "Conflict", "NotFound") + int32 status_code = 4; // HTTP status code (e.g., 409, 404, 500) + string service_name = 5; // Service name for telemetry (e.g., "ai.azure.com", "cognitiveservices") } // Request to register a framework service provider diff --git a/cli/azd/grpc/proto/service_target.proto b/cli/azd/grpc/proto/service_target.proto index 794a59ee213..6d3a5e72efc 100644 --- a/cli/azd/grpc/proto/service_target.proto +++ b/cli/azd/grpc/proto/service_target.proto @@ -17,6 +17,10 @@ service ServiceTargetService { // Envelope for all possible service target messages (requests and responses) message ServiceTargetMessage { string request_id = 1; + // W3C Trace Context traceparent header value for distributed tracing. + // Format: "00-{trace-id}-{span-id}-{trace-flags}" + // See: https://www.w3.org/TR/trace-context/ + string trace_parent = 98; ServiceTargetErrorMessage error = 99; oneof message_type { RegisterServiceTargetRequest register_service_target_request = 2; @@ -83,6 +87,10 @@ message RegisterServiceTargetResponse { message ServiceTargetErrorMessage { string message = 2; string details = 3; + // Structured error information for telemetry + string error_code = 4; // Error code from the service (e.g., "Conflict", "NotFound") + int32 status_code = 5; // HTTP status code (e.g., 409, 404, 500) + string service_name = 6; // Service name for telemetry (e.g., "ai.azure.com", "cognitiveservices") } // GetTargetResource request and response diff --git a/cli/azd/internal/cmd/errors.go b/cli/azd/internal/cmd/errors.go index 629f2aa62c9..a69c1195ed3 100644 --- a/cli/azd/internal/cmd/errors.go +++ b/cli/azd/internal/cmd/errors.go @@ -17,6 +17,7 @@ import ( "github.com/azure/azure-dev/cli/azd/internal/tracing/fields" "github.com/azure/azure-dev/cli/azd/pkg/auth" "github.com/azure/azure-dev/cli/azd/pkg/azapi" + "github.com/azure/azure-dev/cli/azd/pkg/azdext" "github.com/azure/azure-dev/cli/azd/pkg/exec" "github.com/azure/azure-dev/cli/azd/pkg/extensions" "go.opentelemetry.io/otel/attribute" @@ -33,6 +34,7 @@ func MapError(err error, span tracing.Span) { var toolExecErr *exec.ExitError var authFailedErr *auth.AuthFailedError var extensionRunErr *extensions.ExtensionRunError + var extRespErr *azdext.ExtensionResponseError if errors.As(err, &respErr) { serviceName := "other" statusCode := -1 @@ -84,6 +86,24 @@ func MapError(err error, span tracing.Span) { errCode = "service.arm.deployment.failed" } else if errors.As(err, &extensionRunErr) { errCode = "ext.run.failed" + } else if errors.As(err, &extRespErr) { + // Handle structured errors from extensions (e.g., service target providers) + if extRespErr.HasServiceInfo() { + // Extension provided service information - use it for telemetry + serviceName, hostDomain := mapService(extRespErr.ServiceName) + errDetails = append(errDetails, + fields.ServiceName.String(serviceName), + fields.ServiceHost.String(hostDomain), + fields.ServiceStatusCode.Int(extRespErr.StatusCode), + ) + if extRespErr.ErrorCode != "" { + errDetails = append(errDetails, fields.ServiceErrorCode.String(extRespErr.ErrorCode)) + } + errCode = fmt.Sprintf("ext.service.%s.%d", serviceName, extRespErr.StatusCode) + } else { + // Extension error without service info + errCode = "ext.service.failed" + } } else if errors.As(err, &toolExecErr) { toolName := "other" cmdName := cmdAsName(toolExecErr.Cmd) diff --git a/cli/azd/internal/tracing/fields/domains.go b/cli/azd/internal/tracing/fields/domains.go index c9ef873b5e9..c0d7228544d 100644 --- a/cli/azd/internal/tracing/fields/domains.go +++ b/cli/azd/internal/tracing/fields/domains.go @@ -29,6 +29,9 @@ var Domains = []Domain{ {"azurefd.net", "frontdoor"}, {"scm.azurewebsites.net", "kudu"}, {"azurewebsites.net", "websites"}, + {"services.ai.azure.com", "ai"}, + {"cognitiveservices.azure.com", "cognitiveservices"}, + {"openai.azure.com", "openai"}, {"blob.core.windows.net", "blob"}, {"cloudapp.azure.com", "vm"}, {"cloudapp.net", "vm"}, diff --git a/cli/azd/pkg/azdext/event.pb.go b/cli/azd/pkg/azdext/event.pb.go index d3a65208189..4b6f1713949 100644 --- a/cli/azd/pkg/azdext/event.pb.go +++ b/cli/azd/pkg/azdext/event.pb.go @@ -35,7 +35,10 @@ type EventMessage struct { // *EventMessage_SubscribeServiceEvent // *EventMessage_InvokeServiceHandler // *EventMessage_ServiceHandlerStatus - MessageType isEventMessage_MessageType `protobuf_oneof:"message_type"` + MessageType isEventMessage_MessageType `protobuf_oneof:"message_type"` + // W3C traceparent format for distributed tracing propagation. + // Format: "00-{traceId}-{spanId}-{flags}" + TraceParent string `protobuf:"bytes,98,opt,name=trace_parent,json=traceParent,proto3" json:"trace_parent,omitempty"` unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache } @@ -131,6 +134,13 @@ func (x *EventMessage) GetServiceHandlerStatus() *ServiceHandlerStatus { return nil } +func (x *EventMessage) GetTraceParent() string { + if x != nil { + return x.TraceParent + } + return "" +} + type isEventMessage_MessageType interface { isEventMessage_MessageType() } @@ -548,14 +558,15 @@ var File_event_proto protoreflect.FileDescriptor const file_event_proto_rawDesc = "" + "\n" + - "\vevent.proto\x12\x06azdext\x1a\fmodels.proto\"\xa8\x04\n" + + "\vevent.proto\x12\x06azdext\x1a\fmodels.proto\"\xcb\x04\n" + "\fEventMessage\x12W\n" + "\x17subscribe_project_event\x18\x01 \x01(\v2\x1d.azdext.SubscribeProjectEventH\x00R\x15subscribeProjectEvent\x12T\n" + "\x16invoke_project_handler\x18\x02 \x01(\v2\x1c.azdext.InvokeProjectHandlerH\x00R\x14invokeProjectHandler\x12T\n" + "\x16project_handler_status\x18\x03 \x01(\v2\x1c.azdext.ProjectHandlerStatusH\x00R\x14projectHandlerStatus\x12W\n" + "\x17subscribe_service_event\x18\x04 \x01(\v2\x1d.azdext.SubscribeServiceEventH\x00R\x15subscribeServiceEvent\x12T\n" + "\x16invoke_service_handler\x18\x05 \x01(\v2\x1c.azdext.InvokeServiceHandlerH\x00R\x14invokeServiceHandler\x12T\n" + - "\x16service_handler_status\x18\x06 \x01(\v2\x1c.azdext.ServiceHandlerStatusH\x00R\x14serviceHandlerStatusB\x0e\n" + + "\x16service_handler_status\x18\x06 \x01(\v2\x1c.azdext.ServiceHandlerStatusH\x00R\x14serviceHandlerStatus\x12!\n" + + "\ftrace_parent\x18b \x01(\tR\vtraceParentB\x0e\n" + "\fmessage_type\"8\n" + "\x15SubscribeProjectEvent\x12\x1f\n" + "\vevent_names\x18\x01 \x03(\tR\n" + diff --git a/cli/azd/pkg/azdext/event_message_envelope.go b/cli/azd/pkg/azdext/event_message_envelope.go index 16ae9bc7457..3072d1956c0 100644 --- a/cli/azd/pkg/azdext/event_message_envelope.go +++ b/cli/azd/pkg/azdext/event_message_envelope.go @@ -96,6 +96,16 @@ func (ops *EventMessageEnvelope) SetError(msg *EventMessage, err error) { // No-op: EventMessage uses status strings, not Error field } +// GetTraceParent returns the W3C traceparent value from the message for distributed tracing. +func (ops *EventMessageEnvelope) GetTraceParent(msg *EventMessage) string { + return msg.GetTraceParent() +} + +// SetTraceParent sets the W3C traceparent value on the message for distributed tracing. +func (ops *EventMessageEnvelope) SetTraceParent(msg *EventMessage, traceParent string) { + msg.TraceParent = traceParent +} + // GetInnerMessage returns the inner message from the oneof field func (ops *EventMessageEnvelope) GetInnerMessage(msg *EventMessage) any { // The MessageType field is a oneof wrapper. We need to extract the actual inner message. diff --git a/cli/azd/pkg/azdext/extension_error.go b/cli/azd/pkg/azdext/extension_error.go new file mode 100644 index 00000000000..4e81eb0e501 --- /dev/null +++ b/cli/azd/pkg/azdext/extension_error.go @@ -0,0 +1,152 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package azdext + +import ( + "errors" + "fmt" + + "github.com/Azure/azure-sdk-for-go/sdk/azcore" +) + +// ExtensionResponseError represents an HTTP response error returned from an extension over gRPC. +// It mirrors azcore.ResponseError and preserves structured error information for telemetry purposes. +type ExtensionResponseError struct { + // Message is the human-readable error message + Message string + // Details contains additional error details + Details string + // ErrorCode is the error code from the service (e.g., "Conflict", "NotFound") + ErrorCode string + // StatusCode is the HTTP status code (e.g., 409, 404, 500) + StatusCode int + // ServiceName is the service name for telemetry (e.g., "ai.azure.com") + ServiceName string +} + +// Error implements the error interface +func (e *ExtensionResponseError) Error() string { + if e.Details != "" { + return fmt.Sprintf("%s: %s", e.Message, e.Details) + } + return e.Message +} + +// HasServiceInfo returns true if the error contains service information for telemetry +func (e *ExtensionResponseError) HasServiceInfo() bool { + return e.StatusCode > 0 && e.ServiceName != "" +} + +// errorMessage defines the common interface for protobuf error messages +// This allows us to write generic unwrap logic for any generated proto message +type errorMessage interface { + comparable + GetMessage() string + GetDetails() string + GetErrorCode() string + GetStatusCode() int32 + GetServiceName() string +} + +// errorInfo is a helper struct to hold extracted error information +// before converting to a specific protobuf message type +type errorInfo struct { + message string + details string + errorCode string + statusCode int32 + service string +} + +// captureErrorInfo extracts structured error information from a Go error. +// It handles nil errors, ExtensionResponseError, and azcore.ResponseError. +func captureErrorInfo(err error) errorInfo { + if err == nil { + return errorInfo{} + } + + // Default to the error string + info := errorInfo{message: err.Error()} + + // If it's already an ExtensionResponseError, preserve all fields including Details + var extErr *ExtensionResponseError + if errors.As(err, &extErr) { + info.message = extErr.Message + info.details = extErr.Details + info.errorCode = extErr.ErrorCode + info.statusCode = int32(extErr.StatusCode) + info.service = extErr.ServiceName + return info + } + + // Try to extract structured error information from Azure SDK errors + var respErr *azcore.ResponseError + if errors.As(err, &respErr) { + info.errorCode = respErr.ErrorCode + info.statusCode = int32(respErr.StatusCode) + if respErr.RawResponse != nil && respErr.RawResponse.Request != nil { + info.service = respErr.RawResponse.Request.Host + } + } + + return info +} + +// WrapErrorForServiceTarget wraps a Go error into a ServiceTargetErrorMessage for transmission over gRPC. +func WrapErrorForServiceTarget(err error) *ServiceTargetErrorMessage { + info := captureErrorInfo(err) + if info.message == "" { + return nil + } + + return &ServiceTargetErrorMessage{ + Message: info.message, + Details: info.details, + ErrorCode: info.errorCode, + StatusCode: info.statusCode, + ServiceName: info.service, + } +} + +// WrapErrorForFrameworkService wraps a Go error into a FrameworkServiceErrorMessage for transmission over gRPC. +func WrapErrorForFrameworkService(err error) *FrameworkServiceErrorMessage { + info := captureErrorInfo(err) + if info.message == "" { + return nil + } + + return &FrameworkServiceErrorMessage{ + Message: info.message, + Details: info.details, + ErrorCode: info.errorCode, + StatusCode: info.statusCode, + ServiceName: info.service, + } +} + +// unwrapError is a generic helper to convert protobuf error messages back to Go errors +func unwrapError[T errorMessage](msg T) error { + var zero T + if msg == zero || msg.GetMessage() == "" { + return nil + } + + return &ExtensionResponseError{ + Message: msg.GetMessage(), + Details: msg.GetDetails(), + ErrorCode: msg.GetErrorCode(), + StatusCode: int(msg.GetStatusCode()), + ServiceName: msg.GetServiceName(), + } +} + +// UnwrapErrorFromServiceTarget converts a ServiceTargetErrorMessage back to a Go error. +func UnwrapErrorFromServiceTarget(msg *ServiceTargetErrorMessage) error { + return unwrapError(msg) +} + +// UnwrapErrorFromFrameworkService converts a FrameworkServiceErrorMessage back to a Go error. +func UnwrapErrorFromFrameworkService(msg *FrameworkServiceErrorMessage) error { + return unwrapError(msg) +} diff --git a/cli/azd/pkg/azdext/framework_service.pb.go b/cli/azd/pkg/azdext/framework_service.pb.go index 24b9c048c3c..48652135121 100644 --- a/cli/azd/pkg/azdext/framework_service.pb.go +++ b/cli/azd/pkg/azdext/framework_service.pb.go @@ -26,9 +26,13 @@ const ( // Envelope for all possible framework service messages (requests and responses) type FrameworkServiceMessage struct { - state protoimpl.MessageState `protogen:"open.v1"` - RequestId string `protobuf:"bytes,1,opt,name=request_id,json=requestId,proto3" json:"request_id,omitempty"` - Error *FrameworkServiceErrorMessage `protobuf:"bytes,99,opt,name=error,proto3" json:"error,omitempty"` + state protoimpl.MessageState `protogen:"open.v1"` + RequestId string `protobuf:"bytes,1,opt,name=request_id,json=requestId,proto3" json:"request_id,omitempty"` + // W3C Trace Context traceparent header value for distributed tracing. + // Format: "00-{trace-id}-{span-id}-{trace-flags}" + // See: https://www.w3.org/TR/trace-context/ + TraceParent string `protobuf:"bytes,98,opt,name=trace_parent,json=traceParent,proto3" json:"trace_parent,omitempty"` + Error *FrameworkServiceErrorMessage `protobuf:"bytes,99,opt,name=error,proto3" json:"error,omitempty"` // Types that are valid to be assigned to MessageType: // // *FrameworkServiceMessage_RegisterFrameworkServiceRequest @@ -88,6 +92,13 @@ func (x *FrameworkServiceMessage) GetRequestId() string { return "" } +func (x *FrameworkServiceMessage) GetTraceParent() string { + if x != nil { + return x.TraceParent + } + return "" +} + func (x *FrameworkServiceMessage) GetError() *FrameworkServiceErrorMessage { if x != nil { return x.Error @@ -337,9 +348,13 @@ func (*FrameworkServiceMessage_ProgressMessage) isFrameworkServiceMessage_Messag // Error message for framework service operations type FrameworkServiceErrorMessage struct { - state protoimpl.MessageState `protogen:"open.v1"` - Message string `protobuf:"bytes,1,opt,name=message,proto3" json:"message,omitempty"` - Details string `protobuf:"bytes,2,opt,name=details,proto3" json:"details,omitempty"` + state protoimpl.MessageState `protogen:"open.v1"` + Message string `protobuf:"bytes,1,opt,name=message,proto3" json:"message,omitempty"` + Details string `protobuf:"bytes,2,opt,name=details,proto3" json:"details,omitempty"` + // Structured error information for telemetry + ErrorCode string `protobuf:"bytes,3,opt,name=error_code,json=errorCode,proto3" json:"error_code,omitempty"` // Error code from the service (e.g., "Conflict", "NotFound") + StatusCode int32 `protobuf:"varint,4,opt,name=status_code,json=statusCode,proto3" json:"status_code,omitempty"` // HTTP status code (e.g., 409, 404, 500) + ServiceName string `protobuf:"bytes,5,opt,name=service_name,json=serviceName,proto3" json:"service_name,omitempty"` // Service name for telemetry (e.g., "ai.azure.com", "cognitiveservices") unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache } @@ -388,6 +403,27 @@ func (x *FrameworkServiceErrorMessage) GetDetails() string { return "" } +func (x *FrameworkServiceErrorMessage) GetErrorCode() string { + if x != nil { + return x.ErrorCode + } + return "" +} + +func (x *FrameworkServiceErrorMessage) GetStatusCode() int32 { + if x != nil { + return x.StatusCode + } + return 0 +} + +func (x *FrameworkServiceErrorMessage) GetServiceName() string { + if x != nil { + return x.ServiceName + } + return "" +} + // Request to register a framework service provider type RegisterFrameworkServiceRequest struct { state protoimpl.MessageState `protogen:"open.v1"` @@ -1315,10 +1351,11 @@ var File_framework_service_proto protoreflect.FileDescriptor const file_framework_service_proto_rawDesc = "" + "\n" + - "\x17framework_service.proto\x12\x06azdext\x1a\fmodels.proto\x1a\x14service_target.proto\"\xc0\f\n" + + "\x17framework_service.proto\x12\x06azdext\x1a\fmodels.proto\x1a\x14service_target.proto\"\xe3\f\n" + "\x17FrameworkServiceMessage\x12\x1d\n" + "\n" + - "request_id\x18\x01 \x01(\tR\trequestId\x12:\n" + + "request_id\x18\x01 \x01(\tR\trequestId\x12!\n" + + "\ftrace_parent\x18b \x01(\tR\vtraceParent\x12:\n" + "\x05error\x18c \x01(\v2$.azdext.FrameworkServiceErrorMessageR\x05error\x12v\n" + "\"register_framework_service_request\x18\x02 \x01(\v2'.azdext.RegisterFrameworkServiceRequestH\x00R\x1fregisterFrameworkServiceRequest\x12y\n" + "#register_framework_service_response\x18\x03 \x01(\v2(.azdext.RegisterFrameworkServiceResponseH\x00R registerFrameworkServiceResponse\x12Z\n" + @@ -1336,10 +1373,15 @@ const file_framework_service_proto_rawDesc = "" + "\x0fpackage_request\x18\x0e \x01(\v2&.azdext.FrameworkServicePackageRequestH\x00R\x0epackageRequest\x12T\n" + "\x10package_response\x18\x0f \x01(\v2'.azdext.FrameworkServicePackageResponseH\x00R\x0fpackageResponse\x12T\n" + "\x10progress_message\x18\x10 \x01(\v2'.azdext.FrameworkServiceProgressMessageH\x00R\x0fprogressMessageB\x0e\n" + - "\fmessage_type\"R\n" + + "\fmessage_type\"\xb5\x01\n" + "\x1cFrameworkServiceErrorMessage\x12\x18\n" + "\amessage\x18\x01 \x01(\tR\amessage\x12\x18\n" + - "\adetails\x18\x02 \x01(\tR\adetails\"=\n" + + "\adetails\x18\x02 \x01(\tR\adetails\x12\x1d\n" + + "\n" + + "error_code\x18\x03 \x01(\tR\terrorCode\x12\x1f\n" + + "\vstatus_code\x18\x04 \x01(\x05R\n" + + "statusCode\x12!\n" + + "\fservice_name\x18\x05 \x01(\tR\vserviceName\"=\n" + "\x1fRegisterFrameworkServiceRequest\x12\x1a\n" + "\blanguage\x18\x01 \x01(\tR\blanguage\"\"\n" + " RegisterFrameworkServiceResponse\"a\n" + diff --git a/cli/azd/pkg/azdext/framework_service_envelope.go b/cli/azd/pkg/azdext/framework_service_envelope.go index b3560d24b69..bf06874f073 100644 --- a/cli/azd/pkg/azdext/framework_service_envelope.go +++ b/cli/azd/pkg/azdext/framework_service_envelope.go @@ -5,7 +5,6 @@ package azdext import ( "context" - "errors" "github.com/azure/azure-dev/cli/azd/pkg/grpcbroker" ) @@ -32,23 +31,16 @@ func (ops *FrameworkServiceEnvelope) SetRequestId(ctx context.Context, msg *Fram msg.RequestId = id } -// GetError returns the error from the message as a Go error type +// GetError returns the error from the message as a Go error type. +// It returns an ExtensionResponseError that preserves structured error information for telemetry. func (ops *FrameworkServiceEnvelope) GetError(msg *FrameworkServiceMessage) error { - if msg.Error == nil || msg.Error.Message == "" { - return nil - } - return errors.New(msg.Error.Message) + return UnwrapErrorFromFrameworkService(msg.Error) } -// SetError sets an error on the message +// SetError sets an error on the message. +// It extracts structured error information from known error types like azcore.ResponseError. func (ops *FrameworkServiceEnvelope) SetError(msg *FrameworkServiceMessage, err error) { - if err == nil { - msg.Error = nil - return - } - msg.Error = &FrameworkServiceErrorMessage{ - Message: err.Error(), - } + msg.Error = WrapErrorForFrameworkService(err) } // GetInnerMessage returns the inner message from the oneof field @@ -118,3 +110,15 @@ func (ops *FrameworkServiceEnvelope) CreateProgressMessage(requestId string, mes }, } } + +// GetTraceParent returns the W3C traceparent header value from the message. +// This is used to propagate distributed tracing context across the gRPC boundary. +func (ops *FrameworkServiceEnvelope) GetTraceParent(msg *FrameworkServiceMessage) string { + return msg.TraceParent +} + +// SetTraceParent sets the W3C traceparent header value on the message. +// This is used to propagate distributed tracing context across the gRPC boundary. +func (ops *FrameworkServiceEnvelope) SetTraceParent(msg *FrameworkServiceMessage, traceParent string) { + msg.TraceParent = traceParent +} diff --git a/cli/azd/pkg/azdext/service_target.pb.go b/cli/azd/pkg/azdext/service_target.pb.go index 6d77c65d44d..2b1b2cf5d32 100644 --- a/cli/azd/pkg/azdext/service_target.pb.go +++ b/cli/azd/pkg/azdext/service_target.pb.go @@ -27,9 +27,13 @@ const ( // Envelope for all possible service target messages (requests and responses) type ServiceTargetMessage struct { - state protoimpl.MessageState `protogen:"open.v1"` - RequestId string `protobuf:"bytes,1,opt,name=request_id,json=requestId,proto3" json:"request_id,omitempty"` - Error *ServiceTargetErrorMessage `protobuf:"bytes,99,opt,name=error,proto3" json:"error,omitempty"` + state protoimpl.MessageState `protogen:"open.v1"` + RequestId string `protobuf:"bytes,1,opt,name=request_id,json=requestId,proto3" json:"request_id,omitempty"` + // W3C Trace Context traceparent header value for distributed tracing. + // Format: "00-{trace-id}-{span-id}-{trace-flags}" + // See: https://www.w3.org/TR/trace-context/ + TraceParent string `protobuf:"bytes,98,opt,name=trace_parent,json=traceParent,proto3" json:"trace_parent,omitempty"` + Error *ServiceTargetErrorMessage `protobuf:"bytes,99,opt,name=error,proto3" json:"error,omitempty"` // Types that are valid to be assigned to MessageType: // // *ServiceTargetMessage_RegisterServiceTargetRequest @@ -89,6 +93,13 @@ func (x *ServiceTargetMessage) GetRequestId() string { return "" } +func (x *ServiceTargetMessage) GetTraceParent() string { + if x != nil { + return x.TraceParent + } + return "" +} + func (x *ServiceTargetMessage) GetError() *ServiceTargetErrorMessage { if x != nil { return x.Error @@ -737,9 +748,13 @@ func (*RegisterServiceTargetResponse) Descriptor() ([]byte, []int) { } type ServiceTargetErrorMessage struct { - state protoimpl.MessageState `protogen:"open.v1"` - Message string `protobuf:"bytes,2,opt,name=message,proto3" json:"message,omitempty"` - Details string `protobuf:"bytes,3,opt,name=details,proto3" json:"details,omitempty"` + state protoimpl.MessageState `protogen:"open.v1"` + Message string `protobuf:"bytes,2,opt,name=message,proto3" json:"message,omitempty"` + Details string `protobuf:"bytes,3,opt,name=details,proto3" json:"details,omitempty"` + // Structured error information for telemetry + ErrorCode string `protobuf:"bytes,4,opt,name=error_code,json=errorCode,proto3" json:"error_code,omitempty"` // Error code from the service (e.g., "Conflict", "NotFound") + StatusCode int32 `protobuf:"varint,5,opt,name=status_code,json=statusCode,proto3" json:"status_code,omitempty"` // HTTP status code (e.g., 409, 404, 500) + ServiceName string `protobuf:"bytes,6,opt,name=service_name,json=serviceName,proto3" json:"service_name,omitempty"` // Service name for telemetry (e.g., "ai.azure.com", "cognitiveservices") unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache } @@ -788,6 +803,27 @@ func (x *ServiceTargetErrorMessage) GetDetails() string { return "" } +func (x *ServiceTargetErrorMessage) GetErrorCode() string { + if x != nil { + return x.ErrorCode + } + return "" +} + +func (x *ServiceTargetErrorMessage) GetStatusCode() int32 { + if x != nil { + return x.StatusCode + } + return 0 +} + +func (x *ServiceTargetErrorMessage) GetServiceName() string { + if x != nil { + return x.ServiceName + } + return "" +} + // GetTargetResource request and response type GetTargetResourceRequest struct { state protoimpl.MessageState `protogen:"open.v1"` @@ -1640,10 +1676,11 @@ var File_service_target_proto protoreflect.FileDescriptor const file_service_target_proto_rawDesc = "" + "\n" + - "\x14service_target.proto\x12\x06azdext\x1a$include/google/protobuf/struct.proto\x1a\fmodels.proto\"\xc2\v\n" + + "\x14service_target.proto\x12\x06azdext\x1a$include/google/protobuf/struct.proto\x1a\fmodels.proto\"\xe5\v\n" + "\x14ServiceTargetMessage\x12\x1d\n" + "\n" + - "request_id\x18\x01 \x01(\tR\trequestId\x127\n" + + "request_id\x18\x01 \x01(\tR\trequestId\x12!\n" + + "\ftrace_parent\x18b \x01(\tR\vtraceParent\x127\n" + "\x05error\x18c \x01(\v2!.azdext.ServiceTargetErrorMessageR\x05error\x12m\n" + "\x1fregister_service_target_request\x18\x02 \x01(\v2$.azdext.RegisterServiceTargetRequestH\x00R\x1cregisterServiceTargetRequest\x12p\n" + " register_service_target_response\x18\x03 \x01(\v2%.azdext.RegisterServiceTargetResponseH\x00R\x1dregisterServiceTargetResponse\x12W\n" + @@ -1686,10 +1723,15 @@ const file_service_target_proto_rawDesc = "" + "\x05value\x18\x02 \x01(\tR\x05value:\x028\x01\"2\n" + "\x1cRegisterServiceTargetRequest\x12\x12\n" + "\x04host\x18\x01 \x01(\tR\x04host\"\x1f\n" + - "\x1dRegisterServiceTargetResponse\"O\n" + + "\x1dRegisterServiceTargetResponse\"\xb2\x01\n" + "\x19ServiceTargetErrorMessage\x12\x18\n" + "\amessage\x18\x02 \x01(\tR\amessage\x12\x18\n" + - "\adetails\x18\x03 \x01(\tR\adetails\"\xf6\x01\n" + + "\adetails\x18\x03 \x01(\tR\adetails\x12\x1d\n" + + "\n" + + "error_code\x18\x04 \x01(\tR\terrorCode\x12\x1f\n" + + "\vstatus_code\x18\x05 \x01(\x05R\n" + + "statusCode\x12!\n" + + "\fservice_name\x18\x06 \x01(\tR\vserviceName\"\xf6\x01\n" + "\x18GetTargetResourceRequest\x12'\n" + "\x0fsubscription_id\x18\x01 \x01(\tR\x0esubscriptionId\x12<\n" + "\x0eservice_config\x18\x02 \x01(\v2\x15.azdext.ServiceConfigR\rserviceConfig\x12N\n" + diff --git a/cli/azd/pkg/azdext/service_target_envelope.go b/cli/azd/pkg/azdext/service_target_envelope.go index 5ed150a618a..869dc83b21b 100644 --- a/cli/azd/pkg/azdext/service_target_envelope.go +++ b/cli/azd/pkg/azdext/service_target_envelope.go @@ -5,7 +5,6 @@ package azdext import ( "context" - "errors" "github.com/azure/azure-dev/cli/azd/pkg/grpcbroker" ) @@ -32,23 +31,16 @@ func (ops *ServiceTargetEnvelope) SetRequestId(ctx context.Context, msg *Service msg.RequestId = id } -// GetError returns the error from the message as a Go error type +// GetError returns the error from the message as a Go error type. +// It returns an ExtensionResponseError that preserves structured error information for telemetry. func (ops *ServiceTargetEnvelope) GetError(msg *ServiceTargetMessage) error { - if msg.Error == nil || msg.Error.Message == "" { - return nil - } - return errors.New(msg.Error.Message) + return UnwrapErrorFromServiceTarget(msg.Error) } -// SetError sets an error on the message +// SetError sets an error on the message. +// It extracts structured error information from known error types like azcore.ResponseError. func (ops *ServiceTargetEnvelope) SetError(msg *ServiceTargetMessage, err error) { - if err == nil { - msg.Error = nil - return - } - msg.Error = &ServiceTargetErrorMessage{ - Message: err.Error(), - } + msg.Error = WrapErrorForServiceTarget(err) } // GetInnerMessage returns the inner message from the oneof field @@ -118,3 +110,15 @@ func (ops *ServiceTargetEnvelope) CreateProgressMessage(requestId string, messag }, } } + +// GetTraceParent returns the W3C traceparent header value from the message. +// This is used to propagate distributed tracing context across the gRPC boundary. +func (ops *ServiceTargetEnvelope) GetTraceParent(msg *ServiceTargetMessage) string { + return msg.TraceParent +} + +// SetTraceParent sets the W3C traceparent header value on the message. +// This is used to propagate distributed tracing context across the gRPC boundary. +func (ops *ServiceTargetEnvelope) SetTraceParent(msg *ServiceTargetMessage, traceParent string) { + msg.TraceParent = traceParent +} diff --git a/cli/azd/pkg/azdext/trace_context.go b/cli/azd/pkg/azdext/trace_context.go new file mode 100644 index 00000000000..1cfed6a82c2 --- /dev/null +++ b/cli/azd/pkg/azdext/trace_context.go @@ -0,0 +1,20 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package azdext + +import ( + "context" + + "go.opentelemetry.io/otel/propagation" +) + +const traceparentKey = "traceparent" + +// GetTraceParentFromContext extracts the current span context and formats it as a W3C traceparent string. +// Returns empty string if no valid span context is found. +func GetTraceParentFromContext(ctx context.Context) string { + carrier := propagation.MapCarrier{} + propagation.TraceContext{}.Inject(ctx, carrier) + return carrier.Get(traceparentKey) +} diff --git a/cli/azd/pkg/grpcbroker/message_broker.go b/cli/azd/pkg/grpcbroker/message_broker.go index 8a553c09694..da3e6e879d5 100644 --- a/cli/azd/pkg/grpcbroker/message_broker.go +++ b/cli/azd/pkg/grpcbroker/message_broker.go @@ -13,10 +13,13 @@ import ( "strings" "sync" + "go.opentelemetry.io/otel/propagation" "google.golang.org/grpc/codes" "google.golang.org/grpc/status" ) +const traceparentKey = "traceparent" + // ProgressFunc is a callback function for sending progress updates during handler execution type ProgressFunc func(message string) @@ -53,6 +56,15 @@ type MessageEnvelope[T any] interface { // CreateProgressMessage creates a new progress message envelope with the given text. // This is used by server-side handlers to send progress updates back to clients. CreateProgressMessage(requestId string, message string) *T + + // GetTraceParent returns the W3C traceparent header value from the message. + // This is used to propagate distributed tracing context across the gRPC boundary. + // Returns empty string if no trace context is present. + GetTraceParent(msg *T) string + + // SetTraceParent sets the W3C traceparent header value on the message. + // This is used to propagate distributed tracing context across the gRPC boundary. + SetTraceParent(msg *T, traceParent string) } // handlerWrapper wraps a registered handler function with metadata @@ -188,6 +200,18 @@ func (mb *MessageBroker[TMessage]) On(handler any) error { return nil } +// injectTraceContext injects the current trace context into the message envelope +func (mb *MessageBroker[TMessage]) injectTraceContext(ctx context.Context, msg *TMessage) { + tc := propagation.TraceContext{} + // Create a carrier that writes to the message via the envelope + carrier := propagation.MapCarrier{} + tc.Inject(ctx, carrier) + + if traceParent, ok := carrier[traceparentKey]; ok { + mb.envelope.SetTraceParent(msg, traceParent) + } +} + // SendAndWait sends a message and waits for the response func (mb *MessageBroker[TMessage]) SendAndWait(ctx context.Context, msg *TMessage) (*TMessage, error) { requestId := mb.envelope.GetRequestId(ctx, msg) @@ -195,6 +219,9 @@ func (mb *MessageBroker[TMessage]) SendAndWait(ctx context.Context, msg *TMessag return nil, errors.New("message must have a RequestId") } + // Inject trace context + mb.injectTraceContext(ctx, msg) + innerMsg := mb.envelope.GetInnerMessage(msg) msgType := reflect.TypeOf(innerMsg) log.Printf("[%s] [RequestId=%s] Sending request, MessageType=%v", mb.name, requestId, msgType) @@ -206,6 +233,8 @@ func (mb *MessageBroker[TMessage]) SendAndWait(ctx context.Context, msg *TMessag // Send request in goroutine to ensure we're waiting before response arrives errCh := make(chan error, 1) go func() { + mb.sendMu.Lock() + defer mb.sendMu.Unlock() errCh <- mb.stream.Send(msg) }() @@ -255,6 +284,9 @@ func (mb *MessageBroker[TMessage]) SendAndWait(ctx context.Context, msg *TMessag // where no response is expected or needed. // Returns an error only if the send operation itself fails. func (mb *MessageBroker[TMessage]) Send(ctx context.Context, msg *TMessage) error { + // Inject trace context + mb.injectTraceContext(ctx, msg) + innerMsg := mb.envelope.GetInnerMessage(msg) msgType := reflect.TypeOf(innerMsg) requestId := mb.envelope.GetRequestId(ctx, msg) @@ -296,6 +328,9 @@ func (mb *MessageBroker[TMessage]) SendAndWaitWithProgress( return nil, errors.New("message must have a RequestId") } + // Inject trace context + mb.injectTraceContext(ctx, msg) + innerMsg := mb.envelope.GetInnerMessage(msg) msgType := reflect.TypeOf(innerMsg) @@ -312,6 +347,8 @@ func (mb *MessageBroker[TMessage]) SendAndWaitWithProgress( log.Printf("[%s] [RequestId=%s] Sending request, MessageType=%v", mb.name, requestId, msgType) errCh := make(chan error, 1) go func() { + mb.sendMu.Lock() + defer mb.sendMu.Unlock() errCh <- mb.stream.Send(msg) }() @@ -578,6 +615,13 @@ func (mb *MessageBroker[TMessage]) invokeHandler( ) *TMessage { requestId := mb.envelope.GetRequestId(ctx, envelope) + // Extract trace context from the envelope and inject into the context + // This propagates distributed tracing across the gRPC boundary + if traceParent := mb.envelope.GetTraceParent(envelope); traceParent != "" { + tc := propagation.TraceContext{} + ctx = tc.Extract(ctx, propagation.MapCarrier{traceparentKey: traceParent}) + } + // Prepare arguments for handler invocation args := []reflect.Value{ reflect.ValueOf(ctx), From f39ee9be22b0abe069aaa1003933b789eba23299 Mon Sep 17 00:00:00 2001 From: Jeffrey Chen Date: Fri, 5 Dec 2025 00:43:16 +0000 Subject: [PATCH 02/10] Add tests --- cli/azd/internal/cmd/errors_test.go | 18 +++ cli/azd/pkg/azdext/extension_error_test.go | 118 ++++++++++++++++++ cli/azd/pkg/grpcbroker/message_broker_test.go | 9 ++ 3 files changed, 145 insertions(+) create mode 100644 cli/azd/pkg/azdext/extension_error_test.go diff --git a/cli/azd/internal/cmd/errors_test.go b/cli/azd/internal/cmd/errors_test.go index f4706ff3193..4c16619dff8 100644 --- a/cli/azd/internal/cmd/errors_test.go +++ b/cli/azd/internal/cmd/errors_test.go @@ -13,6 +13,7 @@ import ( "github.com/azure/azure-dev/cli/azd/internal/tracing/fields" "github.com/azure/azure-dev/cli/azd/pkg/auth" "github.com/azure/azure-dev/cli/azd/pkg/azapi" + "github.com/azure/azure-dev/cli/azd/pkg/azdext" "github.com/azure/azure-dev/cli/azd/pkg/exec" "github.com/azure/azure-dev/cli/azd/test/mocks/mocktracing" "github.com/stretchr/testify/require" @@ -148,6 +149,23 @@ func Test_MapError(t *testing.T) { fields.ErrorKey(fields.ServiceCorrelationId.Key).String("12345"), }, }, + { + name: "WithExtensionResponseError", + err: &azdext.ExtensionResponseError{ + Message: "Rate limit exceeded", + Details: "Too many requests", + ErrorCode: "RateLimitExceeded", + StatusCode: 429, + ServiceName: "openai.azure.com", + }, + wantErrReason: "ext.service.openai.429", + wantErrDetails: []attribute.KeyValue{ + fields.ErrorKey(fields.ServiceName.Key).String("openai"), + fields.ErrorKey(fields.ServiceHost.Key).String("openai.azure.com"), + fields.ErrorKey(fields.ServiceStatusCode.Key).Int(429), + fields.ErrorKey(fields.ServiceErrorCode.Key).String("RateLimitExceeded"), + }, + }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { diff --git a/cli/azd/pkg/azdext/extension_error_test.go b/cli/azd/pkg/azdext/extension_error_test.go new file mode 100644 index 00000000000..fcfdc42c26b --- /dev/null +++ b/cli/azd/pkg/azdext/extension_error_test.go @@ -0,0 +1,118 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package azdext + +import ( + "errors" + "testing" + + "github.com/Azure/azure-sdk-for-go/sdk/azcore" + "github.com/stretchr/testify/assert" +) + +func TestExtensionError_RoundTrip(t *testing.T) { + tests := []struct { + name string + inputErr error + wantInfo errorInfo + useErrorStringAsMessage bool + }{ + { + name: "NilError", + inputErr: nil, + wantInfo: errorInfo{}, + }, + { + name: "SimpleError", + inputErr: errors.New("simple error"), + wantInfo: errorInfo{ + message: "simple error", + }, + }, + { + name: "ExtensionResponseError", + inputErr: &ExtensionResponseError{ + Message: "Rate limit exceeded", + Details: "Too many requests", + ErrorCode: "RateLimitExceeded", + StatusCode: 429, + ServiceName: "openai.azure.com", + }, + wantInfo: errorInfo{ + message: "Rate limit exceeded", + details: "Too many requests", + errorCode: "RateLimitExceeded", + statusCode: 429, + service: "openai.azure.com", + }, + }, + { + name: "AzCoreResponseError", + inputErr: &azcore.ResponseError{ + ErrorCode: "ResourceNotFound", + StatusCode: 404, + }, + wantInfo: errorInfo{ + errorCode: "ResourceNotFound", + statusCode: 404, + }, + useErrorStringAsMessage: true, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if tt.useErrorStringAsMessage { + tt.wantInfo.message = tt.inputErr.Error() + } + + // Helper to verify the proto message content + type protoMessage interface { + GetMessage() string + GetDetails() string + GetErrorCode() string + GetStatusCode() int32 + GetServiceName() string + } + verifyProto := func(t *testing.T, msg protoMessage) { + assert.NotNil(t, msg) + assert.Equal(t, tt.wantInfo.message, msg.GetMessage()) + assert.Equal(t, tt.wantInfo.details, msg.GetDetails()) + assert.Equal(t, tt.wantInfo.errorCode, msg.GetErrorCode()) + assert.Equal(t, tt.wantInfo.statusCode, msg.GetStatusCode()) + assert.Equal(t, tt.wantInfo.service, msg.GetServiceName()) + } + + // Helper to verify the unwrapped error + verifyUnwrapped := func(t *testing.T, err error) { + var extErr *ExtensionResponseError + if assert.ErrorAs(t, err, &extErr) { + assert.Equal(t, tt.wantInfo.message, extErr.Message) + assert.Equal(t, tt.wantInfo.details, extErr.Details) + assert.Equal(t, tt.wantInfo.errorCode, extErr.ErrorCode) + assert.Equal(t, int(tt.wantInfo.statusCode), extErr.StatusCode) + assert.Equal(t, tt.wantInfo.service, extErr.ServiceName) + } + } + + // Test ServiceTarget wrapping + stMsg := WrapErrorForServiceTarget(tt.inputErr) + if tt.inputErr == nil { + assert.Nil(t, stMsg) + } else { + verifyProto(t, stMsg) + verifyUnwrapped(t, UnwrapErrorFromServiceTarget(stMsg)) + } + + // Test FrameworkService wrapping + fsMsg := WrapErrorForFrameworkService(tt.inputErr) + if tt.inputErr == nil { + assert.Nil(t, fsMsg) + } else { + verifyProto(t, fsMsg) + verifyUnwrapped(t, UnwrapErrorFromFrameworkService(fsMsg)) + } + }) + } +} diff --git a/cli/azd/pkg/grpcbroker/message_broker_test.go b/cli/azd/pkg/grpcbroker/message_broker_test.go index 1cdc451f72b..19eb46f4a78 100644 --- a/cli/azd/pkg/grpcbroker/message_broker_test.go +++ b/cli/azd/pkg/grpcbroker/message_broker_test.go @@ -24,6 +24,7 @@ type TestMessage struct { InnerMsg any IsProgress bool ProgressText string + TraceParent string } // Test request/response types for handler testing @@ -164,6 +165,14 @@ func (e *SimpleMessageEnvelope) CreateProgressMessage(requestId string, message } } +func (e *SimpleMessageEnvelope) GetTraceParent(msg *TestMessage) string { + return msg.TraceParent +} + +func (e *SimpleMessageEnvelope) SetTraceParent(msg *TestMessage, traceParent string) { + msg.TraceParent = traceParent +} + // TestOn_RegistersHandler tests that handlers are registered correctly func TestOn_RegistersHandler(t *testing.T) { sim := NewSimulatedBidiStream() From a0b4908941ce91e5869a4e62fe37efb33d617a00 Mon Sep 17 00:00:00 2001 From: Jeffrey Chen Date: Fri, 5 Dec 2025 00:47:15 +0000 Subject: [PATCH 03/10] Fix lint --- cli/azd/pkg/azdext/extension_error.go | 2 ++ 1 file changed, 2 insertions(+) diff --git a/cli/azd/pkg/azdext/extension_error.go b/cli/azd/pkg/azdext/extension_error.go index 4e81eb0e501..06f05b2e290 100644 --- a/cli/azd/pkg/azdext/extension_error.go +++ b/cli/azd/pkg/azdext/extension_error.go @@ -75,6 +75,7 @@ func captureErrorInfo(err error) errorInfo { info.message = extErr.Message info.details = extErr.Details info.errorCode = extErr.ErrorCode + //nolint:gosec // G115: HTTP status codes are well within int32 range info.statusCode = int32(extErr.StatusCode) info.service = extErr.ServiceName return info @@ -84,6 +85,7 @@ func captureErrorInfo(err error) errorInfo { var respErr *azcore.ResponseError if errors.As(err, &respErr) { info.errorCode = respErr.ErrorCode + //nolint:gosec // G115: HTTP status codes are well within int32 range info.statusCode = int32(respErr.StatusCode) if respErr.RawResponse != nil && respErr.RawResponse.Request != nil { info.service = respErr.RawResponse.Request.Host From 3732a291504c804ece50495072d8275677ecc376 Mon Sep 17 00:00:00 2001 From: Jeffrey Chen Date: Fri, 5 Dec 2025 21:33:07 +0000 Subject: [PATCH 04/10] Propagate trace context to the extension process --- cli/azd/cmd/extensions.go | 5 ++++ cli/azd/docs/extension-framework.md | 36 +++++++++++++++++++++++++++++ cli/azd/pkg/azdext/trace_context.go | 15 +++++++----- 3 files changed, 50 insertions(+), 6 deletions(-) diff --git a/cli/azd/cmd/extensions.go b/cli/azd/cmd/extensions.go index 2944b516788..d73dafa5230 100644 --- a/cli/azd/cmd/extensions.go +++ b/cli/azd/cmd/extensions.go @@ -176,6 +176,11 @@ func (a *extensionAction) Run(ctx context.Context) (*actions.ActionResult, error fmt.Sprintf("AZD_ACCESS_TOKEN=%s", jwtToken), ) + // Propagate trace context to the extension process + if traceEnv := tracing.Environ(ctx); len(traceEnv) > 0 { + allEnv = append(allEnv, traceEnv...) + } + options := &extensions.InvokeOptions{ Args: a.args, Env: allEnv, diff --git a/cli/azd/docs/extension-framework.md b/cli/azd/docs/extension-framework.md index 4842b7ab442..25ddc2a7a45 100644 --- a/cli/azd/docs/extension-framework.md +++ b/cli/azd/docs/extension-framework.md @@ -334,6 +334,42 @@ The build process automatically creates binaries for multiple platforms and arch > [!NOTE] > Build times may vary depending on your hardware and extension complexity. +### Distributed Tracing + +`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. + +**Update `main.go`:** + +```go +import ( + "context" + "os" + "github.com/azure/azure-dev/cli/azd/pkg/azdext" +) + +func main() { + ctx := context.Background() + + // Hydrate context with traceparent from environment if present + // This ensures the extension process participates in the active trace + if traceparent := os.Getenv("TRACEPARENT"); traceparent != "" { + ctx = azdext.ContextFromTraceParent(ctx, traceparent) + } + + rootCmd := cmd.NewRootCommand() + + if err := rootCmd.ExecuteContext(ctx); err != nil { + // Handle error + } +} +``` + +The `ExtensionHost` automatically handles trace propagation for all gRPC communication: +1. **Outgoing**: Traces are injected into messages sent back to `azd` core. +2. **Incoming**: Traces from `azd` core are extracted and injected into the `context.Context` passed to your handlers. + +This ensures that calls to Azure SDKs (which support `TRACEPARENT`) within your handlers will automatically be correlated. + ### Developer Extension The easiest way to get started building extensions is to install the `azd` Developer extension. diff --git a/cli/azd/pkg/azdext/trace_context.go b/cli/azd/pkg/azdext/trace_context.go index 1cfed6a82c2..8f2cec6b656 100644 --- a/cli/azd/pkg/azdext/trace_context.go +++ b/cli/azd/pkg/azdext/trace_context.go @@ -11,10 +11,13 @@ import ( const traceparentKey = "traceparent" -// GetTraceParentFromContext extracts the current span context and formats it as a W3C traceparent string. -// Returns empty string if no valid span context is found. -func GetTraceParentFromContext(ctx context.Context) string { - carrier := propagation.MapCarrier{} - propagation.TraceContext{}.Inject(ctx, carrier) - return carrier.Get(traceparentKey) +// ContextFromTraceParent creates a new context with the span context extracted from the traceparent string. +func ContextFromTraceParent(ctx context.Context, traceparent string) context.Context { + if traceparent == "" { + return ctx + } + carrier := propagation.MapCarrier{ + traceparentKey: traceparent, + } + return propagation.TraceContext{}.Extract(ctx, carrier) } From eea682a13560aef48d258c38503ce6345feabf05 Mon Sep 17 00:00:00 2001 From: Jeffrey Chen Date: Mon, 8 Dec 2025 21:49:19 +0000 Subject: [PATCH 05/10] Use gRPC metadata to pass trace context --- cli/azd/.vscode/cspell.yaml | 3 + cli/azd/cmd/middleware/extensions.go | 6 ++ cli/azd/docs/extension-framework.md | 44 +++++------ cli/azd/grpc/proto/event.proto | 4 - cli/azd/grpc/proto/framework_service.proto | 4 - cli/azd/grpc/proto/service_target.proto | 4 - cli/azd/internal/grpcserver/server.go | 73 ++++++++++++++++++- cli/azd/pkg/azdext/azd_client.go | 20 ++++- cli/azd/pkg/azdext/context.go | 28 +++++++ cli/azd/pkg/azdext/event.pb.go | 17 +---- cli/azd/pkg/azdext/event_message_envelope.go | 10 --- cli/azd/pkg/azdext/framework_service.pb.go | 22 ++---- .../pkg/azdext/framework_service_envelope.go | 12 --- cli/azd/pkg/azdext/service_target.pb.go | 22 ++---- cli/azd/pkg/azdext/service_target_envelope.go | 12 --- cli/azd/pkg/azdext/trace_context.go | 28 +++++-- cli/azd/pkg/grpcbroker/message_broker.go | 40 ---------- 17 files changed, 180 insertions(+), 169 deletions(-) create mode 100644 cli/azd/pkg/azdext/context.go diff --git a/cli/azd/.vscode/cspell.yaml b/cli/azd/.vscode/cspell.yaml index 00bd7a48f7d..09dadf7f11f 100644 --- a/cli/azd/.vscode/cspell.yaml +++ b/cli/azd/.vscode/cspell.yaml @@ -246,6 +246,9 @@ overrides: - filename: cli/azd/pkg/azdext/trace_context.go words: - traceparent + - filename: internal/grpcserver/server.go + words: + - traceparents ignorePaths: - "**/*_test.go" - "**/mock*.go" diff --git a/cli/azd/cmd/middleware/extensions.go b/cli/azd/cmd/middleware/extensions.go index ed21d5c4b68..40149f7613b 100644 --- a/cli/azd/cmd/middleware/extensions.go +++ b/cli/azd/cmd/middleware/extensions.go @@ -15,6 +15,7 @@ import ( "github.com/azure/azure-dev/cli/azd/cmd/actions" "github.com/azure/azure-dev/cli/azd/internal/grpcserver" + "github.com/azure/azure-dev/cli/azd/internal/tracing" "github.com/azure/azure-dev/cli/azd/pkg/extensions" "github.com/azure/azure-dev/cli/azd/pkg/input" "github.com/azure/azure-dev/cli/azd/pkg/ioc" @@ -132,6 +133,11 @@ func (m *ExtensionsMiddleware) Run(ctx context.Context, next NextFn) (*actions.A allEnv = append(allEnv, "FORCE_COLOR=1") } + // Propagate trace context to the extension process + if traceEnv := tracing.Environ(ctx); len(traceEnv) > 0 { + allEnv = append(allEnv, traceEnv...) + } + args := []string{"listen"} if debugEnabled, _ := m.options.Flags.GetBool("debug"); debugEnabled { args = append(args, "--debug") diff --git a/cli/azd/docs/extension-framework.md b/cli/azd/docs/extension-framework.md index 25ddc2a7a45..42f292ac10b 100644 --- a/cli/azd/docs/extension-framework.md +++ b/cli/azd/docs/extension-framework.md @@ -336,40 +336,32 @@ The build process automatically creates binaries for multiple platforms and arch ### Distributed Tracing -`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. +`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. -**Update `main.go`:** +Use `azdext.NewContext()` to hydrate the root context with trace context: ```go -import ( - "context" - "os" - "github.com/azure/azure-dev/cli/azd/pkg/azdext" -) - func main() { - ctx := context.Background() + ctx := azdext.NewContext() + rootCmd := cmd.NewRootCommand() + if err := rootCmd.ExecuteContext(ctx); err != nil { + // Handle error + } +} +``` - // Hydrate context with traceparent from environment if present - // This ensures the extension process participates in the active trace - if traceparent := os.Getenv("TRACEPARENT"); traceparent != "" { - ctx = azdext.ContextFromTraceParent(ctx, traceparent) - } +To correlate Azure SDK calls with the parent trace, add the correlation policy to your client options: - rootCmd := cmd.NewRootCommand() +```go +import "github.com/azure/azure-dev/cli/azd/pkg/azsdk" - if err := rootCmd.ExecuteContext(ctx); err != nil { - // Handle error - } +clientOptions := &policy.ClientOptions{ + PerCallPolicies: []policy.Policy{ + azsdk.NewMsCorrelationPolicy(), + }, } ``` -The `ExtensionHost` automatically handles trace propagation for all gRPC communication: -1. **Outgoing**: Traces are injected into messages sent back to `azd` core. -2. **Incoming**: Traces from `azd` core are extracted and injected into the `context.Context` passed to your handlers. - -This ensures that calls to Azure SDKs (which support `TRACEPARENT`) within your handlers will automatically be correlated. - ### Developer Extension 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 // Register the framework provider func main() { - ctx := azdext.WithAccessToken(context.Background()) + ctx := azdext.WithAccessToken(azdext.NewContext()) azdClient, err := azdext.NewAzdClient() if err != nil { log.Fatal(err) @@ -2047,7 +2039,7 @@ func (v *VMServiceTargetProvider) Endpoints(ctx context.Context, serviceConfig * // Register the service target provider func main() { - ctx := azdext.WithAccessToken(context.Background()) + ctx := azdext.WithAccessToken(azdext.NewContext()) azdClient, err := azdext.NewAzdClient() if err != nil { log.Fatal(err) diff --git a/cli/azd/grpc/proto/event.proto b/cli/azd/grpc/proto/event.proto index 4d4cf35a55b..dba5f76e877 100644 --- a/cli/azd/grpc/proto/event.proto +++ b/cli/azd/grpc/proto/event.proto @@ -25,10 +25,6 @@ message EventMessage { InvokeServiceHandler invoke_service_handler = 5; ServiceHandlerStatus service_handler_status = 6; } - - // W3C traceparent format for distributed tracing propagation. - // Format: "00-{traceId}-{spanId}-{flags}" - string trace_parent = 98; } // Client subscribes to project-related events diff --git a/cli/azd/grpc/proto/framework_service.proto b/cli/azd/grpc/proto/framework_service.proto index 6e1a5c1e538..26fb3c09ce4 100644 --- a/cli/azd/grpc/proto/framework_service.proto +++ b/cli/azd/grpc/proto/framework_service.proto @@ -17,10 +17,6 @@ service FrameworkService { // Envelope for all possible framework service messages (requests and responses) message FrameworkServiceMessage { string request_id = 1; - // W3C Trace Context traceparent header value for distributed tracing. - // Format: "00-{trace-id}-{span-id}-{trace-flags}" - // See: https://www.w3.org/TR/trace-context/ - string trace_parent = 98; FrameworkServiceErrorMessage error = 99; oneof message_type { RegisterFrameworkServiceRequest register_framework_service_request = 2; diff --git a/cli/azd/grpc/proto/service_target.proto b/cli/azd/grpc/proto/service_target.proto index 6d3a5e72efc..3a57956b535 100644 --- a/cli/azd/grpc/proto/service_target.proto +++ b/cli/azd/grpc/proto/service_target.proto @@ -17,10 +17,6 @@ service ServiceTargetService { // Envelope for all possible service target messages (requests and responses) message ServiceTargetMessage { string request_id = 1; - // W3C Trace Context traceparent header value for distributed tracing. - // Format: "00-{trace-id}-{span-id}-{trace-flags}" - // See: https://www.w3.org/TR/trace-context/ - string trace_parent = 98; ServiceTargetErrorMessage error = 99; oneof message_type { RegisterServiceTargetRequest register_service_target_request = 2; diff --git a/cli/azd/internal/grpcserver/server.go b/cli/azd/internal/grpcserver/server.go index fc1873b0c31..eb2e58aef17 100644 --- a/cli/azd/internal/grpcserver/server.go +++ b/cli/azd/internal/grpcserver/server.go @@ -11,6 +11,7 @@ import ( "net" "github.com/azure/azure-dev/cli/azd/pkg/azdext" + "go.opentelemetry.io/otel/propagation" "google.golang.org/grpc" "google.golang.org/grpc/codes" "google.golang.org/grpc/metadata" @@ -81,7 +82,13 @@ func (s *Server) Start() (*ServerInfo, error) { var serverInfo ServerInfo s.grpcServer = grpc.NewServer( - grpc.UnaryInterceptor(s.tokenAuthInterceptor(&serverInfo)), + grpc.ChainUnaryInterceptor( + s.tokenAuthInterceptor(&serverInfo), + traceContextUnaryInterceptor(), + ), + grpc.ChainStreamInterceptor( + traceContextStreamInterceptor(), + ), ) // Use ":0" to let the system assign an available random port @@ -174,3 +181,67 @@ func generateSigningKey() ([]byte, error) { } return bytes, nil } + +// traceContextUnaryInterceptor extracts W3C traceparent from incoming gRPC metadata +// and injects it into the context for distributed tracing correlation. +func traceContextUnaryInterceptor() grpc.UnaryServerInterceptor { + return func( + ctx context.Context, + req interface{}, + info *grpc.UnaryServerInfo, + handler grpc.UnaryHandler, + ) (interface{}, error) { + ctx = extractTraceContextFromMetadata(ctx) + return handler(ctx, req) + } +} + +// traceContextStreamInterceptor extracts W3C traceparent from incoming gRPC metadata +// and injects it into the context for distributed tracing correlation. +func traceContextStreamInterceptor() grpc.StreamServerInterceptor { + return func( + srv interface{}, + ss grpc.ServerStream, + info *grpc.StreamServerInfo, + handler grpc.StreamHandler, + ) error { + ctx := extractTraceContextFromMetadata(ss.Context()) + if ctx != ss.Context() { + // Wrap the stream with the new context so downstream handlers see the trace span. + ss = &wrappedServerStream{ServerStream: ss, ctx: ctx} + } + + return handler(srv, ss) + } +} + +func extractTraceContextFromMetadata(ctx context.Context) context.Context { + md, ok := metadata.FromIncomingContext(ctx) + if !ok { + return ctx + } + + traceparents := md.Get(azdext.TraceparentKey) + if len(traceparents) == 0 { + return ctx + } + + carrier := propagation.MapCarrier{azdext.TraceparentKey: traceparents[0]} + + tracestates := md.Get(azdext.TracestateKey) + if len(tracestates) > 0 && tracestates[0] != "" { + carrier[azdext.TracestateKey] = tracestates[0] + } + + return propagation.TraceContext{}.Extract(ctx, carrier) +} + +// wrappedServerStream wraps a grpc.ServerStream with a custom context. +type wrappedServerStream struct { + grpc.ServerStream + ctx context.Context +} + +func (w *wrappedServerStream) Context() context.Context { + return w.ctx +} diff --git a/cli/azd/pkg/azdext/azd_client.go b/cli/azd/pkg/azdext/azd_client.go index e68f77c208c..d946fecfb18 100644 --- a/cli/azd/pkg/azdext/azd_client.go +++ b/cli/azd/pkg/azdext/azd_client.go @@ -35,7 +35,13 @@ type AzdClient struct { // WithAddress sets the address of the `azd` gRPC server. func WithAddress(address string) AzdClientOption { return func(c *AzdClient) error { - connection, err := grpc.NewClient(address, grpc.WithTransportCredentials(insecure.NewCredentials())) + opts := []grpc.DialOption{ + grpc.WithTransportCredentials(insecure.NewCredentials()), + grpc.WithChainUnaryInterceptor(tracingUnaryInterceptor), + grpc.WithChainStreamInterceptor(tracingStreamInterceptor), + } + + connection, err := grpc.NewClient(address, opts...) if err != nil { return err } @@ -123,7 +129,7 @@ func (c *AzdClient) Deployment() DeploymentServiceClient { return c.deploymentClient } -// Deployment returns the deployment service client. +// Events returns the event service client. func (c *AzdClient) Events() EventServiceClient { if c.eventsClient == nil { c.eventsClient = NewEventServiceClient(c.connection) @@ -189,3 +195,13 @@ func (c *AzdClient) Account() AccountServiceClient { return c.accountClient } + +func tracingUnaryInterceptor(ctx context.Context, method string, req, reply interface{}, cc *grpc.ClientConn, invoker grpc.UnaryInvoker, opts ...grpc.CallOption) error { + ctx = WithTracing(ctx) + return invoker(ctx, method, req, reply, cc, opts...) +} + +func tracingStreamInterceptor(ctx context.Context, desc *grpc.StreamDesc, cc *grpc.ClientConn, method string, streamer grpc.Streamer, opts ...grpc.CallOption) (grpc.ClientStream, error) { + ctx = WithTracing(ctx) + return streamer(ctx, desc, cc, method, opts...) +} diff --git a/cli/azd/pkg/azdext/context.go b/cli/azd/pkg/azdext/context.go new file mode 100644 index 00000000000..17c9a8bd74f --- /dev/null +++ b/cli/azd/pkg/azdext/context.go @@ -0,0 +1,28 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package azdext + +import ( + "context" + "os" + + "go.opentelemetry.io/otel/propagation" +) + +// NewContext initializes a new context with tracing information extracted from environment variables. +func NewContext() context.Context { + ctx := context.Background() + parent := os.Getenv(TraceparentEnv) + state := os.Getenv(TracestateEnv) + + if parent != "" { + tc := propagation.TraceContext{} + return tc.Extract(ctx, propagation.MapCarrier{ + TraceparentKey: parent, + TracestateKey: state, + }) + } + + return ctx +} diff --git a/cli/azd/pkg/azdext/event.pb.go b/cli/azd/pkg/azdext/event.pb.go index 4b6f1713949..d3a65208189 100644 --- a/cli/azd/pkg/azdext/event.pb.go +++ b/cli/azd/pkg/azdext/event.pb.go @@ -35,10 +35,7 @@ type EventMessage struct { // *EventMessage_SubscribeServiceEvent // *EventMessage_InvokeServiceHandler // *EventMessage_ServiceHandlerStatus - MessageType isEventMessage_MessageType `protobuf_oneof:"message_type"` - // W3C traceparent format for distributed tracing propagation. - // Format: "00-{traceId}-{spanId}-{flags}" - TraceParent string `protobuf:"bytes,98,opt,name=trace_parent,json=traceParent,proto3" json:"trace_parent,omitempty"` + MessageType isEventMessage_MessageType `protobuf_oneof:"message_type"` unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache } @@ -134,13 +131,6 @@ func (x *EventMessage) GetServiceHandlerStatus() *ServiceHandlerStatus { return nil } -func (x *EventMessage) GetTraceParent() string { - if x != nil { - return x.TraceParent - } - return "" -} - type isEventMessage_MessageType interface { isEventMessage_MessageType() } @@ -558,15 +548,14 @@ var File_event_proto protoreflect.FileDescriptor const file_event_proto_rawDesc = "" + "\n" + - "\vevent.proto\x12\x06azdext\x1a\fmodels.proto\"\xcb\x04\n" + + "\vevent.proto\x12\x06azdext\x1a\fmodels.proto\"\xa8\x04\n" + "\fEventMessage\x12W\n" + "\x17subscribe_project_event\x18\x01 \x01(\v2\x1d.azdext.SubscribeProjectEventH\x00R\x15subscribeProjectEvent\x12T\n" + "\x16invoke_project_handler\x18\x02 \x01(\v2\x1c.azdext.InvokeProjectHandlerH\x00R\x14invokeProjectHandler\x12T\n" + "\x16project_handler_status\x18\x03 \x01(\v2\x1c.azdext.ProjectHandlerStatusH\x00R\x14projectHandlerStatus\x12W\n" + "\x17subscribe_service_event\x18\x04 \x01(\v2\x1d.azdext.SubscribeServiceEventH\x00R\x15subscribeServiceEvent\x12T\n" + "\x16invoke_service_handler\x18\x05 \x01(\v2\x1c.azdext.InvokeServiceHandlerH\x00R\x14invokeServiceHandler\x12T\n" + - "\x16service_handler_status\x18\x06 \x01(\v2\x1c.azdext.ServiceHandlerStatusH\x00R\x14serviceHandlerStatus\x12!\n" + - "\ftrace_parent\x18b \x01(\tR\vtraceParentB\x0e\n" + + "\x16service_handler_status\x18\x06 \x01(\v2\x1c.azdext.ServiceHandlerStatusH\x00R\x14serviceHandlerStatusB\x0e\n" + "\fmessage_type\"8\n" + "\x15SubscribeProjectEvent\x12\x1f\n" + "\vevent_names\x18\x01 \x03(\tR\n" + diff --git a/cli/azd/pkg/azdext/event_message_envelope.go b/cli/azd/pkg/azdext/event_message_envelope.go index 3072d1956c0..16ae9bc7457 100644 --- a/cli/azd/pkg/azdext/event_message_envelope.go +++ b/cli/azd/pkg/azdext/event_message_envelope.go @@ -96,16 +96,6 @@ func (ops *EventMessageEnvelope) SetError(msg *EventMessage, err error) { // No-op: EventMessage uses status strings, not Error field } -// GetTraceParent returns the W3C traceparent value from the message for distributed tracing. -func (ops *EventMessageEnvelope) GetTraceParent(msg *EventMessage) string { - return msg.GetTraceParent() -} - -// SetTraceParent sets the W3C traceparent value on the message for distributed tracing. -func (ops *EventMessageEnvelope) SetTraceParent(msg *EventMessage, traceParent string) { - msg.TraceParent = traceParent -} - // GetInnerMessage returns the inner message from the oneof field func (ops *EventMessageEnvelope) GetInnerMessage(msg *EventMessage) any { // The MessageType field is a oneof wrapper. We need to extract the actual inner message. diff --git a/cli/azd/pkg/azdext/framework_service.pb.go b/cli/azd/pkg/azdext/framework_service.pb.go index 48652135121..aade60fafba 100644 --- a/cli/azd/pkg/azdext/framework_service.pb.go +++ b/cli/azd/pkg/azdext/framework_service.pb.go @@ -26,13 +26,9 @@ const ( // Envelope for all possible framework service messages (requests and responses) type FrameworkServiceMessage struct { - state protoimpl.MessageState `protogen:"open.v1"` - RequestId string `protobuf:"bytes,1,opt,name=request_id,json=requestId,proto3" json:"request_id,omitempty"` - // W3C Trace Context traceparent header value for distributed tracing. - // Format: "00-{trace-id}-{span-id}-{trace-flags}" - // See: https://www.w3.org/TR/trace-context/ - TraceParent string `protobuf:"bytes,98,opt,name=trace_parent,json=traceParent,proto3" json:"trace_parent,omitempty"` - Error *FrameworkServiceErrorMessage `protobuf:"bytes,99,opt,name=error,proto3" json:"error,omitempty"` + state protoimpl.MessageState `protogen:"open.v1"` + RequestId string `protobuf:"bytes,1,opt,name=request_id,json=requestId,proto3" json:"request_id,omitempty"` + Error *FrameworkServiceErrorMessage `protobuf:"bytes,99,opt,name=error,proto3" json:"error,omitempty"` // Types that are valid to be assigned to MessageType: // // *FrameworkServiceMessage_RegisterFrameworkServiceRequest @@ -92,13 +88,6 @@ func (x *FrameworkServiceMessage) GetRequestId() string { return "" } -func (x *FrameworkServiceMessage) GetTraceParent() string { - if x != nil { - return x.TraceParent - } - return "" -} - func (x *FrameworkServiceMessage) GetError() *FrameworkServiceErrorMessage { if x != nil { return x.Error @@ -1351,11 +1340,10 @@ var File_framework_service_proto protoreflect.FileDescriptor const file_framework_service_proto_rawDesc = "" + "\n" + - "\x17framework_service.proto\x12\x06azdext\x1a\fmodels.proto\x1a\x14service_target.proto\"\xe3\f\n" + + "\x17framework_service.proto\x12\x06azdext\x1a\fmodels.proto\x1a\x14service_target.proto\"\xc0\f\n" + "\x17FrameworkServiceMessage\x12\x1d\n" + "\n" + - "request_id\x18\x01 \x01(\tR\trequestId\x12!\n" + - "\ftrace_parent\x18b \x01(\tR\vtraceParent\x12:\n" + + "request_id\x18\x01 \x01(\tR\trequestId\x12:\n" + "\x05error\x18c \x01(\v2$.azdext.FrameworkServiceErrorMessageR\x05error\x12v\n" + "\"register_framework_service_request\x18\x02 \x01(\v2'.azdext.RegisterFrameworkServiceRequestH\x00R\x1fregisterFrameworkServiceRequest\x12y\n" + "#register_framework_service_response\x18\x03 \x01(\v2(.azdext.RegisterFrameworkServiceResponseH\x00R registerFrameworkServiceResponse\x12Z\n" + diff --git a/cli/azd/pkg/azdext/framework_service_envelope.go b/cli/azd/pkg/azdext/framework_service_envelope.go index bf06874f073..7eec7d195c4 100644 --- a/cli/azd/pkg/azdext/framework_service_envelope.go +++ b/cli/azd/pkg/azdext/framework_service_envelope.go @@ -110,15 +110,3 @@ func (ops *FrameworkServiceEnvelope) CreateProgressMessage(requestId string, mes }, } } - -// GetTraceParent returns the W3C traceparent header value from the message. -// This is used to propagate distributed tracing context across the gRPC boundary. -func (ops *FrameworkServiceEnvelope) GetTraceParent(msg *FrameworkServiceMessage) string { - return msg.TraceParent -} - -// SetTraceParent sets the W3C traceparent header value on the message. -// This is used to propagate distributed tracing context across the gRPC boundary. -func (ops *FrameworkServiceEnvelope) SetTraceParent(msg *FrameworkServiceMessage, traceParent string) { - msg.TraceParent = traceParent -} diff --git a/cli/azd/pkg/azdext/service_target.pb.go b/cli/azd/pkg/azdext/service_target.pb.go index 2b1b2cf5d32..a53918467dd 100644 --- a/cli/azd/pkg/azdext/service_target.pb.go +++ b/cli/azd/pkg/azdext/service_target.pb.go @@ -27,13 +27,9 @@ const ( // Envelope for all possible service target messages (requests and responses) type ServiceTargetMessage struct { - state protoimpl.MessageState `protogen:"open.v1"` - RequestId string `protobuf:"bytes,1,opt,name=request_id,json=requestId,proto3" json:"request_id,omitempty"` - // W3C Trace Context traceparent header value for distributed tracing. - // Format: "00-{trace-id}-{span-id}-{trace-flags}" - // See: https://www.w3.org/TR/trace-context/ - TraceParent string `protobuf:"bytes,98,opt,name=trace_parent,json=traceParent,proto3" json:"trace_parent,omitempty"` - Error *ServiceTargetErrorMessage `protobuf:"bytes,99,opt,name=error,proto3" json:"error,omitempty"` + state protoimpl.MessageState `protogen:"open.v1"` + RequestId string `protobuf:"bytes,1,opt,name=request_id,json=requestId,proto3" json:"request_id,omitempty"` + Error *ServiceTargetErrorMessage `protobuf:"bytes,99,opt,name=error,proto3" json:"error,omitempty"` // Types that are valid to be assigned to MessageType: // // *ServiceTargetMessage_RegisterServiceTargetRequest @@ -93,13 +89,6 @@ func (x *ServiceTargetMessage) GetRequestId() string { return "" } -func (x *ServiceTargetMessage) GetTraceParent() string { - if x != nil { - return x.TraceParent - } - return "" -} - func (x *ServiceTargetMessage) GetError() *ServiceTargetErrorMessage { if x != nil { return x.Error @@ -1676,11 +1665,10 @@ var File_service_target_proto protoreflect.FileDescriptor const file_service_target_proto_rawDesc = "" + "\n" + - "\x14service_target.proto\x12\x06azdext\x1a$include/google/protobuf/struct.proto\x1a\fmodels.proto\"\xe5\v\n" + + "\x14service_target.proto\x12\x06azdext\x1a$include/google/protobuf/struct.proto\x1a\fmodels.proto\"\xc2\v\n" + "\x14ServiceTargetMessage\x12\x1d\n" + "\n" + - "request_id\x18\x01 \x01(\tR\trequestId\x12!\n" + - "\ftrace_parent\x18b \x01(\tR\vtraceParent\x127\n" + + "request_id\x18\x01 \x01(\tR\trequestId\x127\n" + "\x05error\x18c \x01(\v2!.azdext.ServiceTargetErrorMessageR\x05error\x12m\n" + "\x1fregister_service_target_request\x18\x02 \x01(\v2$.azdext.RegisterServiceTargetRequestH\x00R\x1cregisterServiceTargetRequest\x12p\n" + " register_service_target_response\x18\x03 \x01(\v2%.azdext.RegisterServiceTargetResponseH\x00R\x1dregisterServiceTargetResponse\x12W\n" + diff --git a/cli/azd/pkg/azdext/service_target_envelope.go b/cli/azd/pkg/azdext/service_target_envelope.go index 869dc83b21b..ec827216471 100644 --- a/cli/azd/pkg/azdext/service_target_envelope.go +++ b/cli/azd/pkg/azdext/service_target_envelope.go @@ -110,15 +110,3 @@ func (ops *ServiceTargetEnvelope) CreateProgressMessage(requestId string, messag }, } } - -// GetTraceParent returns the W3C traceparent header value from the message. -// This is used to propagate distributed tracing context across the gRPC boundary. -func (ops *ServiceTargetEnvelope) GetTraceParent(msg *ServiceTargetMessage) string { - return msg.TraceParent -} - -// SetTraceParent sets the W3C traceparent header value on the message. -// This is used to propagate distributed tracing context across the gRPC boundary. -func (ops *ServiceTargetEnvelope) SetTraceParent(msg *ServiceTargetMessage, traceParent string) { - msg.TraceParent = traceParent -} diff --git a/cli/azd/pkg/azdext/trace_context.go b/cli/azd/pkg/azdext/trace_context.go index 8f2cec6b656..41ce1ecd2b8 100644 --- a/cli/azd/pkg/azdext/trace_context.go +++ b/cli/azd/pkg/azdext/trace_context.go @@ -7,17 +7,33 @@ import ( "context" "go.opentelemetry.io/otel/propagation" + "google.golang.org/grpc/metadata" ) -const traceparentKey = "traceparent" +const ( + TraceparentKey = "traceparent" + TracestateKey = "tracestate" -// ContextFromTraceParent creates a new context with the span context extracted from the traceparent string. -func ContextFromTraceParent(ctx context.Context, traceparent string) context.Context { + // https://github.com/open-telemetry/opentelemetry-specification/blob/main/specification/context/env-carriers.md + + TraceparentEnv = "TRACEPARENT" + TracestateEnv = "TRACESTATE" +) + +// WithTracing appends the W3C traceparent for the current span to outgoing gRPC metadata. +// If no span context is present, the original context is returned unchanged. +func WithTracing(ctx context.Context) context.Context { + carrier := propagation.MapCarrier{} + propagation.TraceContext{}.Inject(ctx, carrier) + traceparent := carrier.Get(TraceparentKey) if traceparent == "" { return ctx } - carrier := propagation.MapCarrier{ - traceparentKey: traceparent, + + tracestate := carrier.Get(TracestateKey) + if tracestate != "" { + return metadata.AppendToOutgoingContext(ctx, TraceparentKey, traceparent, TracestateKey, tracestate) } - return propagation.TraceContext{}.Extract(ctx, carrier) + + return metadata.AppendToOutgoingContext(ctx, TraceparentKey, traceparent) } diff --git a/cli/azd/pkg/grpcbroker/message_broker.go b/cli/azd/pkg/grpcbroker/message_broker.go index da3e6e879d5..740f6892a3b 100644 --- a/cli/azd/pkg/grpcbroker/message_broker.go +++ b/cli/azd/pkg/grpcbroker/message_broker.go @@ -13,13 +13,10 @@ import ( "strings" "sync" - "go.opentelemetry.io/otel/propagation" "google.golang.org/grpc/codes" "google.golang.org/grpc/status" ) -const traceparentKey = "traceparent" - // ProgressFunc is a callback function for sending progress updates during handler execution type ProgressFunc func(message string) @@ -56,15 +53,6 @@ type MessageEnvelope[T any] interface { // CreateProgressMessage creates a new progress message envelope with the given text. // This is used by server-side handlers to send progress updates back to clients. CreateProgressMessage(requestId string, message string) *T - - // GetTraceParent returns the W3C traceparent header value from the message. - // This is used to propagate distributed tracing context across the gRPC boundary. - // Returns empty string if no trace context is present. - GetTraceParent(msg *T) string - - // SetTraceParent sets the W3C traceparent header value on the message. - // This is used to propagate distributed tracing context across the gRPC boundary. - SetTraceParent(msg *T, traceParent string) } // handlerWrapper wraps a registered handler function with metadata @@ -200,18 +188,6 @@ func (mb *MessageBroker[TMessage]) On(handler any) error { return nil } -// injectTraceContext injects the current trace context into the message envelope -func (mb *MessageBroker[TMessage]) injectTraceContext(ctx context.Context, msg *TMessage) { - tc := propagation.TraceContext{} - // Create a carrier that writes to the message via the envelope - carrier := propagation.MapCarrier{} - tc.Inject(ctx, carrier) - - if traceParent, ok := carrier[traceparentKey]; ok { - mb.envelope.SetTraceParent(msg, traceParent) - } -} - // SendAndWait sends a message and waits for the response func (mb *MessageBroker[TMessage]) SendAndWait(ctx context.Context, msg *TMessage) (*TMessage, error) { requestId := mb.envelope.GetRequestId(ctx, msg) @@ -219,9 +195,6 @@ func (mb *MessageBroker[TMessage]) SendAndWait(ctx context.Context, msg *TMessag return nil, errors.New("message must have a RequestId") } - // Inject trace context - mb.injectTraceContext(ctx, msg) - innerMsg := mb.envelope.GetInnerMessage(msg) msgType := reflect.TypeOf(innerMsg) log.Printf("[%s] [RequestId=%s] Sending request, MessageType=%v", mb.name, requestId, msgType) @@ -284,9 +257,6 @@ func (mb *MessageBroker[TMessage]) SendAndWait(ctx context.Context, msg *TMessag // where no response is expected or needed. // Returns an error only if the send operation itself fails. func (mb *MessageBroker[TMessage]) Send(ctx context.Context, msg *TMessage) error { - // Inject trace context - mb.injectTraceContext(ctx, msg) - innerMsg := mb.envelope.GetInnerMessage(msg) msgType := reflect.TypeOf(innerMsg) requestId := mb.envelope.GetRequestId(ctx, msg) @@ -328,9 +298,6 @@ func (mb *MessageBroker[TMessage]) SendAndWaitWithProgress( return nil, errors.New("message must have a RequestId") } - // Inject trace context - mb.injectTraceContext(ctx, msg) - innerMsg := mb.envelope.GetInnerMessage(msg) msgType := reflect.TypeOf(innerMsg) @@ -615,13 +582,6 @@ func (mb *MessageBroker[TMessage]) invokeHandler( ) *TMessage { requestId := mb.envelope.GetRequestId(ctx, envelope) - // Extract trace context from the envelope and inject into the context - // This propagates distributed tracing across the gRPC boundary - if traceParent := mb.envelope.GetTraceParent(envelope); traceParent != "" { - tc := propagation.TraceContext{} - ctx = tc.Extract(ctx, propagation.MapCarrier{traceparentKey: traceParent}) - } - // Prepare arguments for handler invocation args := []reflect.Value{ reflect.ValueOf(ctx), From 2bca3ba93bf7ea4c0807e8c82b6f0a0f0995ca91 Mon Sep 17 00:00:00 2001 From: Jeffrey Chen Date: Mon, 8 Dec 2025 23:25:52 +0000 Subject: [PATCH 06/10] Fix cspell --- cli/azd/.vscode/cspell.yaml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/cli/azd/.vscode/cspell.yaml b/cli/azd/.vscode/cspell.yaml index 09dadf7f11f..309730aec5e 100644 --- a/cli/azd/.vscode/cspell.yaml +++ b/cli/azd/.vscode/cspell.yaml @@ -246,9 +246,11 @@ overrides: - filename: cli/azd/pkg/azdext/trace_context.go words: - traceparent + - tracestate - filename: internal/grpcserver/server.go words: - traceparents + - tracestates ignorePaths: - "**/*_test.go" - "**/mock*.go" From 18e6c34b3d93921f6a9202a2017dd88f15541c5a Mon Sep 17 00:00:00 2001 From: Jeffrey Chen Date: Mon, 8 Dec 2025 23:28:02 +0000 Subject: [PATCH 07/10] Update tests --- cli/azd/pkg/azdext/context_test.go | 41 ++++++++++ cli/azd/pkg/azdext/trace_context_test.go | 78 +++++++++++++++++++ cli/azd/pkg/grpcbroker/message_broker_test.go | 9 --- 3 files changed, 119 insertions(+), 9 deletions(-) create mode 100644 cli/azd/pkg/azdext/context_test.go create mode 100644 cli/azd/pkg/azdext/trace_context_test.go diff --git a/cli/azd/pkg/azdext/context_test.go b/cli/azd/pkg/azdext/context_test.go new file mode 100644 index 00000000000..93c0762283e --- /dev/null +++ b/cli/azd/pkg/azdext/context_test.go @@ -0,0 +1,41 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package azdext + +import ( + "testing" + + "github.com/stretchr/testify/require" + "go.opentelemetry.io/otel/trace" +) + +func TestNewContext_FromEnvironment(t *testing.T) { + traceparent := "00-4bf92f3577b34da6a3ce929d0e0e4736-00f067aa0ba902b7-01" + tracestate := "rojo=00f067aa0ba902b7" + + t.Setenv(TraceparentEnv, traceparent) + t.Setenv(TracestateEnv, tracestate) + + ctx := NewContext() + sc := trace.SpanContextFromContext(ctx) + + require.True(t, sc.IsValid(), "span context should be extracted from environment") + traceID, err := trace.TraceIDFromHex("4bf92f3577b34da6a3ce929d0e0e4736") + require.NoError(t, err) + spanID, err := trace.SpanIDFromHex("00f067aa0ba902b7") + require.NoError(t, err) + require.Equal(t, traceID, sc.TraceID()) + require.Equal(t, spanID, sc.SpanID()) + require.Equal(t, tracestate, sc.TraceState().String()) +} + +func TestNewContext_NoEnvironment(t *testing.T) { + t.Setenv(TraceparentEnv, "") + t.Setenv(TracestateEnv, "") + + ctx := NewContext() + sc := trace.SpanContextFromContext(ctx) + + require.False(t, sc.IsValid(), "span context should be absent when no environment is set") +} diff --git a/cli/azd/pkg/azdext/trace_context_test.go b/cli/azd/pkg/azdext/trace_context_test.go new file mode 100644 index 00000000000..04a580bf1a4 --- /dev/null +++ b/cli/azd/pkg/azdext/trace_context_test.go @@ -0,0 +1,78 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package azdext + +import ( + "context" + "testing" + + "go.opentelemetry.io/otel/trace" + "google.golang.org/grpc/metadata" + + "github.com/stretchr/testify/require" +) + +func TestWithTracing_NoSpanContext(t *testing.T) { + ctx := context.Background() + + newCtx := WithTracing(ctx) + + _, ok := metadata.FromOutgoingContext(newCtx) + require.False(t, ok, "metadata should be untouched when no span context exists") +} + +func TestWithTracing_AppendsTraceparentAndTracestate(t *testing.T) { + traceID, err := trace.TraceIDFromHex("4bf92f3577b34da6a3ce929d0e0e4736") + require.NoError(t, err) + spanID, err := trace.SpanIDFromHex("00f067aa0ba902b7") + require.NoError(t, err) + traceState, err := trace.ParseTraceState("foo=bar") + require.NoError(t, err) + + sc := trace.NewSpanContext(trace.SpanContextConfig{ + TraceID: traceID, + SpanID: spanID, + TraceFlags: trace.FlagsSampled, + TraceState: traceState, + }) + + ctx := trace.ContextWithSpanContext(context.Background(), sc) + newCtx := WithTracing(ctx) + + md, ok := metadata.FromOutgoingContext(newCtx) + require.True(t, ok, "metadata should be populated when span context exists") + values := md.Get(TraceparentKey) + require.Len(t, values, 1) + require.Equal(t, "00-4bf92f3577b34da6a3ce929d0e0e4736-00f067aa0ba902b7-01", values[0]) + + stateValues := md.Get(TracestateKey) + require.Len(t, stateValues, 1) + require.Equal(t, "foo=bar", stateValues[0]) +} + +func TestWithTracing_AppendsTraceparentOnly(t *testing.T) { + traceID, err := trace.TraceIDFromHex("4bf92f3577b34da6a3ce929d0e0e4736") + require.NoError(t, err) + spanID, err := trace.SpanIDFromHex("00f067aa0ba902b7") + require.NoError(t, err) + + sc := trace.NewSpanContext(trace.SpanContextConfig{ + TraceID: traceID, + SpanID: spanID, + TraceFlags: trace.FlagsSampled, + // No TraceState set + }) + + ctx := trace.ContextWithSpanContext(context.Background(), sc) + newCtx := WithTracing(ctx) + + md, ok := metadata.FromOutgoingContext(newCtx) + require.True(t, ok, "metadata should be populated when span context exists") + values := md.Get(TraceparentKey) + require.Len(t, values, 1) + require.Equal(t, "00-4bf92f3577b34da6a3ce929d0e0e4736-00f067aa0ba902b7-01", values[0]) + + stateValues := md.Get(TracestateKey) + require.Empty(t, stateValues, "tracestate should not be present when empty") +} diff --git a/cli/azd/pkg/grpcbroker/message_broker_test.go b/cli/azd/pkg/grpcbroker/message_broker_test.go index 19eb46f4a78..1cdc451f72b 100644 --- a/cli/azd/pkg/grpcbroker/message_broker_test.go +++ b/cli/azd/pkg/grpcbroker/message_broker_test.go @@ -24,7 +24,6 @@ type TestMessage struct { InnerMsg any IsProgress bool ProgressText string - TraceParent string } // Test request/response types for handler testing @@ -165,14 +164,6 @@ func (e *SimpleMessageEnvelope) CreateProgressMessage(requestId string, message } } -func (e *SimpleMessageEnvelope) GetTraceParent(msg *TestMessage) string { - return msg.TraceParent -} - -func (e *SimpleMessageEnvelope) SetTraceParent(msg *TestMessage, traceParent string) { - msg.TraceParent = traceParent -} - // TestOn_RegistersHandler tests that handlers are registered correctly func TestOn_RegistersHandler(t *testing.T) { sim := NewSimulatedBidiStream() From db15050c1479105f0a129d8a665fa50ff404af80 Mon Sep 17 00:00:00 2001 From: Jeffrey Chen Date: Mon, 8 Dec 2025 23:42:33 +0000 Subject: [PATCH 08/10] Fix lint --- cli/azd/pkg/azdext/azd_client.go | 18 ++++++++++++++++-- 1 file changed, 16 insertions(+), 2 deletions(-) diff --git a/cli/azd/pkg/azdext/azd_client.go b/cli/azd/pkg/azdext/azd_client.go index d946fecfb18..7cdf8387cd0 100644 --- a/cli/azd/pkg/azdext/azd_client.go +++ b/cli/azd/pkg/azdext/azd_client.go @@ -196,12 +196,26 @@ func (c *AzdClient) Account() AccountServiceClient { return c.accountClient } -func tracingUnaryInterceptor(ctx context.Context, method string, req, reply interface{}, cc *grpc.ClientConn, invoker grpc.UnaryInvoker, opts ...grpc.CallOption) error { +func tracingUnaryInterceptor( + ctx context.Context, + method string, + req, reply interface{}, + cc *grpc.ClientConn, + invoker grpc.UnaryInvoker, + opts ...grpc.CallOption, +) error { ctx = WithTracing(ctx) return invoker(ctx, method, req, reply, cc, opts...) } -func tracingStreamInterceptor(ctx context.Context, desc *grpc.StreamDesc, cc *grpc.ClientConn, method string, streamer grpc.Streamer, opts ...grpc.CallOption) (grpc.ClientStream, error) { +func tracingStreamInterceptor( + ctx context.Context, + desc *grpc.StreamDesc, + cc *grpc.ClientConn, + method string, + streamer grpc.Streamer, + opts ...grpc.CallOption, +) (grpc.ClientStream, error) { ctx = WithTracing(ctx) return streamer(ctx, desc, cc, method, opts...) } From 4b0d846cf2204ebb277ed5138b1e4164d3c23147 Mon Sep 17 00:00:00 2001 From: Jeffrey Chen Date: Thu, 11 Dec 2025 00:04:48 +0000 Subject: [PATCH 09/10] Generalize errors --- cli/azd/grpc/proto/errors.proto | 39 +++ cli/azd/grpc/proto/framework_service.proto | 13 +- cli/azd/grpc/proto/service_target.proto | 12 +- cli/azd/internal/cmd/errors.go | 20 +- cli/azd/internal/cmd/errors_test.go | 4 +- cli/azd/pkg/azdext/errors.pb.go | 316 ++++++++++++++++++ cli/azd/pkg/azdext/extension_error.go | 161 ++++----- cli/azd/pkg/azdext/extension_error_test.go | 121 ++++--- cli/azd/pkg/azdext/framework_service.pb.go | 311 +++++++---------- .../pkg/azdext/framework_service_envelope.go | 8 +- cli/azd/pkg/azdext/service_target.pb.go | 286 ++++++---------- cli/azd/pkg/azdext/service_target_envelope.go | 8 +- 12 files changed, 706 insertions(+), 593 deletions(-) create mode 100644 cli/azd/grpc/proto/errors.proto create mode 100644 cli/azd/pkg/azdext/errors.pb.go diff --git a/cli/azd/grpc/proto/errors.proto b/cli/azd/grpc/proto/errors.proto new file mode 100644 index 00000000000..7ba02f4eb43 --- /dev/null +++ b/cli/azd/grpc/proto/errors.proto @@ -0,0 +1,39 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +syntax = "proto3"; + +package azdext; + +option go_package = "github.com/azure/azure-dev/cli/azd/pkg/azdext"; + +// ErrorOrigin indicates where an error originated from. +// This helps with telemetry categorization and error handling strategies. +enum ErrorOrigin { + ERROR_ORIGIN_UNSPECIFIED = 0; // Default/unknown origin + ERROR_ORIGIN_LOCAL = 1; // Local errors: config, filesystem, auth state, validation, etc. + ERROR_ORIGIN_SERVICE = 2; // HTTP/gRPC upstream service errors + ERROR_ORIGIN_TOOL = 3; // Subprocess, plugin, or external tool errors +} + +// ServiceErrorDetail contains structured error information from an HTTP/gRPC service. +// Used when ErrorOrigin is ERROR_ORIGIN_SERVICE. +message ServiceErrorDetail { + string error_code = 1; // Error code from the service (e.g., "Conflict", "NotFound", "RateLimitExceeded") + int32 status_code = 2; // HTTP status code (e.g., 409, 404, 500) + string service_name = 3; // Service host/name for telemetry (e.g., "ai.azure.com", "management.azure.com") +} + +// ExtensionError is a unified error message that can represent errors from different sources. +// It provides structured error information for telemetry and error handling. +message ExtensionError { + string message = 2; // Human-readable error message + string details = 3; // Additional error details + ErrorOrigin origin = 4; // Where the error originated from + + // Source-specific structured details. Only one should be set based on origin. + oneof source { + ServiceErrorDetail service_error = 10; + // ToolErrorDetail tool_error = 11; // Reserved for future use + // LocalErrorDetail local_error = 12; // Reserved for future use + } +} diff --git a/cli/azd/grpc/proto/framework_service.proto b/cli/azd/grpc/proto/framework_service.proto index 26fb3c09ce4..3f99103a243 100644 --- a/cli/azd/grpc/proto/framework_service.proto +++ b/cli/azd/grpc/proto/framework_service.proto @@ -8,6 +8,7 @@ option go_package = "github.com/azure/azure-dev/cli/azd/pkg/azdext"; import "models.proto"; import "service_target.proto"; +import "errors.proto"; service FrameworkService { // Bidirectional stream for framework service requests and responses @@ -17,7 +18,7 @@ service FrameworkService { // Envelope for all possible framework service messages (requests and responses) message FrameworkServiceMessage { string request_id = 1; - FrameworkServiceErrorMessage error = 99; + ExtensionError error = 99; oneof message_type { RegisterFrameworkServiceRequest register_framework_service_request = 2; RegisterFrameworkServiceResponse register_framework_service_response = 3; @@ -37,16 +38,6 @@ message FrameworkServiceMessage { } } -// Error message for framework service operations -message FrameworkServiceErrorMessage { - string message = 1; - string details = 2; - // Structured error information for telemetry - string error_code = 3; // Error code from the service (e.g., "Conflict", "NotFound") - int32 status_code = 4; // HTTP status code (e.g., 409, 404, 500) - string service_name = 5; // Service name for telemetry (e.g., "ai.azure.com", "cognitiveservices") -} - // Request to register a framework service provider message RegisterFrameworkServiceRequest { string language = 1; // unique identifier for the language/framework (e.g., "rust", "go", "php") diff --git a/cli/azd/grpc/proto/service_target.proto b/cli/azd/grpc/proto/service_target.proto index 3a57956b535..e452b81af1a 100644 --- a/cli/azd/grpc/proto/service_target.proto +++ b/cli/azd/grpc/proto/service_target.proto @@ -8,6 +8,7 @@ option go_package = "github.com/azure/azure-dev/cli/azd/pkg/azdext"; import "include/google/protobuf/struct.proto"; import "models.proto"; +import "errors.proto"; service ServiceTargetService { // Bidirectional stream for service target requests and responses @@ -17,7 +18,7 @@ service ServiceTargetService { // Envelope for all possible service target messages (requests and responses) message ServiceTargetMessage { string request_id = 1; - ServiceTargetErrorMessage error = 99; + ExtensionError error = 99; oneof message_type { RegisterServiceTargetRequest register_service_target_request = 2; RegisterServiceTargetResponse register_service_target_response = 3; @@ -80,15 +81,6 @@ message RegisterServiceTargetResponse { // Add fields as needed (empty for now) } -message ServiceTargetErrorMessage { - string message = 2; - string details = 3; - // Structured error information for telemetry - string error_code = 4; // Error code from the service (e.g., "Conflict", "NotFound") - int32 status_code = 5; // HTTP status code (e.g., 409, 404, 500) - string service_name = 6; // Service name for telemetry (e.g., "ai.azure.com", "cognitiveservices") -} - // GetTargetResource request and response message GetTargetResourceRequest { string subscription_id = 1; diff --git a/cli/azd/internal/cmd/errors.go b/cli/azd/internal/cmd/errors.go index a69c1195ed3..665f8ed16d7 100644 --- a/cli/azd/internal/cmd/errors.go +++ b/cli/azd/internal/cmd/errors.go @@ -34,7 +34,7 @@ func MapError(err error, span tracing.Span) { var toolExecErr *exec.ExitError var authFailedErr *auth.AuthFailedError var extensionRunErr *extensions.ExtensionRunError - var extRespErr *azdext.ExtensionResponseError + var extServiceErr *azdext.ServiceError if errors.As(err, &respErr) { serviceName := "other" statusCode := -1 @@ -86,22 +86,20 @@ func MapError(err error, span tracing.Span) { errCode = "service.arm.deployment.failed" } else if errors.As(err, &extensionRunErr) { errCode = "ext.run.failed" - } else if errors.As(err, &extRespErr) { - // Handle structured errors from extensions (e.g., service target providers) - if extRespErr.HasServiceInfo() { - // Extension provided service information - use it for telemetry - serviceName, hostDomain := mapService(extRespErr.ServiceName) + } else if errors.As(err, &extServiceErr) { + // Handle structured service errors from extensions + if extServiceErr.StatusCode > 0 && extServiceErr.ServiceName != "" { + serviceName, hostDomain := mapService(extServiceErr.ServiceName) errDetails = append(errDetails, fields.ServiceName.String(serviceName), fields.ServiceHost.String(hostDomain), - fields.ServiceStatusCode.Int(extRespErr.StatusCode), + fields.ServiceStatusCode.Int(extServiceErr.StatusCode), ) - if extRespErr.ErrorCode != "" { - errDetails = append(errDetails, fields.ServiceErrorCode.String(extRespErr.ErrorCode)) + if extServiceErr.ErrorCode != "" { + errDetails = append(errDetails, fields.ServiceErrorCode.String(extServiceErr.ErrorCode)) } - errCode = fmt.Sprintf("ext.service.%s.%d", serviceName, extRespErr.StatusCode) + errCode = fmt.Sprintf("ext.service.%s.%d", serviceName, extServiceErr.StatusCode) } else { - // Extension error without service info errCode = "ext.service.failed" } } else if errors.As(err, &toolExecErr) { diff --git a/cli/azd/internal/cmd/errors_test.go b/cli/azd/internal/cmd/errors_test.go index 4c16619dff8..3162652c14d 100644 --- a/cli/azd/internal/cmd/errors_test.go +++ b/cli/azd/internal/cmd/errors_test.go @@ -150,8 +150,8 @@ func Test_MapError(t *testing.T) { }, }, { - name: "WithExtensionResponseError", - err: &azdext.ExtensionResponseError{ + name: "WithExtServiceError", + err: &azdext.ServiceError{ Message: "Rate limit exceeded", Details: "Too many requests", ErrorCode: "RateLimitExceeded", diff --git a/cli/azd/pkg/azdext/errors.pb.go b/cli/azd/pkg/azdext/errors.pb.go new file mode 100644 index 00000000000..ce3e177a5ab --- /dev/null +++ b/cli/azd/pkg/azdext/errors.pb.go @@ -0,0 +1,316 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +// Code generated by protoc-gen-go. DO NOT EDIT. +// versions: +// protoc-gen-go v1.36.10 +// protoc v6.32.1 +// source: errors.proto + +package azdext + +import ( + protoreflect "google.golang.org/protobuf/reflect/protoreflect" + protoimpl "google.golang.org/protobuf/runtime/protoimpl" + reflect "reflect" + sync "sync" + unsafe "unsafe" +) + +const ( + // Verify that this generated code is sufficiently up-to-date. + _ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion) + // Verify that runtime/protoimpl is sufficiently up-to-date. + _ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20) +) + +// ErrorOrigin indicates where an error originated from. +// This helps with telemetry categorization and error handling strategies. +type ErrorOrigin int32 + +const ( + ErrorOrigin_ERROR_ORIGIN_UNSPECIFIED ErrorOrigin = 0 // Default/unknown origin + ErrorOrigin_ERROR_ORIGIN_LOCAL ErrorOrigin = 1 // Local errors: config, filesystem, auth state, validation, etc. + ErrorOrigin_ERROR_ORIGIN_SERVICE ErrorOrigin = 2 // HTTP/gRPC upstream service errors + ErrorOrigin_ERROR_ORIGIN_TOOL ErrorOrigin = 3 // Subprocess, plugin, or external tool errors +) + +// Enum value maps for ErrorOrigin. +var ( + ErrorOrigin_name = map[int32]string{ + 0: "ERROR_ORIGIN_UNSPECIFIED", + 1: "ERROR_ORIGIN_LOCAL", + 2: "ERROR_ORIGIN_SERVICE", + 3: "ERROR_ORIGIN_TOOL", + } + ErrorOrigin_value = map[string]int32{ + "ERROR_ORIGIN_UNSPECIFIED": 0, + "ERROR_ORIGIN_LOCAL": 1, + "ERROR_ORIGIN_SERVICE": 2, + "ERROR_ORIGIN_TOOL": 3, + } +) + +func (x ErrorOrigin) Enum() *ErrorOrigin { + p := new(ErrorOrigin) + *p = x + return p +} + +func (x ErrorOrigin) String() string { + return protoimpl.X.EnumStringOf(x.Descriptor(), protoreflect.EnumNumber(x)) +} + +func (ErrorOrigin) Descriptor() protoreflect.EnumDescriptor { + return file_errors_proto_enumTypes[0].Descriptor() +} + +func (ErrorOrigin) Type() protoreflect.EnumType { + return &file_errors_proto_enumTypes[0] +} + +func (x ErrorOrigin) Number() protoreflect.EnumNumber { + return protoreflect.EnumNumber(x) +} + +// Deprecated: Use ErrorOrigin.Descriptor instead. +func (ErrorOrigin) EnumDescriptor() ([]byte, []int) { + return file_errors_proto_rawDescGZIP(), []int{0} +} + +// ServiceErrorDetail contains structured error information from an HTTP/gRPC service. +// Used when ErrorOrigin is ERROR_ORIGIN_SERVICE. +type ServiceErrorDetail struct { + state protoimpl.MessageState `protogen:"open.v1"` + ErrorCode string `protobuf:"bytes,1,opt,name=error_code,json=errorCode,proto3" json:"error_code,omitempty"` // Error code from the service (e.g., "Conflict", "NotFound", "RateLimitExceeded") + StatusCode int32 `protobuf:"varint,2,opt,name=status_code,json=statusCode,proto3" json:"status_code,omitempty"` // HTTP status code (e.g., 409, 404, 500) + ServiceName string `protobuf:"bytes,3,opt,name=service_name,json=serviceName,proto3" json:"service_name,omitempty"` // Service host/name for telemetry (e.g., "ai.azure.com", "management.azure.com") + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *ServiceErrorDetail) Reset() { + *x = ServiceErrorDetail{} + mi := &file_errors_proto_msgTypes[0] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *ServiceErrorDetail) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*ServiceErrorDetail) ProtoMessage() {} + +func (x *ServiceErrorDetail) ProtoReflect() protoreflect.Message { + mi := &file_errors_proto_msgTypes[0] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use ServiceErrorDetail.ProtoReflect.Descriptor instead. +func (*ServiceErrorDetail) Descriptor() ([]byte, []int) { + return file_errors_proto_rawDescGZIP(), []int{0} +} + +func (x *ServiceErrorDetail) GetErrorCode() string { + if x != nil { + return x.ErrorCode + } + return "" +} + +func (x *ServiceErrorDetail) GetStatusCode() int32 { + if x != nil { + return x.StatusCode + } + return 0 +} + +func (x *ServiceErrorDetail) GetServiceName() string { + if x != nil { + return x.ServiceName + } + return "" +} + +// ExtensionError is a unified error message that can represent errors from different sources. +// It provides structured error information for telemetry and error handling. +type ExtensionError struct { + state protoimpl.MessageState `protogen:"open.v1"` + Message string `protobuf:"bytes,2,opt,name=message,proto3" json:"message,omitempty"` // Human-readable error message + Details string `protobuf:"bytes,3,opt,name=details,proto3" json:"details,omitempty"` // Additional error details + Origin ErrorOrigin `protobuf:"varint,4,opt,name=origin,proto3,enum=azdext.ErrorOrigin" json:"origin,omitempty"` // Where the error originated from + // Source-specific structured details. Only one should be set based on origin. + // + // Types that are valid to be assigned to Source: + // + // *ExtensionError_ServiceError + Source isExtensionError_Source `protobuf_oneof:"source"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *ExtensionError) Reset() { + *x = ExtensionError{} + mi := &file_errors_proto_msgTypes[1] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *ExtensionError) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*ExtensionError) ProtoMessage() {} + +func (x *ExtensionError) ProtoReflect() protoreflect.Message { + mi := &file_errors_proto_msgTypes[1] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use ExtensionError.ProtoReflect.Descriptor instead. +func (*ExtensionError) Descriptor() ([]byte, []int) { + return file_errors_proto_rawDescGZIP(), []int{1} +} + +func (x *ExtensionError) GetMessage() string { + if x != nil { + return x.Message + } + return "" +} + +func (x *ExtensionError) GetDetails() string { + if x != nil { + return x.Details + } + return "" +} + +func (x *ExtensionError) GetOrigin() ErrorOrigin { + if x != nil { + return x.Origin + } + return ErrorOrigin_ERROR_ORIGIN_UNSPECIFIED +} + +func (x *ExtensionError) GetSource() isExtensionError_Source { + if x != nil { + return x.Source + } + return nil +} + +func (x *ExtensionError) GetServiceError() *ServiceErrorDetail { + if x != nil { + if x, ok := x.Source.(*ExtensionError_ServiceError); ok { + return x.ServiceError + } + } + return nil +} + +type isExtensionError_Source interface { + isExtensionError_Source() +} + +type ExtensionError_ServiceError struct { + ServiceError *ServiceErrorDetail `protobuf:"bytes,10,opt,name=service_error,json=serviceError,proto3,oneof"` +} + +func (*ExtensionError_ServiceError) isExtensionError_Source() {} + +var File_errors_proto protoreflect.FileDescriptor + +const file_errors_proto_rawDesc = "" + + "\n" + + "\ferrors.proto\x12\x06azdext\"w\n" + + "\x12ServiceErrorDetail\x12\x1d\n" + + "\n" + + "error_code\x18\x01 \x01(\tR\terrorCode\x12\x1f\n" + + "\vstatus_code\x18\x02 \x01(\x05R\n" + + "statusCode\x12!\n" + + "\fservice_name\x18\x03 \x01(\tR\vserviceName\"\xbe\x01\n" + + "\x0eExtensionError\x12\x18\n" + + "\amessage\x18\x02 \x01(\tR\amessage\x12\x18\n" + + "\adetails\x18\x03 \x01(\tR\adetails\x12+\n" + + "\x06origin\x18\x04 \x01(\x0e2\x13.azdext.ErrorOriginR\x06origin\x12A\n" + + "\rservice_error\x18\n" + + " \x01(\v2\x1a.azdext.ServiceErrorDetailH\x00R\fserviceErrorB\b\n" + + "\x06source*t\n" + + "\vErrorOrigin\x12\x1c\n" + + "\x18ERROR_ORIGIN_UNSPECIFIED\x10\x00\x12\x16\n" + + "\x12ERROR_ORIGIN_LOCAL\x10\x01\x12\x18\n" + + "\x14ERROR_ORIGIN_SERVICE\x10\x02\x12\x15\n" + + "\x11ERROR_ORIGIN_TOOL\x10\x03B/Z-github.com/azure/azure-dev/cli/azd/pkg/azdextb\x06proto3" + +var ( + file_errors_proto_rawDescOnce sync.Once + file_errors_proto_rawDescData []byte +) + +func file_errors_proto_rawDescGZIP() []byte { + file_errors_proto_rawDescOnce.Do(func() { + file_errors_proto_rawDescData = protoimpl.X.CompressGZIP(unsafe.Slice(unsafe.StringData(file_errors_proto_rawDesc), len(file_errors_proto_rawDesc))) + }) + return file_errors_proto_rawDescData +} + +var file_errors_proto_enumTypes = make([]protoimpl.EnumInfo, 1) +var file_errors_proto_msgTypes = make([]protoimpl.MessageInfo, 2) +var file_errors_proto_goTypes = []any{ + (ErrorOrigin)(0), // 0: azdext.ErrorOrigin + (*ServiceErrorDetail)(nil), // 1: azdext.ServiceErrorDetail + (*ExtensionError)(nil), // 2: azdext.ExtensionError +} +var file_errors_proto_depIdxs = []int32{ + 0, // 0: azdext.ExtensionError.origin:type_name -> azdext.ErrorOrigin + 1, // 1: azdext.ExtensionError.service_error:type_name -> azdext.ServiceErrorDetail + 2, // [2:2] is the sub-list for method output_type + 2, // [2:2] is the sub-list for method input_type + 2, // [2:2] is the sub-list for extension type_name + 2, // [2:2] is the sub-list for extension extendee + 0, // [0:2] is the sub-list for field type_name +} + +func init() { file_errors_proto_init() } +func file_errors_proto_init() { + if File_errors_proto != nil { + return + } + file_errors_proto_msgTypes[1].OneofWrappers = []any{ + (*ExtensionError_ServiceError)(nil), + } + type x struct{} + out := protoimpl.TypeBuilder{ + File: protoimpl.DescBuilder{ + GoPackagePath: reflect.TypeOf(x{}).PkgPath(), + RawDescriptor: unsafe.Slice(unsafe.StringData(file_errors_proto_rawDesc), len(file_errors_proto_rawDesc)), + NumEnums: 1, + NumMessages: 2, + NumExtensions: 0, + NumServices: 0, + }, + GoTypes: file_errors_proto_goTypes, + DependencyIndexes: file_errors_proto_depIdxs, + EnumInfos: file_errors_proto_enumTypes, + MessageInfos: file_errors_proto_msgTypes, + }.Build() + File_errors_proto = out.File + file_errors_proto_goTypes = nil + file_errors_proto_depIdxs = nil +} diff --git a/cli/azd/pkg/azdext/extension_error.go b/cli/azd/pkg/azdext/extension_error.go index 06f05b2e290..28b45eef2fa 100644 --- a/cli/azd/pkg/azdext/extension_error.go +++ b/cli/azd/pkg/azdext/extension_error.go @@ -10,9 +10,9 @@ import ( "github.com/Azure/azure-sdk-for-go/sdk/azcore" ) -// ExtensionResponseError represents an HTTP response error returned from an extension over gRPC. -// It mirrors azcore.ResponseError and preserves structured error information for telemetry purposes. -type ExtensionResponseError struct { +// ServiceError represents an HTTP/gRPC service error from an extension. +// It preserves structured error information for telemetry and error handling. +type ServiceError struct { // Message is the human-readable error message Message string // Details contains additional error details @@ -21,134 +21,89 @@ type ExtensionResponseError struct { ErrorCode string // StatusCode is the HTTP status code (e.g., 409, 404, 500) StatusCode int - // ServiceName is the service name for telemetry (e.g., "ai.azure.com") + // ServiceName is the service host/name for telemetry (e.g., "ai.azure.com") ServiceName string } // Error implements the error interface -func (e *ExtensionResponseError) Error() string { +func (e *ServiceError) Error() string { if e.Details != "" { return fmt.Sprintf("%s: %s", e.Message, e.Details) } return e.Message } -// HasServiceInfo returns true if the error contains service information for telemetry -func (e *ExtensionResponseError) HasServiceInfo() bool { - return e.StatusCode > 0 && e.ServiceName != "" -} - -// errorMessage defines the common interface for protobuf error messages -// This allows us to write generic unwrap logic for any generated proto message -type errorMessage interface { - comparable - GetMessage() string - GetDetails() string - GetErrorCode() string - GetStatusCode() int32 - GetServiceName() string -} - -// errorInfo is a helper struct to hold extracted error information -// before converting to a specific protobuf message type -type errorInfo struct { - message string - details string - errorCode string - statusCode int32 - service string -} - -// captureErrorInfo extracts structured error information from a Go error. -// It handles nil errors, ExtensionResponseError, and azcore.ResponseError. -func captureErrorInfo(err error) errorInfo { +// WrapError wraps a Go error into an ExtensionError for transmission over gRPC. +// It detects the error type and populates the appropriate source details. +func WrapError(err error) *ExtensionError { if err == nil { - return errorInfo{} + return nil } - // Default to the error string - info := errorInfo{message: err.Error()} + extErr := &ExtensionError{ + Message: err.Error(), + Origin: ErrorOrigin_ERROR_ORIGIN_UNSPECIFIED, + } - // If it's already an ExtensionResponseError, preserve all fields including Details - var extErr *ExtensionResponseError - if errors.As(err, &extErr) { - info.message = extErr.Message - info.details = extErr.Details - info.errorCode = extErr.ErrorCode - //nolint:gosec // G115: HTTP status codes are well within int32 range - info.statusCode = int32(extErr.StatusCode) - info.service = extErr.ServiceName - return info + // Check for extension error types (already structured) + var extServiceErr *ServiceError + if errors.As(err, &extServiceErr) { + extErr.Message = extServiceErr.Message + extErr.Details = extServiceErr.Details + extErr.Origin = ErrorOrigin_ERROR_ORIGIN_SERVICE + extErr.Source = &ExtensionError_ServiceError{ + ServiceError: &ServiceErrorDetail{ + ErrorCode: extServiceErr.ErrorCode, + //nolint:gosec // G115: HTTP status codes are well within int32 range + StatusCode: int32(extServiceErr.StatusCode), + ServiceName: extServiceErr.ServiceName, + }, + } + return extErr } - // Try to extract structured error information from Azure SDK errors + // Try to detect Azure SDK errors var respErr *azcore.ResponseError if errors.As(err, &respErr) { - info.errorCode = respErr.ErrorCode - //nolint:gosec // G115: HTTP status codes are well within int32 range - info.statusCode = int32(respErr.StatusCode) + extErr.Origin = ErrorOrigin_ERROR_ORIGIN_SERVICE + serviceName := "" if respErr.RawResponse != nil && respErr.RawResponse.Request != nil { - info.service = respErr.RawResponse.Request.Host + serviceName = respErr.RawResponse.Request.Host + } + extErr.Source = &ExtensionError_ServiceError{ + ServiceError: &ServiceErrorDetail{ + ErrorCode: respErr.ErrorCode, + //nolint:gosec // G115: HTTP status codes are well within int32 range + StatusCode: int32(respErr.StatusCode), + ServiceName: serviceName, + }, } } - return info -} - -// WrapErrorForServiceTarget wraps a Go error into a ServiceTargetErrorMessage for transmission over gRPC. -func WrapErrorForServiceTarget(err error) *ServiceTargetErrorMessage { - info := captureErrorInfo(err) - if info.message == "" { - return nil - } - - return &ServiceTargetErrorMessage{ - Message: info.message, - Details: info.details, - ErrorCode: info.errorCode, - StatusCode: info.statusCode, - ServiceName: info.service, - } + return extErr } -// WrapErrorForFrameworkService wraps a Go error into a FrameworkServiceErrorMessage for transmission over gRPC. -func WrapErrorForFrameworkService(err error) *FrameworkServiceErrorMessage { - info := captureErrorInfo(err) - if info.message == "" { +// UnwrapError converts an ExtensionError back to a typed Go error. +// It returns the appropriate error type based on the origin field. +func UnwrapError(msg *ExtensionError) error { + if msg == nil || msg.GetMessage() == "" { return nil } - return &FrameworkServiceErrorMessage{ - Message: info.message, - Details: info.details, - ErrorCode: info.errorCode, - StatusCode: info.statusCode, - ServiceName: info.service, - } -} - -// unwrapError is a generic helper to convert protobuf error messages back to Go errors -func unwrapError[T errorMessage](msg T) error { - var zero T - if msg == zero || msg.GetMessage() == "" { - return nil + // Check for service error details + if svcErr := msg.GetServiceError(); svcErr != nil { + return &ServiceError{ + Message: msg.GetMessage(), + Details: msg.GetDetails(), + ErrorCode: svcErr.GetErrorCode(), + StatusCode: int(svcErr.GetStatusCode()), + ServiceName: svcErr.GetServiceName(), + } } - return &ExtensionResponseError{ - Message: msg.GetMessage(), - Details: msg.GetDetails(), - ErrorCode: msg.GetErrorCode(), - StatusCode: int(msg.GetStatusCode()), - ServiceName: msg.GetServiceName(), + // Return a generic service error with just the message/details + return &ServiceError{ + Message: msg.GetMessage(), + Details: msg.GetDetails(), } } - -// UnwrapErrorFromServiceTarget converts a ServiceTargetErrorMessage back to a Go error. -func UnwrapErrorFromServiceTarget(msg *ServiceTargetErrorMessage) error { - return unwrapError(msg) -} - -// UnwrapErrorFromFrameworkService converts a FrameworkServiceErrorMessage back to a Go error. -func UnwrapErrorFromFrameworkService(msg *FrameworkServiceErrorMessage) error { - return unwrapError(msg) -} diff --git a/cli/azd/pkg/azdext/extension_error_test.go b/cli/azd/pkg/azdext/extension_error_test.go index fcfdc42c26b..20881c1c551 100644 --- a/cli/azd/pkg/azdext/extension_error_test.go +++ b/cli/azd/pkg/azdext/extension_error_test.go @@ -9,42 +9,62 @@ import ( "github.com/Azure/azure-sdk-for-go/sdk/azcore" "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" ) func TestExtensionError_RoundTrip(t *testing.T) { tests := []struct { - name string - inputErr error - wantInfo errorInfo - useErrorStringAsMessage bool + name string + inputErr error + wantNil bool + verify func(t *testing.T, protoErr *ExtensionError, goErr error) }{ { name: "NilError", inputErr: nil, - wantInfo: errorInfo{}, + wantNil: true, }, { name: "SimpleError", inputErr: errors.New("simple error"), - wantInfo: errorInfo{ - message: "simple error", + verify: func(t *testing.T, protoErr *ExtensionError, goErr error) { + assert.Equal(t, "simple error", protoErr.GetMessage()) + assert.Equal(t, ErrorOrigin_ERROR_ORIGIN_UNSPECIFIED, protoErr.GetOrigin()) + assert.Nil(t, protoErr.GetSource()) + + // Unspecified origin falls back to ExtServiceError + var svcErr *ServiceError + require.ErrorAs(t, goErr, &svcErr) + assert.Equal(t, "simple error", svcErr.Message) }, }, { - name: "ExtensionResponseError", - inputErr: &ExtensionResponseError{ + name: "ExtServiceError", + inputErr: &ServiceError{ Message: "Rate limit exceeded", Details: "Too many requests", ErrorCode: "RateLimitExceeded", StatusCode: 429, ServiceName: "openai.azure.com", }, - wantInfo: errorInfo{ - message: "Rate limit exceeded", - details: "Too many requests", - errorCode: "RateLimitExceeded", - statusCode: 429, - service: "openai.azure.com", + verify: func(t *testing.T, protoErr *ExtensionError, goErr error) { + assert.Equal(t, "Rate limit exceeded", protoErr.GetMessage()) + assert.Equal(t, "Too many requests", protoErr.GetDetails()) + assert.Equal(t, ErrorOrigin_ERROR_ORIGIN_SERVICE, protoErr.GetOrigin()) + + svcDetail := protoErr.GetServiceError() + require.NotNil(t, svcDetail) + assert.Equal(t, "RateLimitExceeded", svcDetail.GetErrorCode()) + assert.Equal(t, int32(429), svcDetail.GetStatusCode()) + assert.Equal(t, "openai.azure.com", svcDetail.GetServiceName()) + + var svcErr *ServiceError + require.ErrorAs(t, goErr, &svcErr) + assert.Equal(t, "Rate limit exceeded", svcErr.Message) + assert.Equal(t, "Too many requests", svcErr.Details) + assert.Equal(t, "RateLimitExceeded", svcErr.ErrorCode) + assert.Equal(t, 429, svcErr.StatusCode) + assert.Equal(t, "openai.azure.com", svcErr.ServiceName) }, }, { @@ -53,66 +73,37 @@ func TestExtensionError_RoundTrip(t *testing.T) { ErrorCode: "ResourceNotFound", StatusCode: 404, }, - wantInfo: errorInfo{ - errorCode: "ResourceNotFound", - statusCode: 404, + verify: func(t *testing.T, protoErr *ExtensionError, goErr error) { + assert.Equal(t, ErrorOrigin_ERROR_ORIGIN_SERVICE, protoErr.GetOrigin()) + + svcDetail := protoErr.GetServiceError() + require.NotNil(t, svcDetail) + assert.Equal(t, "ResourceNotFound", svcDetail.GetErrorCode()) + assert.Equal(t, int32(404), svcDetail.GetStatusCode()) + + var svcErr *ServiceError + require.ErrorAs(t, goErr, &svcErr) + assert.Equal(t, "ResourceNotFound", svcErr.ErrorCode) + assert.Equal(t, 404, svcErr.StatusCode) }, - useErrorStringAsMessage: true, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - if tt.useErrorStringAsMessage { - tt.wantInfo.message = tt.inputErr.Error() - } + protoErr := WrapError(tt.inputErr) - // Helper to verify the proto message content - type protoMessage interface { - GetMessage() string - GetDetails() string - GetErrorCode() string - GetStatusCode() int32 - GetServiceName() string - } - verifyProto := func(t *testing.T, msg protoMessage) { - assert.NotNil(t, msg) - assert.Equal(t, tt.wantInfo.message, msg.GetMessage()) - assert.Equal(t, tt.wantInfo.details, msg.GetDetails()) - assert.Equal(t, tt.wantInfo.errorCode, msg.GetErrorCode()) - assert.Equal(t, tt.wantInfo.statusCode, msg.GetStatusCode()) - assert.Equal(t, tt.wantInfo.service, msg.GetServiceName()) + if tt.wantNil { + assert.Nil(t, protoErr) + assert.Nil(t, UnwrapError(nil)) + return } - // Helper to verify the unwrapped error - verifyUnwrapped := func(t *testing.T, err error) { - var extErr *ExtensionResponseError - if assert.ErrorAs(t, err, &extErr) { - assert.Equal(t, tt.wantInfo.message, extErr.Message) - assert.Equal(t, tt.wantInfo.details, extErr.Details) - assert.Equal(t, tt.wantInfo.errorCode, extErr.ErrorCode) - assert.Equal(t, int(tt.wantInfo.statusCode), extErr.StatusCode) - assert.Equal(t, tt.wantInfo.service, extErr.ServiceName) - } - } + require.NotNil(t, protoErr) + goErr := UnwrapError(protoErr) + require.NotNil(t, goErr) - // Test ServiceTarget wrapping - stMsg := WrapErrorForServiceTarget(tt.inputErr) - if tt.inputErr == nil { - assert.Nil(t, stMsg) - } else { - verifyProto(t, stMsg) - verifyUnwrapped(t, UnwrapErrorFromServiceTarget(stMsg)) - } - - // Test FrameworkService wrapping - fsMsg := WrapErrorForFrameworkService(tt.inputErr) - if tt.inputErr == nil { - assert.Nil(t, fsMsg) - } else { - verifyProto(t, fsMsg) - verifyUnwrapped(t, UnwrapErrorFromFrameworkService(fsMsg)) - } + tt.verify(t, protoErr, goErr) }) } } diff --git a/cli/azd/pkg/azdext/framework_service.pb.go b/cli/azd/pkg/azdext/framework_service.pb.go index aade60fafba..dc43f6e3a4c 100644 --- a/cli/azd/pkg/azdext/framework_service.pb.go +++ b/cli/azd/pkg/azdext/framework_service.pb.go @@ -26,9 +26,9 @@ const ( // Envelope for all possible framework service messages (requests and responses) type FrameworkServiceMessage struct { - state protoimpl.MessageState `protogen:"open.v1"` - RequestId string `protobuf:"bytes,1,opt,name=request_id,json=requestId,proto3" json:"request_id,omitempty"` - Error *FrameworkServiceErrorMessage `protobuf:"bytes,99,opt,name=error,proto3" json:"error,omitempty"` + state protoimpl.MessageState `protogen:"open.v1"` + RequestId string `protobuf:"bytes,1,opt,name=request_id,json=requestId,proto3" json:"request_id,omitempty"` + Error *ExtensionError `protobuf:"bytes,99,opt,name=error,proto3" json:"error,omitempty"` // Types that are valid to be assigned to MessageType: // // *FrameworkServiceMessage_RegisterFrameworkServiceRequest @@ -88,7 +88,7 @@ func (x *FrameworkServiceMessage) GetRequestId() string { return "" } -func (x *FrameworkServiceMessage) GetError() *FrameworkServiceErrorMessage { +func (x *FrameworkServiceMessage) GetError() *ExtensionError { if x != nil { return x.Error } @@ -335,84 +335,6 @@ func (*FrameworkServiceMessage_PackageResponse) isFrameworkServiceMessage_Messag func (*FrameworkServiceMessage_ProgressMessage) isFrameworkServiceMessage_MessageType() {} -// Error message for framework service operations -type FrameworkServiceErrorMessage struct { - state protoimpl.MessageState `protogen:"open.v1"` - Message string `protobuf:"bytes,1,opt,name=message,proto3" json:"message,omitempty"` - Details string `protobuf:"bytes,2,opt,name=details,proto3" json:"details,omitempty"` - // Structured error information for telemetry - ErrorCode string `protobuf:"bytes,3,opt,name=error_code,json=errorCode,proto3" json:"error_code,omitempty"` // Error code from the service (e.g., "Conflict", "NotFound") - StatusCode int32 `protobuf:"varint,4,opt,name=status_code,json=statusCode,proto3" json:"status_code,omitempty"` // HTTP status code (e.g., 409, 404, 500) - ServiceName string `protobuf:"bytes,5,opt,name=service_name,json=serviceName,proto3" json:"service_name,omitempty"` // Service name for telemetry (e.g., "ai.azure.com", "cognitiveservices") - unknownFields protoimpl.UnknownFields - sizeCache protoimpl.SizeCache -} - -func (x *FrameworkServiceErrorMessage) Reset() { - *x = FrameworkServiceErrorMessage{} - mi := &file_framework_service_proto_msgTypes[1] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) -} - -func (x *FrameworkServiceErrorMessage) String() string { - return protoimpl.X.MessageStringOf(x) -} - -func (*FrameworkServiceErrorMessage) ProtoMessage() {} - -func (x *FrameworkServiceErrorMessage) ProtoReflect() protoreflect.Message { - mi := &file_framework_service_proto_msgTypes[1] - if x != nil { - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - if ms.LoadMessageInfo() == nil { - ms.StoreMessageInfo(mi) - } - return ms - } - return mi.MessageOf(x) -} - -// Deprecated: Use FrameworkServiceErrorMessage.ProtoReflect.Descriptor instead. -func (*FrameworkServiceErrorMessage) Descriptor() ([]byte, []int) { - return file_framework_service_proto_rawDescGZIP(), []int{1} -} - -func (x *FrameworkServiceErrorMessage) GetMessage() string { - if x != nil { - return x.Message - } - return "" -} - -func (x *FrameworkServiceErrorMessage) GetDetails() string { - if x != nil { - return x.Details - } - return "" -} - -func (x *FrameworkServiceErrorMessage) GetErrorCode() string { - if x != nil { - return x.ErrorCode - } - return "" -} - -func (x *FrameworkServiceErrorMessage) GetStatusCode() int32 { - if x != nil { - return x.StatusCode - } - return 0 -} - -func (x *FrameworkServiceErrorMessage) GetServiceName() string { - if x != nil { - return x.ServiceName - } - return "" -} - // Request to register a framework service provider type RegisterFrameworkServiceRequest struct { state protoimpl.MessageState `protogen:"open.v1"` @@ -423,7 +345,7 @@ type RegisterFrameworkServiceRequest struct { func (x *RegisterFrameworkServiceRequest) Reset() { *x = RegisterFrameworkServiceRequest{} - mi := &file_framework_service_proto_msgTypes[2] + mi := &file_framework_service_proto_msgTypes[1] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -435,7 +357,7 @@ func (x *RegisterFrameworkServiceRequest) String() string { func (*RegisterFrameworkServiceRequest) ProtoMessage() {} func (x *RegisterFrameworkServiceRequest) ProtoReflect() protoreflect.Message { - mi := &file_framework_service_proto_msgTypes[2] + mi := &file_framework_service_proto_msgTypes[1] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -448,7 +370,7 @@ func (x *RegisterFrameworkServiceRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use RegisterFrameworkServiceRequest.ProtoReflect.Descriptor instead. func (*RegisterFrameworkServiceRequest) Descriptor() ([]byte, []int) { - return file_framework_service_proto_rawDescGZIP(), []int{2} + return file_framework_service_proto_rawDescGZIP(), []int{1} } func (x *RegisterFrameworkServiceRequest) GetLanguage() string { @@ -466,7 +388,7 @@ type RegisterFrameworkServiceResponse struct { func (x *RegisterFrameworkServiceResponse) Reset() { *x = RegisterFrameworkServiceResponse{} - mi := &file_framework_service_proto_msgTypes[3] + mi := &file_framework_service_proto_msgTypes[2] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -478,7 +400,7 @@ func (x *RegisterFrameworkServiceResponse) String() string { func (*RegisterFrameworkServiceResponse) ProtoMessage() {} func (x *RegisterFrameworkServiceResponse) ProtoReflect() protoreflect.Message { - mi := &file_framework_service_proto_msgTypes[3] + mi := &file_framework_service_proto_msgTypes[2] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -491,7 +413,7 @@ func (x *RegisterFrameworkServiceResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use RegisterFrameworkServiceResponse.ProtoReflect.Descriptor instead. func (*RegisterFrameworkServiceResponse) Descriptor() ([]byte, []int) { - return file_framework_service_proto_rawDescGZIP(), []int{3} + return file_framework_service_proto_rawDescGZIP(), []int{2} } // Initialize request and response @@ -504,7 +426,7 @@ type FrameworkServiceInitializeRequest struct { func (x *FrameworkServiceInitializeRequest) Reset() { *x = FrameworkServiceInitializeRequest{} - mi := &file_framework_service_proto_msgTypes[4] + mi := &file_framework_service_proto_msgTypes[3] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -516,7 +438,7 @@ func (x *FrameworkServiceInitializeRequest) String() string { func (*FrameworkServiceInitializeRequest) ProtoMessage() {} func (x *FrameworkServiceInitializeRequest) ProtoReflect() protoreflect.Message { - mi := &file_framework_service_proto_msgTypes[4] + mi := &file_framework_service_proto_msgTypes[3] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -529,7 +451,7 @@ func (x *FrameworkServiceInitializeRequest) ProtoReflect() protoreflect.Message // Deprecated: Use FrameworkServiceInitializeRequest.ProtoReflect.Descriptor instead. func (*FrameworkServiceInitializeRequest) Descriptor() ([]byte, []int) { - return file_framework_service_proto_rawDescGZIP(), []int{4} + return file_framework_service_proto_rawDescGZIP(), []int{3} } func (x *FrameworkServiceInitializeRequest) GetServiceConfig() *ServiceConfig { @@ -547,7 +469,7 @@ type FrameworkServiceInitializeResponse struct { func (x *FrameworkServiceInitializeResponse) Reset() { *x = FrameworkServiceInitializeResponse{} - mi := &file_framework_service_proto_msgTypes[5] + mi := &file_framework_service_proto_msgTypes[4] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -559,7 +481,7 @@ func (x *FrameworkServiceInitializeResponse) String() string { func (*FrameworkServiceInitializeResponse) ProtoMessage() {} func (x *FrameworkServiceInitializeResponse) ProtoReflect() protoreflect.Message { - mi := &file_framework_service_proto_msgTypes[5] + mi := &file_framework_service_proto_msgTypes[4] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -572,7 +494,7 @@ func (x *FrameworkServiceInitializeResponse) ProtoReflect() protoreflect.Message // Deprecated: Use FrameworkServiceInitializeResponse.ProtoReflect.Descriptor instead. func (*FrameworkServiceInitializeResponse) Descriptor() ([]byte, []int) { - return file_framework_service_proto_rawDescGZIP(), []int{5} + return file_framework_service_proto_rawDescGZIP(), []int{4} } // Required external tools request and response @@ -585,7 +507,7 @@ type FrameworkServiceRequiredExternalToolsRequest struct { func (x *FrameworkServiceRequiredExternalToolsRequest) Reset() { *x = FrameworkServiceRequiredExternalToolsRequest{} - mi := &file_framework_service_proto_msgTypes[6] + mi := &file_framework_service_proto_msgTypes[5] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -597,7 +519,7 @@ func (x *FrameworkServiceRequiredExternalToolsRequest) String() string { func (*FrameworkServiceRequiredExternalToolsRequest) ProtoMessage() {} func (x *FrameworkServiceRequiredExternalToolsRequest) ProtoReflect() protoreflect.Message { - mi := &file_framework_service_proto_msgTypes[6] + mi := &file_framework_service_proto_msgTypes[5] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -610,7 +532,7 @@ func (x *FrameworkServiceRequiredExternalToolsRequest) ProtoReflect() protorefle // Deprecated: Use FrameworkServiceRequiredExternalToolsRequest.ProtoReflect.Descriptor instead. func (*FrameworkServiceRequiredExternalToolsRequest) Descriptor() ([]byte, []int) { - return file_framework_service_proto_rawDescGZIP(), []int{6} + return file_framework_service_proto_rawDescGZIP(), []int{5} } func (x *FrameworkServiceRequiredExternalToolsRequest) GetServiceConfig() *ServiceConfig { @@ -629,7 +551,7 @@ type FrameworkServiceRequiredExternalToolsResponse struct { func (x *FrameworkServiceRequiredExternalToolsResponse) Reset() { *x = FrameworkServiceRequiredExternalToolsResponse{} - mi := &file_framework_service_proto_msgTypes[7] + mi := &file_framework_service_proto_msgTypes[6] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -641,7 +563,7 @@ func (x *FrameworkServiceRequiredExternalToolsResponse) String() string { func (*FrameworkServiceRequiredExternalToolsResponse) ProtoMessage() {} func (x *FrameworkServiceRequiredExternalToolsResponse) ProtoReflect() protoreflect.Message { - mi := &file_framework_service_proto_msgTypes[7] + mi := &file_framework_service_proto_msgTypes[6] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -654,7 +576,7 @@ func (x *FrameworkServiceRequiredExternalToolsResponse) ProtoReflect() protorefl // Deprecated: Use FrameworkServiceRequiredExternalToolsResponse.ProtoReflect.Descriptor instead. func (*FrameworkServiceRequiredExternalToolsResponse) Descriptor() ([]byte, []int) { - return file_framework_service_proto_rawDescGZIP(), []int{7} + return file_framework_service_proto_rawDescGZIP(), []int{6} } func (x *FrameworkServiceRequiredExternalToolsResponse) GetTools() []*ExternalTool { @@ -675,7 +597,7 @@ type ExternalTool struct { func (x *ExternalTool) Reset() { *x = ExternalTool{} - mi := &file_framework_service_proto_msgTypes[8] + mi := &file_framework_service_proto_msgTypes[7] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -687,7 +609,7 @@ func (x *ExternalTool) String() string { func (*ExternalTool) ProtoMessage() {} func (x *ExternalTool) ProtoReflect() protoreflect.Message { - mi := &file_framework_service_proto_msgTypes[8] + mi := &file_framework_service_proto_msgTypes[7] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -700,7 +622,7 @@ func (x *ExternalTool) ProtoReflect() protoreflect.Message { // Deprecated: Use ExternalTool.ProtoReflect.Descriptor instead. func (*ExternalTool) Descriptor() ([]byte, []int) { - return file_framework_service_proto_rawDescGZIP(), []int{8} + return file_framework_service_proto_rawDescGZIP(), []int{7} } func (x *ExternalTool) GetName() string { @@ -726,7 +648,7 @@ type FrameworkServiceRequirementsRequest struct { func (x *FrameworkServiceRequirementsRequest) Reset() { *x = FrameworkServiceRequirementsRequest{} - mi := &file_framework_service_proto_msgTypes[9] + mi := &file_framework_service_proto_msgTypes[8] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -738,7 +660,7 @@ func (x *FrameworkServiceRequirementsRequest) String() string { func (*FrameworkServiceRequirementsRequest) ProtoMessage() {} func (x *FrameworkServiceRequirementsRequest) ProtoReflect() protoreflect.Message { - mi := &file_framework_service_proto_msgTypes[9] + mi := &file_framework_service_proto_msgTypes[8] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -751,7 +673,7 @@ func (x *FrameworkServiceRequirementsRequest) ProtoReflect() protoreflect.Messag // Deprecated: Use FrameworkServiceRequirementsRequest.ProtoReflect.Descriptor instead. func (*FrameworkServiceRequirementsRequest) Descriptor() ([]byte, []int) { - return file_framework_service_proto_rawDescGZIP(), []int{9} + return file_framework_service_proto_rawDescGZIP(), []int{8} } type FrameworkServiceRequirementsResponse struct { @@ -763,7 +685,7 @@ type FrameworkServiceRequirementsResponse struct { func (x *FrameworkServiceRequirementsResponse) Reset() { *x = FrameworkServiceRequirementsResponse{} - mi := &file_framework_service_proto_msgTypes[10] + mi := &file_framework_service_proto_msgTypes[9] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -775,7 +697,7 @@ func (x *FrameworkServiceRequirementsResponse) String() string { func (*FrameworkServiceRequirementsResponse) ProtoMessage() {} func (x *FrameworkServiceRequirementsResponse) ProtoReflect() protoreflect.Message { - mi := &file_framework_service_proto_msgTypes[10] + mi := &file_framework_service_proto_msgTypes[9] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -788,7 +710,7 @@ func (x *FrameworkServiceRequirementsResponse) ProtoReflect() protoreflect.Messa // Deprecated: Use FrameworkServiceRequirementsResponse.ProtoReflect.Descriptor instead. func (*FrameworkServiceRequirementsResponse) Descriptor() ([]byte, []int) { - return file_framework_service_proto_rawDescGZIP(), []int{10} + return file_framework_service_proto_rawDescGZIP(), []int{9} } func (x *FrameworkServiceRequirementsResponse) GetRequirements() *FrameworkRequirements { @@ -808,7 +730,7 @@ type FrameworkRequirements struct { func (x *FrameworkRequirements) Reset() { *x = FrameworkRequirements{} - mi := &file_framework_service_proto_msgTypes[11] + mi := &file_framework_service_proto_msgTypes[10] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -820,7 +742,7 @@ func (x *FrameworkRequirements) String() string { func (*FrameworkRequirements) ProtoMessage() {} func (x *FrameworkRequirements) ProtoReflect() protoreflect.Message { - mi := &file_framework_service_proto_msgTypes[11] + mi := &file_framework_service_proto_msgTypes[10] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -833,7 +755,7 @@ func (x *FrameworkRequirements) ProtoReflect() protoreflect.Message { // Deprecated: Use FrameworkRequirements.ProtoReflect.Descriptor instead. func (*FrameworkRequirements) Descriptor() ([]byte, []int) { - return file_framework_service_proto_rawDescGZIP(), []int{11} + return file_framework_service_proto_rawDescGZIP(), []int{10} } func (x *FrameworkRequirements) GetPackage() *FrameworkPackageRequirements { @@ -853,7 +775,7 @@ type FrameworkPackageRequirements struct { func (x *FrameworkPackageRequirements) Reset() { *x = FrameworkPackageRequirements{} - mi := &file_framework_service_proto_msgTypes[12] + mi := &file_framework_service_proto_msgTypes[11] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -865,7 +787,7 @@ func (x *FrameworkPackageRequirements) String() string { func (*FrameworkPackageRequirements) ProtoMessage() {} func (x *FrameworkPackageRequirements) ProtoReflect() protoreflect.Message { - mi := &file_framework_service_proto_msgTypes[12] + mi := &file_framework_service_proto_msgTypes[11] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -878,7 +800,7 @@ func (x *FrameworkPackageRequirements) ProtoReflect() protoreflect.Message { // Deprecated: Use FrameworkPackageRequirements.ProtoReflect.Descriptor instead. func (*FrameworkPackageRequirements) Descriptor() ([]byte, []int) { - return file_framework_service_proto_rawDescGZIP(), []int{12} + return file_framework_service_proto_rawDescGZIP(), []int{11} } func (x *FrameworkPackageRequirements) GetRequireRestore() bool { @@ -906,7 +828,7 @@ type FrameworkServiceRestoreRequest struct { func (x *FrameworkServiceRestoreRequest) Reset() { *x = FrameworkServiceRestoreRequest{} - mi := &file_framework_service_proto_msgTypes[13] + mi := &file_framework_service_proto_msgTypes[12] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -918,7 +840,7 @@ func (x *FrameworkServiceRestoreRequest) String() string { func (*FrameworkServiceRestoreRequest) ProtoMessage() {} func (x *FrameworkServiceRestoreRequest) ProtoReflect() protoreflect.Message { - mi := &file_framework_service_proto_msgTypes[13] + mi := &file_framework_service_proto_msgTypes[12] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -931,7 +853,7 @@ func (x *FrameworkServiceRestoreRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use FrameworkServiceRestoreRequest.ProtoReflect.Descriptor instead. func (*FrameworkServiceRestoreRequest) Descriptor() ([]byte, []int) { - return file_framework_service_proto_rawDescGZIP(), []int{13} + return file_framework_service_proto_rawDescGZIP(), []int{12} } func (x *FrameworkServiceRestoreRequest) GetServiceConfig() *ServiceConfig { @@ -957,7 +879,7 @@ type FrameworkServiceRestoreResponse struct { func (x *FrameworkServiceRestoreResponse) Reset() { *x = FrameworkServiceRestoreResponse{} - mi := &file_framework_service_proto_msgTypes[14] + mi := &file_framework_service_proto_msgTypes[13] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -969,7 +891,7 @@ func (x *FrameworkServiceRestoreResponse) String() string { func (*FrameworkServiceRestoreResponse) ProtoMessage() {} func (x *FrameworkServiceRestoreResponse) ProtoReflect() protoreflect.Message { - mi := &file_framework_service_proto_msgTypes[14] + mi := &file_framework_service_proto_msgTypes[13] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -982,7 +904,7 @@ func (x *FrameworkServiceRestoreResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use FrameworkServiceRestoreResponse.ProtoReflect.Descriptor instead. func (*FrameworkServiceRestoreResponse) Descriptor() ([]byte, []int) { - return file_framework_service_proto_rawDescGZIP(), []int{14} + return file_framework_service_proto_rawDescGZIP(), []int{13} } func (x *FrameworkServiceRestoreResponse) GetRestoreResult() *ServiceRestoreResult { @@ -1002,7 +924,7 @@ type ServiceRestoreResult struct { func (x *ServiceRestoreResult) Reset() { *x = ServiceRestoreResult{} - mi := &file_framework_service_proto_msgTypes[15] + mi := &file_framework_service_proto_msgTypes[14] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -1014,7 +936,7 @@ func (x *ServiceRestoreResult) String() string { func (*ServiceRestoreResult) ProtoMessage() {} func (x *ServiceRestoreResult) ProtoReflect() protoreflect.Message { - mi := &file_framework_service_proto_msgTypes[15] + mi := &file_framework_service_proto_msgTypes[14] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -1027,7 +949,7 @@ func (x *ServiceRestoreResult) ProtoReflect() protoreflect.Message { // Deprecated: Use ServiceRestoreResult.ProtoReflect.Descriptor instead. func (*ServiceRestoreResult) Descriptor() ([]byte, []int) { - return file_framework_service_proto_rawDescGZIP(), []int{15} + return file_framework_service_proto_rawDescGZIP(), []int{14} } func (x *ServiceRestoreResult) GetArtifacts() []*Artifact { @@ -1048,7 +970,7 @@ type FrameworkServiceBuildRequest struct { func (x *FrameworkServiceBuildRequest) Reset() { *x = FrameworkServiceBuildRequest{} - mi := &file_framework_service_proto_msgTypes[16] + mi := &file_framework_service_proto_msgTypes[15] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -1060,7 +982,7 @@ func (x *FrameworkServiceBuildRequest) String() string { func (*FrameworkServiceBuildRequest) ProtoMessage() {} func (x *FrameworkServiceBuildRequest) ProtoReflect() protoreflect.Message { - mi := &file_framework_service_proto_msgTypes[16] + mi := &file_framework_service_proto_msgTypes[15] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -1073,7 +995,7 @@ func (x *FrameworkServiceBuildRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use FrameworkServiceBuildRequest.ProtoReflect.Descriptor instead. func (*FrameworkServiceBuildRequest) Descriptor() ([]byte, []int) { - return file_framework_service_proto_rawDescGZIP(), []int{16} + return file_framework_service_proto_rawDescGZIP(), []int{15} } func (x *FrameworkServiceBuildRequest) GetServiceConfig() *ServiceConfig { @@ -1099,7 +1021,7 @@ type FrameworkServiceBuildResponse struct { func (x *FrameworkServiceBuildResponse) Reset() { *x = FrameworkServiceBuildResponse{} - mi := &file_framework_service_proto_msgTypes[17] + mi := &file_framework_service_proto_msgTypes[16] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -1111,7 +1033,7 @@ func (x *FrameworkServiceBuildResponse) String() string { func (*FrameworkServiceBuildResponse) ProtoMessage() {} func (x *FrameworkServiceBuildResponse) ProtoReflect() protoreflect.Message { - mi := &file_framework_service_proto_msgTypes[17] + mi := &file_framework_service_proto_msgTypes[16] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -1124,7 +1046,7 @@ func (x *FrameworkServiceBuildResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use FrameworkServiceBuildResponse.ProtoReflect.Descriptor instead. func (*FrameworkServiceBuildResponse) Descriptor() ([]byte, []int) { - return file_framework_service_proto_rawDescGZIP(), []int{17} + return file_framework_service_proto_rawDescGZIP(), []int{16} } func (x *FrameworkServiceBuildResponse) GetResult() *ServiceBuildResult { @@ -1143,7 +1065,7 @@ type ServiceBuildResult struct { func (x *ServiceBuildResult) Reset() { *x = ServiceBuildResult{} - mi := &file_framework_service_proto_msgTypes[18] + mi := &file_framework_service_proto_msgTypes[17] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -1155,7 +1077,7 @@ func (x *ServiceBuildResult) String() string { func (*ServiceBuildResult) ProtoMessage() {} func (x *ServiceBuildResult) ProtoReflect() protoreflect.Message { - mi := &file_framework_service_proto_msgTypes[18] + mi := &file_framework_service_proto_msgTypes[17] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -1168,7 +1090,7 @@ func (x *ServiceBuildResult) ProtoReflect() protoreflect.Message { // Deprecated: Use ServiceBuildResult.ProtoReflect.Descriptor instead. func (*ServiceBuildResult) Descriptor() ([]byte, []int) { - return file_framework_service_proto_rawDescGZIP(), []int{18} + return file_framework_service_proto_rawDescGZIP(), []int{17} } func (x *ServiceBuildResult) GetArtifacts() []*Artifact { @@ -1189,7 +1111,7 @@ type FrameworkServicePackageRequest struct { func (x *FrameworkServicePackageRequest) Reset() { *x = FrameworkServicePackageRequest{} - mi := &file_framework_service_proto_msgTypes[19] + mi := &file_framework_service_proto_msgTypes[18] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -1201,7 +1123,7 @@ func (x *FrameworkServicePackageRequest) String() string { func (*FrameworkServicePackageRequest) ProtoMessage() {} func (x *FrameworkServicePackageRequest) ProtoReflect() protoreflect.Message { - mi := &file_framework_service_proto_msgTypes[19] + mi := &file_framework_service_proto_msgTypes[18] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -1214,7 +1136,7 @@ func (x *FrameworkServicePackageRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use FrameworkServicePackageRequest.ProtoReflect.Descriptor instead. func (*FrameworkServicePackageRequest) Descriptor() ([]byte, []int) { - return file_framework_service_proto_rawDescGZIP(), []int{19} + return file_framework_service_proto_rawDescGZIP(), []int{18} } func (x *FrameworkServicePackageRequest) GetServiceConfig() *ServiceConfig { @@ -1240,7 +1162,7 @@ type FrameworkServicePackageResponse struct { func (x *FrameworkServicePackageResponse) Reset() { *x = FrameworkServicePackageResponse{} - mi := &file_framework_service_proto_msgTypes[20] + mi := &file_framework_service_proto_msgTypes[19] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -1252,7 +1174,7 @@ func (x *FrameworkServicePackageResponse) String() string { func (*FrameworkServicePackageResponse) ProtoMessage() {} func (x *FrameworkServicePackageResponse) ProtoReflect() protoreflect.Message { - mi := &file_framework_service_proto_msgTypes[20] + mi := &file_framework_service_proto_msgTypes[19] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -1265,7 +1187,7 @@ func (x *FrameworkServicePackageResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use FrameworkServicePackageResponse.ProtoReflect.Descriptor instead. func (*FrameworkServicePackageResponse) Descriptor() ([]byte, []int) { - return file_framework_service_proto_rawDescGZIP(), []int{20} + return file_framework_service_proto_rawDescGZIP(), []int{19} } func (x *FrameworkServicePackageResponse) GetPackageResult() *ServicePackageResult { @@ -1287,7 +1209,7 @@ type FrameworkServiceProgressMessage struct { func (x *FrameworkServiceProgressMessage) Reset() { *x = FrameworkServiceProgressMessage{} - mi := &file_framework_service_proto_msgTypes[21] + mi := &file_framework_service_proto_msgTypes[20] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -1299,7 +1221,7 @@ func (x *FrameworkServiceProgressMessage) String() string { func (*FrameworkServiceProgressMessage) ProtoMessage() {} func (x *FrameworkServiceProgressMessage) ProtoReflect() protoreflect.Message { - mi := &file_framework_service_proto_msgTypes[21] + mi := &file_framework_service_proto_msgTypes[20] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -1312,7 +1234,7 @@ func (x *FrameworkServiceProgressMessage) ProtoReflect() protoreflect.Message { // Deprecated: Use FrameworkServiceProgressMessage.ProtoReflect.Descriptor instead. func (*FrameworkServiceProgressMessage) Descriptor() ([]byte, []int) { - return file_framework_service_proto_rawDescGZIP(), []int{21} + return file_framework_service_proto_rawDescGZIP(), []int{20} } func (x *FrameworkServiceProgressMessage) GetRequestId() string { @@ -1340,11 +1262,11 @@ var File_framework_service_proto protoreflect.FileDescriptor const file_framework_service_proto_rawDesc = "" + "\n" + - "\x17framework_service.proto\x12\x06azdext\x1a\fmodels.proto\x1a\x14service_target.proto\"\xc0\f\n" + + "\x17framework_service.proto\x12\x06azdext\x1a\fmodels.proto\x1a\x14service_target.proto\x1a\ferrors.proto\"\xb2\f\n" + "\x17FrameworkServiceMessage\x12\x1d\n" + "\n" + - "request_id\x18\x01 \x01(\tR\trequestId\x12:\n" + - "\x05error\x18c \x01(\v2$.azdext.FrameworkServiceErrorMessageR\x05error\x12v\n" + + "request_id\x18\x01 \x01(\tR\trequestId\x12,\n" + + "\x05error\x18c \x01(\v2\x16.azdext.ExtensionErrorR\x05error\x12v\n" + "\"register_framework_service_request\x18\x02 \x01(\v2'.azdext.RegisterFrameworkServiceRequestH\x00R\x1fregisterFrameworkServiceRequest\x12y\n" + "#register_framework_service_response\x18\x03 \x01(\v2(.azdext.RegisterFrameworkServiceResponseH\x00R registerFrameworkServiceResponse\x12Z\n" + "\x12initialize_request\x18\x04 \x01(\v2).azdext.FrameworkServiceInitializeRequestH\x00R\x11initializeRequest\x12]\n" + @@ -1361,15 +1283,7 @@ const file_framework_service_proto_rawDesc = "" + "\x0fpackage_request\x18\x0e \x01(\v2&.azdext.FrameworkServicePackageRequestH\x00R\x0epackageRequest\x12T\n" + "\x10package_response\x18\x0f \x01(\v2'.azdext.FrameworkServicePackageResponseH\x00R\x0fpackageResponse\x12T\n" + "\x10progress_message\x18\x10 \x01(\v2'.azdext.FrameworkServiceProgressMessageH\x00R\x0fprogressMessageB\x0e\n" + - "\fmessage_type\"\xb5\x01\n" + - "\x1cFrameworkServiceErrorMessage\x12\x18\n" + - "\amessage\x18\x01 \x01(\tR\amessage\x12\x18\n" + - "\adetails\x18\x02 \x01(\tR\adetails\x12\x1d\n" + - "\n" + - "error_code\x18\x03 \x01(\tR\terrorCode\x12\x1f\n" + - "\vstatus_code\x18\x04 \x01(\x05R\n" + - "statusCode\x12!\n" + - "\fservice_name\x18\x05 \x01(\tR\vserviceName\"=\n" + + "\fmessage_type\"=\n" + "\x1fRegisterFrameworkServiceRequest\x12\x1a\n" + "\blanguage\x18\x01 \x01(\tR\blanguage\"\"\n" + " RegisterFrameworkServiceResponse\"a\n" + @@ -1431,64 +1345,64 @@ func file_framework_service_proto_rawDescGZIP() []byte { return file_framework_service_proto_rawDescData } -var file_framework_service_proto_msgTypes = make([]protoimpl.MessageInfo, 22) +var file_framework_service_proto_msgTypes = make([]protoimpl.MessageInfo, 21) var file_framework_service_proto_goTypes = []any{ (*FrameworkServiceMessage)(nil), // 0: azdext.FrameworkServiceMessage - (*FrameworkServiceErrorMessage)(nil), // 1: azdext.FrameworkServiceErrorMessage - (*RegisterFrameworkServiceRequest)(nil), // 2: azdext.RegisterFrameworkServiceRequest - (*RegisterFrameworkServiceResponse)(nil), // 3: azdext.RegisterFrameworkServiceResponse - (*FrameworkServiceInitializeRequest)(nil), // 4: azdext.FrameworkServiceInitializeRequest - (*FrameworkServiceInitializeResponse)(nil), // 5: azdext.FrameworkServiceInitializeResponse - (*FrameworkServiceRequiredExternalToolsRequest)(nil), // 6: azdext.FrameworkServiceRequiredExternalToolsRequest - (*FrameworkServiceRequiredExternalToolsResponse)(nil), // 7: azdext.FrameworkServiceRequiredExternalToolsResponse - (*ExternalTool)(nil), // 8: azdext.ExternalTool - (*FrameworkServiceRequirementsRequest)(nil), // 9: azdext.FrameworkServiceRequirementsRequest - (*FrameworkServiceRequirementsResponse)(nil), // 10: azdext.FrameworkServiceRequirementsResponse - (*FrameworkRequirements)(nil), // 11: azdext.FrameworkRequirements - (*FrameworkPackageRequirements)(nil), // 12: azdext.FrameworkPackageRequirements - (*FrameworkServiceRestoreRequest)(nil), // 13: azdext.FrameworkServiceRestoreRequest - (*FrameworkServiceRestoreResponse)(nil), // 14: azdext.FrameworkServiceRestoreResponse - (*ServiceRestoreResult)(nil), // 15: azdext.ServiceRestoreResult - (*FrameworkServiceBuildRequest)(nil), // 16: azdext.FrameworkServiceBuildRequest - (*FrameworkServiceBuildResponse)(nil), // 17: azdext.FrameworkServiceBuildResponse - (*ServiceBuildResult)(nil), // 18: azdext.ServiceBuildResult - (*FrameworkServicePackageRequest)(nil), // 19: azdext.FrameworkServicePackageRequest - (*FrameworkServicePackageResponse)(nil), // 20: azdext.FrameworkServicePackageResponse - (*FrameworkServiceProgressMessage)(nil), // 21: azdext.FrameworkServiceProgressMessage + (*RegisterFrameworkServiceRequest)(nil), // 1: azdext.RegisterFrameworkServiceRequest + (*RegisterFrameworkServiceResponse)(nil), // 2: azdext.RegisterFrameworkServiceResponse + (*FrameworkServiceInitializeRequest)(nil), // 3: azdext.FrameworkServiceInitializeRequest + (*FrameworkServiceInitializeResponse)(nil), // 4: azdext.FrameworkServiceInitializeResponse + (*FrameworkServiceRequiredExternalToolsRequest)(nil), // 5: azdext.FrameworkServiceRequiredExternalToolsRequest + (*FrameworkServiceRequiredExternalToolsResponse)(nil), // 6: azdext.FrameworkServiceRequiredExternalToolsResponse + (*ExternalTool)(nil), // 7: azdext.ExternalTool + (*FrameworkServiceRequirementsRequest)(nil), // 8: azdext.FrameworkServiceRequirementsRequest + (*FrameworkServiceRequirementsResponse)(nil), // 9: azdext.FrameworkServiceRequirementsResponse + (*FrameworkRequirements)(nil), // 10: azdext.FrameworkRequirements + (*FrameworkPackageRequirements)(nil), // 11: azdext.FrameworkPackageRequirements + (*FrameworkServiceRestoreRequest)(nil), // 12: azdext.FrameworkServiceRestoreRequest + (*FrameworkServiceRestoreResponse)(nil), // 13: azdext.FrameworkServiceRestoreResponse + (*ServiceRestoreResult)(nil), // 14: azdext.ServiceRestoreResult + (*FrameworkServiceBuildRequest)(nil), // 15: azdext.FrameworkServiceBuildRequest + (*FrameworkServiceBuildResponse)(nil), // 16: azdext.FrameworkServiceBuildResponse + (*ServiceBuildResult)(nil), // 17: azdext.ServiceBuildResult + (*FrameworkServicePackageRequest)(nil), // 18: azdext.FrameworkServicePackageRequest + (*FrameworkServicePackageResponse)(nil), // 19: azdext.FrameworkServicePackageResponse + (*FrameworkServiceProgressMessage)(nil), // 20: azdext.FrameworkServiceProgressMessage + (*ExtensionError)(nil), // 21: azdext.ExtensionError (*ServiceConfig)(nil), // 22: azdext.ServiceConfig (*ServiceContext)(nil), // 23: azdext.ServiceContext (*Artifact)(nil), // 24: azdext.Artifact (*ServicePackageResult)(nil), // 25: azdext.ServicePackageResult } var file_framework_service_proto_depIdxs = []int32{ - 1, // 0: azdext.FrameworkServiceMessage.error:type_name -> azdext.FrameworkServiceErrorMessage - 2, // 1: azdext.FrameworkServiceMessage.register_framework_service_request:type_name -> azdext.RegisterFrameworkServiceRequest - 3, // 2: azdext.FrameworkServiceMessage.register_framework_service_response:type_name -> azdext.RegisterFrameworkServiceResponse - 4, // 3: azdext.FrameworkServiceMessage.initialize_request:type_name -> azdext.FrameworkServiceInitializeRequest - 5, // 4: azdext.FrameworkServiceMessage.initialize_response:type_name -> azdext.FrameworkServiceInitializeResponse - 6, // 5: azdext.FrameworkServiceMessage.required_external_tools_request:type_name -> azdext.FrameworkServiceRequiredExternalToolsRequest - 7, // 6: azdext.FrameworkServiceMessage.required_external_tools_response:type_name -> azdext.FrameworkServiceRequiredExternalToolsResponse - 9, // 7: azdext.FrameworkServiceMessage.requirements_request:type_name -> azdext.FrameworkServiceRequirementsRequest - 10, // 8: azdext.FrameworkServiceMessage.requirements_response:type_name -> azdext.FrameworkServiceRequirementsResponse - 13, // 9: azdext.FrameworkServiceMessage.restore_request:type_name -> azdext.FrameworkServiceRestoreRequest - 14, // 10: azdext.FrameworkServiceMessage.restore_response:type_name -> azdext.FrameworkServiceRestoreResponse - 16, // 11: azdext.FrameworkServiceMessage.build_request:type_name -> azdext.FrameworkServiceBuildRequest - 17, // 12: azdext.FrameworkServiceMessage.build_response:type_name -> azdext.FrameworkServiceBuildResponse - 19, // 13: azdext.FrameworkServiceMessage.package_request:type_name -> azdext.FrameworkServicePackageRequest - 20, // 14: azdext.FrameworkServiceMessage.package_response:type_name -> azdext.FrameworkServicePackageResponse - 21, // 15: azdext.FrameworkServiceMessage.progress_message:type_name -> azdext.FrameworkServiceProgressMessage + 21, // 0: azdext.FrameworkServiceMessage.error:type_name -> azdext.ExtensionError + 1, // 1: azdext.FrameworkServiceMessage.register_framework_service_request:type_name -> azdext.RegisterFrameworkServiceRequest + 2, // 2: azdext.FrameworkServiceMessage.register_framework_service_response:type_name -> azdext.RegisterFrameworkServiceResponse + 3, // 3: azdext.FrameworkServiceMessage.initialize_request:type_name -> azdext.FrameworkServiceInitializeRequest + 4, // 4: azdext.FrameworkServiceMessage.initialize_response:type_name -> azdext.FrameworkServiceInitializeResponse + 5, // 5: azdext.FrameworkServiceMessage.required_external_tools_request:type_name -> azdext.FrameworkServiceRequiredExternalToolsRequest + 6, // 6: azdext.FrameworkServiceMessage.required_external_tools_response:type_name -> azdext.FrameworkServiceRequiredExternalToolsResponse + 8, // 7: azdext.FrameworkServiceMessage.requirements_request:type_name -> azdext.FrameworkServiceRequirementsRequest + 9, // 8: azdext.FrameworkServiceMessage.requirements_response:type_name -> azdext.FrameworkServiceRequirementsResponse + 12, // 9: azdext.FrameworkServiceMessage.restore_request:type_name -> azdext.FrameworkServiceRestoreRequest + 13, // 10: azdext.FrameworkServiceMessage.restore_response:type_name -> azdext.FrameworkServiceRestoreResponse + 15, // 11: azdext.FrameworkServiceMessage.build_request:type_name -> azdext.FrameworkServiceBuildRequest + 16, // 12: azdext.FrameworkServiceMessage.build_response:type_name -> azdext.FrameworkServiceBuildResponse + 18, // 13: azdext.FrameworkServiceMessage.package_request:type_name -> azdext.FrameworkServicePackageRequest + 19, // 14: azdext.FrameworkServiceMessage.package_response:type_name -> azdext.FrameworkServicePackageResponse + 20, // 15: azdext.FrameworkServiceMessage.progress_message:type_name -> azdext.FrameworkServiceProgressMessage 22, // 16: azdext.FrameworkServiceInitializeRequest.service_config:type_name -> azdext.ServiceConfig 22, // 17: azdext.FrameworkServiceRequiredExternalToolsRequest.service_config:type_name -> azdext.ServiceConfig - 8, // 18: azdext.FrameworkServiceRequiredExternalToolsResponse.tools:type_name -> azdext.ExternalTool - 11, // 19: azdext.FrameworkServiceRequirementsResponse.requirements:type_name -> azdext.FrameworkRequirements - 12, // 20: azdext.FrameworkRequirements.package:type_name -> azdext.FrameworkPackageRequirements + 7, // 18: azdext.FrameworkServiceRequiredExternalToolsResponse.tools:type_name -> azdext.ExternalTool + 10, // 19: azdext.FrameworkServiceRequirementsResponse.requirements:type_name -> azdext.FrameworkRequirements + 11, // 20: azdext.FrameworkRequirements.package:type_name -> azdext.FrameworkPackageRequirements 22, // 21: azdext.FrameworkServiceRestoreRequest.service_config:type_name -> azdext.ServiceConfig 23, // 22: azdext.FrameworkServiceRestoreRequest.service_context:type_name -> azdext.ServiceContext - 15, // 23: azdext.FrameworkServiceRestoreResponse.restore_result:type_name -> azdext.ServiceRestoreResult + 14, // 23: azdext.FrameworkServiceRestoreResponse.restore_result:type_name -> azdext.ServiceRestoreResult 24, // 24: azdext.ServiceRestoreResult.artifacts:type_name -> azdext.Artifact 22, // 25: azdext.FrameworkServiceBuildRequest.service_config:type_name -> azdext.ServiceConfig 23, // 26: azdext.FrameworkServiceBuildRequest.service_context:type_name -> azdext.ServiceContext - 18, // 27: azdext.FrameworkServiceBuildResponse.result:type_name -> azdext.ServiceBuildResult + 17, // 27: azdext.FrameworkServiceBuildResponse.result:type_name -> azdext.ServiceBuildResult 24, // 28: azdext.ServiceBuildResult.artifacts:type_name -> azdext.Artifact 22, // 29: azdext.FrameworkServicePackageRequest.service_config:type_name -> azdext.ServiceConfig 23, // 30: azdext.FrameworkServicePackageRequest.service_context:type_name -> azdext.ServiceContext @@ -1509,6 +1423,7 @@ func file_framework_service_proto_init() { } file_models_proto_init() file_service_target_proto_init() + file_errors_proto_init() file_framework_service_proto_msgTypes[0].OneofWrappers = []any{ (*FrameworkServiceMessage_RegisterFrameworkServiceRequest)(nil), (*FrameworkServiceMessage_RegisterFrameworkServiceResponse)(nil), @@ -1532,7 +1447,7 @@ func file_framework_service_proto_init() { GoPackagePath: reflect.TypeOf(x{}).PkgPath(), RawDescriptor: unsafe.Slice(unsafe.StringData(file_framework_service_proto_rawDesc), len(file_framework_service_proto_rawDesc)), NumEnums: 0, - NumMessages: 22, + NumMessages: 21, NumExtensions: 0, NumServices: 1, }, diff --git a/cli/azd/pkg/azdext/framework_service_envelope.go b/cli/azd/pkg/azdext/framework_service_envelope.go index 7eec7d195c4..9e462f449f8 100644 --- a/cli/azd/pkg/azdext/framework_service_envelope.go +++ b/cli/azd/pkg/azdext/framework_service_envelope.go @@ -32,15 +32,15 @@ func (ops *FrameworkServiceEnvelope) SetRequestId(ctx context.Context, msg *Fram } // GetError returns the error from the message as a Go error type. -// It returns an ExtensionResponseError that preserves structured error information for telemetry. +// It returns a typed error based on the ErrorOrigin that preserves structured information for telemetry. func (ops *FrameworkServiceEnvelope) GetError(msg *FrameworkServiceMessage) error { - return UnwrapErrorFromFrameworkService(msg.Error) + return UnwrapError(msg.Error) } // SetError sets an error on the message. -// It extracts structured error information from known error types like azcore.ResponseError. +// It detects the error type and populates the appropriate source details. func (ops *FrameworkServiceEnvelope) SetError(msg *FrameworkServiceMessage, err error) { - msg.Error = WrapErrorForFrameworkService(err) + msg.Error = WrapError(err) } // GetInnerMessage returns the inner message from the oneof field diff --git a/cli/azd/pkg/azdext/service_target.pb.go b/cli/azd/pkg/azdext/service_target.pb.go index a53918467dd..94ba2e40b93 100644 --- a/cli/azd/pkg/azdext/service_target.pb.go +++ b/cli/azd/pkg/azdext/service_target.pb.go @@ -27,9 +27,9 @@ const ( // Envelope for all possible service target messages (requests and responses) type ServiceTargetMessage struct { - state protoimpl.MessageState `protogen:"open.v1"` - RequestId string `protobuf:"bytes,1,opt,name=request_id,json=requestId,proto3" json:"request_id,omitempty"` - Error *ServiceTargetErrorMessage `protobuf:"bytes,99,opt,name=error,proto3" json:"error,omitempty"` + state protoimpl.MessageState `protogen:"open.v1"` + RequestId string `protobuf:"bytes,1,opt,name=request_id,json=requestId,proto3" json:"request_id,omitempty"` + Error *ExtensionError `protobuf:"bytes,99,opt,name=error,proto3" json:"error,omitempty"` // Types that are valid to be assigned to MessageType: // // *ServiceTargetMessage_RegisterServiceTargetRequest @@ -89,7 +89,7 @@ func (x *ServiceTargetMessage) GetRequestId() string { return "" } -func (x *ServiceTargetMessage) GetError() *ServiceTargetErrorMessage { +func (x *ServiceTargetMessage) GetError() *ExtensionError { if x != nil { return x.Error } @@ -736,83 +736,6 @@ func (*RegisterServiceTargetResponse) Descriptor() ([]byte, []int) { return file_service_target_proto_rawDescGZIP(), []int{8} } -type ServiceTargetErrorMessage struct { - state protoimpl.MessageState `protogen:"open.v1"` - Message string `protobuf:"bytes,2,opt,name=message,proto3" json:"message,omitempty"` - Details string `protobuf:"bytes,3,opt,name=details,proto3" json:"details,omitempty"` - // Structured error information for telemetry - ErrorCode string `protobuf:"bytes,4,opt,name=error_code,json=errorCode,proto3" json:"error_code,omitempty"` // Error code from the service (e.g., "Conflict", "NotFound") - StatusCode int32 `protobuf:"varint,5,opt,name=status_code,json=statusCode,proto3" json:"status_code,omitempty"` // HTTP status code (e.g., 409, 404, 500) - ServiceName string `protobuf:"bytes,6,opt,name=service_name,json=serviceName,proto3" json:"service_name,omitempty"` // Service name for telemetry (e.g., "ai.azure.com", "cognitiveservices") - unknownFields protoimpl.UnknownFields - sizeCache protoimpl.SizeCache -} - -func (x *ServiceTargetErrorMessage) Reset() { - *x = ServiceTargetErrorMessage{} - mi := &file_service_target_proto_msgTypes[9] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) -} - -func (x *ServiceTargetErrorMessage) String() string { - return protoimpl.X.MessageStringOf(x) -} - -func (*ServiceTargetErrorMessage) ProtoMessage() {} - -func (x *ServiceTargetErrorMessage) ProtoReflect() protoreflect.Message { - mi := &file_service_target_proto_msgTypes[9] - if x != nil { - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - if ms.LoadMessageInfo() == nil { - ms.StoreMessageInfo(mi) - } - return ms - } - return mi.MessageOf(x) -} - -// Deprecated: Use ServiceTargetErrorMessage.ProtoReflect.Descriptor instead. -func (*ServiceTargetErrorMessage) Descriptor() ([]byte, []int) { - return file_service_target_proto_rawDescGZIP(), []int{9} -} - -func (x *ServiceTargetErrorMessage) GetMessage() string { - if x != nil { - return x.Message - } - return "" -} - -func (x *ServiceTargetErrorMessage) GetDetails() string { - if x != nil { - return x.Details - } - return "" -} - -func (x *ServiceTargetErrorMessage) GetErrorCode() string { - if x != nil { - return x.ErrorCode - } - return "" -} - -func (x *ServiceTargetErrorMessage) GetStatusCode() int32 { - if x != nil { - return x.StatusCode - } - return 0 -} - -func (x *ServiceTargetErrorMessage) GetServiceName() string { - if x != nil { - return x.ServiceName - } - return "" -} - // GetTargetResource request and response type GetTargetResourceRequest struct { state protoimpl.MessageState `protogen:"open.v1"` @@ -830,7 +753,7 @@ type GetTargetResourceRequest struct { func (x *GetTargetResourceRequest) Reset() { *x = GetTargetResourceRequest{} - mi := &file_service_target_proto_msgTypes[10] + mi := &file_service_target_proto_msgTypes[9] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -842,7 +765,7 @@ func (x *GetTargetResourceRequest) String() string { func (*GetTargetResourceRequest) ProtoMessage() {} func (x *GetTargetResourceRequest) ProtoReflect() protoreflect.Message { - mi := &file_service_target_proto_msgTypes[10] + mi := &file_service_target_proto_msgTypes[9] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -855,7 +778,7 @@ func (x *GetTargetResourceRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use GetTargetResourceRequest.ProtoReflect.Descriptor instead. func (*GetTargetResourceRequest) Descriptor() ([]byte, []int) { - return file_service_target_proto_rawDescGZIP(), []int{10} + return file_service_target_proto_rawDescGZIP(), []int{9} } func (x *GetTargetResourceRequest) GetSubscriptionId() string { @@ -895,7 +818,7 @@ type GetTargetResourceResponse struct { func (x *GetTargetResourceResponse) Reset() { *x = GetTargetResourceResponse{} - mi := &file_service_target_proto_msgTypes[11] + mi := &file_service_target_proto_msgTypes[10] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -907,7 +830,7 @@ func (x *GetTargetResourceResponse) String() string { func (*GetTargetResourceResponse) ProtoMessage() {} func (x *GetTargetResourceResponse) ProtoReflect() protoreflect.Message { - mi := &file_service_target_proto_msgTypes[11] + mi := &file_service_target_proto_msgTypes[10] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -920,7 +843,7 @@ func (x *GetTargetResourceResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use GetTargetResourceResponse.ProtoReflect.Descriptor instead. func (*GetTargetResourceResponse) Descriptor() ([]byte, []int) { - return file_service_target_proto_rawDescGZIP(), []int{11} + return file_service_target_proto_rawDescGZIP(), []int{10} } func (x *GetTargetResourceResponse) GetTargetResource() *TargetResource { @@ -944,7 +867,7 @@ type TargetResource struct { func (x *TargetResource) Reset() { *x = TargetResource{} - mi := &file_service_target_proto_msgTypes[12] + mi := &file_service_target_proto_msgTypes[11] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -956,7 +879,7 @@ func (x *TargetResource) String() string { func (*TargetResource) ProtoMessage() {} func (x *TargetResource) ProtoReflect() protoreflect.Message { - mi := &file_service_target_proto_msgTypes[12] + mi := &file_service_target_proto_msgTypes[11] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -969,7 +892,7 @@ func (x *TargetResource) ProtoReflect() protoreflect.Message { // Deprecated: Use TargetResource.ProtoReflect.Descriptor instead. func (*TargetResource) Descriptor() ([]byte, []int) { - return file_service_target_proto_rawDescGZIP(), []int{12} + return file_service_target_proto_rawDescGZIP(), []int{11} } func (x *TargetResource) GetSubscriptionId() string { @@ -1019,7 +942,7 @@ type ServiceTargetDeployRequest struct { func (x *ServiceTargetDeployRequest) Reset() { *x = ServiceTargetDeployRequest{} - mi := &file_service_target_proto_msgTypes[13] + mi := &file_service_target_proto_msgTypes[12] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -1031,7 +954,7 @@ func (x *ServiceTargetDeployRequest) String() string { func (*ServiceTargetDeployRequest) ProtoMessage() {} func (x *ServiceTargetDeployRequest) ProtoReflect() protoreflect.Message { - mi := &file_service_target_proto_msgTypes[13] + mi := &file_service_target_proto_msgTypes[12] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -1044,7 +967,7 @@ func (x *ServiceTargetDeployRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use ServiceTargetDeployRequest.ProtoReflect.Descriptor instead. func (*ServiceTargetDeployRequest) Descriptor() ([]byte, []int) { - return file_service_target_proto_rawDescGZIP(), []int{13} + return file_service_target_proto_rawDescGZIP(), []int{12} } func (x *ServiceTargetDeployRequest) GetServiceConfig() *ServiceConfig { @@ -1077,7 +1000,7 @@ type ServiceTargetDeployResponse struct { func (x *ServiceTargetDeployResponse) Reset() { *x = ServiceTargetDeployResponse{} - mi := &file_service_target_proto_msgTypes[14] + mi := &file_service_target_proto_msgTypes[13] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -1089,7 +1012,7 @@ func (x *ServiceTargetDeployResponse) String() string { func (*ServiceTargetDeployResponse) ProtoMessage() {} func (x *ServiceTargetDeployResponse) ProtoReflect() protoreflect.Message { - mi := &file_service_target_proto_msgTypes[14] + mi := &file_service_target_proto_msgTypes[13] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -1102,7 +1025,7 @@ func (x *ServiceTargetDeployResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use ServiceTargetDeployResponse.ProtoReflect.Descriptor instead. func (*ServiceTargetDeployResponse) Descriptor() ([]byte, []int) { - return file_service_target_proto_rawDescGZIP(), []int{14} + return file_service_target_proto_rawDescGZIP(), []int{13} } func (x *ServiceTargetDeployResponse) GetResult() *ServiceDeployResult { @@ -1122,7 +1045,7 @@ type ServicePackageResult struct { func (x *ServicePackageResult) Reset() { *x = ServicePackageResult{} - mi := &file_service_target_proto_msgTypes[15] + mi := &file_service_target_proto_msgTypes[14] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -1134,7 +1057,7 @@ func (x *ServicePackageResult) String() string { func (*ServicePackageResult) ProtoMessage() {} func (x *ServicePackageResult) ProtoReflect() protoreflect.Message { - mi := &file_service_target_proto_msgTypes[15] + mi := &file_service_target_proto_msgTypes[14] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -1147,7 +1070,7 @@ func (x *ServicePackageResult) ProtoReflect() protoreflect.Message { // Deprecated: Use ServicePackageResult.ProtoReflect.Descriptor instead. func (*ServicePackageResult) Descriptor() ([]byte, []int) { - return file_service_target_proto_rawDescGZIP(), []int{15} + return file_service_target_proto_rawDescGZIP(), []int{14} } func (x *ServicePackageResult) GetArtifacts() []*Artifact { @@ -1167,7 +1090,7 @@ type ServicePublishResult struct { func (x *ServicePublishResult) Reset() { *x = ServicePublishResult{} - mi := &file_service_target_proto_msgTypes[16] + mi := &file_service_target_proto_msgTypes[15] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -1179,7 +1102,7 @@ func (x *ServicePublishResult) String() string { func (*ServicePublishResult) ProtoMessage() {} func (x *ServicePublishResult) ProtoReflect() protoreflect.Message { - mi := &file_service_target_proto_msgTypes[16] + mi := &file_service_target_proto_msgTypes[15] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -1192,7 +1115,7 @@ func (x *ServicePublishResult) ProtoReflect() protoreflect.Message { // Deprecated: Use ServicePublishResult.ProtoReflect.Descriptor instead. func (*ServicePublishResult) Descriptor() ([]byte, []int) { - return file_service_target_proto_rawDescGZIP(), []int{16} + return file_service_target_proto_rawDescGZIP(), []int{15} } func (x *ServicePublishResult) GetArtifacts() []*Artifact { @@ -1212,7 +1135,7 @@ type ServiceDeployResult struct { func (x *ServiceDeployResult) Reset() { *x = ServiceDeployResult{} - mi := &file_service_target_proto_msgTypes[17] + mi := &file_service_target_proto_msgTypes[16] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -1224,7 +1147,7 @@ func (x *ServiceDeployResult) String() string { func (*ServiceDeployResult) ProtoMessage() {} func (x *ServiceDeployResult) ProtoReflect() protoreflect.Message { - mi := &file_service_target_proto_msgTypes[17] + mi := &file_service_target_proto_msgTypes[16] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -1237,7 +1160,7 @@ func (x *ServiceDeployResult) ProtoReflect() protoreflect.Message { // Deprecated: Use ServiceDeployResult.ProtoReflect.Descriptor instead. func (*ServiceDeployResult) Descriptor() ([]byte, []int) { - return file_service_target_proto_rawDescGZIP(), []int{17} + return file_service_target_proto_rawDescGZIP(), []int{16} } func (x *ServiceDeployResult) GetArtifacts() []*Artifact { @@ -1258,7 +1181,7 @@ type ServiceTargetPackageRequest struct { func (x *ServiceTargetPackageRequest) Reset() { *x = ServiceTargetPackageRequest{} - mi := &file_service_target_proto_msgTypes[18] + mi := &file_service_target_proto_msgTypes[17] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -1270,7 +1193,7 @@ func (x *ServiceTargetPackageRequest) String() string { func (*ServiceTargetPackageRequest) ProtoMessage() {} func (x *ServiceTargetPackageRequest) ProtoReflect() protoreflect.Message { - mi := &file_service_target_proto_msgTypes[18] + mi := &file_service_target_proto_msgTypes[17] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -1283,7 +1206,7 @@ func (x *ServiceTargetPackageRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use ServiceTargetPackageRequest.ProtoReflect.Descriptor instead. func (*ServiceTargetPackageRequest) Descriptor() ([]byte, []int) { - return file_service_target_proto_rawDescGZIP(), []int{18} + return file_service_target_proto_rawDescGZIP(), []int{17} } func (x *ServiceTargetPackageRequest) GetServiceConfig() *ServiceConfig { @@ -1309,7 +1232,7 @@ type ServiceTargetPackageResponse struct { func (x *ServiceTargetPackageResponse) Reset() { *x = ServiceTargetPackageResponse{} - mi := &file_service_target_proto_msgTypes[19] + mi := &file_service_target_proto_msgTypes[18] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -1321,7 +1244,7 @@ func (x *ServiceTargetPackageResponse) String() string { func (*ServiceTargetPackageResponse) ProtoMessage() {} func (x *ServiceTargetPackageResponse) ProtoReflect() protoreflect.Message { - mi := &file_service_target_proto_msgTypes[19] + mi := &file_service_target_proto_msgTypes[18] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -1334,7 +1257,7 @@ func (x *ServiceTargetPackageResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use ServiceTargetPackageResponse.ProtoReflect.Descriptor instead. func (*ServiceTargetPackageResponse) Descriptor() ([]byte, []int) { - return file_service_target_proto_rawDescGZIP(), []int{19} + return file_service_target_proto_rawDescGZIP(), []int{18} } func (x *ServiceTargetPackageResponse) GetResult() *ServicePackageResult { @@ -1357,7 +1280,7 @@ type ServiceTargetPublishRequest struct { func (x *ServiceTargetPublishRequest) Reset() { *x = ServiceTargetPublishRequest{} - mi := &file_service_target_proto_msgTypes[20] + mi := &file_service_target_proto_msgTypes[19] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -1369,7 +1292,7 @@ func (x *ServiceTargetPublishRequest) String() string { func (*ServiceTargetPublishRequest) ProtoMessage() {} func (x *ServiceTargetPublishRequest) ProtoReflect() protoreflect.Message { - mi := &file_service_target_proto_msgTypes[20] + mi := &file_service_target_proto_msgTypes[19] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -1382,7 +1305,7 @@ func (x *ServiceTargetPublishRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use ServiceTargetPublishRequest.ProtoReflect.Descriptor instead. func (*ServiceTargetPublishRequest) Descriptor() ([]byte, []int) { - return file_service_target_proto_rawDescGZIP(), []int{20} + return file_service_target_proto_rawDescGZIP(), []int{19} } func (x *ServiceTargetPublishRequest) GetServiceConfig() *ServiceConfig { @@ -1422,7 +1345,7 @@ type ServiceTargetPublishResponse struct { func (x *ServiceTargetPublishResponse) Reset() { *x = ServiceTargetPublishResponse{} - mi := &file_service_target_proto_msgTypes[21] + mi := &file_service_target_proto_msgTypes[20] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -1434,7 +1357,7 @@ func (x *ServiceTargetPublishResponse) String() string { func (*ServiceTargetPublishResponse) ProtoMessage() {} func (x *ServiceTargetPublishResponse) ProtoReflect() protoreflect.Message { - mi := &file_service_target_proto_msgTypes[21] + mi := &file_service_target_proto_msgTypes[20] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -1447,7 +1370,7 @@ func (x *ServiceTargetPublishResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use ServiceTargetPublishResponse.ProtoReflect.Descriptor instead. func (*ServiceTargetPublishResponse) Descriptor() ([]byte, []int) { - return file_service_target_proto_rawDescGZIP(), []int{21} + return file_service_target_proto_rawDescGZIP(), []int{20} } func (x *ServiceTargetPublishResponse) GetResult() *ServicePublishResult { @@ -1468,7 +1391,7 @@ type PublishOptions struct { func (x *PublishOptions) Reset() { *x = PublishOptions{} - mi := &file_service_target_proto_msgTypes[22] + mi := &file_service_target_proto_msgTypes[21] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -1480,7 +1403,7 @@ func (x *PublishOptions) String() string { func (*PublishOptions) ProtoMessage() {} func (x *PublishOptions) ProtoReflect() protoreflect.Message { - mi := &file_service_target_proto_msgTypes[22] + mi := &file_service_target_proto_msgTypes[21] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -1493,7 +1416,7 @@ func (x *PublishOptions) ProtoReflect() protoreflect.Message { // Deprecated: Use PublishOptions.ProtoReflect.Descriptor instead. func (*PublishOptions) Descriptor() ([]byte, []int) { - return file_service_target_proto_rawDescGZIP(), []int{22} + return file_service_target_proto_rawDescGZIP(), []int{21} } func (x *PublishOptions) GetImage() string { @@ -1514,7 +1437,7 @@ type ServiceTargetEndpointsRequest struct { func (x *ServiceTargetEndpointsRequest) Reset() { *x = ServiceTargetEndpointsRequest{} - mi := &file_service_target_proto_msgTypes[23] + mi := &file_service_target_proto_msgTypes[22] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -1526,7 +1449,7 @@ func (x *ServiceTargetEndpointsRequest) String() string { func (*ServiceTargetEndpointsRequest) ProtoMessage() {} func (x *ServiceTargetEndpointsRequest) ProtoReflect() protoreflect.Message { - mi := &file_service_target_proto_msgTypes[23] + mi := &file_service_target_proto_msgTypes[22] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -1539,7 +1462,7 @@ func (x *ServiceTargetEndpointsRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use ServiceTargetEndpointsRequest.ProtoReflect.Descriptor instead. func (*ServiceTargetEndpointsRequest) Descriptor() ([]byte, []int) { - return file_service_target_proto_rawDescGZIP(), []int{23} + return file_service_target_proto_rawDescGZIP(), []int{22} } func (x *ServiceTargetEndpointsRequest) GetServiceConfig() *ServiceConfig { @@ -1565,7 +1488,7 @@ type ServiceTargetEndpointsResponse struct { func (x *ServiceTargetEndpointsResponse) Reset() { *x = ServiceTargetEndpointsResponse{} - mi := &file_service_target_proto_msgTypes[24] + mi := &file_service_target_proto_msgTypes[23] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -1577,7 +1500,7 @@ func (x *ServiceTargetEndpointsResponse) String() string { func (*ServiceTargetEndpointsResponse) ProtoMessage() {} func (x *ServiceTargetEndpointsResponse) ProtoReflect() protoreflect.Message { - mi := &file_service_target_proto_msgTypes[24] + mi := &file_service_target_proto_msgTypes[23] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -1590,7 +1513,7 @@ func (x *ServiceTargetEndpointsResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use ServiceTargetEndpointsResponse.ProtoReflect.Descriptor instead. func (*ServiceTargetEndpointsResponse) Descriptor() ([]byte, []int) { - return file_service_target_proto_rawDescGZIP(), []int{24} + return file_service_target_proto_rawDescGZIP(), []int{23} } func (x *ServiceTargetEndpointsResponse) GetEndpoints() []string { @@ -1612,7 +1535,7 @@ type ServiceTargetProgressMessage struct { func (x *ServiceTargetProgressMessage) Reset() { *x = ServiceTargetProgressMessage{} - mi := &file_service_target_proto_msgTypes[25] + mi := &file_service_target_proto_msgTypes[24] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -1624,7 +1547,7 @@ func (x *ServiceTargetProgressMessage) String() string { func (*ServiceTargetProgressMessage) ProtoMessage() {} func (x *ServiceTargetProgressMessage) ProtoReflect() protoreflect.Message { - mi := &file_service_target_proto_msgTypes[25] + mi := &file_service_target_proto_msgTypes[24] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -1637,7 +1560,7 @@ func (x *ServiceTargetProgressMessage) ProtoReflect() protoreflect.Message { // Deprecated: Use ServiceTargetProgressMessage.ProtoReflect.Descriptor instead. func (*ServiceTargetProgressMessage) Descriptor() ([]byte, []int) { - return file_service_target_proto_rawDescGZIP(), []int{25} + return file_service_target_proto_rawDescGZIP(), []int{24} } func (x *ServiceTargetProgressMessage) GetRequestId() string { @@ -1665,11 +1588,11 @@ var File_service_target_proto protoreflect.FileDescriptor const file_service_target_proto_rawDesc = "" + "\n" + - "\x14service_target.proto\x12\x06azdext\x1a$include/google/protobuf/struct.proto\x1a\fmodels.proto\"\xc2\v\n" + + "\x14service_target.proto\x12\x06azdext\x1a$include/google/protobuf/struct.proto\x1a\fmodels.proto\x1a\ferrors.proto\"\xb7\v\n" + "\x14ServiceTargetMessage\x12\x1d\n" + "\n" + - "request_id\x18\x01 \x01(\tR\trequestId\x127\n" + - "\x05error\x18c \x01(\v2!.azdext.ServiceTargetErrorMessageR\x05error\x12m\n" + + "request_id\x18\x01 \x01(\tR\trequestId\x12,\n" + + "\x05error\x18c \x01(\v2\x16.azdext.ExtensionErrorR\x05error\x12m\n" + "\x1fregister_service_target_request\x18\x02 \x01(\v2$.azdext.RegisterServiceTargetRequestH\x00R\x1cregisterServiceTargetRequest\x12p\n" + " register_service_target_response\x18\x03 \x01(\v2%.azdext.RegisterServiceTargetResponseH\x00R\x1dregisterServiceTargetResponse\x12W\n" + "\x12initialize_request\x18\x06 \x01(\v2&.azdext.ServiceTargetInitializeRequestH\x00R\x11initializeRequest\x12Z\n" + @@ -1711,15 +1634,7 @@ const file_service_target_proto_rawDesc = "" + "\x05value\x18\x02 \x01(\tR\x05value:\x028\x01\"2\n" + "\x1cRegisterServiceTargetRequest\x12\x12\n" + "\x04host\x18\x01 \x01(\tR\x04host\"\x1f\n" + - "\x1dRegisterServiceTargetResponse\"\xb2\x01\n" + - "\x19ServiceTargetErrorMessage\x12\x18\n" + - "\amessage\x18\x02 \x01(\tR\amessage\x12\x18\n" + - "\adetails\x18\x03 \x01(\tR\adetails\x12\x1d\n" + - "\n" + - "error_code\x18\x04 \x01(\tR\terrorCode\x12\x1f\n" + - "\vstatus_code\x18\x05 \x01(\x05R\n" + - "statusCode\x12!\n" + - "\fservice_name\x18\x06 \x01(\tR\vserviceName\"\xf6\x01\n" + + "\x1dRegisterServiceTargetResponse\"\xf6\x01\n" + "\x18GetTargetResourceRequest\x12'\n" + "\x0fsubscription_id\x18\x01 \x01(\tR\x0esubscriptionId\x12<\n" + "\x0eservice_config\x18\x02 \x01(\v2\x15.azdext.ServiceConfigR\rserviceConfig\x12N\n" + @@ -1787,7 +1702,7 @@ func file_service_target_proto_rawDescGZIP() []byte { return file_service_target_proto_rawDescData } -var file_service_target_proto_msgTypes = make([]protoimpl.MessageInfo, 28) +var file_service_target_proto_msgTypes = make([]protoimpl.MessageInfo, 27) var file_service_target_proto_goTypes = []any{ (*ServiceTargetMessage)(nil), // 0: azdext.ServiceTargetMessage (*ServiceTargetInputParameter)(nil), // 1: azdext.ServiceTargetInputParameter @@ -1798,71 +1713,71 @@ var file_service_target_proto_goTypes = []any{ (*ServiceTargetOptions)(nil), // 6: azdext.ServiceTargetOptions (*RegisterServiceTargetRequest)(nil), // 7: azdext.RegisterServiceTargetRequest (*RegisterServiceTargetResponse)(nil), // 8: azdext.RegisterServiceTargetResponse - (*ServiceTargetErrorMessage)(nil), // 9: azdext.ServiceTargetErrorMessage - (*GetTargetResourceRequest)(nil), // 10: azdext.GetTargetResourceRequest - (*GetTargetResourceResponse)(nil), // 11: azdext.GetTargetResourceResponse - (*TargetResource)(nil), // 12: azdext.TargetResource - (*ServiceTargetDeployRequest)(nil), // 13: azdext.ServiceTargetDeployRequest - (*ServiceTargetDeployResponse)(nil), // 14: azdext.ServiceTargetDeployResponse - (*ServicePackageResult)(nil), // 15: azdext.ServicePackageResult - (*ServicePublishResult)(nil), // 16: azdext.ServicePublishResult - (*ServiceDeployResult)(nil), // 17: azdext.ServiceDeployResult - (*ServiceTargetPackageRequest)(nil), // 18: azdext.ServiceTargetPackageRequest - (*ServiceTargetPackageResponse)(nil), // 19: azdext.ServiceTargetPackageResponse - (*ServiceTargetPublishRequest)(nil), // 20: azdext.ServiceTargetPublishRequest - (*ServiceTargetPublishResponse)(nil), // 21: azdext.ServiceTargetPublishResponse - (*PublishOptions)(nil), // 22: azdext.PublishOptions - (*ServiceTargetEndpointsRequest)(nil), // 23: azdext.ServiceTargetEndpointsRequest - (*ServiceTargetEndpointsResponse)(nil), // 24: azdext.ServiceTargetEndpointsResponse - (*ServiceTargetProgressMessage)(nil), // 25: azdext.ServiceTargetProgressMessage - nil, // 26: azdext.ServiceTargetOptions.DeploymentStacksEntry - nil, // 27: azdext.TargetResource.MetadataEntry + (*GetTargetResourceRequest)(nil), // 9: azdext.GetTargetResourceRequest + (*GetTargetResourceResponse)(nil), // 10: azdext.GetTargetResourceResponse + (*TargetResource)(nil), // 11: azdext.TargetResource + (*ServiceTargetDeployRequest)(nil), // 12: azdext.ServiceTargetDeployRequest + (*ServiceTargetDeployResponse)(nil), // 13: azdext.ServiceTargetDeployResponse + (*ServicePackageResult)(nil), // 14: azdext.ServicePackageResult + (*ServicePublishResult)(nil), // 15: azdext.ServicePublishResult + (*ServiceDeployResult)(nil), // 16: azdext.ServiceDeployResult + (*ServiceTargetPackageRequest)(nil), // 17: azdext.ServiceTargetPackageRequest + (*ServiceTargetPackageResponse)(nil), // 18: azdext.ServiceTargetPackageResponse + (*ServiceTargetPublishRequest)(nil), // 19: azdext.ServiceTargetPublishRequest + (*ServiceTargetPublishResponse)(nil), // 20: azdext.ServiceTargetPublishResponse + (*PublishOptions)(nil), // 21: azdext.PublishOptions + (*ServiceTargetEndpointsRequest)(nil), // 22: azdext.ServiceTargetEndpointsRequest + (*ServiceTargetEndpointsResponse)(nil), // 23: azdext.ServiceTargetEndpointsResponse + (*ServiceTargetProgressMessage)(nil), // 24: azdext.ServiceTargetProgressMessage + nil, // 25: azdext.ServiceTargetOptions.DeploymentStacksEntry + nil, // 26: azdext.TargetResource.MetadataEntry + (*ExtensionError)(nil), // 27: azdext.ExtensionError (*ServiceConfig)(nil), // 28: azdext.ServiceConfig (*structpb.Struct)(nil), // 29: google.protobuf.Struct (*ServiceContext)(nil), // 30: azdext.ServiceContext (*Artifact)(nil), // 31: azdext.Artifact } var file_service_target_proto_depIdxs = []int32{ - 9, // 0: azdext.ServiceTargetMessage.error:type_name -> azdext.ServiceTargetErrorMessage + 27, // 0: azdext.ServiceTargetMessage.error:type_name -> azdext.ExtensionError 7, // 1: azdext.ServiceTargetMessage.register_service_target_request:type_name -> azdext.RegisterServiceTargetRequest 8, // 2: azdext.ServiceTargetMessage.register_service_target_response:type_name -> azdext.RegisterServiceTargetResponse 4, // 3: azdext.ServiceTargetMessage.initialize_request:type_name -> azdext.ServiceTargetInitializeRequest 5, // 4: azdext.ServiceTargetMessage.initialize_response:type_name -> azdext.ServiceTargetInitializeResponse - 10, // 5: azdext.ServiceTargetMessage.get_target_resource_request:type_name -> azdext.GetTargetResourceRequest - 11, // 6: azdext.ServiceTargetMessage.get_target_resource_response:type_name -> azdext.GetTargetResourceResponse - 13, // 7: azdext.ServiceTargetMessage.deploy_request:type_name -> azdext.ServiceTargetDeployRequest - 14, // 8: azdext.ServiceTargetMessage.deploy_response:type_name -> azdext.ServiceTargetDeployResponse - 25, // 9: azdext.ServiceTargetMessage.progress_message:type_name -> azdext.ServiceTargetProgressMessage - 18, // 10: azdext.ServiceTargetMessage.package_request:type_name -> azdext.ServiceTargetPackageRequest - 19, // 11: azdext.ServiceTargetMessage.package_response:type_name -> azdext.ServiceTargetPackageResponse - 20, // 12: azdext.ServiceTargetMessage.publish_request:type_name -> azdext.ServiceTargetPublishRequest - 21, // 13: azdext.ServiceTargetMessage.publish_response:type_name -> azdext.ServiceTargetPublishResponse - 23, // 14: azdext.ServiceTargetMessage.endpoints_request:type_name -> azdext.ServiceTargetEndpointsRequest - 24, // 15: azdext.ServiceTargetMessage.endpoints_response:type_name -> azdext.ServiceTargetEndpointsResponse + 9, // 5: azdext.ServiceTargetMessage.get_target_resource_request:type_name -> azdext.GetTargetResourceRequest + 10, // 6: azdext.ServiceTargetMessage.get_target_resource_response:type_name -> azdext.GetTargetResourceResponse + 12, // 7: azdext.ServiceTargetMessage.deploy_request:type_name -> azdext.ServiceTargetDeployRequest + 13, // 8: azdext.ServiceTargetMessage.deploy_response:type_name -> azdext.ServiceTargetDeployResponse + 24, // 9: azdext.ServiceTargetMessage.progress_message:type_name -> azdext.ServiceTargetProgressMessage + 17, // 10: azdext.ServiceTargetMessage.package_request:type_name -> azdext.ServiceTargetPackageRequest + 18, // 11: azdext.ServiceTargetMessage.package_response:type_name -> azdext.ServiceTargetPackageResponse + 19, // 12: azdext.ServiceTargetMessage.publish_request:type_name -> azdext.ServiceTargetPublishRequest + 20, // 13: azdext.ServiceTargetMessage.publish_response:type_name -> azdext.ServiceTargetPublishResponse + 22, // 14: azdext.ServiceTargetMessage.endpoints_request:type_name -> azdext.ServiceTargetEndpointsRequest + 23, // 15: azdext.ServiceTargetMessage.endpoints_response:type_name -> azdext.ServiceTargetEndpointsResponse 28, // 16: azdext.ServiceTargetInitializeRequest.service_config:type_name -> azdext.ServiceConfig - 26, // 17: azdext.ServiceTargetOptions.deployment_stacks:type_name -> azdext.ServiceTargetOptions.DeploymentStacksEntry + 25, // 17: azdext.ServiceTargetOptions.deployment_stacks:type_name -> azdext.ServiceTargetOptions.DeploymentStacksEntry 29, // 18: azdext.ServiceTargetOptions.config:type_name -> google.protobuf.Struct 28, // 19: azdext.GetTargetResourceRequest.service_config:type_name -> azdext.ServiceConfig - 12, // 20: azdext.GetTargetResourceRequest.default_target_resource:type_name -> azdext.TargetResource - 12, // 21: azdext.GetTargetResourceResponse.target_resource:type_name -> azdext.TargetResource - 27, // 22: azdext.TargetResource.metadata:type_name -> azdext.TargetResource.MetadataEntry + 11, // 20: azdext.GetTargetResourceRequest.default_target_resource:type_name -> azdext.TargetResource + 11, // 21: azdext.GetTargetResourceResponse.target_resource:type_name -> azdext.TargetResource + 26, // 22: azdext.TargetResource.metadata:type_name -> azdext.TargetResource.MetadataEntry 28, // 23: azdext.ServiceTargetDeployRequest.service_config:type_name -> azdext.ServiceConfig 30, // 24: azdext.ServiceTargetDeployRequest.service_context:type_name -> azdext.ServiceContext - 12, // 25: azdext.ServiceTargetDeployRequest.target_resource:type_name -> azdext.TargetResource - 17, // 26: azdext.ServiceTargetDeployResponse.result:type_name -> azdext.ServiceDeployResult + 11, // 25: azdext.ServiceTargetDeployRequest.target_resource:type_name -> azdext.TargetResource + 16, // 26: azdext.ServiceTargetDeployResponse.result:type_name -> azdext.ServiceDeployResult 31, // 27: azdext.ServicePackageResult.artifacts:type_name -> azdext.Artifact 31, // 28: azdext.ServicePublishResult.artifacts:type_name -> azdext.Artifact 31, // 29: azdext.ServiceDeployResult.artifacts:type_name -> azdext.Artifact 28, // 30: azdext.ServiceTargetPackageRequest.service_config:type_name -> azdext.ServiceConfig 30, // 31: azdext.ServiceTargetPackageRequest.service_context:type_name -> azdext.ServiceContext - 15, // 32: azdext.ServiceTargetPackageResponse.result:type_name -> azdext.ServicePackageResult + 14, // 32: azdext.ServiceTargetPackageResponse.result:type_name -> azdext.ServicePackageResult 28, // 33: azdext.ServiceTargetPublishRequest.service_config:type_name -> azdext.ServiceConfig 30, // 34: azdext.ServiceTargetPublishRequest.service_context:type_name -> azdext.ServiceContext - 12, // 35: azdext.ServiceTargetPublishRequest.target_resource:type_name -> azdext.TargetResource - 22, // 36: azdext.ServiceTargetPublishRequest.publish_options:type_name -> azdext.PublishOptions - 16, // 37: azdext.ServiceTargetPublishResponse.result:type_name -> azdext.ServicePublishResult + 11, // 35: azdext.ServiceTargetPublishRequest.target_resource:type_name -> azdext.TargetResource + 21, // 36: azdext.ServiceTargetPublishRequest.publish_options:type_name -> azdext.PublishOptions + 15, // 37: azdext.ServiceTargetPublishResponse.result:type_name -> azdext.ServicePublishResult 28, // 38: azdext.ServiceTargetEndpointsRequest.service_config:type_name -> azdext.ServiceConfig - 12, // 39: azdext.ServiceTargetEndpointsRequest.target_resource:type_name -> azdext.TargetResource + 11, // 39: azdext.ServiceTargetEndpointsRequest.target_resource:type_name -> azdext.TargetResource 0, // 40: azdext.ServiceTargetService.Stream:input_type -> azdext.ServiceTargetMessage 0, // 41: azdext.ServiceTargetService.Stream:output_type -> azdext.ServiceTargetMessage 41, // [41:42] is the sub-list for method output_type @@ -1878,6 +1793,7 @@ func file_service_target_proto_init() { return } file_models_proto_init() + file_errors_proto_init() file_service_target_proto_msgTypes[0].OneofWrappers = []any{ (*ServiceTargetMessage_RegisterServiceTargetRequest)(nil), (*ServiceTargetMessage_RegisterServiceTargetResponse)(nil), @@ -1901,7 +1817,7 @@ func file_service_target_proto_init() { GoPackagePath: reflect.TypeOf(x{}).PkgPath(), RawDescriptor: unsafe.Slice(unsafe.StringData(file_service_target_proto_rawDesc), len(file_service_target_proto_rawDesc)), NumEnums: 0, - NumMessages: 28, + NumMessages: 27, NumExtensions: 0, NumServices: 1, }, diff --git a/cli/azd/pkg/azdext/service_target_envelope.go b/cli/azd/pkg/azdext/service_target_envelope.go index ec827216471..1660d83fd2c 100644 --- a/cli/azd/pkg/azdext/service_target_envelope.go +++ b/cli/azd/pkg/azdext/service_target_envelope.go @@ -32,15 +32,15 @@ func (ops *ServiceTargetEnvelope) SetRequestId(ctx context.Context, msg *Service } // GetError returns the error from the message as a Go error type. -// It returns an ExtensionResponseError that preserves structured error information for telemetry. +// It returns a typed error based on the ErrorOrigin that preserves structured information for telemetry. func (ops *ServiceTargetEnvelope) GetError(msg *ServiceTargetMessage) error { - return UnwrapErrorFromServiceTarget(msg.Error) + return UnwrapError(msg.Error) } // SetError sets an error on the message. -// It extracts structured error information from known error types like azcore.ResponseError. +// It detects the error type and populates the appropriate source details. func (ops *ServiceTargetEnvelope) SetError(msg *ServiceTargetMessage, err error) { - msg.Error = WrapErrorForServiceTarget(err) + msg.Error = WrapError(err) } // GetInnerMessage returns the inner message from the oneof field From 6f8d2de85ba6b2f9d46dc528e548e9d6e7b68c56 Mon Sep 17 00:00:00 2001 From: Jeffrey Chen Date: Thu, 11 Dec 2025 00:26:10 +0000 Subject: [PATCH 10/10] Revert metadata changes --- cli/azd/.vscode/cspell.yaml | 8 --- cli/azd/docs/extension-framework.md | 2 +- cli/azd/internal/grpcserver/server.go | 73 +--------------------- cli/azd/pkg/azdext/azd_client.go | 32 +--------- cli/azd/pkg/azdext/context.go | 10 +++ cli/azd/pkg/azdext/trace_context.go | 39 ------------ cli/azd/pkg/azdext/trace_context_test.go | 78 ------------------------ 7 files changed, 13 insertions(+), 229 deletions(-) delete mode 100644 cli/azd/pkg/azdext/trace_context.go delete mode 100644 cli/azd/pkg/azdext/trace_context_test.go diff --git a/cli/azd/.vscode/cspell.yaml b/cli/azd/.vscode/cspell.yaml index 309730aec5e..4721aa0864e 100644 --- a/cli/azd/.vscode/cspell.yaml +++ b/cli/azd/.vscode/cspell.yaml @@ -243,14 +243,6 @@ overrides: - userosscache - docstates - dylib - - filename: cli/azd/pkg/azdext/trace_context.go - words: - - traceparent - - tracestate - - filename: internal/grpcserver/server.go - words: - - traceparents - - tracestates ignorePaths: - "**/*_test.go" - "**/mock*.go" diff --git a/cli/azd/docs/extension-framework.md b/cli/azd/docs/extension-framework.md index 42f292ac10b..e20ef891ce4 100644 --- a/cli/azd/docs/extension-framework.md +++ b/cli/azd/docs/extension-framework.md @@ -336,7 +336,7 @@ The build process automatically creates binaries for multiple platforms and arch ### Distributed Tracing -`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. +`azd` uses OpenTelemetry and W3C Trace Context for distributed tracing. `azd` sets `TRACEPARENT` in the environment when it launches the extension process. Use `azdext.NewContext()` to hydrate the root context with trace context: diff --git a/cli/azd/internal/grpcserver/server.go b/cli/azd/internal/grpcserver/server.go index eb2e58aef17..fc1873b0c31 100644 --- a/cli/azd/internal/grpcserver/server.go +++ b/cli/azd/internal/grpcserver/server.go @@ -11,7 +11,6 @@ import ( "net" "github.com/azure/azure-dev/cli/azd/pkg/azdext" - "go.opentelemetry.io/otel/propagation" "google.golang.org/grpc" "google.golang.org/grpc/codes" "google.golang.org/grpc/metadata" @@ -82,13 +81,7 @@ func (s *Server) Start() (*ServerInfo, error) { var serverInfo ServerInfo s.grpcServer = grpc.NewServer( - grpc.ChainUnaryInterceptor( - s.tokenAuthInterceptor(&serverInfo), - traceContextUnaryInterceptor(), - ), - grpc.ChainStreamInterceptor( - traceContextStreamInterceptor(), - ), + grpc.UnaryInterceptor(s.tokenAuthInterceptor(&serverInfo)), ) // Use ":0" to let the system assign an available random port @@ -181,67 +174,3 @@ func generateSigningKey() ([]byte, error) { } return bytes, nil } - -// traceContextUnaryInterceptor extracts W3C traceparent from incoming gRPC metadata -// and injects it into the context for distributed tracing correlation. -func traceContextUnaryInterceptor() grpc.UnaryServerInterceptor { - return func( - ctx context.Context, - req interface{}, - info *grpc.UnaryServerInfo, - handler grpc.UnaryHandler, - ) (interface{}, error) { - ctx = extractTraceContextFromMetadata(ctx) - return handler(ctx, req) - } -} - -// traceContextStreamInterceptor extracts W3C traceparent from incoming gRPC metadata -// and injects it into the context for distributed tracing correlation. -func traceContextStreamInterceptor() grpc.StreamServerInterceptor { - return func( - srv interface{}, - ss grpc.ServerStream, - info *grpc.StreamServerInfo, - handler grpc.StreamHandler, - ) error { - ctx := extractTraceContextFromMetadata(ss.Context()) - if ctx != ss.Context() { - // Wrap the stream with the new context so downstream handlers see the trace span. - ss = &wrappedServerStream{ServerStream: ss, ctx: ctx} - } - - return handler(srv, ss) - } -} - -func extractTraceContextFromMetadata(ctx context.Context) context.Context { - md, ok := metadata.FromIncomingContext(ctx) - if !ok { - return ctx - } - - traceparents := md.Get(azdext.TraceparentKey) - if len(traceparents) == 0 { - return ctx - } - - carrier := propagation.MapCarrier{azdext.TraceparentKey: traceparents[0]} - - tracestates := md.Get(azdext.TracestateKey) - if len(tracestates) > 0 && tracestates[0] != "" { - carrier[azdext.TracestateKey] = tracestates[0] - } - - return propagation.TraceContext{}.Extract(ctx, carrier) -} - -// wrappedServerStream wraps a grpc.ServerStream with a custom context. -type wrappedServerStream struct { - grpc.ServerStream - ctx context.Context -} - -func (w *wrappedServerStream) Context() context.Context { - return w.ctx -} diff --git a/cli/azd/pkg/azdext/azd_client.go b/cli/azd/pkg/azdext/azd_client.go index 7cdf8387cd0..91aa4f4d8dc 100644 --- a/cli/azd/pkg/azdext/azd_client.go +++ b/cli/azd/pkg/azdext/azd_client.go @@ -35,13 +35,7 @@ type AzdClient struct { // WithAddress sets the address of the `azd` gRPC server. func WithAddress(address string) AzdClientOption { return func(c *AzdClient) error { - opts := []grpc.DialOption{ - grpc.WithTransportCredentials(insecure.NewCredentials()), - grpc.WithChainUnaryInterceptor(tracingUnaryInterceptor), - grpc.WithChainStreamInterceptor(tracingStreamInterceptor), - } - - connection, err := grpc.NewClient(address, opts...) + connection, err := grpc.NewClient(address, grpc.WithTransportCredentials(insecure.NewCredentials())) if err != nil { return err } @@ -195,27 +189,3 @@ func (c *AzdClient) Account() AccountServiceClient { return c.accountClient } - -func tracingUnaryInterceptor( - ctx context.Context, - method string, - req, reply interface{}, - cc *grpc.ClientConn, - invoker grpc.UnaryInvoker, - opts ...grpc.CallOption, -) error { - ctx = WithTracing(ctx) - return invoker(ctx, method, req, reply, cc, opts...) -} - -func tracingStreamInterceptor( - ctx context.Context, - desc *grpc.StreamDesc, - cc *grpc.ClientConn, - method string, - streamer grpc.Streamer, - opts ...grpc.CallOption, -) (grpc.ClientStream, error) { - ctx = WithTracing(ctx) - return streamer(ctx, desc, cc, method, opts...) -} diff --git a/cli/azd/pkg/azdext/context.go b/cli/azd/pkg/azdext/context.go index 17c9a8bd74f..47e4b1915fe 100644 --- a/cli/azd/pkg/azdext/context.go +++ b/cli/azd/pkg/azdext/context.go @@ -10,6 +10,16 @@ import ( "go.opentelemetry.io/otel/propagation" ) +const ( + TraceparentKey = "traceparent" + TracestateKey = "tracestate" + + // https://github.com/open-telemetry/opentelemetry-specification/blob/main/specification/context/env-carriers.md + + TraceparentEnv = "TRACEPARENT" + TracestateEnv = "TRACESTATE" +) + // NewContext initializes a new context with tracing information extracted from environment variables. func NewContext() context.Context { ctx := context.Background() diff --git a/cli/azd/pkg/azdext/trace_context.go b/cli/azd/pkg/azdext/trace_context.go deleted file mode 100644 index 41ce1ecd2b8..00000000000 --- a/cli/azd/pkg/azdext/trace_context.go +++ /dev/null @@ -1,39 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. - -package azdext - -import ( - "context" - - "go.opentelemetry.io/otel/propagation" - "google.golang.org/grpc/metadata" -) - -const ( - TraceparentKey = "traceparent" - TracestateKey = "tracestate" - - // https://github.com/open-telemetry/opentelemetry-specification/blob/main/specification/context/env-carriers.md - - TraceparentEnv = "TRACEPARENT" - TracestateEnv = "TRACESTATE" -) - -// WithTracing appends the W3C traceparent for the current span to outgoing gRPC metadata. -// If no span context is present, the original context is returned unchanged. -func WithTracing(ctx context.Context) context.Context { - carrier := propagation.MapCarrier{} - propagation.TraceContext{}.Inject(ctx, carrier) - traceparent := carrier.Get(TraceparentKey) - if traceparent == "" { - return ctx - } - - tracestate := carrier.Get(TracestateKey) - if tracestate != "" { - return metadata.AppendToOutgoingContext(ctx, TraceparentKey, traceparent, TracestateKey, tracestate) - } - - return metadata.AppendToOutgoingContext(ctx, TraceparentKey, traceparent) -} diff --git a/cli/azd/pkg/azdext/trace_context_test.go b/cli/azd/pkg/azdext/trace_context_test.go deleted file mode 100644 index 04a580bf1a4..00000000000 --- a/cli/azd/pkg/azdext/trace_context_test.go +++ /dev/null @@ -1,78 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. - -package azdext - -import ( - "context" - "testing" - - "go.opentelemetry.io/otel/trace" - "google.golang.org/grpc/metadata" - - "github.com/stretchr/testify/require" -) - -func TestWithTracing_NoSpanContext(t *testing.T) { - ctx := context.Background() - - newCtx := WithTracing(ctx) - - _, ok := metadata.FromOutgoingContext(newCtx) - require.False(t, ok, "metadata should be untouched when no span context exists") -} - -func TestWithTracing_AppendsTraceparentAndTracestate(t *testing.T) { - traceID, err := trace.TraceIDFromHex("4bf92f3577b34da6a3ce929d0e0e4736") - require.NoError(t, err) - spanID, err := trace.SpanIDFromHex("00f067aa0ba902b7") - require.NoError(t, err) - traceState, err := trace.ParseTraceState("foo=bar") - require.NoError(t, err) - - sc := trace.NewSpanContext(trace.SpanContextConfig{ - TraceID: traceID, - SpanID: spanID, - TraceFlags: trace.FlagsSampled, - TraceState: traceState, - }) - - ctx := trace.ContextWithSpanContext(context.Background(), sc) - newCtx := WithTracing(ctx) - - md, ok := metadata.FromOutgoingContext(newCtx) - require.True(t, ok, "metadata should be populated when span context exists") - values := md.Get(TraceparentKey) - require.Len(t, values, 1) - require.Equal(t, "00-4bf92f3577b34da6a3ce929d0e0e4736-00f067aa0ba902b7-01", values[0]) - - stateValues := md.Get(TracestateKey) - require.Len(t, stateValues, 1) - require.Equal(t, "foo=bar", stateValues[0]) -} - -func TestWithTracing_AppendsTraceparentOnly(t *testing.T) { - traceID, err := trace.TraceIDFromHex("4bf92f3577b34da6a3ce929d0e0e4736") - require.NoError(t, err) - spanID, err := trace.SpanIDFromHex("00f067aa0ba902b7") - require.NoError(t, err) - - sc := trace.NewSpanContext(trace.SpanContextConfig{ - TraceID: traceID, - SpanID: spanID, - TraceFlags: trace.FlagsSampled, - // No TraceState set - }) - - ctx := trace.ContextWithSpanContext(context.Background(), sc) - newCtx := WithTracing(ctx) - - md, ok := metadata.FromOutgoingContext(newCtx) - require.True(t, ok, "metadata should be populated when span context exists") - values := md.Get(TraceparentKey) - require.Len(t, values, 1) - require.Equal(t, "00-4bf92f3577b34da6a3ce929d0e0e4736-00f067aa0ba902b7-01", values[0]) - - stateValues := md.Get(TracestateKey) - require.Empty(t, stateValues, "tracestate should not be present when empty") -}