-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy patherrorcontext.go
More file actions
198 lines (176 loc) · 5.15 KB
/
Copy patherrorcontext.go
File metadata and controls
198 lines (176 loc) · 5.15 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
package errorcontext
import (
"bufio"
"bytes"
"errors"
"fmt"
"runtime/debug"
"strings"
)
type BaseError[T any] struct {
originalErr error
contextFields T
isPanic bool
}
func NewBaseError[T any](originalErr error, initialContext T) *BaseError[T] {
return &BaseError[T]{
originalErr: originalErr,
contextFields: initialContext,
}
}
func (e *BaseError[T]) Error() string {
return e.originalErr.Error()
}
// Unwrap allows the original error to be resolved by errors.Is & errors.As.
func (e *BaseError[T]) Unwrap() error {
return e.originalErr
}
func (e *BaseError[T]) IsZero() bool {
if e == nil {
return true
}
return e.originalErr == nil
}
func (e *BaseError[T]) ContextFields() T {
return e.contextFields
}
// SetContextFields is a setter that replaces the attached error context.
func (e *BaseError[T]) SetContextFields(f T) {
e.contextFields = f
}
func (e *BaseError[T]) MarkAsPanic() *BaseError[T] {
e.isPanic = true
return e
}
func (e *BaseError[T]) IsPanic() bool {
if e == nil {
return false
}
return e.isPanic
}
// Collect finds aggregates all errors that match the given target type,
// within the error chain of err. The resulting slice contains target error instances
// in reverse order.
func Collect[T error](err error) []T {
if err == nil {
return nil
}
var found []T
currentErr := err
for currentErr != nil {
var target T
if errors.As(currentErr, &target) {
var last T
if len(found) > 0 {
last = found[len(found)-1]
}
if !errors.Is(target, last) {
found = append(found, target)
}
}
currentErr = errors.Unwrap(currentErr)
}
return found
}
const FieldNamePanicStackTrace = "stack"
const FieldNamePanicMessage = "panic"
type Panic struct {
Message string
Stack []string
}
type ErrorGenerator[T error] func(p Panic) T
func DefaultErrorGenerator(p Panic) error {
return fmt.Errorf("%s\n%s", p.Message, strings.Join(p.Stack, "\n"))
}
var _ ErrorGenerator[error] = DefaultErrorGenerator
type Recoverer[T error] struct {
// newErrorFunc converts a Panic value to a type with the `error` interface.
// This is a required parameter.
newErrorFunc ErrorGenerator[T]
// PanicValueTransform if set will try to format arbitrary panic value types,
// such as a struct or a map.
PanicValueTransform func(r any) (string, error)
// SkippedStackTraceLines sets the number of stack trace lines to be skipped.
// In-library stack trace lines may be considered irrelevant or noise and
// thus can be optionally skipped. By default, no lines are skipped.
SkippedStackTraceLines uint
}
func NewRecoverer[T error](newError ErrorGenerator[T]) Recoverer[T] {
if newError == nil {
panic(ErrNewErrorFuncNotSet)
}
return Recoverer[T]{
newErrorFunc: newError,
}
}
var ErrNewErrorFuncNotSet = errors.New("error generator function is not set")
// Wrap allows recovery from panics for the given function.
// Panics are translated and propagated as errors that can be handled accordingly.
// Note: unrecovered panics can cause an abnormal program exit.
func (r Recoverer[T]) Wrap(fn func() error) (err error) {
if r.newErrorFunc == nil {
return fn()
}
defer func() {
if rv := recover(); rv != nil {
err = r.newErrorFunc(r.Format(rv))
}
}()
err = fn()
return err
}
// WrapFunc is a convenience wrapper that returns a decorated function,
// ensuring that panics are converted to error values.
//
// A common use case is to pass the function directly to errgroup.Submit:
//
// grp := errgroup.Group{}
// recoverer := errorcontext.NewRecoverer[error](errorcontext.DefaultErrorGenerator)
// grp.Go(recoverer.WrapFunc(func() error {
// panic("something bad happened")
// }))
func (r Recoverer[T]) WrapFunc(fn func() error) func() error {
return func() error {
return r.Wrap(fn)
}
}
// Format transforms an arbitrary value thrown by panic to an error message
// along with providing the current goroutine stack trace for the panic root cause.
// If PanicValueTransform is non-nil, an attempt to format the recovered value is performed.
// If the formatter function returns an error, a fallback approach is used and the failure
// error message is appended to the standard message template.
// Note: this method is intended to be public in order to facilitate testing.
func (r Recoverer[T]) Format(rv any) Panic {
var baseMessage string
switch v := rv.(type) {
case error, string:
baseMessage = fmt.Sprintf("%s: %s", FieldNamePanicMessage, v)
default:
if r.PanicValueTransform != nil {
formatted, err := r.PanicValueTransform(rv)
if err != nil {
baseMessage = fmt.Sprintf("%s: %v\nfailed to transform: %s", FieldNamePanicMessage, v, err.Error())
} else {
baseMessage = fmt.Sprintf("%s: %s", FieldNamePanicMessage, formatted)
}
}
if baseMessage == "" {
baseMessage = fmt.Sprintf("%s: %v", FieldNamePanicMessage, v)
}
}
debugStack := debug.Stack()
stackLines := make([]string, 0, bytes.Count(debugStack, []byte{'\n'}))
scanner := bufio.NewScanner(bytes.NewReader(debugStack))
var lineNumber uint
for scanner.Scan() {
lineNumber++
if lineNumber <= r.SkippedStackTraceLines {
continue
}
stackLines = append(stackLines, scanner.Text())
}
return Panic{
Message: baseMessage,
Stack: stackLines,
}
}