This repository has been archived by the owner on Sep 11, 2023. It is now read-only.
-
Notifications
You must be signed in to change notification settings - Fork 36
/
rollbar.go
383 lines (317 loc) · 10.1 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
375
376
377
378
379
380
381
382
383
package rollbar
import (
"bytes"
"encoding/json"
"fmt"
"io"
"net/http"
"net/url"
"os"
"reflect"
"regexp"
"runtime"
"strings"
"sync"
"time"
)
const (
// NAME is the name of this notifier library as reported to the Rollbar API.
NAME = "go-rollbar"
// VERSION is the version number of this notifier library as reported to the
// Rollbar API.
VERSION = "0.5.1"
// CRIT is the critical Rollbar severity level as reported to the Rollbar
// API.
CRIT = "critical"
// ERR is the error Rollbar severity level as reported to the Rollbar API.
ERR = "error"
// WARN is the warning Rollbar severity level as reported to the Rollbar API.
WARN = "warning"
// INFO is the info Rollbar severity level as reported to the Rollbar API.
INFO = "info"
// DEBUG is the debug Rollbar severity level as reported to the Rollbar API.
DEBUG = "debug"
// FILTERED is the text that replaces all sensitive values in items sent to
// the Rollbar API.
FILTERED = "[FILTERED]"
)
var (
// Token is the Rollbar access token under which all items will be reported.
// If Token is blank, no errors will be reported to Rollbar.
Token = ""
// Environment is the environment under which all items will be reported.
Environment = "development"
// Platform is the platform reported for all Rollbar items. The default is
// the running operating system (darwin, freebsd, linux, etc.) but it can
// also be application specific (client, heroku, etc.).
Platform = runtime.GOOS
// Endpoint is the URL destination for all Rollbar item POST requests.
Endpoint = "https://api.rollbar.com/api/1/item/"
// Buffer is the maximum number of errors that will be queued for sending.
// When the buffer is full, new errors are dropped on the floor until the API
// can catch up.
Buffer = 1000
// FilterFields is a regular expression that matches field names that should
// not be sent to Rollbar. Values for these fields are replaced with
// "[FILTERED]".
FilterFields = regexp.MustCompile("password|secret|token")
// ErrorWriter is the destination for errors encountered while POSTing items
// to Rollbar. By default, this is stderr. This can be nil.
ErrorWriter io.Writer = os.Stderr
// CodeVersion is the optional code version reported to the Rollbar API for
// all items.
CodeVersion = ""
bodyChannel chan map[string]interface{}
waitGroup sync.WaitGroup
postErrors chan error
nilErrTitle = "<nil>"
)
// Field is a custom data field used to report arbitrary data to the Rollbar
// API.
type Field struct {
Name string
Data interface{}
}
// -- Setup
func init() {
bodyChannel = make(chan map[string]interface{}, Buffer)
postErrors = make(chan error, Buffer)
go func() {
var err error
for body := range bodyChannel {
err = post(body)
if err != nil {
if len(postErrors) == cap(postErrors) {
<-postErrors
}
postErrors <- err
}
waitGroup.Done()
}
close(postErrors)
}()
}
// -- Error reporting
// Errorf asynchronously sends an error to Rollbar with the given severity
// level. You can pass, optionally, a format specifier that the error message
// will be formatted according to.
func Errorf(level string, format string, args ...interface{}) {
ErrorWithStackSkip(level, fmt.Errorf(format, args...), 1)
}
// 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, append(fields, &Field{Name: "request", Data: errorRequest(r)})...)
}
func buildError(level string, err error, stack Stack, fields ...*Field) map[string]interface{} {
title := nilErrTitle
if err != nil {
title = err.Error()
}
body := buildBody(level, title)
data := body["data"].(map[string]interface{})
errBody := errorBody(err, stack)
data["body"] = errBody
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.
// PostErrors returns a channel that receives all errors encountered while
// POSTing items to the Rollbar API.
func PostErrors() <-chan error {
return postErrors
}
// 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()
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,
},
}
if CodeVersion != "" {
data["code_version"] = CodeVersion
}
return map[string]interface{}{
"access_token": Token,
"data": data,
}
}
// errorBody generates a Rollbar error body with a given stack trace.
func errorBody(err error, stack Stack) map[string]interface{} {
message := nilErrTitle
if err != nil {
message = err.Error()
}
errBody := map[string]interface{}{
"trace": map[string]interface{}{
"frames": stack,
"exception": map[string]interface{}{
"class": errorClass(err),
"message": message,
},
},
}
return errBody
}
// 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)),
"user_ip": r.RemoteAddr,
}
}
// 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 {
if err == nil {
return nilErrTitle
}
class := reflect.TypeOf(err).String()
if class == "" {
return "panic"
} 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{}) error {
if len(Token) == 0 {
stderr("empty token")
return nil
}
jsonBody, err := json.Marshal(body)
if err != nil {
stderr("failed to encode payload: %s", err.Error())
return err
}
resp, err := http.Post(Endpoint, "application/json", bytes.NewReader(jsonBody))
if err != nil {
stderr("POST failed: %s", err.Error())
return err
}
defer resp.Body.Close()
if resp.StatusCode != 200 {
stderr("received response: %s", resp.Status)
return ErrHTTPError(resp.StatusCode)
}
return nil
}
// -- stderr
func stderr(format string, args ...interface{}) {
if ErrorWriter != nil {
format = "Rollbar error: " + format + "\n"
fmt.Fprintf(ErrorWriter, format, args...)
}
}