Skip to content

Commit f414abe

Browse files
authored
map all errors (#6389)
* add explicit and auto error mapping * split deployment from validate and preview errors
1 parent c3d4edd commit f414abe

File tree

9 files changed

+282
-41
lines changed

9 files changed

+282
-41
lines changed

cli/azd/internal/cmd/errors.go

Lines changed: 76 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -9,33 +9,51 @@ import (
99
"fmt"
1010
"log"
1111
"path/filepath"
12+
"reflect"
1213
"strings"
1314

1415
"github.com/AlecAivazis/survey/v2/terminal"
1516
"github.com/Azure/azure-sdk-for-go/sdk/azcore"
17+
"github.com/azure/azure-dev/cli/azd/internal"
1618
"github.com/azure/azure-dev/cli/azd/internal/tracing"
1719
"github.com/azure/azure-dev/cli/azd/internal/tracing/fields"
1820
"github.com/azure/azure-dev/cli/azd/pkg/auth"
1921
"github.com/azure/azure-dev/cli/azd/pkg/azapi"
2022
"github.com/azure/azure-dev/cli/azd/pkg/azdext"
2123
"github.com/azure/azure-dev/cli/azd/pkg/exec"
2224
"github.com/azure/azure-dev/cli/azd/pkg/extensions"
25+
"github.com/azure/azure-dev/cli/azd/pkg/tools"
2326
"go.opentelemetry.io/otel/attribute"
2427
"go.opentelemetry.io/otel/codes"
2528
)
2629

2730
// MapError maps the given error to a telemetry span, setting relevant status and attributes.
2831
func MapError(err error, span tracing.Span) {
29-
errCode := "UnknownError"
32+
var errCode string
3033
var errDetails []attribute.KeyValue
3134

35+
// external service errors
3236
var respErr *azcore.ResponseError
3337
var armDeployErr *azapi.AzureDeploymentError
34-
var toolExecErr *exec.ExitError
3538
var authFailedErr *auth.AuthFailedError
36-
var extensionRunErr *extensions.ExtensionRunError
3739
var extServiceErr *azdext.ServiceError
38-
if errors.As(err, &respErr) {
40+
41+
// external tool errors
42+
var toolExecErr *exec.ExitError
43+
var toolCheckErr *tools.MissingToolErrors
44+
var extensionRunErr *extensions.ExtensionRunError
45+
46+
// internal errors
47+
var errWithSuggestion *internal.ErrorWithSuggestion
48+
var loginErr *auth.ReLoginRequiredError
49+
50+
if errors.As(err, &loginErr) {
51+
errCode = "auth.login_required"
52+
} else if errors.As(err, &errWithSuggestion) {
53+
errCode = "error.suggestion"
54+
errType := errorType(errWithSuggestion.Unwrap())
55+
span.SetAttributes(fields.ErrType.String(errType))
56+
} else if errors.As(err, &respErr) {
3957
serviceName := "other"
4058
statusCode := -1
4159
errDetails = append(errDetails, fields.ServiceErrorCode.String(respErr.ErrorCode))
@@ -83,7 +101,13 @@ func MapError(err error, span tracing.Span) {
83101
}
84102
}
85103

86-
errCode = "service.arm.deployment.failed"
104+
// Use operation-specific error code if available
105+
operation := armDeployErr.Operation
106+
if operation == azapi.DeploymentOperationDeploy {
107+
// use 'deployment' instead of 'deploy' for consistency with prior naming
108+
operation = "deployment"
109+
}
110+
errCode = fmt.Sprintf("service.arm.%s.failed", operation)
87111
} else if errors.As(err, &extensionRunErr) {
88112
errCode = "ext.run.failed"
89113
} else if errors.As(err, &extServiceErr) {
@@ -114,6 +138,15 @@ func MapError(err error, span tracing.Span) {
114138
fields.ToolName.String(toolName))
115139

116140
errCode = fmt.Sprintf("tool.%s.failed", toolName)
141+
} else if errors.As(err, &toolCheckErr) {
142+
if len(toolCheckErr.ToolNames) == 1 {
143+
toolName := toolCheckErr.ToolNames[0]
144+
errCode = fmt.Sprintf("tool.%s.missing", toolName)
145+
errDetails = append(errDetails, fields.ToolName.String(toolName))
146+
} else {
147+
errCode = "tool.multiple.missing"
148+
errDetails = append(errDetails, fields.ToolName.String(strings.Join(toolCheckErr.ToolNames, ",")))
149+
}
117150
} else if errors.As(err, &authFailedErr) {
118151
errDetails = append(errDetails, fields.ServiceName.String("aad"))
119152
if authFailedErr.Parsed != nil {
@@ -130,6 +163,11 @@ func MapError(err error, span tracing.Span) {
130163
errCode = "service.aad.failed"
131164
} else if errors.Is(err, terminal.InterruptErr) {
132165
errCode = "user.canceled"
166+
} else {
167+
errType := errorType(err)
168+
span.SetAttributes(fields.ErrType.String(errType))
169+
errCode = fmt.Sprintf("internal.%s",
170+
strings.ReplaceAll(strings.ReplaceAll(errType, ".", "_"), "*", ""))
133171
}
134172

135173
if len(errDetails) > 0 {
@@ -143,6 +181,39 @@ func MapError(err error, span tracing.Span) {
143181
span.SetStatus(codes.Error, errCode)
144182
}
145183

184+
// errorType returns the type name of the given error, unwrapping as needed to find the root cause(s).
185+
func errorType(err error) string {
186+
if err == nil {
187+
return "<nil>"
188+
}
189+
190+
//nolint:errorlint // Type switch is intentionally used to check for Unwrap() methods
191+
for {
192+
switch x := err.(type) {
193+
case interface{ Unwrap() error }:
194+
err = x.Unwrap()
195+
if err == nil {
196+
return reflect.TypeOf(x).String()
197+
}
198+
case interface{ Unwrap() []error }:
199+
result := ""
200+
for _, err := range x.Unwrap() {
201+
if err == nil {
202+
continue
203+
}
204+
if result != "" {
205+
result += ","
206+
}
207+
208+
result += reflect.TypeOf(err).String()
209+
}
210+
return result
211+
default:
212+
return reflect.TypeOf(x).String()
213+
}
214+
}
215+
}
216+
146217
type deploymentErrorCode struct {
147218
Code string `json:"error.code"`
148219
Frame int `json:"error.frame"`

cli/azd/internal/cmd/errors_test.go

Lines changed: 139 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -28,16 +28,20 @@ func Test_MapError(t *testing.T) {
2828
wantErrDetails []attribute.KeyValue
2929
}{
3030
{
31-
name: "WithNilError",
32-
err: nil,
33-
wantErrReason: "UnknownError",
34-
wantErrDetails: nil,
31+
name: "WithNilError",
32+
err: nil,
33+
wantErrReason: "internal.<nil>",
34+
wantErrDetails: []attribute.KeyValue{
35+
fields.ErrType.String("<nil>"),
36+
},
3537
},
3638
{
37-
name: "WithOtherError",
38-
err: errors.New("something bad happened!"),
39-
wantErrReason: "UnknownError",
40-
wantErrDetails: nil,
39+
name: "WithOtherError",
40+
err: errors.New("something bad happened!"),
41+
wantErrReason: "internal.errors_errorString",
42+
wantErrDetails: []attribute.KeyValue{
43+
fields.ErrType.String("*errors.errorString"),
44+
},
4145
},
4246
{
4347
name: "WithToolExitError",
@@ -54,6 +58,7 @@ func Test_MapError(t *testing.T) {
5458
{
5559
name: "WithArmDeploymentError",
5660
err: &azapi.AzureDeploymentError{
61+
Operation: azapi.DeploymentOperationDeploy,
5762
Details: &azapi.DeploymentErrorLine{
5863
Code: "",
5964
Inner: []*azapi.DeploymentErrorLine{
@@ -106,6 +111,33 @@ func Test_MapError(t *testing.T) {
106111
})),
107112
},
108113
},
114+
{
115+
name: "WithArmValidationError",
116+
err: &azapi.AzureDeploymentError{
117+
Operation: azapi.DeploymentOperationValidate,
118+
Details: &azapi.DeploymentErrorLine{
119+
Code: "InvalidTemplate",
120+
Inner: []*azapi.DeploymentErrorLine{
121+
{Code: "TemplateValidationFailed"},
122+
},
123+
},
124+
},
125+
wantErrReason: "service.arm.validate.failed",
126+
wantErrDetails: []attribute.KeyValue{
127+
fields.ErrorKey(fields.ServiceName.Key).String("arm"),
128+
fields.ErrorKey(fields.ServiceErrorCode.Key).String(mustMarshalJson(
129+
[]map[string]interface{}{
130+
{
131+
string(fields.ErrCode.Key): "InvalidTemplate",
132+
string(fields.ErrFrame.Key): 0,
133+
},
134+
{
135+
string(fields.ErrCode.Key): "TemplateValidationFailed",
136+
string(fields.ErrFrame.Key): 1,
137+
},
138+
})),
139+
},
140+
},
109141
{
110142
name: "WithResponseError",
111143
err: &azcore.ResponseError{
@@ -198,6 +230,105 @@ func Test_cmdAsName(t *testing.T) {
198230
}
199231
}
200232

233+
func Test_errorType(t *testing.T) {
234+
tests := []struct {
235+
name string
236+
err error
237+
want string
238+
}{
239+
{
240+
name: "NilError",
241+
err: nil,
242+
want: "<nil>",
243+
},
244+
{
245+
name: "SimpleError",
246+
err: errors.New("simple error"),
247+
want: "*errors.errorString",
248+
},
249+
{
250+
name: "SingleUnwrapError",
251+
err: &exec.ExitError{
252+
Cmd: "test",
253+
ExitCode: 1,
254+
},
255+
want: "*exec.ExitError",
256+
},
257+
{
258+
name: "NestedUnwrapError",
259+
err: func() error {
260+
inner := errors.New("inner error")
261+
return &singleUnwrapError{
262+
msg: "wrapped error",
263+
err: inner,
264+
}
265+
}(),
266+
want: "*errors.errorString",
267+
},
268+
{
269+
name: "MultipleUnwrapErrors",
270+
err: func() error {
271+
err1 := errors.New("error 1")
272+
err2 := errors.New("error 2")
273+
return &multiUnwrapError{
274+
errs: []error{err1, err2},
275+
}
276+
}(),
277+
want: "*errors.errorString,*errors.errorString",
278+
},
279+
{
280+
name: "MultipleUnwrapErrorsWithNil",
281+
err: func() error {
282+
err1 := errors.New("error 1")
283+
return &multiUnwrapError{
284+
errs: []error{err1, nil, errors.New("error 2")},
285+
}
286+
}(),
287+
want: "*errors.errorString,*errors.errorString",
288+
},
289+
{
290+
name: "UnwrapReturnsNil",
291+
err: &singleUnwrapError{
292+
msg: "test error",
293+
err: nil,
294+
},
295+
want: "*cmd.singleUnwrapError",
296+
},
297+
}
298+
for _, tt := range tests {
299+
t.Run(tt.name, func(t *testing.T) {
300+
got := errorType(tt.err)
301+
require.Equal(t, tt.want, got)
302+
})
303+
}
304+
}
305+
306+
// Test helper types for errorType tests
307+
type singleUnwrapError struct {
308+
msg string
309+
err error
310+
}
311+
312+
func (e *singleUnwrapError) Error() string {
313+
return e.msg
314+
}
315+
316+
func (e *singleUnwrapError) Unwrap() error {
317+
return e.err
318+
}
319+
320+
type multiUnwrapError struct {
321+
errs []error
322+
}
323+
324+
func (e *multiUnwrapError) Error() string {
325+
return "multiple errors"
326+
}
327+
328+
func (e *multiUnwrapError) Unwrap() []error {
329+
return e.errs
330+
}
331+
201332
func mustMarshalJson(v interface{}) string {
202333
b, err := json.Marshal(v)
203334
if err != nil {

cli/azd/internal/tracing/fields/fields.go

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -320,6 +320,13 @@ var (
320320
Purpose: PerformanceAndHealth,
321321
}
322322

323+
// Error type.
324+
ErrType = AttributeKey{
325+
Key: attribute.Key("error.type"),
326+
Classification: SystemMetadata,
327+
Purpose: PerformanceAndHealth,
328+
}
329+
323330
// Inner error.
324331
ErrInner = AttributeKey{
325332
Key: attribute.Key("error.inner"),

cli/azd/pkg/azapi/azure_deployment_error.go

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -29,15 +29,16 @@ func newErrorLine(code string, message string, inner []*DeploymentErrorLine) *De
2929
}
3030

3131
type AzureDeploymentError struct {
32-
Json string
33-
Inner error
34-
Title string
32+
Json string
33+
Inner error
34+
Title string
35+
Operation DeploymentOperation
3536

3637
Details *DeploymentErrorLine
3738
}
3839

39-
func NewAzureDeploymentError(title string, jsonErrorResponse string) *AzureDeploymentError {
40-
err := &AzureDeploymentError{Title: title, Json: jsonErrorResponse}
40+
func NewAzureDeploymentError(title string, jsonErrorResponse string, operation DeploymentOperation) *AzureDeploymentError {
41+
err := &AzureDeploymentError{Title: title, Json: jsonErrorResponse, Operation: operation}
4142
err.init()
4243
return err
4344
}

cli/azd/pkg/azapi/azure_deployment_error_test.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -47,7 +47,7 @@ func assertOutputsMatch(t *testing.T, jsonPath string, expectedOutputPath string
4747
}
4848

4949
errorJson := string(data)
50-
deploymentError := NewAzureDeploymentError("Title", errorJson)
50+
deploymentError := NewAzureDeploymentError("Title", errorJson, DeploymentOperationDeploy)
5151
errorString := deploymentError.Error()
5252

5353
actualLines := strings.Split(errorString, "\n")

0 commit comments

Comments
 (0)