1
0
mirror of https://github.com/open-telemetry/opentelemetry-go.git synced 2026-06-03 18:35:08 +02:00

Fix semconv generated error type to check error chain for custom type declaration (#7994)

Fix `ErrorType` detection to work through wrapped error chains.

`errorType` previously only checked whether the top-level error value
implemented `ErrorType() string`. In common Go usage, errors are often
wrapped, so this could miss `ErrorType` implementations behind wrappers.
This commit is contained in:
Tyler Yahn
2026-03-04 06:57:57 -08:00
committed by GitHub
parent b2b3250897
commit a7624f50f7
4 changed files with 68 additions and 10 deletions
@@ -4,6 +4,7 @@
package semconv // import "go.opentelemetry.io/otel/semconv/{{.TagVer}}" package semconv // import "go.opentelemetry.io/otel/semconv/{{.TagVer}}"
import ( import (
"errors"
"reflect" "reflect"
"go.opentelemetry.io/otel/attribute" "go.opentelemetry.io/otel/attribute"
@@ -14,12 +15,14 @@ import (
// If err is nil, the returned attribute has the default value // If err is nil, the returned attribute has the default value
// [ErrorTypeOther]. // [ErrorTypeOther].
// //
// If err's type has the method // If err or one of the errors in its chain has the method
// //
// ErrorType() string // ErrorType() string
// //
// then the returned attribute has the value of err.ErrorType(). Otherwise, the // the returned attribute has that method's return value. If multiple errors in
// returned attribute has a value derived from the concrete type of err. // 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]. // The key of the returned attribute is [ErrorTypeKey].
func ErrorType(err error) attribute.KeyValue { func ErrorType(err error) attribute.KeyValue {
@@ -33,8 +36,15 @@ func ErrorType(err error) attribute.KeyValue {
func errorType(err error) string { func errorType(err error) string {
var s string var s string
if et, ok := err.(interface{ ErrorType() string }); ok { 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() 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 == "" { if s == "" {
// Fallback to reflection if the ErrorType method is not supported or // Fallback to reflection if the ErrorType method is not supported or
@@ -14,7 +14,10 @@ func TestErrorType(t *testing.T) {
check(t, nil, ErrorTypeOther.Value.AsString()) check(t, nil, ErrorTypeOther.Value.AsString())
check(t, errors.New("msg"), "*errors.errorString") check(t, errors.New("msg"), "*errors.errorString")
check(t, custom("aborted"), "aborted") 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) { func check(t *testing.T, err error, want string) {
@@ -32,6 +35,10 @@ func custom(typ string) error {
return ErrCustomType{Type: typ} return ErrCustomType{Type: typ}
} }
func wrapped(err error) error {
return wrappedErr{err: err}
}
type ErrCustomType struct { type ErrCustomType struct {
Type string Type string
} }
@@ -43,3 +50,15 @@ func (e ErrCustomType) Error() string {
func (e ErrCustomType) ErrorType() string { func (e ErrCustomType) ErrorType() string {
return e.Type 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
}
+14 -4
View File
@@ -4,6 +4,7 @@
package semconv // import "go.opentelemetry.io/otel/semconv/v1.40.0" package semconv // import "go.opentelemetry.io/otel/semconv/v1.40.0"
import ( import (
"errors"
"reflect" "reflect"
"go.opentelemetry.io/otel/attribute" "go.opentelemetry.io/otel/attribute"
@@ -14,12 +15,14 @@ import (
// If err is nil, the returned attribute has the default value // If err is nil, the returned attribute has the default value
// [ErrorTypeOther]. // [ErrorTypeOther].
// //
// If err's type has the method // If err or one of the errors in its chain has the method
// //
// ErrorType() string // ErrorType() string
// //
// then the returned attribute has the value of err.ErrorType(). Otherwise, the // the returned attribute has that method's return value. If multiple errors in
// returned attribute has a value derived from the concrete type of err. // 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]. // The key of the returned attribute is [ErrorTypeKey].
func ErrorType(err error) attribute.KeyValue { func ErrorType(err error) attribute.KeyValue {
@@ -33,8 +36,15 @@ func ErrorType(err error) attribute.KeyValue {
func errorType(err error) string { func errorType(err error) string {
var s string var s string
if et, ok := err.(interface{ ErrorType() string }); ok { 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() 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 == "" { if s == "" {
// Fallback to reflection if the ErrorType method is not supported or // Fallback to reflection if the ErrorType method is not supported or
+20 -1
View File
@@ -14,7 +14,10 @@ func TestErrorType(t *testing.T) {
check(t, nil, ErrorTypeOther.Value.AsString()) check(t, nil, ErrorTypeOther.Value.AsString())
check(t, errors.New("msg"), "*errors.errorString") check(t, errors.New("msg"), "*errors.errorString")
check(t, custom("aborted"), "aborted") 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) { func check(t *testing.T, err error, want string) {
@@ -32,6 +35,10 @@ func custom(typ string) error {
return ErrCustomType{Type: typ} return ErrCustomType{Type: typ}
} }
func wrapped(err error) error {
return wrappedErr{err: err}
}
type ErrCustomType struct { type ErrCustomType struct {
Type string Type string
} }
@@ -43,3 +50,15 @@ func (e ErrCustomType) Error() string {
func (e ErrCustomType) ErrorType() string { func (e ErrCustomType) ErrorType() string {
return e.Type 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
}