diff --git a/internal/tools/semconvkit/templates/error_type.go.tmpl b/internal/tools/semconvkit/templates/error_type.go.tmpl index 0f4355aed..193c43ae0 100644 --- a/internal/tools/semconvkit/templates/error_type.go.tmpl +++ b/internal/tools/semconvkit/templates/error_type.go.tmpl @@ -4,6 +4,7 @@ package semconv // import "go.opentelemetry.io/otel/semconv/{{.TagVer}}" import ( + "errors" "reflect" "go.opentelemetry.io/otel/attribute" @@ -14,12 +15,14 @@ import ( // If err is nil, the returned attribute has the default value // [ErrorTypeOther]. // -// If err's type has the method +// If err or one of the errors in its chain has the method // // ErrorType() string // -// then the returned attribute has the value of err.ErrorType(). Otherwise, the -// returned attribute has a value derived from the concrete type of err. +// the returned attribute has that method's return value. If multiple errors in +// the chain implement this method, the value from the first match found by +// [errors.As] is used. Otherwise, the returned attribute has a value derived +// from the concrete type of err. // // The key of the returned attribute is [ErrorTypeKey]. func ErrorType(err error) attribute.KeyValue { @@ -33,8 +36,15 @@ func ErrorType(err error) attribute.KeyValue { func errorType(err error) string { var s string if et, ok := err.(interface{ ErrorType() string }); ok { - // Prioritize the ErrorType method if available. + // Fast path: check the top-level error first. s = et.ErrorType() + } else { + // Fallback: search the error chain for an ErrorType method. + var et interface{ ErrorType() string } + if errors.As(err, &et) { + // Prioritize the ErrorType method if available. + s = et.ErrorType() + } } if s == "" { // Fallback to reflection if the ErrorType method is not supported or diff --git a/internal/tools/semconvkit/templates/error_type_test.go.tmpl b/internal/tools/semconvkit/templates/error_type_test.go.tmpl index 52ac82881..1d8a82f5f 100644 --- a/internal/tools/semconvkit/templates/error_type_test.go.tmpl +++ b/internal/tools/semconvkit/templates/error_type_test.go.tmpl @@ -14,7 +14,10 @@ func TestErrorType(t *testing.T) { check(t, nil, ErrorTypeOther.Value.AsString()) check(t, errors.New("msg"), "*errors.errorString") check(t, custom("aborted"), "aborted") - check(t, custom(""), pkg+".ErrCustomType") // empty ErrorType, use concrete type. + check(t, errors.Join(custom("left"), custom("right")), "left") // first errors.As match is used. + check(t, custom(""), pkg+".ErrCustomType") // empty ErrorType, use concrete type. + check(t, wrapped(custom("wrapped-aborted")), "wrapped-aborted") + check(t, wrapped(custom("")), pkg+".wrappedErr") // empty ErrorType in chain, use concrete top-level type. } func check(t *testing.T, err error, want string) { @@ -32,6 +35,10 @@ func custom(typ string) error { return ErrCustomType{Type: typ} } +func wrapped(err error) error { + return wrappedErr{err: err} +} + type ErrCustomType struct { Type string } @@ -43,3 +50,15 @@ func (e ErrCustomType) Error() string { func (e ErrCustomType) ErrorType() string { return e.Type } + +type wrappedErr struct { + err error +} + +func (e wrappedErr) Error() string { + return "wrapped: " + e.err.Error() +} + +func (e wrappedErr) Unwrap() error { + return e.err +} diff --git a/semconv/v1.40.0/error_type.go b/semconv/v1.40.0/error_type.go index 09c270294..6d26e5282 100644 --- a/semconv/v1.40.0/error_type.go +++ b/semconv/v1.40.0/error_type.go @@ -4,6 +4,7 @@ package semconv // import "go.opentelemetry.io/otel/semconv/v1.40.0" import ( + "errors" "reflect" "go.opentelemetry.io/otel/attribute" @@ -14,12 +15,14 @@ import ( // If err is nil, the returned attribute has the default value // [ErrorTypeOther]. // -// If err's type has the method +// If err or one of the errors in its chain has the method // // ErrorType() string // -// then the returned attribute has the value of err.ErrorType(). Otherwise, the -// returned attribute has a value derived from the concrete type of err. +// the returned attribute has that method's return value. If multiple errors in +// the chain implement this method, the value from the first match found by +// [errors.As] is used. Otherwise, the returned attribute has a value derived +// from the concrete type of err. // // The key of the returned attribute is [ErrorTypeKey]. func ErrorType(err error) attribute.KeyValue { @@ -33,8 +36,15 @@ func ErrorType(err error) attribute.KeyValue { func errorType(err error) string { var s string if et, ok := err.(interface{ ErrorType() string }); ok { - // Prioritize the ErrorType method if available. + // Fast path: check the top-level error first. s = et.ErrorType() + } else { + // Fallback: search the error chain for an ErrorType method. + var et interface{ ErrorType() string } + if errors.As(err, &et) { + // Prioritize the ErrorType method if available. + s = et.ErrorType() + } } if s == "" { // Fallback to reflection if the ErrorType method is not supported or diff --git a/semconv/v1.40.0/error_type_test.go b/semconv/v1.40.0/error_type_test.go index 2caffa878..b01ea7964 100644 --- a/semconv/v1.40.0/error_type_test.go +++ b/semconv/v1.40.0/error_type_test.go @@ -14,7 +14,10 @@ func TestErrorType(t *testing.T) { check(t, nil, ErrorTypeOther.Value.AsString()) check(t, errors.New("msg"), "*errors.errorString") check(t, custom("aborted"), "aborted") - check(t, custom(""), pkg+".ErrCustomType") // empty ErrorType, use concrete type. + check(t, errors.Join(custom("left"), custom("right")), "left") // first errors.As match is used. + check(t, custom(""), pkg+".ErrCustomType") // empty ErrorType, use concrete type. + check(t, wrapped(custom("wrapped-aborted")), "wrapped-aborted") + check(t, wrapped(custom("")), pkg+".wrappedErr") // empty ErrorType in chain, use concrete top-level type. } func check(t *testing.T, err error, want string) { @@ -32,6 +35,10 @@ func custom(typ string) error { return ErrCustomType{Type: typ} } +func wrapped(err error) error { + return wrappedErr{err: err} +} + type ErrCustomType struct { Type string } @@ -43,3 +50,15 @@ func (e ErrCustomType) Error() string { func (e ErrCustomType) ErrorType() string { return e.Type } + +type wrappedErr struct { + err error +} + +func (e wrappedErr) Error() string { + return "wrapped: " + e.err.Error() +} + +func (e wrappedErr) Unwrap() error { + return e.err +}