385 lines
10 KiB
Go
385 lines
10 KiB
Go
package rollbar
|
|
|
|
import (
|
|
"bytes"
|
|
"encoding/json"
|
|
"fmt"
|
|
"hash/adler32"
|
|
"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.4.0"
|
|
|
|
// 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
|
|
|
|
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 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{}) 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...)
|
|
}
|
|
}
|