You've already forked opentelemetry-go
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:
@@ -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
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user