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}}"
|
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
|
||||||
|
}
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user