blob: d5d543494aa4efbaf5e6112445e712cdfbf0d03d [file] [log] [blame]
package airbrake
import (
"bytes"
"crypto/tls"
"errors"
"io/ioutil"
"log"
"net"
"net/http"
"os"
"reflect"
"regexp"
"runtime"
"sync"
"text/template"
"time"
)
var (
ApiKey = ""
Endpoint = "https://api.airbrake.io/notifier_api/v2/notices"
Environment = "development"
Verbose = false
// PrettyParams allows including request query/form parameters on the Environment tab
// which is more readable than the raw text of the Parameters tab (in Errbit).
// The param keys will be rendered as "?<param>" so they will sort together at the top of the tab.
PrettyParams = false
sensitive = regexp.MustCompile(`password|token|secret|key`)
badResponse = errors.New("Bad response")
apiKeyMissing = errors.New("Please set the airbrake.ApiKey before doing calls")
dunno = []byte("???")
centerDot = []byte("·")
dot = []byte(".")
tmpl = template.Must(template.New("error").Parse(source))
)
type Line struct {
Function string
File string
Line int
}
// stack implements Stack, skipping N frames
func stacktrace(skip int) (lines []Line) {
for i := skip; ; i++ {
pc, file, line, ok := runtime.Caller(i)
if !ok {
break
}
item := Line{string(function(pc)), string(file), line}
// ignore panic method
if item.Function != "panic" {
lines = append(lines, item)
}
}
return
}
var channel chan map[string]interface{}
var once sync.Once
// function returns, if possible, the name of the function containing the PC.
func function(pc uintptr) []byte {
fn := runtime.FuncForPC(pc)
if fn == nil {
return dunno
}
name := []byte(fn.Name())
// The name includes the path name to the package, which is unnecessary
// since the file name is already included. Plus, it has center dots.
// That is, we see
// runtime/debug.*T·ptrmethod
// and want
// *T.ptrmethod
if period := bytes.Index(name, dot); period >= 0 {
name = name[period+1:]
}
name = bytes.Replace(name, centerDot, dot, -1)
return name
}
func initChannel() {
channel = make(chan map[string]interface{}, 100)
go func() {
for params := range channel {
post(params)
}
}()
}
func post(params map[string]interface{}) error {
buffer := bytes.NewBufferString("")
if err := tmpl.Execute(buffer, params); err != nil {
log.Printf("Airbrake error: %s", err)
return err
}
if Verbose {
log.Printf("Airbrake payload for endpoint %s: %s", Endpoint, buffer)
}
var httpClient = &http.Client{
Transport: &http.Transport{
Proxy: http.ProxyFromEnvironment,
Dial: (&net.Dialer{
Timeout: 30 * time.Second,
KeepAlive: 30 * time.Second,
}).Dial,
TLSHandshakeTimeout: 10 * time.Second,
TLSClientConfig: &tls.Config{
ClientSessionCache: tls.NewLRUClientSessionCache(1024),
},
MaxIdleConnsPerHost: 100,
ResponseHeaderTimeout: 10 * time.Second,
},
Timeout: 10 * time.Second,
}
response, err := httpClient.Post(Endpoint, "text/xml", buffer)
if err != nil {
log.Printf("Airbrake error: %s", err)
return err
}
if Verbose {
body, _ := ioutil.ReadAll(response.Body)
log.Printf("response: %s", body)
}
response.Body.Close()
if Verbose {
log.Printf("Airbrake post: %s status code: %d", params["Error"], response.StatusCode)
}
return nil
}
func Error(e error, request *http.Request) error {
once.Do(initChannel)
if ApiKey == "" {
return apiKeyMissing
}
return post(params(e, request))
}
func Notify(e error) error {
once.Do(initChannel)
if ApiKey == "" {
return apiKeyMissing
}
return post(params(e, nil))
}
func params(e error, request *http.Request) map[string]interface{} {
params := map[string]interface{}{
"Class": reflect.TypeOf(e).String(),
"Error": e,
"ApiKey": ApiKey,
"ErrorName": e.Error(),
"Environment": Environment,
}
if params["Class"] == "" {
params["Class"] = "Panic"
}
pwd, err := os.Getwd()
if err == nil {
params["Pwd"] = pwd
}
hostname, err := os.Hostname()
if err == nil {
params["Hostname"] = hostname
}
params["Backtrace"] = stacktrace(3)
if request == nil || request.ParseForm() != nil {
return params
}
// Compile relevant request parameters into a map.
req := make(map[string]interface{})
params["Request"] = req
req["Component"] = ""
req["Action"] = ""
// Nested http Muxes muck with the URL, prefer RequestURI.
if request.RequestURI != "" {
req["URL"] = request.RequestURI
} else {
req["URL"] = request.URL
}
// Compile header parameters.
header := make(map[string]string)
req["Header"] = header
header["Method"] = request.Method
header["Protocol"] = request.Proto
for k, v := range request.Header {
if !omit(k, v) {
header[k] = v[0]
}
}
// Compile query/form parameters.
form := make(map[string]string)
req["Form"] = form
for k, v := range request.Form {
if !omit(k, v) {
form[k] = v[0]
if PrettyParams {
header["?"+k] = v[0]
}
}
}
return params
}
// omit checks the key, values for emptiness or sensitivity.
func omit(key string, values []string) bool {
return len(key) == 0 || len(values) == 0 || len(values[0]) == 0 || sensitive.FindString(key) != ""
}
func CapturePanic(r *http.Request) {
if rec := recover(); rec != nil {
if err, ok := rec.(error); ok {
log.Printf("Recording err %s", err)
Error(err, r)
} else if err, ok := rec.(string); ok {
log.Printf("Recording string %s", err)
Error(errors.New(err), r)
}
panic(rec)
}
}
const source = `<?xml version="1.0" encoding="UTF-8"?>
<notice version="2.0">
<api-key>{{ .ApiKey }}</api-key>
<notifier>
<name>Airbrake Golang</name>
<version>0.0.1</version>
<url>http://airbrake.io</url>
</notifier>
<error>
<class>{{ html .Class }}</class>
<message>{{ html .ErrorName }}</message>
<backtrace>{{ range .Backtrace }}
<line method="{{ html .Function}}" file="{{ html .File}}" number="{{.Line}}"/>{{ end }}
</backtrace>
</error>{{ with .Request }}
<request>
<url>{{html .URL}}</url>
<component>{{ .Component }}</component>
<action>{{ .Action }}</action>
<params>{{ range $key, $value := .Form }}
<var key="{{ $key }}">{{ $value }}</var>{{ end }}
</params>
<cgi-data>{{ range $key, $value := .Header }}
<var key="{{ $key }}">{{ $value }}</var>{{ end }}
</cgi-data>
</request>{{ end }}
<server-environment>
<project-root>{{ html .Pwd }}</project-root>
<environment-name>{{ .Environment }}</environment-name>
<hostname>{{ html .Hostname }}</hostname>
</server-environment>
</notice>`