-
-
Notifications
You must be signed in to change notification settings - Fork 0
/
stack.go
120 lines (109 loc) · 3.25 KB
/
stack.go
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
package serrors
import (
"errors"
"strings"
)
// StackFrame represents a single stack frame of an error.
type StackFrame struct {
File string `json:"file" yaml:"file"`
Func string `json:"func" yaml:"func"`
Line int `json:"line" yaml:"line"`
}
// ErrorStack holds an error and its relevant information.
type ErrorStack struct {
error error
ErrorMessage string `json:"error_message" yaml:"error_message"`
Fields map[string]any `json:"fields" yaml:"fields"`
StackTrace []StackFrame `json:"stack_trace" yaml:"stack_trace"`
}
// Error returns the main error.
func (es *ErrorStack) Error() error {
return es.error
}
// GetStack returns the errors that are present in the provided error.
func GetStack(err error) []ErrorStack {
if err == nil {
return nil
}
var collectedErrors []ErrorStack
for err != nil {
errorsToAdd := buildErrorStack(err)
collectedErrors = append(collectedErrors, errorsToAdd)
err = errors.Unwrap(err)
}
return cleanStack(collectedErrors)
}
func buildErrorStack(err error) ErrorStack {
if serr, ok := err.(*Error); ok {
return ErrorStack{
error: err,
ErrorMessage: serr.message,
Fields: serr.fields,
StackTrace: resolveStackForStackFrames(serr.stack),
}
}
return buildErrorStackForThirdPartyError(err)
}
func cleanStack(stackFrames []ErrorStack) []ErrorStack {
// the following code only exists for cleaning up error formats.
// e.g. when using pkg/errors.Wrap function two errors are added to the stack.
// See github.com/pkg/errors@v0.9.1/errors.go:184
// Before cleanup the ErrorStack would look like this:
// [
// {
// "error_message": "serrors",
// "fields": null,
// "stack_trace": [{...}]
// },
// {
// "error_message": "pkgerrors: errors",
// "fields": null,
// "stack_trace": [{...}]
// ]
// },
// {
// "error_message": "pkgerrors: errors",
// "fields": null,
// "stack_trace": null
// },
// {
// "error_message": "errors",
// "fields": null,
// "stack_trace": null
// }
// ]
// remove errors from the list when they have no stack and have the same message as the next one
for i := len(stackFrames) - 1; i > 0; i-- {
if len(stackFrames[i].StackTrace) == 0 && len(stackFrames[i-1].StackTrace) > 0 &&
stackFrames[i].ErrorMessage == stackFrames[i-1].ErrorMessage {
appendToFields(&stackFrames[i-1].Fields, stackFrames[i].Fields)
stackFrames = append(stackFrames[:i], stackFrames[i+1:]...)
}
}
// clean up duplicate messages
//nolint:gomnd // we only need to clear up when we have more than 1 stack frame
if size := len(stackFrames); size > 1 {
fullErrorText := ": " + stackFrames[size-1].ErrorMessage
//nolint:gomnd //iterate backwards and start at the second last stack frame
for i := size - 2; i >= 0; i-- {
s, found := strings.CutSuffix(stackFrames[i].ErrorMessage, fullErrorText)
if found {
stackFrames[i].ErrorMessage = s
}
fullErrorText = ": " + stackFrames[i].ErrorMessage
}
}
return stackFrames
}
//nolint:gocritic // allow passing a pointer to a map, we do this so we don't create a new map on every run.
func appendToFields(dst *map[string]any, src map[string]any) {
if len(src) == 0 {
return
}
if *dst == nil {
*dst = make(map[string]any)
}
for s, a := range src {
(*dst)[s] = a
}
}