forked from stvp/rollbar
/
rollbar.go
374 lines (312 loc) · 10.6 KB
/
rollbar.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
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
package rollbar
import (
"bytes"
"encoding/json"
"fmt"
"hash/adler32"
"net/http"
"net/url"
"os"
"reflect"
"regexp"
"runtime"
"strings"
"sync"
"time"
)
const (
NAME = "go-rollbar"
VERSION = "0.3.1"
// Severity levels
CRIT = "critical"
ERR = "error"
WARN = "warning"
INFO = "info"
DEBUG = "debug"
FILTERED = "[FILTERED]"
)
var (
// Rollbar access token. If this is blank, no errors will be reported to
// Rollbar.
Token = ""
// All errors and messages will be submitted under this environment.
Environment = "development"
// Platform, default to OS, but could be change ('client' for instance)
Platform = runtime.GOOS
// API endpoint for Rollbar.
Endpoint = "https://api.rollbar.com/api/1/item/"
// Maximum number of errors allowed in the sending queue before we start
// dropping new errors on the floor.
Buffer = 1000
// Filter GET and POST parameters from being sent to Rollbar.
FilterFields = regexp.MustCompile("password|secret|token")
// Output of error, by default stderr
ErrorWriter = os.Stderr
// Queue of messages to be sent.
bodyChannel chan map[string]interface{}
waitGroup sync.WaitGroup
)
// Fields can be used to pass arbitrary data to the Rollbar API.
type Field struct {
Name string
Data interface{}
}
// -- Setup
func init() {
bodyChannel = make(chan map[string]interface{}, Buffer)
go func() {
for body := range bodyChannel {
post(body, nil)
waitGroup.Done()
}
}()
}
// -- Error reporting
// Error asynchronously sends an error to Rollbar with the given severity
// level. You can pass, optionally, custom Fields to be passed on to Rollbar.
func Error(level string, err error, fields ...*Field) {
ErrorWithStackSkip(level, err, 1, fields...)
}
// ErrorWithStackSkip asynchronously sends an error to Rollbar with the given
// severity level and a given number of stack trace frames skipped. You can
// pass, optionally, custom Fields to be passed on to Rollbar.
func ErrorWithStackSkip(level string, err error, skip int, fields ...*Field) {
stack := BuildStack(2 + skip)
ErrorWithStack(level, err, stack, fields...)
}
// ErrorWithStack asynchronously sends and error to Rollbar with the given
// stacktrace and (optionally) custom Fields to be passed on to Rollbar.
func ErrorWithStack(level string, err error, stack Stack, fields ...*Field) {
buildAndPushError(level, err, stack, fields...)
}
// RequestError asynchronously sends an error to Rollbar with the given
// severity level and request-specific information. You can pass, optionally,
// custom Fields to be passed on to Rollbar.
func RequestError(level string, r *http.Request, err error, fields ...*Field) {
RequestErrorWithStackSkip(level, r, err, 1, fields...)
}
// RequestErrorWithStackSkip asynchronously sends an error to Rollbar with the
// given severity level and a given number of stack trace frames skipped, in
// addition to extra request-specific information. You can pass, optionally,
// custom Fields to be passed on to Rollbar.
func RequestErrorWithStackSkip(level string, r *http.Request, err error, skip int, fields ...*Field) {
stack := BuildStack(2 + skip)
RequestErrorWithStack(level, r, err, stack, fields...)
}
// RequestErrorWithStack asynchronously sends an error to Rollbar with the
// given severity level, request-specific information provided by the given
// http.Request, and a custom Stack. You You can pass, optionally, custom
// Fields to be passed on to Rollbar.
func RequestErrorWithStack(level string, r *http.Request, err error, stack Stack, fields ...*Field) {
buildAndPushError(level, err, stack, &Field{Name: "request", Data: errorRequest(r)})
}
func buildError(level string, err error, stack Stack, fields ...*Field) map[string]interface{} {
body := buildBody(level, err.Error())
data := body["data"].(map[string]interface{})
errBody, fingerprint := errorBody(err, stack)
data["body"] = errBody
data["fingerprint"] = fingerprint
for _, field := range fields {
data[field.Name] = field.Data
}
return body
}
func buildAndPushError(level string, err error, stack Stack, fields ...*Field) {
push(buildError(level, err, stack, fields...))
}
// -- Message reporting
// Message asynchronously sends a message to Rollbar with the given severity
// level.
func Message(level string, msg string) {
body := buildBody(level, msg)
data := body["data"].(map[string]interface{})
data["body"] = messageBody(msg)
push(body)
}
// -- Misc.
// Wait will block until the queue of errors / messages is empty. This allows
// you to ensure that errors / messages are sent to Rollbar before exiting an
// application.
func Wait() {
waitGroup.Wait()
}
// Build the main JSON structure that will be sent to Rollbar with the
// appropriate metadata.
func buildBody(level, title string) map[string]interface{} {
timestamp := time.Now().Unix()
hostname, _ := os.Hostname()
return map[string]interface{}{
"access_token": Token,
"data": map[string]interface{}{
"environment": Environment,
"title": title,
"level": level,
"timestamp": timestamp,
"platform": Platform,
"language": "go",
"server": map[string]interface{}{
"host": hostname,
},
"notifier": map[string]interface{}{
"name": NAME,
"version": VERSION,
},
},
}
}
// errorBody generates a Rollbar error body with a given stack trace.
func errorBody(err error, stack Stack) (map[string]interface{}, string) {
fingerprint := stack.Fingerprint()
errBody := map[string]interface{}{
"trace": map[string]interface{}{
"frames": stack,
"exception": map[string]interface{}{
"class": errorClass(err),
"message": err.Error(),
},
},
}
return errBody, fingerprint
}
// errorRequest extracts details from a Request in a format that Rollbar
// accepts.
func errorRequest(r *http.Request) map[string]interface{} {
cleanQuery := filterParams(r.URL.Query())
return map[string]interface{}{
"url": r.URL.String(),
"method": r.Method,
"headers": flattenValues(r.Header),
// GET params
"query_string": url.Values(cleanQuery).Encode(),
"GET": flattenValues(cleanQuery),
// POST / PUT params
"POST": flattenValues(filterParams(r.Form)),
}
}
// filterParams filters sensitive information like passwords from being sent to
// Rollbar.
func filterParams(values map[string][]string) map[string][]string {
for key, _ := range values {
if FilterFields.Match([]byte(key)) {
values[key] = []string{FILTERED}
}
}
return values
}
func flattenValues(values map[string][]string) map[string]interface{} {
result := make(map[string]interface{})
for k, v := range values {
if len(v) == 1 {
result[k] = v[0]
} else {
result[k] = v
}
}
return result
}
// Build a message inner-body for the given message string.
func messageBody(s string) map[string]interface{} {
return map[string]interface{}{
"message": map[string]interface{}{
"body": s,
},
}
}
func errorClass(err error) string {
class := reflect.TypeOf(err).String()
if class == "" {
return "panic"
} else if class == "*errors.errorString" {
checksum := adler32.Checksum([]byte(err.Error()))
return fmt.Sprintf("{%x}", checksum)
} else {
return strings.TrimPrefix(class, "*")
}
}
// -- POST handling
// Queue the given JSON body to be POSTed to Rollbar.
func push(body map[string]interface{}) {
if len(bodyChannel) < Buffer {
waitGroup.Add(1)
bodyChannel <- body
} else {
stderr("buffer full, dropping error on the floor")
}
}
// POST the given JSON body to Rollbar synchronously.
func post(body map[string]interface{}, httpClient *http.Client) {
if len(Token) == 0 {
stderr("empty token")
return
}
jsonBody, err := json.Marshal(body)
if err != nil {
stderr("failed to encode payload: %s", err.Error())
return
}
if httpClient == nil {
httpClient = &http.Client{}
}
resp, err := httpClient.Post(Endpoint, "application/json", bytes.NewReader(jsonBody))
if err != nil {
stderr("POST failed: %s", err.Error())
return
} else if resp.StatusCode != 200 {
stderr("received response: %s", resp.Status)
}
if resp != nil {
resp.Body.Close()
}
defer resp.Body.Close()
}
type Client struct {
httpClient *http.Client
}
// Error asynchronously sends an error to Rollbar with the given severity
// level. You can pass, optionally, custom Fields to be passed on to Rollbar.
func (c *Client) Error(level string, err error, fields ...*Field) {
c.ErrorWithStackSkip(level, err, 1, fields...)
}
// ErrorWithStackSkip asynchronously sends an error to Rollbar with the given
// severity level and a given number of stack trace frames skipped. You can
// pass, optionally, custom Fields to be passed on to Rollbar.
func (c *Client) ErrorWithStackSkip(level string, err error, skip int, fields ...*Field) {
stack := BuildStack(2 + skip)
c.ErrorWithStack(level, err, stack, fields...)
}
// ErrorWithStack asynchronously sends and error to Rollbar with the given
// stacktrace and (optionally) custom Fields to be passed on to Rollbar.
func (c *Client) ErrorWithStack(level string, err error, stack Stack, fields ...*Field) {
c.buildAndPushError(level, err, stack, fields...)
}
// RequestError asynchronously sends an error to Rollbar with the given
// severity level and request-specific information. You can pass, optionally,
// custom Fields to be passed on to Rollbar.
func (c *Client) RequestError(level string, r *http.Request, err error, fields ...*Field) {
c.RequestErrorWithStackSkip(level, r, err, 1, fields...)
}
// RequestErrorWithStackSkip asynchronously sends an error to Rollbar with the
// given severity level and a given number of stack trace frames skipped, in
// addition to extra request-specific information. You can pass, optionally,
// custom Fields to be passed on to Rollbar.
func (c *Client) RequestErrorWithStackSkip(level string, r *http.Request, err error, skip int, fields ...*Field) {
stack := BuildStack(2 + skip)
c.RequestErrorWithStack(level, r, err, stack, fields...)
}
// RequestErrorWithStack asynchronously sends an error to Rollbar with the
// given severity level, request-specific information provided by the given
// http.Request, and a custom Stack. You You can pass, optionally, custom
// Fields to be passed on to Rollbar.
func (c *Client) RequestErrorWithStack(level string, r *http.Request, err error, stack Stack, fields ...*Field) {
c.buildAndPushError(level, err, stack, &Field{Name: "request", Data: errorRequest(r)})
}
func (c *Client) buildAndPushError(level string, err error, stack Stack, fields ...*Field) {
go post(buildError(level, err, stack, fields...), c.httpClient)
}
// -- stderr
func stderr(format string, args ...interface{}) {
if ErrorWriter != nil {
format = "Rollbar error: " + format + "\n"
fmt.Fprintf(ErrorWriter, format, args...)
}
}