Merge pull request #14 from mkobetic/request_params
Populate the Environement and Parameters tab in errbit reports
diff --git a/airbrake.go b/airbrake.go
index 7019d6c..ab02d51 100644
--- a/airbrake.go
+++ b/airbrake.go
@@ -8,6 +8,7 @@
"net/http"
"os"
"reflect"
+ "regexp"
"runtime"
"sync"
"text/template"
@@ -19,6 +20,12 @@
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("???")
@@ -84,12 +91,12 @@
}()
}
-func post(params map[string]interface{}) {
+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
+ return err
}
if Verbose {
@@ -99,7 +106,7 @@
response, err := http.Post(Endpoint, "text/xml", buffer)
if err != nil {
log.Printf("Airbrake error: %s", err)
- return
+ return err
}
if Verbose {
@@ -112,6 +119,7 @@
log.Printf("Airbrake post: %s status code: %d", params["Error"], response.StatusCode)
}
+ return nil
}
func Error(e error, request *http.Request) error {
@@ -121,34 +129,7 @@
return apiKeyMissing
}
- params := map[string]interface{}{
- "Class": reflect.TypeOf(e).String(),
- "Error": e,
- "ApiKey": ApiKey,
- "ErrorName": e.Error(),
- "Environment": Environment,
- "Request": request,
- }
-
- 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)
-
- post(params)
-
- return nil
+ return post(params(e, request))
}
func Notify(e error) error {
@@ -158,6 +139,10 @@
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,
@@ -182,9 +167,51 @@
params["Backtrace"] = stacktrace(3)
- post(params)
- return nil
+ 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) {
@@ -212,22 +239,24 @@
</notifier>
<error>
<class>{{ html .Class }}</class>
- <message>{{ with .ErrorName }}{{html .}}{{ end }}</message>
- <backtrace>
- {{ range .Backtrace }}
- <line method="{{ html .Function}}" file="{{ html .File}}" number="{{.Line}}"/>
- {{ end }}
+ <message>{{ html .ErrorName }}</message>
+ <backtrace>{{ range .Backtrace }}
+ <line method="{{ html .Function}}" file="{{ html .File}}" number="{{.Line}}"/>{{ end }}
</backtrace>
- </error>
- {{ with .Request }}
+ </error>{{ with .Request }}
<request>
- <url>{{ html .URL }}</url>
- <component/>
- <action/>
- </request>
- {{ end }}
+ <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>
+ <project-root>{{ html .Pwd }}</project-root>
<environment-name>{{ .Environment }}</environment-name>
<hostname>{{ html .Hostname }}</hostname>
</server-environment>
diff --git a/airbrake_test.go b/airbrake_test.go
index 10000d1..ddd3674 100644
--- a/airbrake_test.go
+++ b/airbrake_test.go
@@ -4,6 +4,7 @@
"bytes"
"errors"
"net/http"
+ "regexp"
"testing"
"time"
)
@@ -44,12 +45,81 @@
Verbose = true
ApiKey = API_KEY
Endpoint = "https://api.airbrake.io/notifier_api/v2/notices"
-
+
err := Notify(errors.New("Test Error"))
-
+
if err != nil {
t.Error(err)
}
time.Sleep(1e9)
}
+
+// Make sure we match https://help.airbrake.io/kb/api-2/notifier-api-version-23
+func TestTemplateV2(t *testing.T) {
+ var p map[string]interface{}
+ request, _ := http.NewRequest("GET", "/query?t=xxx&q=SHOW+x+BY+y+FROM+z&key=sesame&timezone=", nil)
+ request.Header.Set("Host", "Zulu")
+ request.Header.Set("Keep_Secret", "Sesame")
+ PrettyParams = true
+ defer func() { PrettyParams = false }()
+
+ // Trigger and recover a panic, so that we have something to render.
+ func() {
+ defer func() {
+ if r := recover(); r != nil {
+ p = params(r.(error), request)
+ }
+ }()
+ panic(errors.New("Boom!"))
+ }()
+
+ // Did that work?
+ if p == nil {
+ t.Fail()
+ }
+
+ // Crude backtrace check.
+ if len(p["Backtrace"].([]Line)) < 3 {
+ t.Fail()
+ }
+
+ // It's messy to generically test rendered backtrace, drop it.
+ delete(p, "Backtrace")
+
+ // Render the params.
+ var b bytes.Buffer
+ if err := tmpl.Execute(&b, p); err != nil {
+ t.Fatalf("Template error: %s", err)
+ }
+
+ // Validate the <error> node.
+ chunk := regexp.MustCompile(`(?s)<error>.*<backtrace>`).FindString(b.String())
+ if chunk != `<error>
+ <class>*errors.errorString</class>
+ <message>Boom!</message>
+ <backtrace>` {
+ t.Fatal(chunk)
+ }
+
+ // Validate the <request> node.
+ chunk = regexp.MustCompile(`(?s)<request>.*</request>`).FindString(b.String())
+ if chunk != `<request>
+ <url>/query?t=xxx&q=SHOW+x+BY+y+FROM+z&key=sesame&timezone=</url>
+ <component></component>
+ <action></action>
+ <params>
+ <var key="q">SHOW x BY y FROM z</var>
+ <var key="t">xxx</var>
+ </params>
+ <cgi-data>
+ <var key="?q">SHOW x BY y FROM z</var>
+ <var key="?t">xxx</var>
+ <var key="Host">Zulu</var>
+ <var key="Method">GET</var>
+ <var key="Protocol">HTTP/1.1</var>
+ </cgi-data>
+ </request>` {
+ t.Fatal(chunk)
+ }
+}