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
unwrap error chains created with fmt.Errorf (#8133)
Fixes https://github.com/open-telemetry/opentelemetry-go/issues/7975 Per https://github.com/open-telemetry/opentelemetry-go/issues/7975#issuecomment-4183251694 Per https://github.com/open-telemetry/semantic-conventions/issues/3588 Credits: - Balaji01-4D for https://github.com/open-telemetry/opentelemetry-go/pull/8018 - seh for providing valuable feedback
This commit is contained in:
@@ -12,6 +12,11 @@ This project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.htm
|
||||
|
||||
- Add `ByteSlice` and `ByteSliceValue` functions for new `BYTESLICE` attribute type in `go.opentelemetry.io/otel/attribute`. (#7948)
|
||||
|
||||
### Changed
|
||||
|
||||
- `ErrorType` in `go.opentelemetry.io/otel/semconv` now unwraps errors created with `fmt.Errorf` when deriving the `error.type` attribute. (#8133)
|
||||
- `go.opentelemetry.io/otel/sdk/log` now unwraps error chains created with `fmt.Errorf` when deriving the `error.type` attribute from errors on log records. (#8133)
|
||||
|
||||
<!-- Released section -->
|
||||
<!-- Don't change this section unless doing release -->
|
||||
|
||||
|
||||
@@ -5,6 +5,7 @@ package semconv // import "go.opentelemetry.io/otel/semconv/{{.TagVer}}"
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"reflect"
|
||||
|
||||
"go.opentelemetry.io/otel/attribute"
|
||||
@@ -22,7 +23,8 @@ import (
|
||||
// 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.
|
||||
// from the concrete type of err after unwrapping any wrappers created with
|
||||
// [fmt.Errorf].
|
||||
//
|
||||
// The key of the returned attribute is [ErrorTypeKey].
|
||||
func ErrorType(err error) attribute.KeyValue {
|
||||
@@ -50,7 +52,7 @@ func errorType(err error) string {
|
||||
// Fallback to reflection if the ErrorType method is not supported or
|
||||
// returns an empty value.
|
||||
|
||||
t := reflect.TypeOf(err)
|
||||
t := reflect.TypeOf(unwrapFmtWrapped(err))
|
||||
pkg, name := t.PkgPath(), t.Name()
|
||||
if pkg != "" && name != "" {
|
||||
s = pkg + "." + name
|
||||
@@ -64,3 +66,16 @@ func errorType(err error) string {
|
||||
}
|
||||
return s
|
||||
}
|
||||
|
||||
var fmtWrapErrorType = reflect.TypeOf(fmt.Errorf("wrapped: %w", errors.New("err")))
|
||||
|
||||
func unwrapFmtWrapped(err error) error {
|
||||
for reflect.TypeOf(err) == fmtWrapErrorType {
|
||||
u := errors.Unwrap(err)
|
||||
if u == nil {
|
||||
return err // When the wrapped error is nil, use the concrete type of the wrapper.
|
||||
}
|
||||
err = u
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -5,6 +5,7 @@ package semconv // import "go.opentelemetry.io/otel/semconv/{{.TagVer}}"
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"testing"
|
||||
)
|
||||
|
||||
@@ -18,6 +19,10 @@ func TestErrorType(t *testing.T) {
|
||||
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.
|
||||
check(t, fmtWrapped(custom("")), pkg+".ErrCustomType")
|
||||
check(t, fmtWrapped(wrapped(custom(""))), pkg+".wrappedErr")
|
||||
check(t, fmtWrapped(fmtWrapped(custom(""))), pkg+".ErrCustomType")
|
||||
check(t, fmtWrapped(nil), fmtWrapErrorType.String()) // fmt.Errorf with nil error, use concrete type of the wrapper.
|
||||
}
|
||||
|
||||
func check(t *testing.T, err error, want string) {
|
||||
@@ -39,6 +44,10 @@ func wrapped(err error) error {
|
||||
return wrappedErr{err: err}
|
||||
}
|
||||
|
||||
func fmtWrapped(err error) error {
|
||||
return fmt.Errorf("wrapped: %w", err)
|
||||
}
|
||||
|
||||
type ErrCustomType struct {
|
||||
Type string
|
||||
}
|
||||
|
||||
+16
-1
@@ -5,6 +5,8 @@ package log // import "go.opentelemetry.io/otel/sdk/log"
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"reflect"
|
||||
"time"
|
||||
|
||||
@@ -164,7 +166,7 @@ func errorType(err error) string {
|
||||
}
|
||||
}
|
||||
|
||||
t := reflect.TypeOf(err)
|
||||
t := reflect.TypeOf(unwrapFmtWrapped(err))
|
||||
if t == nil {
|
||||
return ""
|
||||
}
|
||||
@@ -183,3 +185,16 @@ func errorType(err error) string {
|
||||
// This is not guaranteed to be unique, but is a best effort.
|
||||
return t.String()
|
||||
}
|
||||
|
||||
var fmtWrapErrorType = reflect.TypeOf(fmt.Errorf("wrapped: %w", errors.New("err")))
|
||||
|
||||
func unwrapFmtWrapped(err error) error {
|
||||
for reflect.TypeOf(err) == fmtWrapErrorType {
|
||||
u := errors.Unwrap(err)
|
||||
if u == nil {
|
||||
return err // When the wrapped error is nil, use the concrete type of the wrapper.
|
||||
}
|
||||
err = u
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -6,6 +6,7 @@ package log // import "go.opentelemetry.io/otel/sdk/log"
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"strconv"
|
||||
"testing"
|
||||
"time"
|
||||
@@ -404,6 +405,21 @@ func TestErrorType(t *testing.T) {
|
||||
var err error = struct{ baseErr }{}
|
||||
assert.Contains(t, errorType(err), "struct")
|
||||
})
|
||||
|
||||
t.Run("FmtWrappedFallsBackToWrappedType", func(t *testing.T) {
|
||||
err := fmt.Errorf("wrapped: %w", errWithType{msg: "boom", typ: ""})
|
||||
assert.Equal(t, "go.opentelemetry.io/otel/sdk/log.errWithType", errorType(err))
|
||||
})
|
||||
|
||||
t.Run("CustomWrapperStaysTopLevel", func(t *testing.T) {
|
||||
err := wrappedErr{err: errWithType{msg: "boom", typ: ""}}
|
||||
assert.Equal(t, "go.opentelemetry.io/otel/sdk/log.wrappedErr", errorType(err))
|
||||
})
|
||||
|
||||
t.Run("FmtWrappedNilError", func(t *testing.T) {
|
||||
err := fmt.Errorf("wrapped: %w", nil)
|
||||
assert.Equal(t, fmtWrapErrorType.String(), errorType(err))
|
||||
})
|
||||
}
|
||||
|
||||
type errWithType struct {
|
||||
@@ -419,6 +435,14 @@ type baseErr struct{}
|
||||
|
||||
func (baseErr) Error() string { return "boom" }
|
||||
|
||||
type wrappedErr struct {
|
||||
err error
|
||||
}
|
||||
|
||||
func (e wrappedErr) Error() string { return "wrapped: " + e.err.Error() }
|
||||
|
||||
func (e wrappedErr) Unwrap() error { return e.err }
|
||||
|
||||
func TestNewRecordSkipsExceptionWhenPresent(t *testing.T) {
|
||||
l := newLogger(NewLoggerProvider(), instrumentation.Scope{})
|
||||
|
||||
|
||||
@@ -5,6 +5,7 @@ package semconv // import "go.opentelemetry.io/otel/semconv/v1.40.0"
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"reflect"
|
||||
|
||||
"go.opentelemetry.io/otel/attribute"
|
||||
@@ -22,7 +23,8 @@ import (
|
||||
// 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.
|
||||
// from the concrete type of err after unwrapping any wrappers created with
|
||||
// [fmt.Errorf].
|
||||
//
|
||||
// The key of the returned attribute is [ErrorTypeKey].
|
||||
func ErrorType(err error) attribute.KeyValue {
|
||||
@@ -50,7 +52,7 @@ func errorType(err error) string {
|
||||
// Fallback to reflection if the ErrorType method is not supported or
|
||||
// returns an empty value.
|
||||
|
||||
t := reflect.TypeOf(err)
|
||||
t := reflect.TypeOf(unwrapFmtWrapped(err))
|
||||
pkg, name := t.PkgPath(), t.Name()
|
||||
if pkg != "" && name != "" {
|
||||
s = pkg + "." + name
|
||||
@@ -64,3 +66,16 @@ func errorType(err error) string {
|
||||
}
|
||||
return s
|
||||
}
|
||||
|
||||
var fmtWrapErrorType = reflect.TypeOf(fmt.Errorf("wrapped: %w", errors.New("err")))
|
||||
|
||||
func unwrapFmtWrapped(err error) error {
|
||||
for reflect.TypeOf(err) == fmtWrapErrorType {
|
||||
u := errors.Unwrap(err)
|
||||
if u == nil {
|
||||
return err // Should never happen, but avoid returning nil if unwrapping fails.
|
||||
}
|
||||
err = u
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -5,6 +5,7 @@ package semconv // import "go.opentelemetry.io/otel/semconv/v1.40.0"
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"testing"
|
||||
)
|
||||
|
||||
@@ -18,6 +19,10 @@ func TestErrorType(t *testing.T) {
|
||||
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.
|
||||
check(t, fmtWrapped(custom("")), pkg+".ErrCustomType")
|
||||
check(t, fmtWrapped(wrapped(custom(""))), pkg+".wrappedErr")
|
||||
check(t, fmtWrapped(fmtWrapped(custom(""))), pkg+".ErrCustomType")
|
||||
check(t, fmtWrapped(nil), fmtWrapErrorType.String()) // fmt.Errorf with nil error, use concrete type of the wrapper.
|
||||
}
|
||||
|
||||
func check(t *testing.T, err error, want string) {
|
||||
@@ -39,6 +44,10 @@ func wrapped(err error) error {
|
||||
return wrappedErr{err: err}
|
||||
}
|
||||
|
||||
func fmtWrapped(err error) error {
|
||||
return fmt.Errorf("wrapped: %w", err)
|
||||
}
|
||||
|
||||
type ErrCustomType struct {
|
||||
Type string
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user