Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -211,6 +211,7 @@ The `oops.OopsError` builder must finish with either `.Errorf(...)`, `.Wrap(...)
| `.Request(*http.Request, bool)` | `err.Request() *http.Request` | Supply http request |
| `.Response(*http.Response, bool)` | `err.Response() *http.Response` | Supply http response |
| `.FromContext(context.Context)` | | Reuse an existing OopsErrorBuilder transported in a Go context |
| | `err.Layers() []*OopsError` | Return all OopsError layers in the chain from outermost to innermost, so callers can inspect attributes at any layer (non-OopsError errors are skipped) |
Comment thread
samber marked this conversation as resolved.

#### Examples

Expand Down
89 changes: 87 additions & 2 deletions error.go
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,41 @@ var (
_ slog.LogValuer = (*OopsError)(nil)
)

// OopsErrorLayer holds the attributes of a single layer in an error chain,
// as returned by Layers(). Every field reflects the value set at that specific
// wrapping level only — no chain traversal is performed. Fields that were not
// set on that layer carry their zero value.
type OopsErrorLayer struct {
// Core error information
Code any
Time time.Time
Duration time.Duration

// Contextual information
Domain string
Tags []string
Context map[string]any

// Tracing information
Trace string
Span string

// Developer-facing information
Hint string
Public string
Owner string

// User and tenant information
UserID string
UserData map[string]any
TenantID string
TenantData map[string]any

// HTTP request/response information
Request *http.Request
Response *http.Response
}

// outputBlock is one node in the error chain, holding pre-filtered frames.
type outputBlock struct {
err error
Expand Down Expand Up @@ -109,6 +144,56 @@ func (o OopsError) Unwrap() error {
return o.err
}

// toLayer converts the current OopsError into an OopsErrorLayer containing
// only the attributes set at this specific layer. No chain traversal is performed.
// Map values are processed the same way as in ToMap: lazy functions are evaluated
// and pointer values are dereferenced.
func (o OopsError) toLayer() *OopsErrorLayer {
layer := &OopsErrorLayer{
Code: o.code,
Duration: o.duration,
Domain: o.domain,
Tags: o.tags,
Context: dereferencePointers(lazyMapEvaluation(o.context)),
Trace: o.trace,
Span: o.span,
Hint: o.hint,
Public: o.public,
Owner: o.owner,
UserID: o.userID,
UserData: dereferencePointers(lazyMapEvaluation(o.userData)),
TenantID: o.tenantID,
TenantData: dereferencePointers(lazyMapEvaluation(o.tenantData)),
}
if !o.time.IsZero() {
layer.Time = o.time.In(Local)
}
if o.req != nil {
layer.Request = o.req.A
}
if o.res != nil {
layer.Response = o.res.A
}
return layer
}

// Layers returns a slice of all OopsError layers in the error chain,
// from outermost to innermost. Each element represents one wrapping layer
// with its own attributes, allowing callers to inspect or select attributes
// from any layer rather than only the deepest one.
//
// Only OopsError layers are included; non-OopsError errors in the chain
// (e.g. a plain fmt.Errorf or sentinel error at the root) are skipped.
// Use Unwrap() on the innermost layer to access the underlying error.
func (o OopsError) Layers() []*OopsErrorLayer {
var layers []*OopsErrorLayer
recursive(o, func(e OopsError) bool {
layers = append(layers, e.toLayer())
return true
})
Comment thread
samber marked this conversation as resolved.
return layers
}

// Is implements the errors.Is interface.
//
// OopsError contains non-comparable fields (maps, slices), so the default
Expand Down Expand Up @@ -446,7 +531,7 @@ func (o OopsError) Stacktrace() string {

stBlocks := make([]lo.Tuple3[error, string, []oopsStacktraceFrame], len(blocks))
for i, b := range blocks {
stBlocks[i] = lo.T3[error, string, []oopsStacktraceFrame](b.err, b.msg, b.frames)
stBlocks[i] = lo.T3(b.err, b.msg, b.frames)
}
return "Oops: " + strings.Join(framesToStacktraceBlocks(stBlocks), "\nThrown: ")
}
Expand Down Expand Up @@ -482,7 +567,7 @@ func (o OopsError) Sources() string {

srcBlocks := make([]lo.Tuple2[string, *oopsStacktrace], len(blocks))
for i, b := range blocks {
srcBlocks[i] = lo.T2[string, *oopsStacktrace](b.msg, &oopsStacktrace{frames: b.frames})
srcBlocks[i] = lo.T2(b.msg, &oopsStacktrace{frames: b.frames})
}
return "Oops: " + strings.Join(framesToSourceBlocks(srcBlocks), "\n\nThrown: ")
}
Expand Down
81 changes: 81 additions & 0 deletions oops_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -591,6 +591,87 @@ func TestOopsMixedWithGetters(t *testing.T) {
is.Equal("a message 42", err.(OopsError).Unwrap().(OopsError).msg)
}

func TestOopsLayers(t *testing.T) {
is := assert.New(t)
t.Parallel()

// single layer
oopsErr, ok := AsOops(New("hello"))
is.True(ok)
layers := oopsErr.Layers()
is.Len(layers, 1)

// multiple layers with distinct attributes
inner := Code("inner_code").Public("inner public").New("inner")
outer := Code("outer_code").Public("outer public").Wrap(inner)
oopsErr, ok = AsOops(outer)
is.True(ok)
layers = oopsErr.Layers()
is.Len(layers, 2)
is.Equal("outer_code", layers[0].Code)
is.Equal("outer public", layers[0].Public)
is.Equal("inner_code", layers[1].Code)
is.Equal("inner public", layers[1].Public)

// non-OopsError root is skipped
root := errors.New("plain error")
wrapped := Code("outer code").Public("outer public").Wrap(root)
oopsErr, ok = AsOops(wrapped)
is.True(ok)
layers = oopsErr.Layers()
is.Len(layers, 1)
is.Equal("outer code", layers[0].Code)
is.Equal("outer public", layers[0].Public)

// three layers deep
l1 := Code("l1").New("level 1")
l2 := Code("l2").Wrap(l1)
l3 := Code("l3").Wrap(l2)
oopsErr, ok = AsOops(l3)
is.True(ok)
layers = oopsErr.Layers()
is.Len(layers, 3)
is.Equal("l3", layers[0].Code)
is.Equal("l2", layers[1].Code)
is.Equal("l1", layers[2].Code)

// layers are pointers (not shared)
is.NotSame(layers[0], layers[1])
is.NotSame(layers[1], layers[2])

// Mixed chain: oopserror -> error -> error -> oopserror -> oopserror -> error
//
// Construction (innermost to outermost):
// plainRoot plain error
// oopsInner2 OopsError wrapping plainRoot
// oopsInner1 OopsError wrapping oopsInner2
// plainMid2 plain error wrapping oopsInner1 (via fmt.Errorf %w)
// plainMid1 plain error wrapping plainMid2 (via fmt.Errorf %w)
// oopsOuter OopsError wrapping plainMid1
//
// Layers() only surfaces OopsError nodes; plain errors are transparent.
// Expected layers (outermost first): oopsOuter, oopsInner1, oopsInner2
plainRoot := errors.New("plain root")
oopsInner2 := Code("inner2").Public("public inner2").Wrap(plainRoot)
oopsInner1 := Code("inner1").Public("public inner1").Wrap(oopsInner2)
plainMid2 := fmt.Errorf("plain mid2: %w", oopsInner1)
plainMid1 := fmt.Errorf("plain mid1: %w", plainMid2)
oopsOuter := Code("outer").Public("public outer").Wrap(plainMid1)

oopsErr, ok = AsOops(oopsOuter)
is.True(ok)
layers = oopsErr.Layers()
is.Len(layers, 3)
is.Equal("outer", layers[0].Code)
is.Equal("public outer", layers[0].Public)
is.Equal("inner1", layers[1].Code)
is.Equal("public inner1", layers[1].Public)
is.Equal("inner2", layers[2].Code)
is.Equal("public inner2", layers[2].Public)
is.NotSame(layers[0], layers[1])
is.NotSame(layers[1], layers[2])
}

func TestOopsLogValue(t *testing.T) {
is := assert.New(t)
t.Parallel()
Expand Down
Loading