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
+14 -4
View File
@@ -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
+20 -1
View File
@@ -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
}