-
Notifications
You must be signed in to change notification settings - Fork 39
/
stacktrace.go
249 lines (207 loc) · 6.63 KB
/
stacktrace.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
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
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
// Copyright 2016 Palantir Technologies
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package stacktrace
import (
"fmt"
"math"
"runtime"
"strings"
"github.com/palantir/stacktrace/cleanpath"
)
/*
CleanPath function is applied to file paths before adding them to a stacktrace.
By default, it makes the path relative to the $GOPATH environment variable.
To remove some additional prefix like "github.com" from file paths in
stacktraces, use something like:
stacktrace.CleanPath = func(path string) string {
path = cleanpath.RemoveGoPath(path)
path = strings.TrimPrefix(path, "github.com/")
return path
}
*/
var CleanPath = cleanpath.RemoveGoPath
/*
NewError is a drop-in replacement for fmt.Errorf that includes line number
information. The canonical call looks like this:
if !IsOkay(arg) {
return stacktrace.NewError("Expected %v to be okay", arg)
}
*/
func NewError(msg string, vals ...interface{}) error {
return create(nil, NoCode, msg, vals...)
}
/*
Propagate wraps an error to include line number information. The msg and vals
arguments work like the ones for fmt.Errorf.
The message passed to Propagate should describe the action that failed,
resulting in the cause. The canonical call looks like this:
result, err := process(arg)
if err != nil {
return nil, stacktrace.Propagate(err, "Failed to process %v", arg)
}
To write the message, ask yourself "what does this call do?" What does
process(arg) do? It processes ${arg}, so the message is that we failed to
process ${arg}.
Pay attention that the message is not redundant with the one in err. If it is
not possible to add any useful contextual information beyond what is already
included in an error, msg can be an empty string:
func Something() error {
mutex.Lock()
defer mutex.Unlock()
err := reallySomething()
return stacktrace.Propagate(err, "")
}
If cause is nil, Propagate returns nil. This allows elision of some "if err !=
nil" checks.
*/
func Propagate(cause error, msg string, vals ...interface{}) error {
if cause == nil {
// Allow calling Propagate without checking whether there is error
return nil
}
return create(cause, NoCode, msg, vals...)
}
/*
ErrorCode is a code that can be attached to an error as it is passed/propagated
up the stack.
There is no predefined set of error codes. You define the ones relevant to your
application:
const (
EcodeManifestNotFound = stacktrace.ErrorCode(iota)
EcodeBadInput
EcodeTimeout
)
The one predefined error code is NoCode, which has a value of math.MaxUint16.
Avoid using that value as an error code.
An ordinary stacktrace.Propagate call preserves the error code of an error.
*/
type ErrorCode uint16
/*
NoCode is the error code of errors with no code explicitly attached.
*/
const NoCode ErrorCode = math.MaxUint16
/*
NewErrorWithCode is similar to NewError but also attaches an error code.
*/
func NewErrorWithCode(code ErrorCode, msg string, vals ...interface{}) error {
return create(nil, code, msg, vals...)
}
/*
PropagateWithCode is similar to Propagate but also attaches an error code.
_, err := os.Stat(manifestPath)
if os.IsNotExist(err) {
return stacktrace.PropagateWithCode(err, EcodeManifestNotFound, "")
}
*/
func PropagateWithCode(cause error, code ErrorCode, msg string, vals ...interface{}) error {
if cause == nil {
// Allow calling PropagateWithCode without checking whether there is error
return nil
}
return create(cause, code, msg, vals...)
}
/*
NewMessageWithCode returns an error that prints just like fmt.Errorf with no
line number, but including a code. The error code mechanism can be useful by
itself even where stack traces with line numbers are not warranted.
ttl := req.URL.Query().Get("ttl")
if ttl == "" {
return 0, stacktrace.NewMessageWithCode(EcodeBadInput, "Missing ttl query parameter")
}
*/
func NewMessageWithCode(code ErrorCode, msg string, vals ...interface{}) error {
return &stacktrace{
message: fmt.Sprintf(msg, vals...),
code: code,
}
}
/*
GetCode extracts the error code from an error.
for i := 0; i < attempts; i++ {
err := Do()
if stacktrace.GetCode(err) != EcodeTimeout {
return err
}
// try a few more times
}
return stacktrace.NewError("timed out after %d attempts", attempts)
GetCode returns the special value stacktrace.NoCode if err is nil or if there is
no error code attached to err.
*/
func GetCode(err error) ErrorCode {
if err, ok := err.(*stacktrace); ok {
return err.code
}
return NoCode
}
type stacktrace struct {
message string
cause error
code ErrorCode
file string
function string
line int
}
func create(cause error, code ErrorCode, msg string, vals ...interface{}) error {
// If no error code specified, inherit error code from the cause.
if code == NoCode {
code = GetCode(cause)
}
err := &stacktrace{
message: fmt.Sprintf(msg, vals...),
cause: cause,
code: code,
}
// Caller of create is NewError or Propagate, so user's code is 2 up.
pc, file, line, ok := runtime.Caller(2)
if !ok {
return err
}
if CleanPath != nil {
file = CleanPath(file)
}
err.file, err.line = file, line
f := runtime.FuncForPC(pc)
if f == nil {
return err
}
err.function = shortFuncName(f)
return err
}
/* "FuncName" or "Receiver.MethodName" */
func shortFuncName(f *runtime.Func) string {
// f.Name() is like one of these:
// - "github.com/palantir/shield/package.FuncName"
// - "github.com/palantir/shield/package.Receiver.MethodName"
// - "github.com/palantir/shield/package.(*PtrReceiver).MethodName"
longName := f.Name()
withoutPath := longName[strings.LastIndex(longName, "/")+1:]
withoutPackage := withoutPath[strings.Index(withoutPath, ".")+1:]
shortName := withoutPackage
shortName = strings.Replace(shortName, "(", "", 1)
shortName = strings.Replace(shortName, "*", "", 1)
shortName = strings.Replace(shortName, ")", "", 1)
return shortName
}
func (st *stacktrace) Error() string {
return fmt.Sprint(st)
}
// ExitCode returns the exit code associated with the stacktrace error based on its error code. If the error code is
// NoCode, return 1 (default); otherwise, returns the value of the error code.
func (st *stacktrace) ExitCode() int {
if st.code == NoCode {
return 1
}
return int(st.code)
}