1
0
mirror of synced 2025-04-18 12:24:02 +03:00

use vendored logrus (closes #306)

This commit is contained in:
Andrew Gallagher 2024-05-23 18:44:59 +01:00
parent f4d5707500
commit 8ec6c76ecb
No known key found for this signature in database
GPG Key ID: 5C1EC404D5906629
209 changed files with 2918 additions and 32661 deletions

View File

@ -56,6 +56,8 @@ lint: lint-go
lint-go:
cd $(SRCDIR) && ! go fmt $(project)/... | awk '/./ {print "ERROR: go fmt made unexpected changes:", $$0}' | grep .
cd $(SRCDIR) && go mod tidy
cd $(SRCDIR) && go mod vendor
cd $(SRCDIR) && go vet $(project)/...
test: test-go

View File

@ -42,8 +42,6 @@ This repository is structured as follows:
Abstract back-end storage model.
* `mock`
Partial in-memory storage implementation for unit testing only.
* `logrus`
Logging library, forked from `github.com/Sirupsen/logrus`.
* `metrics`
Metrics endpoint for prometheus to query.
* `openpgp`
@ -75,7 +73,7 @@ These were added with the following commands:
git subtree add --prefix=src/hockeypuck/server https://github.com/hockeypuck/server master --squash
git subtree add --prefix=src/hockeypuck/testing https://github.com/hockeypuck/testing master --squash
(Note that the mgohkp back end has since been removed)
(Note that the logrus and mgohkp trees have since been removed)
The upstream Github projects have been archived.
Any new development on Hockeypuck should be proposed here.

View File

@ -28,7 +28,7 @@ import (
"net"
"time"
log "hockeypuck/logrus"
log "github.com/sirupsen/logrus"
"github.com/pkg/errors"

View File

@ -38,7 +38,7 @@ import (
cf "hockeypuck/conflux"
"hockeypuck/conflux/recon"
log "hockeypuck/logrus"
log "github.com/sirupsen/logrus"
)
type prefixTree struct {

View File

@ -29,7 +29,7 @@ import (
"sync"
"time"
log "hockeypuck/logrus"
log "github.com/sirupsen/logrus"
"github.com/pkg/errors"
"gopkg.in/tomb.v2"

View File

@ -28,7 +28,7 @@ import (
"sync"
"time"
log "hockeypuck/logrus"
log "github.com/sirupsen/logrus"
gc "gopkg.in/check.v1"
"gopkg.in/tomb.v2"

View File

@ -6,19 +6,15 @@ require (
github.com/BurntSushi/toml v1.3.2
github.com/Masterminds/sprig v2.22.0+incompatible
github.com/ProtonMail/go-crypto v0.0.0-20230923063757-afb1ddc0824c
github.com/bugsnag/bugsnag-go v2.2.0+incompatible
github.com/carbocation/interpose v0.0.0-20161206215253-723534742ba3
github.com/getsentry/raven-go v0.2.0
github.com/hashicorp/golang-lru v1.0.2
github.com/jmcvetta/randutil v0.0.0-20150817122601-2bb1b664bcff
github.com/julienschmidt/httprouter v1.3.0
github.com/lib/pq v1.10.9
github.com/pkg/errors v0.9.1
github.com/prometheus/client_golang v1.17.0
github.com/stretchr/testify v1.7.0
github.com/stvp/go-udp-testing v0.0.0-20171104055251-c4434f09ec13
github.com/sirupsen/logrus v1.9.3
github.com/syndtr/goleveldb v0.0.0-20200815110645-5c35d600f0ca
github.com/tobi/airbrake-go v0.0.0-20151005181455-a3cdd910a3ff
golang.org/x/exp v0.0.0-20231127185646-65229373498e
gopkg.in/basen.v1 v1.0.0-20150613233243-308119dd1d4c
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c
@ -29,15 +25,11 @@ require (
github.com/Masterminds/goutils v1.1.1 // indirect
github.com/Masterminds/semver v1.5.0 // indirect
github.com/beorn7/perks v1.0.1 // indirect
github.com/bitly/go-simplejson v0.5.0 // indirect
github.com/bugsnag/panicwrap v1.3.4 // indirect
github.com/carbocation/handlers v0.0.0-20140528190747-c939c6d9ef31 // indirect
github.com/certifi/gocertifi v0.0.0-20210507211836-431795d63e8d // indirect
github.com/cespare/xxhash/v2 v2.2.0 // indirect
github.com/cloudflare/circl v1.3.6 // indirect
github.com/cmars/basen v0.0.0-20150613233007-fe3947df716e // indirect
github.com/codegangsta/inject v0.0.0-20150114235600-33e0aa1cb7c0 // indirect
github.com/davecgh/go-spew v1.1.1 // indirect
github.com/go-martini/martini v0.0.0-20170121215854-22fa46961aab // indirect
github.com/golang/snappy v0.0.4 // indirect
github.com/goods/httpbuf v0.0.0-20120503183857-5709e9bb814c // indirect
@ -46,7 +38,6 @@ require (
github.com/imdario/mergo v0.3.16 // indirect
github.com/interpose/middleware v0.0.0-20150216143757-05ed56ed52fa // indirect
github.com/justinas/nosurf v0.0.0-20190416172904-05988550ea18 // indirect
github.com/kardianos/osext v0.0.0-20190222173326-2bc1f35cddc0 // indirect
github.com/kr/pretty v0.3.1 // indirect
github.com/kr/text v0.2.0 // indirect
github.com/matttproud/golang_protobuf_extensions/v2 v2.0.0 // indirect
@ -54,18 +45,15 @@ require (
github.com/mitchellh/copystructure v1.2.0 // indirect
github.com/mitchellh/reflectwalk v1.0.2 // indirect
github.com/phyber/negroni-gzip v0.0.0-20180113114010-ef6356a5d029 // indirect
github.com/pmezard/go-difflib v1.0.0 // indirect
github.com/prometheus/client_model v0.5.0 // indirect
github.com/prometheus/common v0.45.0 // indirect
github.com/prometheus/procfs v0.12.0 // indirect
github.com/rogpeppe/go-internal v1.11.0 // indirect
github.com/sirupsen/logrus v1.9.3 // indirect
github.com/urfave/negroni v1.0.0 // indirect
golang.org/x/crypto v0.15.0 // indirect
golang.org/x/net v0.18.0 // indirect
golang.org/x/sys v0.14.0 // indirect
google.golang.org/protobuf v1.31.0 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
launchpad.net/gocheck v0.0.0-20140225173054-000000000087 // indirect
)

View File

@ -8,19 +8,11 @@ github.com/Masterminds/sprig v2.22.0+incompatible h1:z4yfnGrZ7netVz+0EDJ0Wi+5VZC
github.com/Masterminds/sprig v2.22.0+incompatible/go.mod h1:y6hNFY5UBTIWBxnzTeuNhlNS5hqE0NB0E6fgfo2Br3o=
github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM=
github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw=
github.com/bitly/go-simplejson v0.5.0 h1:6IH+V8/tVMab511d5bn4M7EwGXZf9Hj6i2xSwkNEM+Y=
github.com/bitly/go-simplejson v0.5.0/go.mod h1:cXHtHw4XUPsvGaxgjIAn8PhEWG9NfngEKAMDJEczWVA=
github.com/bugsnag/bugsnag-go v2.2.0+incompatible h1:ewHS3GgD2fBtipwmaa4Gcfhum4PSAHIwVypmjXyp/Dg=
github.com/bugsnag/bugsnag-go v2.2.0+incompatible/go.mod h1:2oa8nejYd4cQ/b0hMIopN0lCRxU0bueqREvZLWFrtK8=
github.com/bugsnag/panicwrap v1.3.4 h1:A6sXFtDGsgU/4BLf5JT0o5uYg3EeKgGx3Sfs+/uk3pU=
github.com/bugsnag/panicwrap v1.3.4/go.mod h1:D/8v3kj0zr8ZAKg1AQ6crr+5VwKN5eIywRkfhyM/+dE=
github.com/bwesterb/go-ristretto v1.2.3/go.mod h1:fUIoIZaG73pV5biE2Blr2xEzDoMj7NFEuV9ekS419A0=
github.com/carbocation/handlers v0.0.0-20140528190747-c939c6d9ef31 h1:SDMgCFII5drFRIyAaihze9ceRMpTt1FW6Q5jjpc2u4c=
github.com/carbocation/handlers v0.0.0-20140528190747-c939c6d9ef31/go.mod h1:iGISoFvZYz358DFlmHvYFlh4CgRdzPLXB2NJE48x6lY=
github.com/carbocation/interpose v0.0.0-20161206215253-723534742ba3 h1:RtCys6GUprNaPOP04Zuo65wS10PMbSPPZNvIb9xYYLE=
github.com/carbocation/interpose v0.0.0-20161206215253-723534742ba3/go.mod h1:4PGcghc3ZjA/uozANO8lCHo/gnHyMsm8iFYppSkVE/M=
github.com/certifi/gocertifi v0.0.0-20210507211836-431795d63e8d h1:S2NE3iHSwP0XV47EEXL8mWmRdEfGscSJ+7EgePNgt0s=
github.com/certifi/gocertifi v0.0.0-20210507211836-431795d63e8d/go.mod h1:sGbDF6GwGcLpkNXPUTkMRoywsNa/ol15pxFe6ERfguA=
github.com/cespare/xxhash/v2 v2.2.0 h1:DC2CZ1Ep5Y4k3ZQ899DldepgrayRUGE6BBZ/cd9Cj44=
github.com/cespare/xxhash/v2 v2.2.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
github.com/cloudflare/circl v1.3.3/go.mod h1:5XYMA4rFBvNIrhs50XuiBJ15vF2pZn4nnUKZrLbUZFA=
@ -37,8 +29,6 @@ github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSs
github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo=
github.com/fsnotify/fsnotify v1.4.9 h1:hsms1Qyu0jgnwNXIxa+/V/PDsU6CfLf6CNO8H7IWoS4=
github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ=
github.com/getsentry/raven-go v0.2.0 h1:no+xWJRb5ZI7eE8TWgIq1jLulQiIoLG0IfYxv5JYMGs=
github.com/getsentry/raven-go v0.2.0/go.mod h1:KungGk8q33+aIAZUIVWZDr2OfAEBsO49PX4NzFV5kcQ=
github.com/go-martini/martini v0.0.0-20170121215854-22fa46961aab h1:xveKWz2iaueeTaUgdetzel+U7exyigDYBryyVfV/rZk=
github.com/go-martini/martini v0.0.0-20170121215854-22fa46961aab/go.mod h1:/P9AEU963A2AYjv4d1V5eVL1CQbEJq6aCNHDDjibzu8=
github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
@ -76,8 +66,6 @@ github.com/julienschmidt/httprouter v1.3.0 h1:U0609e9tgbseu3rBINet9P48AI/D3oJs4d
github.com/julienschmidt/httprouter v1.3.0/go.mod h1:JR6WtHb+2LUe8TCKY3cZOxFyyO8IZAc4RVcycCCAKdM=
github.com/justinas/nosurf v0.0.0-20190416172904-05988550ea18 h1:ci3v0mUqcCewO25ntt7hprt2ZMNA0AWI6s6qV0rSpc0=
github.com/justinas/nosurf v0.0.0-20190416172904-05988550ea18/go.mod h1:Aucr5I5chr4OCuuVB4LTuHVrKHBuyRSo7vM2hqrcb7E=
github.com/kardianos/osext v0.0.0-20190222173326-2bc1f35cddc0 h1:iQTw/8FWTuc7uiaSepXwyf3o52HaUYcV+Tu66S3F5GA=
github.com/kardianos/osext v0.0.0-20190222173326-2bc1f35cddc0/go.mod h1:1NbS8ALrpOvjt0rHPNLyCIeMtbizbir8U//inJ+zuB8=
github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI=
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
@ -129,12 +117,8 @@ github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVs
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY=
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stvp/go-udp-testing v0.0.0-20171104055251-c4434f09ec13 h1:WYRIgR83bWdH2zjqXalfLuQYtgBG1KKxDRxinx2ygMI=
github.com/stvp/go-udp-testing v0.0.0-20171104055251-c4434f09ec13/go.mod h1:7jxmlfBCDBXRzr0eAQJ48XC1hBu1np4CS5+cHEYfwpc=
github.com/syndtr/goleveldb v0.0.0-20200815110645-5c35d600f0ca h1:sfc0HNWRJzVnj3//j4OHj0uzakLfAGH34CJ9jHYXdqM=
github.com/syndtr/goleveldb v0.0.0-20200815110645-5c35d600f0ca/go.mod h1:u2MKkTVTVJWe5D1rCvame8WqhBd88EuIwODJZ1VHCPM=
github.com/tobi/airbrake-go v0.0.0-20151005181455-a3cdd910a3ff h1:cnykn0Sc8aVuDg3OUWlWj0dRH4359BTkhoI9MBDLmqI=
github.com/tobi/airbrake-go v0.0.0-20151005181455-a3cdd910a3ff/go.mod h1:oDQe1wQu4UeC9B+tlVxYGgiN42g06QsyC1hMxUtDT9I=
github.com/urfave/negroni v1.0.0 h1:kIimOitoypq34K7TG7DUaJ9kq/N4Ofuwi1sjz0KipXc=
github.com/urfave/negroni v1.0.0/go.mod h1:Meg73S6kFm/4PpbYdq35yYWoCZ9mS/YSx+lKnmiohz4=
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=

View File

@ -40,7 +40,7 @@ import (
"hockeypuck/hkp/jsonhkp"
"hockeypuck/hkp/sks"
"hockeypuck/hkp/storage"
log "hockeypuck/logrus"
log "github.com/sirupsen/logrus"
"hockeypuck/openpgp"
)

View File

@ -27,7 +27,7 @@ import (
"github.com/pkg/errors"
"gopkg.in/tomb.v2"
log "hockeypuck/logrus"
log "github.com/sirupsen/logrus"
"hockeypuck/openpgp"
"hockeypuck/hkp/storage"

View File

@ -36,7 +36,7 @@ import (
"hockeypuck/conflux/recon"
"hockeypuck/conflux/recon/leveldb"
"hockeypuck/hkp/storage"
log "hockeypuck/logrus"
log "github.com/sirupsen/logrus"
"hockeypuck/openpgp"
)

View File

@ -1 +0,0 @@
logrus

View File

@ -1,8 +0,0 @@
language: go
go:
- 1.2
- 1.3
- 1.4
- tip
install:
- go get -t ./...

View File

@ -1,7 +0,0 @@
# 0.7.3
formatter/\*: allow configuration of timestamp layout
# 0.7.2
formatter/text: Add configuration option for time format (#158)

View File

@ -1,349 +0,0 @@
# Logrus <img src="http://i.imgur.com/hTeVwmJ.png" width="40" height="40" alt=":walrus:" class="emoji" title=":walrus:"/>&nbsp;[![Build Status](https://travis-ci.org/Sirupsen/logrus.svg?branch=master)](https://travis-ci.org/Sirupsen/logrus)&nbsp;[![godoc reference](https://godoc.org/github.com/Sirupsen/logrus?status.png)][godoc]
Logrus is a structured logger for Go (golang), completely API compatible with
the standard library logger. [Godoc][godoc]. **Please note the Logrus API is not
yet stable (pre 1.0). Logrus itself is completely stable and has been used in
many large deployments. The core API is unlikely to change much but please
version control your Logrus to make sure you aren't fetching latest `master` on
every build.**
Nicely color-coded in development (when a TTY is attached, otherwise just
plain text):
![Colored](http://i.imgur.com/PY7qMwd.png)
With `log.Formatter = new(logrus.JSONFormatter)`, for easy parsing by logstash
or Splunk:
```json
{"animal":"walrus","level":"info","msg":"A group of walrus emerges from the
ocean","size":10,"time":"2014-03-10 19:57:38.562264131 -0400 EDT"}
{"level":"warning","msg":"The group's number increased tremendously!",
"number":122,"omg":true,"time":"2014-03-10 19:57:38.562471297 -0400 EDT"}
{"animal":"walrus","level":"info","msg":"A giant walrus appears!",
"size":10,"time":"2014-03-10 19:57:38.562500591 -0400 EDT"}
{"animal":"walrus","level":"info","msg":"Tremendously sized cow enters the ocean.",
"size":9,"time":"2014-03-10 19:57:38.562527896 -0400 EDT"}
{"level":"fatal","msg":"The ice breaks!","number":100,"omg":true,
"time":"2014-03-10 19:57:38.562543128 -0400 EDT"}
```
With the default `log.Formatter = new(logrus.TextFormatter)` when a TTY is not
attached, the output is compatible with the
[logfmt](http://godoc.org/github.com/kr/logfmt) format:
```text
time="2015-03-26T01:27:38-04:00" level=debug msg="Started observing beach" animal=walrus number=8
time="2015-03-26T01:27:38-04:00" level=info msg="A group of walrus emerges from the ocean" animal=walrus size=10
time="2015-03-26T01:27:38-04:00" level=warning msg="The group's number increased tremendously!" number=122 omg=true
time="2015-03-26T01:27:38-04:00" level=debug msg="Temperature changes" temperature=-4
time="2015-03-26T01:27:38-04:00" level=panic msg="It's over 9000!" animal=orca size=9009
time="2015-03-26T01:27:38-04:00" level=fatal msg="The ice breaks!" err=&{0x2082280c0 map[animal:orca size:9009] 2015-03-26 01:27:38.441574009 -0400 EDT panic It's over 9000!} number=100 omg=true
exit status 1
```
#### Example
The simplest way to use Logrus is simply the package-level exported logger:
```go
package main
import (
log "github.com/Sirupsen/logrus"
)
func main() {
log.WithFields(log.Fields{
"animal": "walrus",
}).Info("A walrus appears")
}
```
Note that it's completely api-compatible with the stdlib logger, so you can
replace your `log` imports everywhere with `log "github.com/Sirupsen/logrus"`
and you'll now have the flexibility of Logrus. You can customize it all you
want:
```go
package main
import (
"os"
log "github.com/Sirupsen/logrus"
"github.com/Sirupsen/logrus/hooks/airbrake"
)
func init() {
// Log as JSON instead of the default ASCII formatter.
log.SetFormatter(&log.JSONFormatter{})
// Use the Airbrake hook to report errors that have Error severity or above to
// an exception tracker. You can create custom hooks, see the Hooks section.
log.AddHook(airbrake.NewHook("https://example.com", "xyz", "development"))
// Output to stderr instead of stdout, could also be a file.
log.SetOutput(os.Stderr)
// Only log the warning severity or above.
log.SetLevel(log.WarnLevel)
}
func main() {
log.WithFields(log.Fields{
"animal": "walrus",
"size": 10,
}).Info("A group of walrus emerges from the ocean")
log.WithFields(log.Fields{
"omg": true,
"number": 122,
}).Warn("The group's number increased tremendously!")
log.WithFields(log.Fields{
"omg": true,
"number": 100,
}).Fatal("The ice breaks!")
// A common pattern is to re-use fields between logging statements by re-using
// the logrus.Entry returned from WithFields()
contextLogger := log.WithFields(log.Fields{
"common": "this is a common field",
"other": "I also should be logged always",
})
contextLogger.Info("I'll be logged with common and other field")
contextLogger.Info("Me too")
}
```
For more advanced usage such as logging to multiple locations from the same
application, you can also create an instance of the `logrus` Logger:
```go
package main
import (
"github.com/Sirupsen/logrus"
)
// Create a new instance of the logger. You can have any number of instances.
var log = logrus.New()
func main() {
// The API for setting attributes is a little different than the package level
// exported logger. See Godoc.
log.Out = os.Stderr
log.WithFields(logrus.Fields{
"animal": "walrus",
"size": 10,
}).Info("A group of walrus emerges from the ocean")
}
```
#### Fields
Logrus encourages careful, structured logging though logging fields instead of
long, unparseable error messages. For example, instead of: `log.Fatalf("Failed
to send event %s to topic %s with key %d")`, you should log the much more
discoverable:
```go
log.WithFields(log.Fields{
"event": event,
"topic": topic,
"key": key,
}).Fatal("Failed to send event")
```
We've found this API forces you to think about logging in a way that produces
much more useful logging messages. We've been in countless situations where just
a single added field to a log statement that was already there would've saved us
hours. The `WithFields` call is optional.
In general, with Logrus using any of the `printf`-family functions should be
seen as a hint you should add a field, however, you can still use the
`printf`-family functions with Logrus.
#### Hooks
You can add hooks for logging levels. For example to send errors to an exception
tracking service on `Error`, `Fatal` and `Panic`, info to StatsD or log to
multiple places simultaneously, e.g. syslog.
Logrus comes with [built-in hooks](hooks/). Add those, or your custom hook, in
`init`:
```go
import (
log "github.com/Sirupsen/logrus"
"github.com/Sirupsen/logrus/hooks/airbrake"
"github.com/Sirupsen/logrus/hooks/syslog"
"log/syslog"
)
func init() {
log.AddHook(airbrake.NewHook("https://example.com", "xyz", "development"))
hook, err := logrus_syslog.NewSyslogHook("udp", "localhost:514", syslog.LOG_INFO, "")
if err != nil {
log.Error("Unable to connect to local syslog daemon")
} else {
log.AddHook(hook)
}
}
```
| Hook | Description |
| ----- | ----------- |
| [Airbrake](https://github.com/Sirupsen/logrus/blob/master/hooks/airbrake/airbrake.go) | Send errors to an exception tracking service compatible with the Airbrake API. Uses [`airbrake-go`](https://github.com/tobi/airbrake-go) behind the scenes. |
| [Papertrail](https://github.com/Sirupsen/logrus/blob/master/hooks/papertrail/papertrail.go) | Send errors to the Papertrail hosted logging service via UDP. |
| [Syslog](https://github.com/Sirupsen/logrus/blob/master/hooks/syslog/syslog.go) | Send errors to remote syslog server. Uses standard library `log/syslog` behind the scenes. |
| [BugSnag](https://github.com/Sirupsen/logrus/blob/master/hooks/bugsnag/bugsnag.go) | Send errors to the Bugsnag exception tracking service. |
| [Hiprus](https://github.com/nubo/hiprus) | Send errors to a channel in hipchat. |
| [Logrusly](https://github.com/sebest/logrusly) | Send logs to [Loggly](https://www.loggly.com/) |
| [Slackrus](https://github.com/johntdyer/slackrus) | Hook for Slack chat. |
| [Journalhook](https://github.com/wercker/journalhook) | Hook for logging to `systemd-journald` |
| [Graylog](https://github.com/gemnasium/logrus-hooks/tree/master/graylog) | Hook for logging to [Graylog](http://graylog2.org/) |
#### Level logging
Logrus has six logging levels: Debug, Info, Warning, Error, Fatal and Panic.
```go
log.Debug("Useful debugging information.")
log.Info("Something noteworthy happened!")
log.Warn("You should probably take a look at this.")
log.Error("Something failed but I'm not quitting.")
// Calls os.Exit(1) after logging
log.Fatal("Bye.")
// Calls panic() after logging
log.Panic("I'm bailing.")
```
You can set the logging level on a `Logger`, then it will only log entries with
that severity or anything above it:
```go
// Will log anything that is info or above (warn, error, fatal, panic). Default.
log.SetLevel(log.InfoLevel)
```
It may be useful to set `log.Level = logrus.DebugLevel` in a debug or verbose
environment if your application has that.
#### Entries
Besides the fields added with `WithField` or `WithFields` some fields are
automatically added to all logging events:
1. `time`. The timestamp when the entry was created.
2. `msg`. The logging message passed to `{Info,Warn,Error,Fatal,Panic}` after
the `AddFields` call. E.g. `Failed to send event.`
3. `level`. The logging level. E.g. `info`.
#### Environments
Logrus has no notion of environment.
If you wish for hooks and formatters to only be used in specific environments,
you should handle that yourself. For example, if your application has a global
variable `Environment`, which is a string representation of the environment you
could do:
```go
import (
log "github.com/Sirupsen/logrus"
)
init() {
// do something here to set environment depending on an environment variable
// or command-line flag
if Environment == "production" {
log.SetFormatter(logrus.JSONFormatter)
} else {
// The TextFormatter is default, you don't actually have to do this.
log.SetFormatter(logrus.TextFormatter)
}
}
```
This configuration is how `logrus` was intended to be used, but JSON in
production is mostly only useful if you do log aggregation with tools like
Splunk or Logstash.
#### Formatters
The built-in logging formatters are:
* `logrus.TextFormatter`. Logs the event in colors if stdout is a tty, otherwise
without colors.
* *Note:* to force colored output when there is no TTY, set the `ForceColors`
field to `true`. To force no colored output even if there is a TTY set the
`DisableColors` field to `true`
* `logrus.JSONFormatter`. Logs fields as JSON.
* `logrus_logstash.LogstashFormatter`. Logs fields as Logstash Events (http://logstash.net).
```go
logrus.SetFormatter(&logrus_logstash.LogstashFormatter{Type: “application_name"})
```
Third party logging formatters:
* [`zalgo`](https://github.com/aybabtme/logzalgo): invoking the P͉̫o̳̼̊w̖͈̰͎e̬͔̭͂r͚̼̹̲ ̫͓͉̳͈ō̠͕͖̚f̝͍̠ ͕̲̞͖͑Z̖̫̤̫ͪa͉̬͈̗l͖͎g̳̥o̰̥̅!̣͔̲̻͊̄ ̙̘̦̹̦.
You can define your formatter by implementing the `Formatter` interface,
requiring a `Format` method. `Format` takes an `*Entry`. `entry.Data` is a
`Fields` type (`map[string]interface{}`) with all your fields as well as the
default ones (see Entries section above):
```go
type MyJSONFormatter struct {
}
log.SetFormatter(new(MyJSONFormatter))
func (f *JSONFormatter) Format(entry *Entry) ([]byte, error) {
// Note this doesn't include Time, Level and Message which are available on
// the Entry. Consult `godoc` on information about those fields or read the
// source of the official loggers.
serialized, err := json.Marshal(entry.Data)
if err != nil {
return nil, fmt.Errorf("Failed to marshal fields to JSON, %v", err)
}
return append(serialized, '\n'), nil
}
```
#### Logger as an `io.Writer`
Logrus can be transormed into an `io.Writer`. That writer is the end of an `io.Pipe` and it is your responsibility to close it.
```go
w := logger.Writer()
defer w.Close()
srv := http.Server{
// create a stdlib log.Logger that writes to
// logrus.Logger.
ErrorLog: log.New(w, "", 0),
}
```
Each line written to that writer will be printed the usual way, using formatters
and hooks. The level for those entries is `info`.
#### Rotation
Log rotation is not provided with Logrus. Log rotation should be done by an
external program (like `logrotate(8)`) that can compress and delete old log
entries. It should not be a feature of the application-level logger.
[godoc]: https://godoc.org/github.com/Sirupsen/logrus

View File

@ -1,252 +0,0 @@
package logrus
import (
"bytes"
"fmt"
"io"
"os"
"time"
)
// An entry is the final or intermediate Logrus logging entry. It contains all
// the fields passed with WithField{,s}. It's finally logged when Debug, Info,
// Warn, Error, Fatal or Panic is called on it. These objects can be reused and
// passed around as much as you wish to avoid field duplication.
type Entry struct {
Logger *Logger
// Contains all the fields set by the user.
Data Fields
// Time at which the log entry was created
Time time.Time
// Level the log entry was logged at: Debug, Info, Warn, Error, Fatal or Panic
Level Level
// Message passed to Debug, Info, Warn, Error, Fatal or Panic
Message string
}
func NewEntry(logger *Logger) *Entry {
return &Entry{
Logger: logger,
// Default is three fields, give a little extra room
Data: make(Fields, 5),
}
}
// Returns a reader for the entry, which is a proxy to the formatter.
func (entry *Entry) Reader() (*bytes.Buffer, error) {
serialized, err := entry.Logger.Formatter.Format(entry)
return bytes.NewBuffer(serialized), err
}
// Returns the string representation from the reader and ultimately the
// formatter.
func (entry *Entry) String() (string, error) {
reader, err := entry.Reader()
if err != nil {
return "", err
}
return reader.String(), err
}
// Add a single field to the Entry.
func (entry *Entry) WithField(key string, value interface{}) *Entry {
return entry.WithFields(Fields{key: value})
}
// Add a map of fields to the Entry.
func (entry *Entry) WithFields(fields Fields) *Entry {
data := Fields{}
for k, v := range entry.Data {
data[k] = v
}
for k, v := range fields {
data[k] = v
}
return &Entry{Logger: entry.Logger, Data: data}
}
func (entry *Entry) log(level Level, msg string) {
entry.Time = time.Now()
entry.Level = level
entry.Message = msg
if err := entry.Logger.Hooks.Fire(level, entry); err != nil {
entry.Logger.mu.Lock()
fmt.Fprintf(os.Stderr, "Failed to fire hook: %v\n", err)
entry.Logger.mu.Unlock()
}
reader, err := entry.Reader()
if err != nil {
entry.Logger.mu.Lock()
fmt.Fprintf(os.Stderr, "Failed to obtain reader, %v\n", err)
entry.Logger.mu.Unlock()
}
entry.Logger.mu.Lock()
defer entry.Logger.mu.Unlock()
_, err = io.Copy(entry.Logger.Out, reader)
if err != nil {
fmt.Fprintf(os.Stderr, "Failed to write to log, %v\n", err)
}
// To avoid Entry#log() returning a value that only would make sense for
// panic() to use in Entry#Panic(), we avoid the allocation by checking
// directly here.
if level <= PanicLevel {
panic(entry)
}
}
func (entry *Entry) Debug(args ...interface{}) {
if entry.Logger.Level >= DebugLevel {
entry.log(DebugLevel, fmt.Sprint(args...))
}
}
func (entry *Entry) Print(args ...interface{}) {
entry.Info(args...)
}
func (entry *Entry) Info(args ...interface{}) {
if entry.Logger.Level >= InfoLevel {
entry.log(InfoLevel, fmt.Sprint(args...))
}
}
func (entry *Entry) Warn(args ...interface{}) {
if entry.Logger.Level >= WarnLevel {
entry.log(WarnLevel, fmt.Sprint(args...))
}
}
func (entry *Entry) Warning(args ...interface{}) {
entry.Warn(args...)
}
func (entry *Entry) Error(args ...interface{}) {
if entry.Logger.Level >= ErrorLevel {
entry.log(ErrorLevel, fmt.Sprint(args...))
}
}
func (entry *Entry) Fatal(args ...interface{}) {
if entry.Logger.Level >= FatalLevel {
entry.log(FatalLevel, fmt.Sprint(args...))
}
os.Exit(1)
}
func (entry *Entry) Panic(args ...interface{}) {
if entry.Logger.Level >= PanicLevel {
entry.log(PanicLevel, fmt.Sprint(args...))
}
panic(fmt.Sprint(args...))
}
// Entry Printf family functions
func (entry *Entry) Debugf(format string, args ...interface{}) {
if entry.Logger.Level >= DebugLevel {
entry.Debug(fmt.Sprintf(format, args...))
}
}
func (entry *Entry) Infof(format string, args ...interface{}) {
if entry.Logger.Level >= InfoLevel {
entry.Info(fmt.Sprintf(format, args...))
}
}
func (entry *Entry) Printf(format string, args ...interface{}) {
entry.Infof(format, args...)
}
func (entry *Entry) Warnf(format string, args ...interface{}) {
if entry.Logger.Level >= WarnLevel {
entry.Warn(fmt.Sprintf(format, args...))
}
}
func (entry *Entry) Warningf(format string, args ...interface{}) {
entry.Warnf(format, args...)
}
func (entry *Entry) Errorf(format string, args ...interface{}) {
if entry.Logger.Level >= ErrorLevel {
entry.Error(fmt.Sprintf(format, args...))
}
}
func (entry *Entry) Fatalf(format string, args ...interface{}) {
if entry.Logger.Level >= FatalLevel {
entry.Fatal(fmt.Sprintf(format, args...))
}
}
func (entry *Entry) Panicf(format string, args ...interface{}) {
if entry.Logger.Level >= PanicLevel {
entry.Panic(fmt.Sprintf(format, args...))
}
}
// Entry Println family functions
func (entry *Entry) Debugln(args ...interface{}) {
if entry.Logger.Level >= DebugLevel {
entry.Debug(entry.sprintlnn(args...))
}
}
func (entry *Entry) Infoln(args ...interface{}) {
if entry.Logger.Level >= InfoLevel {
entry.Info(entry.sprintlnn(args...))
}
}
func (entry *Entry) Println(args ...interface{}) {
entry.Infoln(args...)
}
func (entry *Entry) Warnln(args ...interface{}) {
if entry.Logger.Level >= WarnLevel {
entry.Warn(entry.sprintlnn(args...))
}
}
func (entry *Entry) Warningln(args ...interface{}) {
entry.Warnln(args...)
}
func (entry *Entry) Errorln(args ...interface{}) {
if entry.Logger.Level >= ErrorLevel {
entry.Error(entry.sprintlnn(args...))
}
}
func (entry *Entry) Fatalln(args ...interface{}) {
if entry.Logger.Level >= FatalLevel {
entry.Fatal(entry.sprintlnn(args...))
}
}
func (entry *Entry) Panicln(args ...interface{}) {
if entry.Logger.Level >= PanicLevel {
entry.Panic(entry.sprintlnn(args...))
}
}
// Sprintlnn => Sprint no newline. This is to get the behavior of how
// fmt.Sprintln where spaces are always added between operands, regardless of
// their type. Instead of vendoring the Sprintln implementation to spare a
// string allocation, we do the simplest thing.
func (entry *Entry) sprintlnn(args ...interface{}) string {
msg := fmt.Sprintln(args...)
return msg[:len(msg)-1]
}

View File

@ -1,53 +0,0 @@
package logrus
import (
"bytes"
"fmt"
"testing"
"github.com/stretchr/testify/assert"
)
func TestEntryPanicln(t *testing.T) {
errBoom := fmt.Errorf("boom time")
defer func() {
p := recover()
assert.NotNil(t, p)
switch pVal := p.(type) {
case *Entry:
assert.Equal(t, "kaboom", pVal.Message)
assert.Equal(t, errBoom, pVal.Data["err"])
default:
t.Fatalf("want type *Entry, got %T: %#v", pVal, pVal)
}
}()
logger := New()
logger.Out = &bytes.Buffer{}
entry := NewEntry(logger)
entry.WithField("err", errBoom).Panicln("kaboom")
}
func TestEntryPanicf(t *testing.T) {
errBoom := fmt.Errorf("boom again")
defer func() {
p := recover()
assert.NotNil(t, p)
switch pVal := p.(type) {
case *Entry:
assert.Equal(t, "kaboom true", pVal.Message)
assert.Equal(t, errBoom, pVal.Data["err"])
default:
t.Fatalf("want type *Entry, got %T: %#v", pVal, pVal)
}
}()
logger := New()
logger.Out = &bytes.Buffer{}
entry := NewEntry(logger)
entry.WithField("err", errBoom).Panicf("kaboom %v", true)
}

View File

@ -1,50 +0,0 @@
package main
import (
"hockeypuck/logrus"
)
var log = logrus.New()
func init() {
log.Formatter = new(logrus.JSONFormatter)
log.Formatter = new(logrus.TextFormatter) // default
log.Level = logrus.DebugLevel
}
func main() {
defer func() {
err := recover()
if err != nil {
log.WithFields(logrus.Fields{
"omg": true,
"err": err,
"number": 100,
}).Fatal("The ice breaks!")
}
}()
log.WithFields(logrus.Fields{
"animal": "walrus",
"number": 8,
}).Debug("Started observing beach")
log.WithFields(logrus.Fields{
"animal": "walrus",
"size": 10,
}).Info("A group of walrus emerges from the ocean")
log.WithFields(logrus.Fields{
"omg": true,
"number": 122,
}).Warn("The group's number increased tremendously!")
log.WithFields(logrus.Fields{
"temperature": -4,
}).Debug("Temperature changes")
log.WithFields(logrus.Fields{
"animal": "orca",
"size": 9009,
}).Panic("It's over 9000!")
}

View File

@ -1,30 +0,0 @@
package main
import (
"hockeypuck/logrus"
"hockeypuck/logrus/hooks/airbrake"
)
var log = logrus.New()
func init() {
log.Formatter = new(logrus.TextFormatter) // default
log.Hooks.Add(airbrake.NewHook("https://example.com", "xyz", "development"))
}
func main() {
log.WithFields(logrus.Fields{
"animal": "walrus",
"size": 10,
}).Info("A group of walrus emerges from the ocean")
log.WithFields(logrus.Fields{
"omg": true,
"number": 122,
}).Warn("The group's number increased tremendously!")
log.WithFields(logrus.Fields{
"omg": true,
"number": 100,
}).Fatal("The ice breaks!")
}

View File

@ -1,48 +0,0 @@
package logrus
import "time"
const DefaultTimestampFormat = time.RFC3339
// The Formatter interface is used to implement a custom Formatter. It takes an
// `Entry`. It exposes all the fields, including the default ones:
//
// * `entry.Data["msg"]`. The message passed from Info, Warn, Error ..
// * `entry.Data["time"]`. The timestamp.
// * `entry.Data["level"]. The level the entry was logged at.
//
// Any additional fields added with `WithField` or `WithFields` are also in
// `entry.Data`. Format is expected to return an array of bytes which are then
// logged to `logger.Out`.
type Formatter interface {
Format(*Entry) ([]byte, error)
}
// This is to not silently overwrite `time`, `msg` and `level` fields when
// dumping it. If this code wasn't there doing:
//
// logrus.WithField("level", 1).Info("hello")
//
// Would just silently drop the user provided level. Instead with this code
// it'll logged as:
//
// {"level": "info", "fields.level": 1, "msg": "hello", "time": "..."}
//
// It's not exported because it's still using Data in an opinionated way. It's to
// avoid code duplication between the two default formatters.
func prefixFieldClashes(data Fields) {
_, ok := data["time"]
if ok {
data["fields.time"] = data["time"]
}
_, ok = data["msg"]
if ok {
data["fields.msg"] = data["msg"]
}
_, ok = data["level"]
if ok {
data["fields.level"] = data["level"]
}
}

View File

@ -1,88 +0,0 @@
package logrus
import (
"testing"
"time"
)
// smallFields is a small size data set for benchmarking
var smallFields = Fields{
"foo": "bar",
"baz": "qux",
"one": "two",
"three": "four",
}
// largeFields is a large size data set for benchmarking
var largeFields = Fields{
"foo": "bar",
"baz": "qux",
"one": "two",
"three": "four",
"five": "six",
"seven": "eight",
"nine": "ten",
"eleven": "twelve",
"thirteen": "fourteen",
"fifteen": "sixteen",
"seventeen": "eighteen",
"nineteen": "twenty",
"a": "b",
"c": "d",
"e": "f",
"g": "h",
"i": "j",
"k": "l",
"m": "n",
"o": "p",
"q": "r",
"s": "t",
"u": "v",
"w": "x",
"y": "z",
"this": "will",
"make": "thirty",
"entries": "yeah",
}
func BenchmarkSmallTextFormatter(b *testing.B) {
doBenchmark(b, &TextFormatter{DisableColors: true}, smallFields)
}
func BenchmarkLargeTextFormatter(b *testing.B) {
doBenchmark(b, &TextFormatter{DisableColors: true}, largeFields)
}
func BenchmarkSmallColoredTextFormatter(b *testing.B) {
doBenchmark(b, &TextFormatter{ForceColors: true}, smallFields)
}
func BenchmarkLargeColoredTextFormatter(b *testing.B) {
doBenchmark(b, &TextFormatter{ForceColors: true}, largeFields)
}
func BenchmarkSmallJSONFormatter(b *testing.B) {
doBenchmark(b, &JSONFormatter{}, smallFields)
}
func BenchmarkLargeJSONFormatter(b *testing.B) {
doBenchmark(b, &JSONFormatter{}, largeFields)
}
func doBenchmark(b *testing.B, formatter Formatter, fields Fields) {
entry := &Entry{
Time: time.Time{},
Level: InfoLevel,
Message: "message",
Data: fields,
}
var d []byte
var err error
for i := 0; i < b.N; i++ {
d, err = formatter.Format(entry)
if err != nil {
b.Fatal(err)
}
b.SetBytes(int64(len(d)))
}
}

View File

@ -1,56 +0,0 @@
package logstash
import (
"encoding/json"
"fmt"
"hockeypuck/logrus"
)
// Formatter generates json in logstash format.
// Logstash site: http://logstash.net/
type LogstashFormatter struct {
Type string // if not empty use for logstash type field.
// TimestampFormat sets the format used for timestamps.
TimestampFormat string
}
func (f *LogstashFormatter) Format(entry *logrus.Entry) ([]byte, error) {
entry.Data["@version"] = 1
if f.TimestampFormat == "" {
f.TimestampFormat = logrus.DefaultTimestampFormat
}
entry.Data["@timestamp"] = entry.Time.Format(f.TimestampFormat)
// set message field
v, ok := entry.Data["message"]
if ok {
entry.Data["fields.message"] = v
}
entry.Data["message"] = entry.Message
// set level field
v, ok = entry.Data["level"]
if ok {
entry.Data["fields.level"] = v
}
entry.Data["level"] = entry.Level.String()
// set type field
if f.Type != "" {
v, ok = entry.Data["type"]
if ok {
entry.Data["fields.type"] = v
}
entry.Data["type"] = f.Type
}
serialized, err := json.Marshal(entry.Data)
if err != nil {
return nil, fmt.Errorf("Failed to marshal fields to JSON, %v", err)
}
return append(serialized, '\n'), nil
}

View File

@ -1,52 +0,0 @@
package logstash
import (
"bytes"
"encoding/json"
"github.com/stretchr/testify/assert"
"hockeypuck/logrus"
"testing"
)
func TestLogstashFormatter(t *testing.T) {
assert := assert.New(t)
lf := LogstashFormatter{Type: "abc"}
fields := logrus.Fields{
"message": "def",
"level": "ijk",
"type": "lmn",
"one": 1,
"pi": 3.14,
"bool": true,
}
entry := logrus.WithFields(fields)
entry.Message = "msg"
entry.Level = logrus.InfoLevel
b, _ := lf.Format(entry)
var data map[string]interface{}
dec := json.NewDecoder(bytes.NewReader(b))
dec.UseNumber()
dec.Decode(&data)
// base fields
assert.Equal(json.Number("1"), data["@version"])
assert.NotEmpty(data["@timestamp"])
assert.Equal("abc", data["type"])
assert.Equal("msg", data["message"])
assert.Equal("info", data["level"])
// substituted fields
assert.Equal("def", data["fields.message"])
assert.Equal("ijk", data["fields.level"])
assert.Equal("lmn", data["fields.type"])
// formats
assert.Equal(json.Number("1"), data["one"])
assert.Equal(json.Number("3.14"), data["pi"])
assert.Equal(true, data["bool"])
}

View File

@ -1,122 +0,0 @@
package logrus
import (
"testing"
"github.com/stretchr/testify/assert"
)
type TestHook struct {
Fired bool
}
func (hook *TestHook) Fire(entry *Entry) error {
hook.Fired = true
return nil
}
func (hook *TestHook) Levels() []Level {
return []Level{
DebugLevel,
InfoLevel,
WarnLevel,
ErrorLevel,
FatalLevel,
PanicLevel,
}
}
func TestHookFires(t *testing.T) {
hook := new(TestHook)
LogAndAssertJSON(t, func(log *Logger) {
log.Hooks.Add(hook)
assert.Equal(t, hook.Fired, false)
log.Print("test")
}, func(fields Fields) {
assert.Equal(t, hook.Fired, true)
})
}
type ModifyHook struct {
}
func (hook *ModifyHook) Fire(entry *Entry) error {
entry.Data["wow"] = "whale"
return nil
}
func (hook *ModifyHook) Levels() []Level {
return []Level{
DebugLevel,
InfoLevel,
WarnLevel,
ErrorLevel,
FatalLevel,
PanicLevel,
}
}
func TestHookCanModifyEntry(t *testing.T) {
hook := new(ModifyHook)
LogAndAssertJSON(t, func(log *Logger) {
log.Hooks.Add(hook)
log.WithField("wow", "elephant").Print("test")
}, func(fields Fields) {
assert.Equal(t, fields["wow"], "whale")
})
}
func TestCanFireMultipleHooks(t *testing.T) {
hook1 := new(ModifyHook)
hook2 := new(TestHook)
LogAndAssertJSON(t, func(log *Logger) {
log.Hooks.Add(hook1)
log.Hooks.Add(hook2)
log.WithField("wow", "elephant").Print("test")
}, func(fields Fields) {
assert.Equal(t, fields["wow"], "whale")
assert.Equal(t, hook2.Fired, true)
})
}
type ErrorHook struct {
Fired bool
}
func (hook *ErrorHook) Fire(entry *Entry) error {
hook.Fired = true
return nil
}
func (hook *ErrorHook) Levels() []Level {
return []Level{
ErrorLevel,
}
}
func TestErrorHookShouldntFireOnInfo(t *testing.T) {
hook := new(ErrorHook)
LogAndAssertJSON(t, func(log *Logger) {
log.Hooks.Add(hook)
log.Info("test")
}, func(fields Fields) {
assert.Equal(t, hook.Fired, false)
})
}
func TestErrorHookShouldFireOnError(t *testing.T) {
hook := new(ErrorHook)
LogAndAssertJSON(t, func(log *Logger) {
log.Hooks.Add(hook)
log.Error("test")
}, func(fields Fields) {
assert.Equal(t, hook.Fired, true)
})
}

View File

@ -1,54 +0,0 @@
package airbrake
import (
"errors"
"fmt"
"github.com/tobi/airbrake-go"
"hockeypuck/logrus"
)
// AirbrakeHook to send exceptions to an exception-tracking service compatible
// with the Airbrake API.
type airbrakeHook struct {
APIKey string
Endpoint string
Environment string
}
func NewHook(endpoint, apiKey, env string) *airbrakeHook {
return &airbrakeHook{
APIKey: apiKey,
Endpoint: endpoint,
Environment: env,
}
}
func (hook *airbrakeHook) Fire(entry *logrus.Entry) error {
airbrake.ApiKey = hook.APIKey
airbrake.Endpoint = hook.Endpoint
airbrake.Environment = hook.Environment
var notifyErr error
err, ok := entry.Data["error"].(error)
if ok {
notifyErr = err
} else {
notifyErr = errors.New(entry.Message)
}
airErr := airbrake.Notify(notifyErr)
if airErr != nil {
return fmt.Errorf("Failed to send error to Airbrake: %s", airErr)
}
return nil
}
func (hook *airbrakeHook) Levels() []logrus.Level {
return []logrus.Level{
logrus.ErrorLevel,
logrus.FatalLevel,
logrus.PanicLevel,
}
}

View File

@ -1,133 +0,0 @@
package airbrake
import (
"encoding/xml"
"net/http"
"net/http/httptest"
"testing"
"time"
"hockeypuck/logrus"
)
type notice struct {
Error NoticeError `xml:"error"`
}
type NoticeError struct {
Class string `xml:"class"`
Message string `xml:"message"`
}
type customErr struct {
msg string
}
func (e *customErr) Error() string {
return e.msg
}
const (
testAPIKey = "abcxyz"
testEnv = "development"
expectedClass = "*airbrake.customErr"
expectedMsg = "foo"
unintendedMsg = "Airbrake will not see this string"
)
var (
noticeError = make(chan NoticeError, 1)
)
// TestLogEntryMessageReceived checks if invoking Logrus' log.Error
// method causes an XML payload containing the log entry message is received
// by a HTTP server emulating an Airbrake-compatible endpoint.
func TestLogEntryMessageReceived(t *testing.T) {
log := logrus.New()
ts := startAirbrakeServer(t)
defer ts.Close()
hook := NewHook(ts.URL, testAPIKey, "production")
log.Hooks.Add(hook)
log.Error(expectedMsg)
select {
case received := <-noticeError:
if received.Message != expectedMsg {
t.Errorf("Unexpected message received: %s", received.Message)
}
case <-time.After(time.Second):
t.Error("Timed out; no notice received by Airbrake API")
}
}
// TestLogEntryMessageReceived confirms that, when passing an error type using
// logrus.Fields, a HTTP server emulating an Airbrake endpoint receives the
// error message returned by the Error() method on the error interface
// rather than the logrus.Entry.Message string.
func TestLogEntryWithErrorReceived(t *testing.T) {
log := logrus.New()
ts := startAirbrakeServer(t)
defer ts.Close()
hook := NewHook(ts.URL, testAPIKey, "production")
log.Hooks.Add(hook)
log.WithFields(logrus.Fields{
"error": &customErr{expectedMsg},
}).Error(unintendedMsg)
select {
case received := <-noticeError:
if received.Message != expectedMsg {
t.Errorf("Unexpected message received: %s", received.Message)
}
if received.Class != expectedClass {
t.Errorf("Unexpected error class: %s", received.Class)
}
case <-time.After(time.Second):
t.Error("Timed out; no notice received by Airbrake API")
}
}
// TestLogEntryWithNonErrorTypeNotReceived confirms that, when passing a
// non-error type using logrus.Fields, a HTTP server emulating an Airbrake
// endpoint receives the logrus.Entry.Message string.
//
// Only error types are supported when setting the 'error' field using
// logrus.WithFields().
func TestLogEntryWithNonErrorTypeNotReceived(t *testing.T) {
log := logrus.New()
ts := startAirbrakeServer(t)
defer ts.Close()
hook := NewHook(ts.URL, testAPIKey, "production")
log.Hooks.Add(hook)
log.WithFields(logrus.Fields{
"error": expectedMsg,
}).Error(unintendedMsg)
select {
case received := <-noticeError:
if received.Message != unintendedMsg {
t.Errorf("Unexpected message received: %s", received.Message)
}
case <-time.After(time.Second):
t.Error("Timed out; no notice received by Airbrake API")
}
}
func startAirbrakeServer(t *testing.T) *httptest.Server {
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
var notice notice
if err := xml.NewDecoder(r.Body).Decode(&notice); err != nil {
t.Error(err)
}
r.Body.Close()
noticeError <- notice.Error
}))
return ts
}

View File

@ -1,68 +0,0 @@
package logrus_bugsnag
import (
"errors"
"github.com/bugsnag/bugsnag-go"
"hockeypuck/logrus"
)
type bugsnagHook struct{}
// ErrBugsnagUnconfigured is returned if NewBugsnagHook is called before
// bugsnag.Configure. Bugsnag must be configured before the hook.
var ErrBugsnagUnconfigured = errors.New("bugsnag must be configured before installing this logrus hook")
// ErrBugsnagSendFailed indicates that the hook failed to submit an error to
// bugsnag. The error was successfully generated, but `bugsnag.Notify()`
// failed.
type ErrBugsnagSendFailed struct {
err error
}
func (e ErrBugsnagSendFailed) Error() string {
return "failed to send error to Bugsnag: " + e.err.Error()
}
// NewBugsnagHook initializes a logrus hook which sends exceptions to an
// exception-tracking service compatible with the Bugsnag API. Before using
// this hook, you must call bugsnag.Configure(). The returned object should be
// registered with a log via `AddHook()`
//
// Entries that trigger an Error, Fatal or Panic should now include an "error"
// field to send to Bugsnag.
func NewBugsnagHook() (*bugsnagHook, error) {
if bugsnag.Config.APIKey == "" {
return nil, ErrBugsnagUnconfigured
}
return &bugsnagHook{}, nil
}
// Fire forwards an error to Bugsnag. Given a logrus.Entry, it extracts the
// "error" field (or the Message if the error isn't present) and sends it off.
func (hook *bugsnagHook) Fire(entry *logrus.Entry) error {
var notifyErr error
err, ok := entry.Data["error"].(error)
if ok {
notifyErr = err
} else {
notifyErr = errors.New(entry.Message)
}
bugsnagErr := bugsnag.Notify(notifyErr)
if bugsnagErr != nil {
return ErrBugsnagSendFailed{bugsnagErr}
}
return nil
}
// Levels enumerates the log levels on which the error should be forwarded to
// bugsnag: everything at or above the "Error" level.
func (hook *bugsnagHook) Levels() []logrus.Level {
return []logrus.Level{
logrus.ErrorLevel,
logrus.FatalLevel,
logrus.PanicLevel,
}
}

View File

@ -1,64 +0,0 @@
package logrus_bugsnag
import (
"encoding/json"
"errors"
"io/ioutil"
"net/http"
"net/http/httptest"
"testing"
"time"
"github.com/bugsnag/bugsnag-go"
"hockeypuck/logrus"
)
type notice struct {
Events []struct {
Exceptions []struct {
Message string `json:"message"`
} `json:"exceptions"`
} `json:"events"`
}
func TestNoticeReceived(t *testing.T) {
msg := make(chan string, 1)
expectedMsg := "foo"
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
var notice notice
data, _ := ioutil.ReadAll(r.Body)
if err := json.Unmarshal(data, &notice); err != nil {
t.Error(err)
}
_ = r.Body.Close()
msg <- notice.Events[0].Exceptions[0].Message
}))
defer ts.Close()
hook := &bugsnagHook{}
bugsnag.Configure(bugsnag.Configuration{
Endpoint: ts.URL,
ReleaseStage: "production",
APIKey: "12345678901234567890123456789012",
Synchronous: true,
})
log := logrus.New()
log.Hooks.Add(hook)
log.WithFields(logrus.Fields{
"error": errors.New(expectedMsg),
}).Error("Bugsnag will not see this string")
select {
case received := <-msg:
if received != expectedMsg {
t.Errorf("Unexpected message received: %s", received)
}
case <-time.After(time.Second):
t.Error("Timed out; no notice received by Bugsnag API")
}
}

View File

@ -1,28 +0,0 @@
# Papertrail Hook for Logrus <img src="http://i.imgur.com/hTeVwmJ.png" width="40" height="40" alt=":walrus:" class="emoji" title=":walrus:" />
[Papertrail](https://papertrailapp.com) provides hosted log management. Once stored in Papertrail, you can [group](http://help.papertrailapp.com/kb/how-it-works/groups/) your logs on various dimensions, [search](http://help.papertrailapp.com/kb/how-it-works/search-syntax) them, and trigger [alerts](http://help.papertrailapp.com/kb/how-it-works/alerts).
In most deployments, you'll want to send logs to Papertrail via their [remote_syslog](http://help.papertrailapp.com/kb/configuration/configuring-centralized-logging-from-text-log-files-in-unix/) daemon, which requires no application-specific configuration. This hook is intended for relatively low-volume logging, likely in managed cloud hosting deployments where installing `remote_syslog` is not possible.
## Usage
You can find your Papertrail UDP port on your [Papertrail account page](https://papertrailapp.com/account/destinations). Substitute it below for `YOUR_PAPERTRAIL_UDP_PORT`.
For `YOUR_APP_NAME`, substitute a short string that will readily identify your application or service in the logs.
```go
import (
"log/syslog"
"github.com/Sirupsen/logrus"
"github.com/Sirupsen/logrus/hooks/papertrail"
)
func main() {
log := logrus.New()
hook, err := logrus_papertrail.NewPapertrailHook("logs.papertrailapp.com", YOUR_PAPERTRAIL_UDP_PORT, YOUR_APP_NAME)
if err == nil {
log.Hooks.Add(hook)
}
}
```

View File

@ -1,55 +0,0 @@
package logrus_papertrail
import (
"fmt"
"net"
"os"
"time"
"hockeypuck/logrus"
)
const (
format = "Jan 2 15:04:05"
)
// PapertrailHook to send logs to a logging service compatible with the Papertrail API.
type PapertrailHook struct {
Host string
Port int
AppName string
UDPConn net.Conn
}
// NewPapertrailHook creates a hook to be added to an instance of logger.
func NewPapertrailHook(host string, port int, appName string) (*PapertrailHook, error) {
conn, err := net.Dial("udp", fmt.Sprintf("%s:%d", host, port))
return &PapertrailHook{host, port, appName, conn}, err
}
// Fire is called when a log event is fired.
func (hook *PapertrailHook) Fire(entry *logrus.Entry) error {
date := time.Now().Format(format)
msg, _ := entry.String()
payload := fmt.Sprintf("<22> %s %s: %s", date, hook.AppName, msg)
bytesWritten, err := hook.UDPConn.Write([]byte(payload))
if err != nil {
fmt.Fprintf(os.Stderr, "Unable to send log line to Papertrail via UDP. Wrote %d bytes before error: %v", bytesWritten, err)
return err
}
return nil
}
// Levels returns the available logging levels.
func (hook *PapertrailHook) Levels() []logrus.Level {
return []logrus.Level{
logrus.PanicLevel,
logrus.FatalLevel,
logrus.ErrorLevel,
logrus.WarnLevel,
logrus.InfoLevel,
logrus.DebugLevel,
}
}

View File

@ -1,26 +0,0 @@
package logrus_papertrail
import (
"fmt"
"testing"
"github.com/stvp/go-udp-testing"
"hockeypuck/logrus"
)
func TestWritingToUDP(t *testing.T) {
port := 16661
udp.SetAddr(fmt.Sprintf(":%d", port))
hook, err := NewPapertrailHook("localhost", port, "test")
if err != nil {
t.Errorf("Unable to connect to local UDP server.")
}
log := logrus.New()
log.Hooks.Add(hook)
udp.ShouldReceive(t, "foo", func() {
log.Info("foo")
})
}

View File

@ -1,61 +0,0 @@
# Sentry Hook for Logrus <img src="http://i.imgur.com/hTeVwmJ.png" width="40" height="40" alt=":walrus:" class="emoji" title=":walrus:" />
[Sentry](https://getsentry.com) provides both self-hosted and hosted
solutions for exception tracking.
Both client and server are
[open source](https://github.com/getsentry/sentry).
## Usage
Every sentry application defined on the server gets a different
[DSN](https://www.getsentry.com/docs/). In the example below replace
`YOUR_DSN` with the one created for your application.
```go
import (
"github.com/Sirupsen/logrus"
"github.com/Sirupsen/logrus/hooks/sentry"
)
func main() {
log := logrus.New()
hook, err := logrus_sentry.NewSentryHook(YOUR_DSN, []logrus.Level{
logrus.PanicLevel,
logrus.FatalLevel,
logrus.ErrorLevel,
})
if err == nil {
log.Hooks.Add(hook)
}
}
```
## Special fields
Some logrus fields have a special meaning in this hook,
these are server_name and logger.
When logs are sent to sentry these fields are treated differently.
- server_name (also known as hostname) is the name of the server which
is logging the event (hostname.example.com)
- logger is the part of the application which is logging the event.
In go this usually means setting it to the name of the package.
## Timeout
`Timeout` is the time the sentry hook will wait for a response
from the sentry server.
If this time elapses with no response from
the server an error will be returned.
If `Timeout` is set to 0 the SentryHook will not wait for a reply
and will assume a correct delivery.
The SentryHook has a default timeout of `100 milliseconds` when created
with a call to `NewSentryHook`. This can be changed by assigning a value to the `Timeout` field:
```go
hook, _ := logrus_sentry.NewSentryHook(...)
hook.Timeout = 20*time.Second
```

View File

@ -1,100 +0,0 @@
package logrus_sentry
import (
"fmt"
"time"
"github.com/getsentry/raven-go"
"hockeypuck/logrus"
)
var (
severityMap = map[logrus.Level]raven.Severity{
logrus.DebugLevel: raven.DEBUG,
logrus.InfoLevel: raven.INFO,
logrus.WarnLevel: raven.WARNING,
logrus.ErrorLevel: raven.ERROR,
logrus.FatalLevel: raven.FATAL,
logrus.PanicLevel: raven.FATAL,
}
)
func getAndDel(d logrus.Fields, key string) (string, bool) {
var (
ok bool
v interface{}
val string
)
if v, ok = d[key]; !ok {
return "", false
}
if val, ok = v.(string); !ok {
return "", false
}
delete(d, key)
return val, true
}
// SentryHook delivers logs to a sentry server.
type SentryHook struct {
// Timeout sets the time to wait for a delivery error from the sentry server.
// If this is set to zero the server will not wait for any response and will
// consider the message correctly sent
Timeout time.Duration
client *raven.Client
levels []logrus.Level
}
// NewSentryHook creates a hook to be added to an instance of logger
// and initializes the raven client.
// This method sets the timeout to 100 milliseconds.
func NewSentryHook(DSN string, levels []logrus.Level) (*SentryHook, error) {
client, err := raven.NewClient(DSN, nil)
if err != nil {
return nil, err
}
return &SentryHook{100 * time.Millisecond, client, levels}, nil
}
// Called when an event should be sent to sentry
// Special fields that sentry uses to give more information to the server
// are extracted from entry.Data (if they are found)
// These fields are: logger and server_name
func (hook *SentryHook) Fire(entry *logrus.Entry) error {
packet := &raven.Packet{
Message: entry.Message,
Timestamp: raven.Timestamp(entry.Time),
Level: severityMap[entry.Level],
Platform: "go",
}
d := entry.Data
if logger, ok := getAndDel(d, "logger"); ok {
packet.Logger = logger
}
if serverName, ok := getAndDel(d, "server_name"); ok {
packet.ServerName = serverName
}
packet.Extra = map[string]interface{}(d)
_, errCh := hook.client.Capture(packet, nil)
timeout := hook.Timeout
if timeout != 0 {
timeoutCh := time.After(timeout)
select {
case err := <-errCh:
return err
case <-timeoutCh:
return fmt.Errorf("no response from sentry server in %s", timeout)
}
}
return nil
}
// Levels returns the available logging levels.
func (hook *SentryHook) Levels() []logrus.Level {
return hook.levels
}

View File

@ -1,97 +0,0 @@
package logrus_sentry
import (
"encoding/json"
"fmt"
"io/ioutil"
"net/http"
"net/http/httptest"
"strings"
"testing"
"github.com/getsentry/raven-go"
"hockeypuck/logrus"
)
const (
message = "error message"
server_name = "testserver.internal"
logger_name = "test.logger"
)
func getTestLogger() *logrus.Logger {
l := logrus.New()
l.Out = ioutil.Discard
return l
}
func WithTestDSN(t *testing.T, tf func(string, <-chan *raven.Packet)) {
pch := make(chan *raven.Packet, 1)
s := httptest.NewServer(http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) {
defer req.Body.Close()
d := json.NewDecoder(req.Body)
p := &raven.Packet{}
err := d.Decode(p)
if err != nil {
t.Fatal(err.Error())
}
pch <- p
}))
defer s.Close()
fragments := strings.SplitN(s.URL, "://", 2)
dsn := fmt.Sprintf(
"%s://public:secret@%s/sentry/project-id",
fragments[0],
fragments[1],
)
tf(dsn, pch)
}
func TestSpecialFields(t *testing.T) {
WithTestDSN(t, func(dsn string, pch <-chan *raven.Packet) {
logger := getTestLogger()
hook, err := NewSentryHook(dsn, []logrus.Level{
logrus.ErrorLevel,
})
if err != nil {
t.Fatal(err.Error())
}
logger.Hooks.Add(hook)
logger.WithFields(logrus.Fields{
"server_name": server_name,
"logger": logger_name,
}).Error(message)
packet := <-pch
if packet.Logger != logger_name {
t.Errorf("logger should have been %s, was %s", logger_name, packet.Logger)
}
if packet.ServerName != server_name {
t.Errorf("server_name should have been %s, was %s", server_name, packet.ServerName)
}
})
}
func TestSentryHandler(t *testing.T) {
WithTestDSN(t, func(dsn string, pch <-chan *raven.Packet) {
logger := getTestLogger()
hook, err := NewSentryHook(dsn, []logrus.Level{
logrus.ErrorLevel,
})
if err != nil {
t.Fatal(err.Error())
}
logger.Hooks.Add(hook)
logger.Error(message)
packet := <-pch
if packet.Message != message {
t.Errorf("message should have been %s, was %s", message, packet.Message)
}
})
}

View File

@ -1,20 +0,0 @@
# Syslog Hooks for Logrus <img src="http://i.imgur.com/hTeVwmJ.png" width="40" height="40" alt=":walrus:" class="emoji" title=":walrus:"/>
## Usage
```go
import (
"log/syslog"
"github.com/Sirupsen/logrus"
logrus_syslog "github.com/Sirupsen/logrus/hooks/syslog"
)
func main() {
log := logrus.New()
hook, err := logrus_syslog.NewSyslogHook("udp", "localhost:514", syslog.LOG_INFO, "")
if err == nil {
log.Hooks.Add(hook)
}
}
```

View File

@ -1,59 +0,0 @@
package logrus_syslog
import (
"fmt"
"hockeypuck/logrus"
"log/syslog"
"os"
)
// SyslogHook to send logs via syslog.
type SyslogHook struct {
Writer *syslog.Writer
SyslogNetwork string
SyslogRaddr string
}
// Creates a hook to be added to an instance of logger. This is called with
// `hook, err := NewSyslogHook("udp", "localhost:514", syslog.LOG_DEBUG, "")`
// `if err == nil { log.Hooks.Add(hook) }`
func NewSyslogHook(network, raddr string, priority syslog.Priority, tag string) (*SyslogHook, error) {
w, err := syslog.Dial(network, raddr, priority, tag)
return &SyslogHook{w, network, raddr}, err
}
func (hook *SyslogHook) Fire(entry *logrus.Entry) error {
line, err := entry.String()
if err != nil {
fmt.Fprintf(os.Stderr, "Unable to read entry, %v", err)
return err
}
switch entry.Level {
case logrus.PanicLevel:
return hook.Writer.Crit(line)
case logrus.FatalLevel:
return hook.Writer.Crit(line)
case logrus.ErrorLevel:
return hook.Writer.Err(line)
case logrus.WarnLevel:
return hook.Writer.Warning(line)
case logrus.InfoLevel:
return hook.Writer.Info(line)
case logrus.DebugLevel:
return hook.Writer.Debug(line)
default:
return nil
}
}
func (hook *SyslogHook) Levels() []logrus.Level {
return []logrus.Level{
logrus.PanicLevel,
logrus.FatalLevel,
logrus.ErrorLevel,
logrus.WarnLevel,
logrus.InfoLevel,
logrus.DebugLevel,
}
}

View File

@ -1,26 +0,0 @@
package logrus_syslog
import (
"hockeypuck/logrus"
"log/syslog"
"testing"
)
func TestLocalhostAddAndPrint(t *testing.T) {
log := logrus.New()
hook, err := NewSyslogHook("udp", "localhost:514", syslog.LOG_INFO, "")
if err != nil {
t.Errorf("Unable to connect to local syslog.")
}
log.Hooks.Add(hook)
for _, level := range hook.Levels() {
if len(log.Hooks[level]) != 1 {
t.Errorf("SyslogHook was not added. The length of log.Hooks[%v]: %v", level, len(log.Hooks[level]))
}
}
log.Info("Congratulations!")
}

View File

@ -1,40 +0,0 @@
package logrus
import (
"encoding/json"
"fmt"
)
type JSONFormatter struct {
// TimestampFormat sets the format used for marshaling timestamps.
TimestampFormat string
}
func (f *JSONFormatter) Format(entry *Entry) ([]byte, error) {
data := make(Fields, len(entry.Data)+3)
for k, v := range entry.Data {
switch v := v.(type) {
case error:
// Otherwise errors are ignored by `encoding/json`
// https://github.com/Sirupsen/logrus/issues/137
data[k] = v.Error()
default:
data[k] = v
}
}
prefixFieldClashes(data)
if f.TimestampFormat == "" {
f.TimestampFormat = DefaultTimestampFormat
}
data["time"] = entry.Time.Format(f.TimestampFormat)
data["msg"] = entry.Message
data["level"] = entry.Level.String()
serialized, err := json.Marshal(data)
if err != nil {
return nil, fmt.Errorf("Failed to marshal fields to JSON, %v", err)
}
return append(serialized, '\n'), nil
}

View File

@ -1,120 +0,0 @@
package logrus
import (
"encoding/json"
"errors"
"testing"
)
func TestErrorNotLost(t *testing.T) {
formatter := &JSONFormatter{}
b, err := formatter.Format(WithField("error", errors.New("wild walrus")))
if err != nil {
t.Fatal("Unable to format entry: ", err)
}
entry := make(map[string]interface{})
err = json.Unmarshal(b, &entry)
if err != nil {
t.Fatal("Unable to unmarshal formatted entry: ", err)
}
if entry["error"] != "wild walrus" {
t.Fatal("Error field not set")
}
}
func TestErrorNotLostOnFieldNotNamedError(t *testing.T) {
formatter := &JSONFormatter{}
b, err := formatter.Format(WithField("omg", errors.New("wild walrus")))
if err != nil {
t.Fatal("Unable to format entry: ", err)
}
entry := make(map[string]interface{})
err = json.Unmarshal(b, &entry)
if err != nil {
t.Fatal("Unable to unmarshal formatted entry: ", err)
}
if entry["omg"] != "wild walrus" {
t.Fatal("Error field not set")
}
}
func TestFieldClashWithTime(t *testing.T) {
formatter := &JSONFormatter{}
b, err := formatter.Format(WithField("time", "right now!"))
if err != nil {
t.Fatal("Unable to format entry: ", err)
}
entry := make(map[string]interface{})
err = json.Unmarshal(b, &entry)
if err != nil {
t.Fatal("Unable to unmarshal formatted entry: ", err)
}
if entry["fields.time"] != "right now!" {
t.Fatal("fields.time not set to original time field")
}
if entry["time"] != "0001-01-01T00:00:00Z" {
t.Fatal("time field not set to current time, was: ", entry["time"])
}
}
func TestFieldClashWithMsg(t *testing.T) {
formatter := &JSONFormatter{}
b, err := formatter.Format(WithField("msg", "something"))
if err != nil {
t.Fatal("Unable to format entry: ", err)
}
entry := make(map[string]interface{})
err = json.Unmarshal(b, &entry)
if err != nil {
t.Fatal("Unable to unmarshal formatted entry: ", err)
}
if entry["fields.msg"] != "something" {
t.Fatal("fields.msg not set to original msg field")
}
}
func TestFieldClashWithLevel(t *testing.T) {
formatter := &JSONFormatter{}
b, err := formatter.Format(WithField("level", "something"))
if err != nil {
t.Fatal("Unable to format entry: ", err)
}
entry := make(map[string]interface{})
err = json.Unmarshal(b, &entry)
if err != nil {
t.Fatal("Unable to unmarshal formatted entry: ", err)
}
if entry["fields.level"] != "something" {
t.Fatal("fields.level not set to original level field")
}
}
func TestJSONEntryEndsWithNewline(t *testing.T) {
formatter := &JSONFormatter{}
b, err := formatter.Format(WithField("level", "something"))
if err != nil {
t.Fatal("Unable to format entry: ", err)
}
if b[len(b)-1] != '\n' {
t.Fatal("Expected JSON log entry to end with a newline")
}
}

View File

@ -1,203 +0,0 @@
package logrus
import (
"io"
"os"
"sync"
)
type Logger struct {
// The logs are `io.Copy`'d to this in a mutex. It's common to set this to a
// file, or leave it default which is `os.Stdout`. You can also set this to
// something more adventorous, such as logging to Kafka.
Out io.Writer
// Hooks for the logger instance. These allow firing events based on logging
// levels and log entries. For example, to send errors to an error tracking
// service, log to StatsD or dump the core on fatal errors.
Hooks levelHooks
// All log entries pass through the formatter before logged to Out. The
// included formatters are `TextFormatter` and `JSONFormatter` for which
// TextFormatter is the default. In development (when a TTY is attached) it
// logs with colors, but to a file it wouldn't. You can easily implement your
// own that implements the `Formatter` interface, see the `README` or included
// formatters for examples.
Formatter Formatter
// The logging level the logger should log at. This is typically (and defaults
// to) `logrus.Info`, which allows Info(), Warn(), Error() and Fatal() to be
// logged. `logrus.Debug` is useful in
Level Level
// Used to sync writing to the log.
mu sync.Mutex
}
// Creates a new logger. Configuration should be set by changing `Formatter`,
// `Out` and `Hooks` directly on the default logger instance. You can also just
// instantiate your own:
//
// var log = &Logger{
// Out: os.Stderr,
// Formatter: new(JSONFormatter),
// Hooks: make(levelHooks),
// Level: logrus.DebugLevel,
// }
//
// It's recommended to make this a global instance called `log`.
func New() *Logger {
return &Logger{
Out: os.Stdout,
Formatter: new(TextFormatter),
Hooks: make(levelHooks),
Level: InfoLevel,
}
}
// Adds a field to the log entry, note that you it doesn't log until you call
// Debug, Print, Info, Warn, Fatal or Panic. It only creates a log entry.
// Ff you want multiple fields, use `WithFields`.
func (logger *Logger) WithField(key string, value interface{}) *Entry {
return NewEntry(logger).WithField(key, value)
}
// Adds a struct of fields to the log entry. All it does is call `WithField` for
// each `Field`.
func (logger *Logger) WithFields(fields Fields) *Entry {
return NewEntry(logger).WithFields(fields)
}
func (logger *Logger) Debugf(format string, args ...interface{}) {
if logger.Level >= DebugLevel {
NewEntry(logger).Debugf(format, args...)
}
}
func (logger *Logger) Infof(format string, args ...interface{}) {
if logger.Level >= InfoLevel {
NewEntry(logger).Infof(format, args...)
}
}
func (logger *Logger) Printf(format string, args ...interface{}) {
NewEntry(logger).Printf(format, args...)
}
func (logger *Logger) Warnf(format string, args ...interface{}) {
if logger.Level >= WarnLevel {
NewEntry(logger).Warnf(format, args...)
}
}
func (logger *Logger) Warningf(format string, args ...interface{}) {
if logger.Level >= WarnLevel {
NewEntry(logger).Warnf(format, args...)
}
}
func (logger *Logger) Errorf(format string, args ...interface{}) {
if logger.Level >= ErrorLevel {
NewEntry(logger).Errorf(format, args...)
}
}
func (logger *Logger) Fatalf(format string, args ...interface{}) {
if logger.Level >= FatalLevel {
NewEntry(logger).Fatalf(format, args...)
}
}
func (logger *Logger) Panicf(format string, args ...interface{}) {
if logger.Level >= PanicLevel {
NewEntry(logger).Panicf(format, args...)
}
}
func (logger *Logger) Debug(args ...interface{}) {
if logger.Level >= DebugLevel {
NewEntry(logger).Debug(args...)
}
}
func (logger *Logger) Info(args ...interface{}) {
if logger.Level >= InfoLevel {
NewEntry(logger).Info(args...)
}
}
func (logger *Logger) Print(args ...interface{}) {
NewEntry(logger).Info(args...)
}
func (logger *Logger) Warn(args ...interface{}) {
if logger.Level >= WarnLevel {
NewEntry(logger).Warn(args...)
}
}
func (logger *Logger) Warning(args ...interface{}) {
if logger.Level >= WarnLevel {
NewEntry(logger).Warn(args...)
}
}
func (logger *Logger) Error(args ...interface{}) {
if logger.Level >= ErrorLevel {
NewEntry(logger).Error(args...)
}
}
func (logger *Logger) Fatal(args ...interface{}) {
if logger.Level >= FatalLevel {
NewEntry(logger).Fatal(args...)
}
}
func (logger *Logger) Panic(args ...interface{}) {
if logger.Level >= PanicLevel {
NewEntry(logger).Panic(args...)
}
}
func (logger *Logger) Debugln(args ...interface{}) {
if logger.Level >= DebugLevel {
NewEntry(logger).Debugln(args...)
}
}
func (logger *Logger) Infoln(args ...interface{}) {
if logger.Level >= InfoLevel {
NewEntry(logger).Infoln(args...)
}
}
func (logger *Logger) Println(args ...interface{}) {
NewEntry(logger).Println(args...)
}
func (logger *Logger) Warnln(args ...interface{}) {
if logger.Level >= WarnLevel {
NewEntry(logger).Warnln(args...)
}
}
func (logger *Logger) Warningln(args ...interface{}) {
if logger.Level >= WarnLevel {
NewEntry(logger).Warnln(args...)
}
}
func (logger *Logger) Errorln(args ...interface{}) {
if logger.Level >= ErrorLevel {
NewEntry(logger).Errorln(args...)
}
}
func (logger *Logger) Fatalln(args ...interface{}) {
if logger.Level >= FatalLevel {
NewEntry(logger).Fatalln(args...)
}
}
func (logger *Logger) Panicln(args ...interface{}) {
if logger.Level >= PanicLevel {
NewEntry(logger).Panicln(args...)
}
}

View File

@ -1,94 +0,0 @@
package logrus
import (
"fmt"
"log"
)
// Fields type, used to pass to `WithFields`.
type Fields map[string]interface{}
// Level type
type Level uint8
// Convert the Level to a string. E.g. PanicLevel becomes "panic".
func (level Level) String() string {
switch level {
case DebugLevel:
return "debug"
case InfoLevel:
return "info"
case WarnLevel:
return "warning"
case ErrorLevel:
return "error"
case FatalLevel:
return "fatal"
case PanicLevel:
return "panic"
}
return "unknown"
}
// ParseLevel takes a string level and returns the Logrus log level constant.
func ParseLevel(lvl string) (Level, error) {
switch lvl {
case "panic":
return PanicLevel, nil
case "fatal":
return FatalLevel, nil
case "error":
return ErrorLevel, nil
case "warn", "warning":
return WarnLevel, nil
case "info":
return InfoLevel, nil
case "debug":
return DebugLevel, nil
}
var l Level
return l, fmt.Errorf("not a valid logrus Level: %q", lvl)
}
// These are the different logging levels. You can set the logging level to log
// on your instance of logger, obtained with `logrus.New()`.
const (
// PanicLevel level, highest level of severity. Logs and then calls panic with the
// message passed to Debug, Info, ...
PanicLevel Level = iota
// FatalLevel level. Logs and then calls `os.Exit(1)`. It will exit even if the
// logging level is set to Panic.
FatalLevel
// ErrorLevel level. Logs. Used for errors that should definitely be noted.
// Commonly used for hooks to send errors to an error tracking service.
ErrorLevel
// WarnLevel level. Non-critical entries that deserve eyes.
WarnLevel
// InfoLevel level. General operational entries about what's going on inside the
// application.
InfoLevel
// DebugLevel level. Usually only enabled when debugging. Very verbose logging.
DebugLevel
)
// Won't compile if StdLogger can't be realized by a log.Logger
var _ StdLogger = &log.Logger{}
// StdLogger is what your logrus-enabled library should take, that way
// it'll accept a stdlib logger and a logrus logger. There's no standard
// interface, this is the closest we get, unfortunately.
type StdLogger interface {
Print(...interface{})
Printf(string, ...interface{})
Println(...interface{})
Fatal(...interface{})
Fatalf(string, ...interface{})
Fatalln(...interface{})
Panic(...interface{})
Panicf(string, ...interface{})
Panicln(...interface{})
}

View File

@ -1,301 +0,0 @@
package logrus
import (
"bytes"
"encoding/json"
"strconv"
"strings"
"sync"
"testing"
"github.com/stretchr/testify/assert"
)
func LogAndAssertJSON(t *testing.T, log func(*Logger), assertions func(fields Fields)) {
var buffer bytes.Buffer
var fields Fields
logger := New()
logger.Out = &buffer
logger.Formatter = new(JSONFormatter)
log(logger)
err := json.Unmarshal(buffer.Bytes(), &fields)
assert.Nil(t, err)
assertions(fields)
}
func LogAndAssertText(t *testing.T, log func(*Logger), assertions func(fields map[string]string)) {
var buffer bytes.Buffer
logger := New()
logger.Out = &buffer
logger.Formatter = &TextFormatter{
DisableColors: true,
}
log(logger)
fields := make(map[string]string)
for _, kv := range strings.Split(buffer.String(), " ") {
if !strings.Contains(kv, "=") {
continue
}
kvArr := strings.Split(kv, "=")
key := strings.TrimSpace(kvArr[0])
val := kvArr[1]
if kvArr[1][0] == '"' {
var err error
val, err = strconv.Unquote(val)
assert.NoError(t, err)
}
fields[key] = val
}
assertions(fields)
}
func TestPrint(t *testing.T) {
LogAndAssertJSON(t, func(log *Logger) {
log.Print("test")
}, func(fields Fields) {
assert.Equal(t, fields["msg"], "test")
assert.Equal(t, fields["level"], "info")
})
}
func TestInfo(t *testing.T) {
LogAndAssertJSON(t, func(log *Logger) {
log.Info("test")
}, func(fields Fields) {
assert.Equal(t, fields["msg"], "test")
assert.Equal(t, fields["level"], "info")
})
}
func TestWarn(t *testing.T) {
LogAndAssertJSON(t, func(log *Logger) {
log.Warn("test")
}, func(fields Fields) {
assert.Equal(t, fields["msg"], "test")
assert.Equal(t, fields["level"], "warning")
})
}
func TestInfolnShouldAddSpacesBetweenStrings(t *testing.T) {
LogAndAssertJSON(t, func(log *Logger) {
log.Infoln("test", "test")
}, func(fields Fields) {
assert.Equal(t, fields["msg"], "test test")
})
}
func TestInfolnShouldAddSpacesBetweenStringAndNonstring(t *testing.T) {
LogAndAssertJSON(t, func(log *Logger) {
log.Infoln("test", 10)
}, func(fields Fields) {
assert.Equal(t, fields["msg"], "test 10")
})
}
func TestInfolnShouldAddSpacesBetweenTwoNonStrings(t *testing.T) {
LogAndAssertJSON(t, func(log *Logger) {
log.Infoln(10, 10)
}, func(fields Fields) {
assert.Equal(t, fields["msg"], "10 10")
})
}
func TestInfoShouldAddSpacesBetweenTwoNonStrings(t *testing.T) {
LogAndAssertJSON(t, func(log *Logger) {
log.Infoln(10, 10)
}, func(fields Fields) {
assert.Equal(t, fields["msg"], "10 10")
})
}
func TestInfoShouldNotAddSpacesBetweenStringAndNonstring(t *testing.T) {
LogAndAssertJSON(t, func(log *Logger) {
log.Info("test", 10)
}, func(fields Fields) {
assert.Equal(t, fields["msg"], "test10")
})
}
func TestInfoShouldNotAddSpacesBetweenStrings(t *testing.T) {
LogAndAssertJSON(t, func(log *Logger) {
log.Info("test", "test")
}, func(fields Fields) {
assert.Equal(t, fields["msg"], "testtest")
})
}
func TestWithFieldsShouldAllowAssignments(t *testing.T) {
var buffer bytes.Buffer
var fields Fields
logger := New()
logger.Out = &buffer
logger.Formatter = new(JSONFormatter)
localLog := logger.WithFields(Fields{
"key1": "value1",
})
localLog.WithField("key2", "value2").Info("test")
err := json.Unmarshal(buffer.Bytes(), &fields)
assert.Nil(t, err)
assert.Equal(t, "value2", fields["key2"])
assert.Equal(t, "value1", fields["key1"])
buffer = bytes.Buffer{}
fields = Fields{}
localLog.Info("test")
err = json.Unmarshal(buffer.Bytes(), &fields)
assert.Nil(t, err)
_, ok := fields["key2"]
assert.Equal(t, false, ok)
assert.Equal(t, "value1", fields["key1"])
}
func TestUserSuppliedFieldDoesNotOverwriteDefaults(t *testing.T) {
LogAndAssertJSON(t, func(log *Logger) {
log.WithField("msg", "hello").Info("test")
}, func(fields Fields) {
assert.Equal(t, fields["msg"], "test")
})
}
func TestUserSuppliedMsgFieldHasPrefix(t *testing.T) {
LogAndAssertJSON(t, func(log *Logger) {
log.WithField("msg", "hello").Info("test")
}, func(fields Fields) {
assert.Equal(t, fields["msg"], "test")
assert.Equal(t, fields["fields.msg"], "hello")
})
}
func TestUserSuppliedTimeFieldHasPrefix(t *testing.T) {
LogAndAssertJSON(t, func(log *Logger) {
log.WithField("time", "hello").Info("test")
}, func(fields Fields) {
assert.Equal(t, fields["fields.time"], "hello")
})
}
func TestUserSuppliedLevelFieldHasPrefix(t *testing.T) {
LogAndAssertJSON(t, func(log *Logger) {
log.WithField("level", 1).Info("test")
}, func(fields Fields) {
assert.Equal(t, fields["level"], "info")
assert.Equal(t, fields["fields.level"], float64(1))
})
}
func TestDefaultFieldsAreNotPrefixed(t *testing.T) {
LogAndAssertText(t, func(log *Logger) {
ll := log.WithField("herp", "derp")
ll.Info("hello")
ll.Info("bye")
}, func(fields map[string]string) {
for _, fieldName := range []string{"fields.level", "fields.time", "fields.msg"} {
if _, ok := fields[fieldName]; ok {
t.Fatalf("should not have prefixed %q: %v", fieldName, fields)
}
}
})
}
func TestDoubleLoggingDoesntPrefixPreviousFields(t *testing.T) {
var buffer bytes.Buffer
var fields Fields
logger := New()
logger.Out = &buffer
logger.Formatter = new(JSONFormatter)
llog := logger.WithField("context", "eating raw fish")
llog.Info("looks delicious")
err := json.Unmarshal(buffer.Bytes(), &fields)
assert.NoError(t, err, "should have decoded first message")
assert.Equal(t, len(fields), 4, "should only have msg/time/level/context fields")
assert.Equal(t, fields["msg"], "looks delicious")
assert.Equal(t, fields["context"], "eating raw fish")
buffer.Reset()
llog.Warn("omg it is!")
err = json.Unmarshal(buffer.Bytes(), &fields)
assert.NoError(t, err, "should have decoded second message")
assert.Equal(t, len(fields), 4, "should only have msg/time/level/context fields")
assert.Equal(t, fields["msg"], "omg it is!")
assert.Equal(t, fields["context"], "eating raw fish")
assert.Nil(t, fields["fields.msg"], "should not have prefixed previous `msg` entry")
}
func TestConvertLevelToString(t *testing.T) {
assert.Equal(t, "debug", DebugLevel.String())
assert.Equal(t, "info", InfoLevel.String())
assert.Equal(t, "warning", WarnLevel.String())
assert.Equal(t, "error", ErrorLevel.String())
assert.Equal(t, "fatal", FatalLevel.String())
assert.Equal(t, "panic", PanicLevel.String())
}
func TestParseLevel(t *testing.T) {
l, err := ParseLevel("panic")
assert.Nil(t, err)
assert.Equal(t, PanicLevel, l)
l, err = ParseLevel("fatal")
assert.Nil(t, err)
assert.Equal(t, FatalLevel, l)
l, err = ParseLevel("error")
assert.Nil(t, err)
assert.Equal(t, ErrorLevel, l)
l, err = ParseLevel("warn")
assert.Nil(t, err)
assert.Equal(t, WarnLevel, l)
l, err = ParseLevel("warning")
assert.Nil(t, err)
assert.Equal(t, WarnLevel, l)
l, err = ParseLevel("info")
assert.Nil(t, err)
assert.Equal(t, InfoLevel, l)
l, err = ParseLevel("debug")
assert.Nil(t, err)
assert.Equal(t, DebugLevel, l)
l, err = ParseLevel("invalid")
assert.Equal(t, "not a valid logrus Level: \"invalid\"", err.Error())
}
func TestGetSetLevelRace(t *testing.T) {
wg := sync.WaitGroup{}
for i := 0; i < 100; i++ {
wg.Add(1)
go func(i int) {
defer wg.Done()
if i%2 == 0 {
SetLevel(InfoLevel)
} else {
GetLevel()
}
}(i)
}
wg.Wait()
}

View File

@ -1,12 +0,0 @@
// Based on ssh/terminal:
// Copyright 2013 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
package logrus
import "syscall"
const ioctlReadTermios = syscall.TIOCGETA
type Termios syscall.Termios

View File

@ -1,20 +0,0 @@
/*
Go 1.2 doesn't include Termios for FreeBSD. This should be added in 1.3 and this could be merged with terminal_darwin.
*/
package logrus
import (
"syscall"
)
const ioctlReadTermios = syscall.TIOCGETA
type Termios struct {
Iflag uint32
Oflag uint32
Cflag uint32
Lflag uint32
Cc [20]uint8
Ispeed uint32
Ospeed uint32
}

View File

@ -1,12 +0,0 @@
// Based on ssh/terminal:
// Copyright 2013 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
package logrus
import "syscall"
const ioctlReadTermios = syscall.TCGETS
type Termios syscall.Termios

View File

@ -1,22 +0,0 @@
// Based on ssh/terminal:
// Copyright 2011 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
//go:build linux || darwin || freebsd || openbsd
// +build linux darwin freebsd openbsd
package logrus
import (
"syscall"
"unsafe"
)
// IsTerminal returns true if the given file descriptor is a terminal.
func IsTerminal() bool {
fd := syscall.Stdout
var termios Termios
_, _, err := syscall.Syscall6(syscall.SYS_IOCTL, uintptr(fd), ioctlReadTermios, uintptr(unsafe.Pointer(&termios)), 0, 0, 0)
return err == 0
}

View File

@ -1,7 +0,0 @@
package logrus
import "syscall"
const ioctlReadTermios = syscall.TIOCGETA
type Termios syscall.Termios

View File

@ -1,28 +0,0 @@
// Based on ssh/terminal:
// Copyright 2011 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
//go:build windows
// +build windows
package logrus
import (
"syscall"
"unsafe"
)
var kernel32 = syscall.NewLazyDLL("kernel32.dll")
var (
procGetConsoleMode = kernel32.NewProc("GetConsoleMode")
)
// IsTerminal returns true if the given file descriptor is a terminal.
func IsTerminal() bool {
fd := syscall.Stdout
var st uint32
r, _, e := syscall.Syscall(procGetConsoleMode.Addr(), 2, uintptr(fd), uintptr(unsafe.Pointer(&st)), 0)
return r != 0 && e == 0
}

View File

@ -1,149 +0,0 @@
package logrus
import (
"bytes"
"fmt"
"sort"
"strings"
"time"
)
const (
nocolor = 0
red = 31
green = 32
yellow = 33
blue = 34
gray = 37
)
var (
baseTimestamp time.Time
isTerminal bool
)
func init() {
baseTimestamp = time.Now()
isTerminal = IsTerminal()
}
func miniTS() int {
return int(time.Since(baseTimestamp) / time.Second)
}
type TextFormatter struct {
// Set to true to bypass checking for a TTY before outputting colors.
ForceColors bool
// Force disabling colors.
DisableColors bool
// Disable timestamp logging. useful when output is redirected to logging
// system that already adds timestamps.
DisableTimestamp bool
// Enable logging the full timestamp when a TTY is attached instead of just
// the time passed since beginning of execution.
FullTimestamp bool
// TimestampFormat to use for display when a full timestamp is printed
TimestampFormat string
// The fields are sorted by default for a consistent output. For applications
// that log extremely frequently and don't use the JSON formatter this may not
// be desired.
DisableSorting bool
}
func (f *TextFormatter) Format(entry *Entry) ([]byte, error) {
var keys []string = make([]string, 0, len(entry.Data))
for k := range entry.Data {
keys = append(keys, k)
}
if !f.DisableSorting {
sort.Strings(keys)
}
b := &bytes.Buffer{}
prefixFieldClashes(entry.Data)
isColored := (f.ForceColors || isTerminal) && !f.DisableColors
if f.TimestampFormat == "" {
f.TimestampFormat = DefaultTimestampFormat
}
if isColored {
f.printColored(b, entry, keys)
} else {
if !f.DisableTimestamp {
f.appendKeyValue(b, "time", entry.Time.Format(f.TimestampFormat))
}
f.appendKeyValue(b, "level", entry.Level.String())
f.appendKeyValue(b, "msg", entry.Message)
for _, key := range keys {
f.appendKeyValue(b, key, entry.Data[key])
}
}
b.WriteByte('\n')
return b.Bytes(), nil
}
func (f *TextFormatter) printColored(b *bytes.Buffer, entry *Entry, keys []string) {
var levelColor int
switch entry.Level {
case DebugLevel:
levelColor = gray
case WarnLevel:
levelColor = yellow
case ErrorLevel, FatalLevel, PanicLevel:
levelColor = red
default:
levelColor = blue
}
levelText := strings.ToUpper(entry.Level.String())[0:4]
if !f.FullTimestamp {
fmt.Fprintf(b, "\x1b[%dm%s\x1b[0m[%04d] %-44s ", levelColor, levelText, miniTS(), entry.Message)
} else {
fmt.Fprintf(b, "\x1b[%dm%s\x1b[0m[%s] %-44s ", levelColor, levelText, entry.Time.Format(f.TimestampFormat), entry.Message)
}
for _, k := range keys {
v := entry.Data[k]
fmt.Fprintf(b, " \x1b[%dm%s\x1b[0m=%v", levelColor, k, v)
}
}
func needsQuoting(text string) bool {
for _, ch := range text {
if !((ch >= 'a' && ch <= 'z') ||
(ch >= 'A' && ch <= 'Z') ||
(ch >= '0' && ch <= '9') ||
ch == '-' || ch == '.') {
return false
}
}
return true
}
func (f *TextFormatter) appendKeyValue(b *bytes.Buffer, key, value interface{}) {
switch value.(type) {
case string:
if needsQuoting(value.(string)) {
fmt.Fprintf(b, "%v=%s ", key, value)
} else {
fmt.Fprintf(b, "%v=%q ", key, value)
}
case error:
if needsQuoting(value.(error).Error()) {
fmt.Fprintf(b, "%v=%s ", key, value)
} else {
fmt.Fprintf(b, "%v=%q ", key, value)
}
default:
fmt.Fprintf(b, "%v=%v ", key, value)
}
}

View File

@ -1,61 +0,0 @@
package logrus
import (
"bytes"
"errors"
"testing"
"time"
)
func TestQuoting(t *testing.T) {
tf := &TextFormatter{DisableColors: true}
checkQuoting := func(q bool, value interface{}) {
b, _ := tf.Format(WithField("test", value))
idx := bytes.Index(b, ([]byte)("test="))
cont := bytes.Contains(b[idx+5:], []byte{'"'})
if cont != q {
if q {
t.Errorf("quoting expected for: %#v", value)
} else {
t.Errorf("quoting not expected for: %#v", value)
}
}
}
checkQuoting(false, "abcd")
checkQuoting(false, "v1.0")
checkQuoting(false, "1234567890")
checkQuoting(true, "/foobar")
checkQuoting(true, "x y")
checkQuoting(true, "x,y")
checkQuoting(false, errors.New("invalid"))
checkQuoting(true, errors.New("invalid argument"))
}
func TestTimestampFormat(t *testing.T) {
checkTimeStr := func(format string) {
customFormatter := &TextFormatter{DisableColors: true, TimestampFormat: format}
customStr, _ := customFormatter.Format(WithField("test", "test"))
timeStart := bytes.Index(customStr, ([]byte)("time="))
timeEnd := bytes.Index(customStr, ([]byte)("level="))
timeStr := customStr[timeStart+5 : timeEnd-1]
if timeStr[0] == '"' && timeStr[len(timeStr)-1] == '"' {
timeStr = timeStr[1 : len(timeStr)-1]
}
if format == "" {
format = time.RFC3339
}
_, e := time.Parse(format, (string)(timeStr))
if e != nil {
t.Errorf("time string \"%s\" did not match provided time format \"%s\": %s", timeStr, format, e)
}
}
checkTimeStr("2006-01-02T15:04:05.000000000Z07:00")
checkTimeStr("Mon Jan _2 15:04:05 2006")
checkTimeStr("")
}
// TODO add tests for sorting etc., this requires a parser for the text
// formatter output.

View File

@ -1,31 +0,0 @@
package logrus
import (
"bufio"
"io"
"runtime"
)
func (logger *Logger) Writer() *io.PipeWriter {
reader, writer := io.Pipe()
go logger.writerScanner(reader)
runtime.SetFinalizer(writer, writerFinalizer)
return writer
}
func (logger *Logger) writerScanner(reader *io.PipeReader) {
scanner := bufio.NewScanner(reader)
for scanner.Scan() {
logger.Print(scanner.Text())
}
if err := scanner.Err(); err != nil {
logger.Errorf("Error while reading from Writer: %s", err)
}
reader.Close()
}
func writerFinalizer(writer *io.PipeWriter) {
writer.Close()
}

View File

@ -8,7 +8,7 @@ import (
"github.com/prometheus/client_golang/prometheus/promhttp"
"gopkg.in/tomb.v2"
log "hockeypuck/logrus"
log "github.com/sirupsen/logrus"
)
type Metrics struct {

View File

@ -12,7 +12,7 @@ import (
"github.com/ProtonMail/go-crypto/openpgp/armor"
"github.com/pkg/errors"
log "hockeypuck/logrus"
log "github.com/sirupsen/logrus"
"hockeypuck/openpgp"
)

View File

@ -35,7 +35,7 @@ import (
"github.com/pkg/errors"
"golang.org/x/exp/slices"
log "hockeypuck/logrus"
log "github.com/sirupsen/logrus"
)
var ErrMissingSignature = fmt.Errorf("key material missing an expected signature")

View File

@ -20,7 +20,7 @@ package openpgp
import (
"crypto/md5"
"encoding/hex"
log "hockeypuck/logrus"
log "github.com/sirupsen/logrus"
"github.com/ProtonMail/go-crypto/openpgp/packet"
"github.com/pkg/errors"

View File

@ -35,7 +35,7 @@ import (
"hockeypuck/hkp/jsonhkp"
hkpstorage "hockeypuck/hkp/storage"
log "hockeypuck/logrus"
log "github.com/sirupsen/logrus"
"hockeypuck/openpgp"
)

View File

@ -9,7 +9,7 @@ import (
"github.com/pkg/errors"
log "hockeypuck/logrus"
log "github.com/sirupsen/logrus"
)
// Die prints the error and exits with a non-zero exit code

View File

@ -14,7 +14,7 @@ import (
cf "hockeypuck/conflux"
"hockeypuck/hkp/sks"
"hockeypuck/hkp/storage"
log "hockeypuck/logrus"
log "github.com/sirupsen/logrus"
"hockeypuck/openpgp"
"hockeypuck/server"
"hockeypuck/server/cmd"

View File

@ -13,7 +13,7 @@ import (
cf "hockeypuck/conflux"
"hockeypuck/hkp/sks"
"hockeypuck/hkp/storage"
log "hockeypuck/logrus"
log "github.com/sirupsen/logrus"
"hockeypuck/server"
"hockeypuck/server/cmd"
)

View File

@ -20,7 +20,7 @@ import (
"hockeypuck/hkp"
"hockeypuck/hkp/sks"
"hockeypuck/hkp/storage"
log "hockeypuck/logrus"
log "github.com/sirupsen/logrus"
"hockeypuck/metrics"
"hockeypuck/openpgp"
"hockeypuck/pghkp"

View File

@ -1,5 +0,0 @@
# Ignore maze runner generated files
maze_output
vendor
features/fixtures/testbuild

View File

@ -1,366 +0,0 @@
# Changelog
## 2.2.0 (2022-10-12)
### Enhancements
* Support pkg/errors `Unwrap()` on `errors.Error` objects
[#194](https://github.com/bugsnag/bugsnag-go/pull/194)
[Jayce Pulsipher](https://github.com/jaycetde)
* Document double star glob patterns are available for `ProjectPackages`
subpackage names.
[#184](https://github.com/bugsnag/bugsnag-go/pull/184)
[Genta Kamitani](https://github.com/genkami)
### Bug fixes
* Replace the gofrs/uuid dependency to maintain support for older versions of Go
[#196](https://github.com/bugsnag/bugsnag-go/pull/196)
## 1.9.1 (2022-10-12)
### Bug fixes
* Replace the gofrs/uuid dependency to maintain support for older versions of Go
[#196](https://github.com/bugsnag/bugsnag-go/pull/196)
## 2.1.2 (2021-08-24)
### Enhancements
* Update panicwrap dependency to v1.3.4 which fixes build support for linux & darwin arm64.
## 2.1.1 (2021-04-19)
### Enhancements
* Update panicwrap dependency to 1.3.2, adding support for darwin arm64
## 2.1.0 (2021-01-27)
### Enhancements
* Support appending metadata through environment variables prefixed with
`BUGSNAG_METADATA_`
### Bug fixes
* Fix `GOPATH`, `SourceRoot` and project package path stripping from stack
traces on Windows by using the correct path separators.
## 2.0.0 (2021-01-18)
The v2 release adds support for Go modules, removes web framework
integrations from the main repository, and supports library configuration
through environment variables.
The new module is available via:
```go
import "github.com/bugsnag/bugsnag-go/v2"
```
### Breaking Changes
* Removed `Configuration.Endpoint`. Use `Configuration.Endpoints` instead. For
more info and an example, see the [Upgrading guide](./UPGRADING.md)
* Web framework integrations have been moved to separate repositories:
* [bugsnag-go-gin](https://github.com/bugsnag/bugsnag-go-gin)
* [bugsnag-go-negroni](https://github.com/bugsnag/bugsnag-go-negroni)
* [bugsnag-go-revel](https://github.com/bugsnag/bugsnag-go-revel)
* The `martini` framework integration has been retired
* `bugsnag.VERSION` has been renamed `bugsnag.Version`
### Enhancements
* Support configuring Bugsnag through environment variables
* Support reporting panics caused by overflowing the stack
## 1.9.0 (2021-01-05)
### Enhancements
* Support capturing "fatal error"-style panics from go, such as from concurrent
map read/writes, out of memory errors, and nil goroutines.
## 1.8.0 (2020-12-03)
### Enhancements
* Support unwrapping the underlying causes from an error, including attached
stack trace contents if available.
Any reported error which implements the following interface:
```go
type errorWithCause interface {
Unwrap() error
}
```
will have the cause included as a previous error in the resulting event. The
cause information will be available on the Bugsnag dashboard and is available
for inspection in callbacks on the `errors.Error` object.
```go
bugsnag.OnBeforeNotify(func(event *bugsnag.Event, config *bugsnag.Configuration) error {
if event.Error.Cause != nil {
fmt.Printf("This error was caused by %v", event.Error.Cause.Error())
}
return nil
})
```
## 1.7.0 (2020-11-18)
### Enhancements
* Support for changing the handled-ness of an event prior to delivery. This
allows for otherwise handled events to affect a project's stability score.
```go
bugsnag.Notify(err, func(event *bugsnag.Event) {
event.Unhandled = true
})
```
## 1.6.0 (2020-11-12)
### Enhancements
* Extract stacktrace contents on errors wrapped by
[`pkg/errors`](https://github.com/pkg/errors).
[#144](https://github.com/bugsnag/bugsnag-go/pull/144)
* Support modifying an individual event using a callback function argument.
```go
bugsnag.Notify(err, func(event *bugsnag.Event) {
event.ErrorClass = "Unexpected Termination"
event.MetaData.Update(loadJobData())
if event.Stacktrace[0].File = "mylogger.go" {
event.Stacktrace = event.Stacktrace[1:]
}
})
```
The stack trace of an event is now mutable so frames can be removed or
modified.
[#146](https://github.com/bugsnag/bugsnag-go/pull/146)
### Bug fixes
* Send web framework name with severity reason if set. Previously this value was
ignored, obscuring the severity reason for failed web requests captured by
bugsnag middleware.
[#143](https://github.com/bugsnag/bugsnag-go/pull/143)
## 1.5.4 (2020-10-28)
### Bug fixes
* Account for inlined frames when unwinding stack traces by using
`runtime.CallersFrames`.
[#114](https://github.com/bugsnag/bugsnag-go/pull/114)
[#140](https://github.com/bugsnag/bugsnag-go/pull/140)
## 1.5.3 (2019-07-11)
This release adds runtime version data to the report and session payloads, which will show up under the Device tab in the Bugsnag dashboard.
### Enhancements
* Ignore Gin unit tests when running against the latest version of Gin on Go versions below 1.10 as Gin has dropped support for these versions.
[#121](https://github.com/bugsnag/bugsnag-go/pull/121)
* Introduce runtime version data to the report and session payloads. Additionally adds the OS name to reports.
[#122](https://github.com/bugsnag/bugsnag-go/pull/122)
## 1.5.2 (2019-05-20)
This release adds `"access_token"` to the default list of keys to filter and introduces filtering of URL query parameters under the request tab.
### Enhancements
* Adds filtering of URL parameters in the request tab of an event. Additionally adds `access_token` to the `ParamsFilters` by default.
[#117](https://github.com/bugsnag/bugsnag-go/pull/117)
[Adam Renberg Tamm](https://github.com/tgwizard)
* Ignore Gin unit tests when running against the latest version of Gin on Go 1.7 as Gin has dropped support for Go 1.6 and Go 1.7.
[#118](https://github.com/bugsnag/bugsnag-go/pull/118)
## 1.5.1 (2019-04-15)
This release re-introduces prioritizing user specified error classes over the inferred error class.
### Bug fixes
* Fixes a bug introduced in `v1.4.0` where `bugsnag.Notify(err, bugsnag.ErrorClass{Name: "MyCustomErrorClass"})` is not respected.
[#115](https://github.com/bugsnag/bugsnag-go/pull/115)
## 1.5.0 (2019-03-26)
### Enhancements
* Testing improvements [#105](https://github.com/bugsnag/bugsnag-go/pull/105)
* Only run full test suite on PRs targeting master
* Test against the latest release of go (currently 1.12) rather than go's unstable master branch
* App engine has not been supported for a while. This release removes the app engine-specific code and tests from the codebase [#109](https://github.com/bugsnag/bugsnag-go/pull/109).
## 1.4.1 (2019-03-18)
This release fixes a compilation error on Windows.
Due to a missing implementation in the Go library, Windows users may have to send two interrupt signals to interrupt the application. Other signals are unaffected.
Additionally, ensure data sanitisation behaves the same for both request data and metadata.
### Bug fixes
* Use the `os` package instead of `syscall` to re-send signals, as `syscall` varies per platform, which caused a compilation error.
* Make sure that all data sanitization using `Config.ParamsFilters` behaves the same.
[#104](https://github.com/bugsnag/bugsnag-go/pull/104)
[Adam Renberg Tamm](https://github.com/tgwizard)
## 1.4.0 (2018-11-19)
This release is a big non-breaking revamp of the notifier. Most importantly, this release introduces session tracking to Go applications.
As of this release we require that you use Go 1.7 or higher.
### Features
* Session tracking to be able to show a stability score in the dashboard. Automatic recording of sessions for net/http, gin, revel, negroni and martini. Automatic capturing of sessions can be disabled using the `AutoCaptureSessions` configuration parameter.
* Automatic recording of HTTP request information such as HTTP method, headers, URL and query parameters.
### Enhancements
* Migrate report payload version from 3 to 4.
* Improve test coverage and introduce maze runner tests. Simplify integration tests for Negroni, Gin and Martini.
* Deprecate the use of the old `Endpoint` configuration parameter, and allow users of on-premise to configure both the notify endpoint and the sessions endpoint.
* `bugsnag.Notify()` now accepts a `context.Context` object, generally from `*http.Request`'s `r.Context()`, which Bugsnag can extract session and request information from.
* Improve and augment examples (`bugsnag_example_test.go`) for documentation.
* Improve example applications (`examples/` directory) to get up and running faster.
* Clarify and improve GoDocs.
* Improved serialization performance and safety of the report payload.
* Filter HTTP headers based on the `FiltersParams`.
* Revel enhancements:
* Ensure all non-code configuration options are configurable from config file.
* Stop using deprecated logger.
* Attempt to configure a what we can from the revel configuration options.
* Make NotifyReleaseStages work consistently with other notifiers, both for sessions and for reports.
* Also filter out 'authorization' and 'cookie' by default, to match other notifiers.
### Bug fixes
* Address compile errors test failures that failed the build.
* Don't crash when calling `bugsnag.Notify(nil)`
* Other minor bug fixes that came to light after improving test coverage.
## 1.3.2 (2018-10-05)
### Bug fixes
* Ensure error reports for fatal crashes gets sent
[#77](https://github.com/bugsnag/bugsnag-go/pull/77)
## 1.3.1 (2018-03-14)
### Bug fixes
* Add support for Revel v0.18
[#63](https://github.com/bugsnag/bugsnag-go/pull/63)
[Cameron Halter](https://github.com/EightB1ts)
## 1.3.0 (2017-10-02)
### Enhancements
* Track whether an error report was captured automatically
* Add SourceRoot as a configuration option, defaulting to `$GOPATH`
## 1.2.2 (2017-08-25)
### Bug fixes
* Point osext dependency at upstream, update with fixes
## 1.2.1 (2017-07-31)
### Bug fixes
* Improve goroutine panic reporting by sending reports synchronously in the
case that a goroutine is about to be cleaned up
[#52](https://github.com/bugsnag/bugsnag-go/pull/52)
## 1.2.0 (2017-07-03)
### Enhancements
* Support custom stack frame implementations
[alexanderwilling](https://github.com/alexanderwilling)
[#43](https://github.com/bugsnag/bugsnag-go/issues/43)
* Support app.type in error reports
[Jascha Ephraim](https://github.com/jaschaephraim)
[#51](https://github.com/bugsnag/bugsnag-go/pull/51)
### Bug fixes
* Mend nil pointer panic in metadata
[Johan Sageryd](https://github.com/jsageryd)
[#46](https://github.com/bugsnag/bugsnag-go/pull/46)
## 1.1.1 (2016-12-16)
### Bug fixes
* Replace empty error class property in reports with "error"
## 1.1.0 (2016-11-07)
### Enhancements
* Add middleware for Gin
[Mike Bull](https://github.com/bullmo)
[#40](https://github.com/bugsnag/bugsnag-go/pull/40)
* Add middleware for Negroni
[am-manideep](https://github.com/am-manideep)
[#28](https://github.com/bugsnag/bugsnag-go/pull/28)
* Support stripping subpackage names
[Facundo Ferrer](https://github.com/fjferrer)
[#25](https://github.com/bugsnag/bugsnag-go/pull/25)
* Support using `ErrorWithCallers` to create a stacktrace for errors
[Conrad Irwin](https://github.com/ConradIrwin)
[#35](https://github.com/bugsnag/bugsnag-go/pull/35)
## 1.0.5
### Bug fixes
* Avoid swallowing errors which occur upon delivery
1.0.4
-----
- Fix appengine integration broken by 1.0.3
1.0.3
-----
- Allow any Logger with a Printf method.
1.0.2
-----
- Use bugsnag copies of dependencies to avoid potential link rot
1.0.1
-----
- gofmt/golint/govet docs improvements.
1.0.0
-----

View File

@ -1,90 +0,0 @@
Contributing
============
- [Fork](https://help.github.com/articles/fork-a-repo) the [notifier on github](https://github.com/bugsnag/bugsnag-go)
- Build and test your changes
- Commit and push until you are happy with your contribution
- [Make a pull request](https://help.github.com/articles/using-pull-requests)
- Thanks!
Installing the go development environment
-------------------------------------
1. Install homebrew
```
ruby -e "$(curl -fsSL https://raw.github.com/Homebrew/homebrew/go/install)"
```
1. Install go
```
brew install go --cross-compile-all
```
1. Configure `$GOPATH` in `~/.bashrc`
```
export GOPATH="$HOME/go"
export PATH=$PATH:$GOPATH/bin
```
Downloading the code
--------------------
You can download the code and its dependencies using
```
go get -t github.com/bugsnag/bugsnag-go/v2
```
It will be put into "$GOPATH/src/github.com/bugsnag/bugsnag-go"
Then install depend
Running Tests
-------------
You can run the tests with
```shell
go test ./...
```
Making PRs
----------
All PRs should target the `next` branch as their base. This means that we can land them and stage them for a release without making multiple changes to `master` (which would cause multiple releases due to `go get`'s behaviour).
The exception to this rule is for an urgent bug fix when `next` is already ahead of `master`. See [hotfixes](#hotfixes) for what to do then.
Releasing a New Version
-----------------------
If you are a project maintainer, you can build and release a new version of
`bugsnag-go` as follows:
#### Planned releases
**Prerequisite**: All code changes should already have been reviewed and PR'd into the `next` branch before making a release.
1. Decide on a version number and date for this release
1. Add an entry (or update the `TBD` entry if it exists) for this release in `CHANGELOG.md` so that it includes the version number, release date and granular description of what changed
1. Update the README if necessary
1. Update the version number in `v2/bugsnag.go` and verify that tests pass.
1. Commit these changes `git commit -am "Preparing release"`
1. Create a PR from `next` -> `master` titled `Release vX.X.X`, adding a description to help the reviewer understand the scope of the release
1. Await PR approval and CI pass
1. Merge to master on GitHub, using the UI to set the merge commit message to be `vX.X.X`
1. Create a release from current `master` on GitHub called `vX.X.X`. Copy and paste the markdown from this release's notes in `CHANGELOG.md` (this will create a git tag for you).
1. Ensure setup guides for Go (and its frameworks) on docs.bugsnag.com are correct and up to date.
1. Merge `master` into `next` (since we just did a merge commit the other way, this will be a fastforward update) and push it so that it is ready for future PRs.
#### Hotfixes
If a `next` branch already exists and is ahead of `master` but there is a bug fix which needs to go out urgently, check out the latest `master` and create a new hotfix branch `git checkout -b hotfix`. You can then proceed to follow the above steps, substituting `next` for `hotfix`.
Once released, ensure `master` is merged into `next` so that the changes made on `hotfix` are included.

View File

@ -1,3 +0,0 @@
source 'https://rubygems.org'
gem 'bugsnag-maze-runner', git: 'https://github.com/bugsnag/maze-runner', branch: 'v1'

View File

@ -1,55 +0,0 @@
GIT
remote: https://github.com/bugsnag/maze-runner
revision: 7377529a77eb7585afc66cd2080fcdc4eea3306a
branch: v1
specs:
bugsnag-maze-runner (1.1.0)
cucumber (~> 3.1.0)
cucumber-expressions (= 5.0.15)
minitest (~> 5.0)
os (~> 1.0.0)
rack (~> 2.0.0)
rake (~> 12.3.3)
test-unit (~> 3.2.0)
GEM
remote: https://rubygems.org/
specs:
backports (3.21.0)
builder (3.2.4)
cucumber (3.1.0)
builder (>= 2.1.2)
cucumber-core (~> 3.1.0)
cucumber-expressions (~> 5.0.4)
cucumber-wire (~> 0.0.1)
diff-lcs (~> 1.3)
gherkin (~> 5.0)
multi_json (>= 1.7.5, < 2.0)
multi_test (>= 0.1.2)
cucumber-core (3.1.0)
backports (>= 3.8.0)
cucumber-tag_expressions (~> 1.1.0)
gherkin (>= 5.0.0)
cucumber-expressions (5.0.15)
cucumber-tag_expressions (1.1.1)
cucumber-wire (0.0.1)
diff-lcs (1.4.4)
gherkin (5.1.0)
minitest (5.14.4)
multi_json (1.15.0)
multi_test (0.1.2)
os (1.0.1)
power_assert (2.0.0)
rack (2.0.9)
rake (12.3.3)
test-unit (3.2.9)
power_assert
PLATFORMS
ruby
DEPENDENCIES
bugsnag-maze-runner!
BUNDLED WITH
2.1.4

View File

@ -1,20 +0,0 @@
Copyright (c) 2014 Bugsnag
Permission is hereby granted, free of charge, to any person obtaining
a copy of this software and associated documentation files (the
"Software"), to deal in the Software without restriction, including
without limitation the rights to use, copy, modify, merge, publish,
distribute, sublicense, and/or sell copies of the Software, and to
permit persons to whom the Software is furnished to do so, subject to
the following conditions:
The above copyright notice and this permission notice shall be
included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

View File

@ -1,63 +0,0 @@
TEST?=./...
export GO111MODULE=auto
default: alldeps test
deps:
go get -v -d ./...
alldeps:
go get -v -d -t ./...
updatedeps:
go get -v -d -u ./...
test: alldeps
@# skipping Gin if the Go version is lower than 1.9, as the latest version of Gin has dropped support for these versions.
@if [ "$(GO_VERSION)" = "1.7" ] || [ "$(GO_VERSION)" = "1.8" ] || [ "$(GO_VERSION)" = "1.9" ]; then \
go test . ./errors ./martini ./negroni ./sessions ./headers; \
else \
go test . ./errors ./gin ./martini ./negroni ./sessions ./headers; \
fi
@go vet 2>/dev/null ; if [ $$? -eq 3 ]; then \
go get golang.org/x/tools/cmd/vet; \
fi
@go vet $(TEST) ; if [ $$? -eq 1 ]; then \
echo "go-vet: Issues running go vet ./..."; \
exit 1; \
fi
maze:
bundle install
bundle exec bugsnag-maze-runner
ci: alldeps test
bench:
go test --bench=.*
testsetup:
gem update --system
gem install bundler
bundle install
testplain: testsetup
bundle exec bugsnag-maze-runner -c features/plain_features
testnethttp: testsetup
bundle exec bugsnag-maze-runner -c features/net_http_features
testgin: testsetup
bundle exec bugsnag-maze-runner -c features/gin_features
testmartini: testsetup
bundle exec bugsnag-maze-runner -c features/martini_features
testnegroni: testsetup
bundle exec bugsnag-maze-runner -c features/negroni_features
testrevel: testsetup
bundle exec bugsnag-maze-runner -c features/revel_features
.PHONY: bin checkversion ci default deps generate releasebin test testacc testrace updatedeps testsetup testplain testnethttp testgin testmartini testrevel

View File

@ -1,46 +0,0 @@
# Bugsnag error reporter for Go
[![Latest Version](http://img.shields.io/github/release/bugsnag/bugsnag-go.svg?style=flat-square)](https://github.com/bugsnag/bugsnag-go/releases)
[![Build Status](https://travis-ci.com/bugsnag/bugsnag-go.svg?branch=master)](https://travis-ci.com/bugsnag/bugsnag-go?branch=master)
[![Go Documentation](http://img.shields.io/badge/godoc-documentation-blue.svg?style=flat-square)](http://godoc.org/github.com/bugsnag/bugsnag-go)
Automatically detect crashes and report errors in your Go apps. Get alerts about errors and panics in real-time, including detailed error reports with diagnostic information. Understand and resolve issues as fast as possible.
Learn more about Bugsnag's [Go error monitoring and error reporting](https://www.bugsnag.com/platforms/go-lang-error-reporting/) solution.
## Features
* Automatically report unhandled errors and panics
* Report handled errors
* Attach user information to determine how many people are affected by a crash
* Send customized diagnostic data
## Getting Started
1. [Create a Bugsnag account](https://bugsnag.com)
2. Complete the instructions in the integration guide for your framework:
* [Martini](https://docs.bugsnag.com/platforms/go/martini)
* [Negroni](https://docs.bugsnag.com/platforms/go/negroni)
* [net/http](https://docs.bugsnag.com/platforms/go/net-http)
* [Revel](https://docs.bugsnag.com/platforms/go/revel)
* [Other Go apps](https://docs.bugsnag.com/platforms/go/other)
3. Relax!
## Support
* Read the configuration reference:
* [Martini](https://docs.bugsnag.com/platforms/go/martini/configuration-options/)
* [Negroni](https://docs.bugsnag.com/platforms/go/negroni/configuration-options/)
* [net/http](https://docs.bugsnag.com/platforms/go/net-http/configuration-options/)
* [Revel](https://docs.bugsnag.com/platforms/go/revel/configuration-options/)
* [Other Go apps](https://docs.bugsnag.com/platforms/go/other/configuration-options/)
* [Search open and closed issues](https://github.com/bugsnag/bugsnag-go/issues?utf8=✓&q=is%3Aissue) for similar problems
* [Report a bug or request a feature](https://github.com/bugsnag/bugsnag-go/issues/new)
## Contributing
All contributors are welcome! For information on how to build, test and release `bugsnag-go`, see our [contributing guide](CONTRIBUTING.md).
## License
The Bugsnag exception reporter for Go is free software released under the MIT License. See [LICENSE.txt](LICENSE.txt) for details.

View File

@ -1,57 +0,0 @@
# Upgrading guide
## v1 to v2
The v2 release adds support for Go modules, removes web framework
integrations from the main repository, and supports library configuration
through environment variables. The following breaking changes occurred as a part
of this release:
### Importing the package
```diff+go
- import "github.com/bugsnag/bugsnag-go"
+ import "github.com/bugsnag/bugsnag-go/v2"
```
### Removed `Configuration.Endpoint`
The `Endpoint` configuration option was deprecated as a part of the v1.4.0
release in November 2018. It was replaced with `Endpoints`, which includes
options for configuring both event and session delivery.
```diff+go
- config.Endpoint = "https://notify.myserver.example.com"
+ config.Endpoints = {
+ Notify: "https://notify.myserver.example.com",
+ Sessions: "https://sessions.myserver.example.com"
+ }
```
### Moved web framework integrations into separate repositories
Integrations with Negroni, Revel, and Gin now live in separate repositories, to
prevent implicit dependencies on every framework and to improve the ease of
updating each integration independently.
```diff+go
- import "github.com/bugsnag/bugsnag-go/negroni"
+ import "github.com/bugsnag/bugsnag-go-negroni"
```
```diff+go
- import "github.com/bugsnag/bugsnag-go/revel"
+ import "github.com/bugsnag/bugsnag-go-revel"
```
```diff+go
- import "github.com/bugsnag/bugsnag-go/gin"
+ import "github.com/bugsnag/bugsnag-go-gin"
```
### Renamed constants for platform consistency
```diff+go
- bugsnag.VERSION
+ bugsnag.Version
```

View File

@ -1,275 +0,0 @@
package bugsnag
import (
"context"
"fmt"
"log"
"net/http"
"os"
"path/filepath"
"runtime"
"sync"
"time"
"github.com/bugsnag/bugsnag-go/device"
"github.com/bugsnag/bugsnag-go/errors"
"github.com/bugsnag/bugsnag-go/sessions"
// Fixes a bug with SHA-384 intermediate certs on some platforms.
// - https://github.com/bugsnag/bugsnag-go/issues/9
_ "crypto/sha512"
)
// VERSION defines the version of this Bugsnag notifier
const VERSION = "1.9.1"
var panicHandlerOnce sync.Once
var sessionTrackerOnce sync.Once
var middleware middlewareStack
// Config is the configuration for the default bugsnag notifier.
var Config Configuration
var sessionTrackingConfig sessions.SessionTrackingConfiguration
// DefaultSessionPublishInterval defines how often sessions should be sent to
// Bugsnag.
// Deprecated: Exposed for developer sanity in testing. Modify at own risk.
var DefaultSessionPublishInterval = 60 * time.Second
var defaultNotifier = Notifier{&Config, nil}
var sessionTracker sessions.SessionTracker
// Configure Bugsnag. The only required setting is the APIKey, which can be
// obtained by clicking on "Settings" in your Bugsnag dashboard. This function
// is also responsible for installing the global panic handler, so it should be
// called as early as possible in your initialization process.
func Configure(config Configuration) {
Config.update(&config)
updateSessionConfig()
// Only do once in case the user overrides the default panichandler, and
// configures multiple times.
panicHandlerOnce.Do(Config.PanicHandler)
}
// StartSession creates new context from the context.Context instance with
// Bugsnag session data attached. Will start the session tracker if not already
// started
func StartSession(ctx context.Context) context.Context {
sessionTrackerOnce.Do(startSessionTracking)
return sessionTracker.StartSession(ctx)
}
// Notify sends an error.Error to Bugsnag along with the current stack trace.
// If at all possible, it is recommended to pass in a context.Context, e.g.
// from a http.Request or bugsnag.StartSession() as Bugsnag will be able to
// extract additional information in some cases. The rawData is used to send
// extra information along with the error. For example you can pass the current
// http.Request to Bugsnag to see information about it in the dashboard, or set
// the severity of the notification. For a detailed list of the information
// that can be extracted, see
// https://docs.bugsnag.com/platforms/go/reporting-handled-errors/
func Notify(err error, rawData ...interface{}) error {
if e := checkForEmptyError(err); e != nil {
return e
}
// Stripping one stackframe to not include this function in the stacktrace
// for a manual notification.
skipFrames := 1
return defaultNotifier.Notify(errors.New(err, skipFrames), rawData...)
}
// AutoNotify logs a panic on a goroutine and then repanics.
// It should only be used in places that have existing panic handlers further
// up the stack.
// Although it's not strictly enforced, it's highly recommended to pass a
// context.Context object that has at one-point been returned from
// bugsnag.StartSession. Doing so ensures your stability score remains accurate,
// and future versions of Bugsnag may extract more useful information from this
// context.
// The rawData is used to send extra information along with any
// panics that are handled this way.
// Usage:
// go func() {
// ctx := bugsnag.StartSession(context.Background())
// defer bugsnag.AutoNotify(ctx)
// // (possibly crashy code)
// }()
// See also: bugsnag.Recover()
func AutoNotify(rawData ...interface{}) {
if err := recover(); err != nil {
severity := defaultNotifier.getDefaultSeverity(rawData, SeverityError)
state := HandledState{SeverityReasonHandledPanic, severity, true, ""}
rawData = append([]interface{}{state}, rawData...)
// We strip the following stackframes as they don't add much info
// - runtime/$arch - e.g. runtime/asm_amd64.s#call32
// - runtime/panic.go#gopanic
// Panics have their own stacktrace, so no stripping of the current stack
skipFrames := 2
defaultNotifier.NotifySync(errors.New(err, skipFrames), true, rawData...)
sessionTracker.FlushSessions()
panic(err)
}
}
// Recover logs a panic on a goroutine and then recovers.
// Although it's not strictly enforced, it's highly recommended to pass a
// context.Context object that has at one-point been returned from
// bugsnag.StartSession. Doing so ensures your stability score remains accurate,
// and future versions of Bugsnag may extract more useful information from this
// context.
// The rawData is used to send extra information along with
// any panics that are handled this way
// Usage:
// go func() {
// ctx := bugsnag.StartSession(context.Background())
// defer bugsnag.Recover(ctx)
// // (possibly crashy code)
// }()
// If you wish that any panics caught by the call to Recover shall affect your
// stability score (it does not by default):
// go func() {
// ctx := bugsnag.StartSession(context.Background())
// defer bugsnag.Recover(ctx, bugsnag.HandledState{Unhandled: true})
// // (possibly crashy code)
// }()
// See also: bugsnag.AutoNotify()
func Recover(rawData ...interface{}) {
if err := recover(); err != nil {
severity := defaultNotifier.getDefaultSeverity(rawData, SeverityWarning)
state := HandledState{SeverityReasonHandledPanic, severity, false, ""}
rawData = append([]interface{}{state}, rawData...)
// We strip the following stackframes as they don't add much info
// - runtime/$arch - e.g. runtime/asm_amd64.s#call32
// - runtime/panic.go#gopanic
// Panics have their own stacktrace, so no stripping of the current stack
skipFrames := 2
defaultNotifier.Notify(errors.New(err, skipFrames), rawData...)
}
}
// OnBeforeNotify adds a callback to be run before a notification is sent to
// Bugsnag. It can be used to modify the event or its MetaData. Changes made
// to the configuration are local to notifying about this event. To prevent the
// event from being sent to Bugsnag return an error, this error will be
// returned from bugsnag.Notify() and the event will not be sent.
func OnBeforeNotify(callback func(event *Event, config *Configuration) error) {
middleware.OnBeforeNotify(callback)
}
// Handler creates an http Handler that notifies Bugsnag any panics that
// happen. It then repanics so that the default http Server panic handler can
// handle the panic too. The rawData is used to send extra information along
// with any panics that are handled this way.
func Handler(h http.Handler, rawData ...interface{}) http.Handler {
notifier := New(rawData...)
if h == nil {
h = http.DefaultServeMux
}
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
request := r
// Record a session if auto notify session is enabled
ctx := r.Context()
if Config.IsAutoCaptureSessions() {
ctx = StartSession(ctx)
}
ctx = AttachRequestData(ctx, request)
request = r.WithContext(ctx)
defer notifier.AutoNotify(ctx, request)
h.ServeHTTP(w, request)
})
}
// HandlerFunc creates an http HandlerFunc that notifies Bugsnag about any
// panics that happen. It then repanics so that the default http Server panic
// handler can handle the panic too. The rawData is used to send extra
// information along with any panics that are handled this way. If you have
// already wrapped your http server using bugsnag.Handler() you don't also need
// to wrap each HandlerFunc.
func HandlerFunc(h http.HandlerFunc, rawData ...interface{}) http.HandlerFunc {
notifier := New(rawData...)
return func(w http.ResponseWriter, r *http.Request) {
request := r
// Record a session if auto notify session is enabled
ctx := request.Context()
if notifier.Config.IsAutoCaptureSessions() {
ctx = StartSession(ctx)
}
ctx = AttachRequestData(ctx, request)
request = request.WithContext(ctx)
defer notifier.AutoNotify(ctx)
h(w, request)
}
}
// checkForEmptyError checks if the given error (to be reported to Bugsnag) is
// nil. If it is, then log an error message and return another error wrapping
// this error message.
func checkForEmptyError(err error) error {
if err != nil {
return nil
}
msg := "attempted to notify Bugsnag without supplying an error. Bugsnag not notified"
Config.Logger.Printf("ERROR: " + msg)
return fmt.Errorf(msg)
}
func init() {
// Set up builtin middlewarez
OnBeforeNotify(httpRequestMiddleware)
// Default configuration
sourceRoot := ""
if gopath := os.Getenv("GOPATH"); len(gopath) > 0 {
sourceRoot = filepath.Join(gopath, "src") + "/"
} else {
sourceRoot = filepath.Join(runtime.GOROOT(), "src") + "/"
}
Config.update(&Configuration{
APIKey: "",
Endpoints: Endpoints{
Notify: "https://notify.bugsnag.com",
Sessions: "https://sessions.bugsnag.com",
},
Hostname: device.GetHostname(),
AppType: "",
AppVersion: "",
AutoCaptureSessions: true,
ReleaseStage: "",
ParamsFilters: []string{"password", "secret", "authorization", "cookie", "access_token"},
SourceRoot: sourceRoot,
ProjectPackages: []string{"main*"},
NotifyReleaseStages: nil,
Logger: log.New(os.Stdout, log.Prefix(), log.Flags()),
PanicHandler: defaultPanicHandler,
Transport: http.DefaultTransport,
flushSessionsOnRepanic: true,
})
updateSessionConfig()
}
func startSessionTracking() {
if sessionTracker == nil {
updateSessionConfig()
sessionTracker = sessions.NewSessionTracker(&sessionTrackingConfig)
}
}
func updateSessionConfig() {
sessionTrackingConfig.Update(&sessions.SessionTrackingConfiguration{
APIKey: Config.APIKey,
AutoCaptureSessions: Config.AutoCaptureSessions,
Endpoint: Config.Endpoints.Sessions,
Version: VERSION,
PublishInterval: DefaultSessionPublishInterval,
Transport: Config.Transport,
ReleaseStage: Config.ReleaseStage,
Hostname: Config.Hostname,
AppType: Config.AppType,
AppVersion: Config.AppVersion,
NotifyReleaseStages: Config.NotifyReleaseStages,
Logger: Config.Logger,
})
}

View File

@ -1,264 +0,0 @@
package bugsnag
import (
"log"
"net/http"
"path/filepath"
"strings"
)
// Endpoints hold the HTTP endpoints of the notifier.
type Endpoints struct {
Sessions string
Notify string
}
// Configuration sets up and customizes communication with the Bugsnag API.
type Configuration struct {
// Your Bugsnag API key, e.g. "c9d60ae4c7e70c4b6c4ebd3e8056d2b8". You can
// find this by clicking Settings on https://bugsnag.com/.
APIKey string
// Deprecated: Use Endpoints (with an 's') instead.
// The Endpoint to notify about crashes. This defaults to
// "https://notify.bugsnag.com/", if you're using Bugsnag Enterprise then
// set it to your internal Bugsnag endpoint.
Endpoint string
// Endpoints define the HTTP endpoints that the notifier should notify
// about crashes and sessions. These default to notify.bugsnag.com for
// error reports and sessions.bugsnag.com for sessions.
// If you are using bugsnag on-premise you will have to set these to your
// Event Server and Session Server endpoints. If the notify endpoint is set
// but the sessions endpoint is not, session tracking will be disabled
// automatically to avoid leaking session information outside of your
// server configuration, and a warning will be logged.
Endpoints Endpoints
// The current release stage. This defaults to "production" and is used to
// filter errors in the Bugsnag dashboard.
ReleaseStage string
// A specialized type of the application, such as the worker queue or web
// framework used, like "rails", "mailman", or "celery"
AppType string
// The currently running version of the app. This is used to filter errors
// in the Bugsnag dasboard. If you set this then Bugsnag will only re-open
// resolved errors if they happen in different app versions.
AppVersion string
// AutoCaptureSessions can be set to false to disable automatic session
// tracking. If you want control over what is deemed a session, you can
// switch off automatic session tracking with this configuration, and call
// bugsnag.StartSession() when appropriate for your application. See the
// official docs for instructions and examples of associating handled
// errors with sessions and ensuring error rate accuracy on the Bugsnag
// dashboard. This will default to true, but is stored as an interface to enable
// us to detect when this option has not been set.
AutoCaptureSessions interface{}
// The hostname of the current server. This defaults to the return value of
// os.Hostname() and is graphed in the Bugsnag dashboard.
Hostname string
// The Release stages to notify in. If you set this then bugsnag-go will
// only send notifications to Bugsnag if the ReleaseStage is listed here.
NotifyReleaseStages []string
// packages that are part of your app. Bugsnag uses this to determine how
// to group errors and how to display them on your dashboard. You should
// include any packages that are part of your app, and exclude libraries
// and helpers. You can list wildcards here, and they'll be expanded using
// filepath.Glob. The default value is []string{"main*"}
ProjectPackages []string
// The SourceRoot is the directory where the application is built, and the
// assumed prefix of lines on the stacktrace originating in the parent
// application. When set, the prefix is trimmed from callstack file names
// before ProjectPackages for better readability and to better group errors
// on the Bugsnag dashboard. The default value is $GOPATH/src or $GOROOT/src
// if $GOPATH is unset. At runtime, $GOROOT is the root used during the Go
// build.
SourceRoot string
// Any meta-data that matches these filters will be marked as [FILTERED]
// before sending a Notification to Bugsnag. It defaults to
// []string{"password", "secret"} so that request parameters like password,
// password_confirmation and auth_secret will not be sent to Bugsnag.
ParamsFilters []string
// The PanicHandler is used by Bugsnag to catch unhandled panics in your
// application. The default panicHandler uses mitchellh's panicwrap library,
// and you can disable this feature by passing an empty: func() {}
PanicHandler func()
// The logger that Bugsnag should log to. Uses the same defaults as go's
// builtin logging package. bugsnag-go logs whenever it notifies Bugsnag
// of an error, and when any error occurs inside the library itself.
Logger interface {
Printf(format string, v ...interface{}) // limited to the functions used
}
// The http Transport to use, defaults to the default http Transport. This
// can be configured if you are in an environment
// that has stringent conditions on making http requests.
Transport http.RoundTripper
// Whether bugsnag should notify synchronously. This defaults to false which
// causes bugsnag-go to spawn a new goroutine for each notification.
Synchronous bool
// Whether the notifier should send all sessions recorded so far to Bugsnag
// when repanicking to ensure that no session information is lost in a
// fatal crash.
flushSessionsOnRepanic bool
// TODO: remember to update the update() function when modifying this struct
}
func (config *Configuration) update(other *Configuration) *Configuration {
if other.APIKey != "" {
config.APIKey = other.APIKey
}
if other.Hostname != "" {
config.Hostname = other.Hostname
}
if other.AppType != "" {
config.AppType = other.AppType
}
if other.AppVersion != "" {
config.AppVersion = other.AppVersion
}
if other.SourceRoot != "" {
config.SourceRoot = other.SourceRoot
}
if other.ReleaseStage != "" {
config.ReleaseStage = other.ReleaseStage
}
if other.ParamsFilters != nil {
config.ParamsFilters = other.ParamsFilters
}
if other.ProjectPackages != nil {
config.ProjectPackages = other.ProjectPackages
}
if other.Logger != nil {
config.Logger = other.Logger
}
if other.NotifyReleaseStages != nil {
config.NotifyReleaseStages = other.NotifyReleaseStages
}
if other.PanicHandler != nil {
config.PanicHandler = other.PanicHandler
}
if other.Transport != nil {
config.Transport = other.Transport
}
if other.Synchronous {
config.Synchronous = true
}
if other.AutoCaptureSessions != nil {
config.AutoCaptureSessions = other.AutoCaptureSessions
}
config.updateEndpoints(other.Endpoint, &other.Endpoints)
return config
}
// IsAutoCaptureSessions identifies whether or not the notifier should
// automatically capture sessions as requests come in. It's a convenience
// wrapper that allows automatic session capturing to be enabled by default.
func (config *Configuration) IsAutoCaptureSessions() bool {
if config.AutoCaptureSessions == nil {
return true // enabled by default
}
if val, ok := config.AutoCaptureSessions.(bool); ok {
return val
}
// It has been configured to *something* (although not a valid value)
// assume the user wanted to disable this option.
return false
}
func (config *Configuration) updateEndpoints(endpoint string, endpoints *Endpoints) {
if endpoint != "" {
config.Logger.Printf("WARNING: the 'Endpoint' Bugsnag configuration parameter is deprecated in favor of 'Endpoints'")
config.Endpoints.Notify = endpoint
config.Endpoints.Sessions = ""
}
if endpoints.Notify != "" {
config.Endpoints.Notify = endpoints.Notify
if endpoints.Sessions == "" {
config.Logger.Printf("WARNING: Bugsnag notify endpoint configured without also configuring the sessions endpoint. No sessions will be recorded")
config.Endpoints.Sessions = ""
}
}
if endpoints.Sessions != "" {
if endpoints.Notify == "" {
panic("FATAL: Bugsnag sessions endpoint configured without also changing the notify endpoint. Bugsnag cannot identify where to report errors")
}
config.Endpoints.Sessions = endpoints.Sessions
}
}
func (config *Configuration) merge(other *Configuration) *Configuration {
return config.clone().update(other)
}
func (config *Configuration) clone() *Configuration {
clone := *config
return &clone
}
func (config *Configuration) isProjectPackage(pkg string) bool {
for _, p := range config.ProjectPackages {
if d, f := filepath.Split(p); f == "**" {
if strings.HasPrefix(pkg, d) {
return true
}
}
if match, _ := filepath.Match(p, pkg); match {
return true
}
}
return false
}
func (config *Configuration) stripProjectPackages(file string) string {
trimmedFile := file
if strings.HasPrefix(trimmedFile, config.SourceRoot) {
trimmedFile = strings.TrimPrefix(trimmedFile, config.SourceRoot)
}
for _, p := range config.ProjectPackages {
if len(p) > 2 && p[len(p)-2] == '/' && p[len(p)-1] == '*' {
p = p[:len(p)-1]
} else if p[len(p)-1] == '*' && p[len(p)-2] == '*' {
p = p[:len(p)-2]
} else {
p = p + "/"
}
if strings.HasPrefix(trimmedFile, p) {
return strings.TrimPrefix(trimmedFile, p)
}
}
return trimmedFile
}
func (config *Configuration) logf(fmt string, args ...interface{}) {
if config != nil && config.Logger != nil {
config.Logger.Printf(fmt, args...)
} else {
log.Printf(fmt, args...)
}
}
func (config *Configuration) notifyInReleaseStage() bool {
if config.NotifyReleaseStages == nil {
return true
}
if config.ReleaseStage == "" {
return true
}
for _, r := range config.NotifyReleaseStages {
if r == config.ReleaseStage {
return true
}
}
return false
}

View File

@ -1,15 +0,0 @@
package device
import "os"
var hostname string
// GetHostname returns the hostname of the current device. Caches the hostname
// between calls to ensure this is performant. Returns a blank string in case
// that the hostname cannot be identified.
func GetHostname() string {
if hostname == "" {
hostname, _ = os.Hostname()
}
return hostname
}

View File

@ -1,50 +0,0 @@
package device
import (
"runtime"
)
// Cached runtime versions that can be updated globally by framework
// integrations through AddVersion.
var versions *RuntimeVersions
// RuntimeVersions define the various versions of Go and any framework that may
// be in use.
// As a user of the notifier you're unlikely to need to modify this struct.
// As such, the authors reserve the right to introduce breaking changes to the
// properties in this struct. In particular the framework versions are liable
// to change in new versions of the notifier in minor/patch versions.
type RuntimeVersions struct {
Go string `json:"go"`
Gin string `json:"gin,omitempty"`
Martini string `json:"martini,omitempty"`
Negroni string `json:"negroni,omitempty"`
Revel string `json:"revel,omitempty"`
}
// GetRuntimeVersions retrieves the recorded runtime versions in a goroutine-safe manner.
func GetRuntimeVersions() *RuntimeVersions {
if versions == nil {
versions = &RuntimeVersions{Go: runtime.Version()}
}
return versions
}
// AddVersion permits a framework to register its version, assuming it's one of
// the officially supported frameworks.
func AddVersion(framework, version string) {
if versions == nil {
versions = &RuntimeVersions{Go: runtime.Version()}
}
switch framework {
case "Martini":
versions.Martini = version
case "Gin":
versions.Gin = version
case "Negroni":
versions.Negroni = version
case "Revel":
versions.Revel = version
}
}

View File

@ -1,69 +0,0 @@
/*
Package bugsnag captures errors in real-time and reports them to Bugsnag (http://bugsnag.com).
Using bugsnag-go is a three-step process.
1. As early as possible in your program configure the notifier with your APIKey. This sets up
handling of panics that would otherwise crash your app.
func init() {
bugsnag.Configure(bugsnag.Configuration{
APIKey: "YOUR_API_KEY_HERE",
})
}
2. Add bugsnag to places that already catch panics. For example you should add it to the HTTP server
when you call ListenAndServer:
http.ListenAndServe(":8080", bugsnag.Handler(nil))
If that's not possible, you can also wrap each
HTTP handler manually:
http.HandleFunc("/" bugsnag.HandlerFunc(func (w http.ResponseWriter, r *http.Request) {
...
})
3. To notify Bugsnag of an error that is not a panic, pass it to bugsnag.Notify. This will also
log the error message using the configured Logger.
if err != nil {
bugsnag.Notify(err)
}
For detailed integration instructions see https://bugsnag.com/docs/notifiers/go.
Configuration
The only required configuration is the Bugsnag API key which can be obtained by clicking "Settings"
on the top of https://bugsnag.com/ after signing up. We also recommend you set the ReleaseStage,
AppType, and AppVersion if these make sense for your deployment workflow.
RawData
If you need to attach extra data to Bugsnag notifications you can do that using
the rawData mechanism. Most of the functions that send errors to Bugsnag allow
you to pass in any number of interface{} values as rawData. The rawData can
consist of the Severity, Context, User or MetaData types listed below, and
there is also builtin support for *http.Requests.
bugsnag.Notify(err, bugsnag.SeverityError)
If you want to add custom tabs to your bugsnag dashboard you can pass any value in as rawData,
and then process it into the event's metadata using a bugsnag.OnBeforeNotify() hook.
bugsnag.Notify(err, account)
bugsnag.OnBeforeNotify(func (e *bugsnag.Event, c *bugsnag.Configuration) {
for datum := range e.RawData {
if account, ok := datum.(Account); ok {
e.MetaData.Add("account", "name", account.Name)
e.MetaData.Add("account", "url", account.URL)
}
}
})
If necessary you can pass Configuration in as rawData, or modify the Configuration object passed
into OnBeforeNotify hooks. Configuration passed in this way only affects the current notification.
*/
package bugsnag

View File

@ -1,6 +0,0 @@
Adds stacktraces to errors in golang.
This was made to help build the Bugsnag notifier but can be used standalone if
you like to have stacktraces on errors.
See [Godoc](https://godoc.org/github.com/bugsnag/bugsnag-go/errors) for the API docs.

View File

@ -1,195 +0,0 @@
// Package errors provides errors that have stack-traces.
package errors
import (
"bytes"
"fmt"
"github.com/pkg/errors"
"reflect"
"runtime"
)
// The maximum number of stackframes on any error.
var MaxStackDepth = 50
// Error is an error with an attached stacktrace. It can be used
// wherever the builtin error interface is expected.
type Error struct {
Err error
Cause *Error
stack []uintptr
frames []StackFrame
}
// ErrorWithCallers allows passing in error objects that
// also have caller information attached.
type ErrorWithCallers interface {
Error() string
Callers() []uintptr
}
// ErrorWithStackFrames allows the stack to be rebuilt from the stack frames, thus
// allowing to use the Error type when the program counter is not available.
type ErrorWithStackFrames interface {
Error() string
StackFrames() []StackFrame
}
type errorWithStack interface {
StackTrace() errors.StackTrace
Error() string
}
type errorWithCause interface {
Unwrap() error
}
// New makes an Error from the given value. If that value is already an
// error then it will be used directly, if not, it will be passed to
// fmt.Errorf("%v"). The skip parameter indicates how far up the stack
// to start the stacktrace. 0 is from the current call, 1 from its caller, etc.
func New(e interface{}, skip int) *Error {
var err error
switch e := e.(type) {
case *Error:
return e
case ErrorWithCallers:
return &Error{
Err: e,
stack: e.Callers(),
Cause: unwrapCause(e),
}
case errorWithStack:
trace := e.StackTrace()
stack := make([]uintptr, len(trace))
for i, ptr := range trace {
stack[i] = uintptr(ptr) - 1
}
return &Error{
Err: e,
Cause: unwrapCause(e),
stack: stack,
}
case ErrorWithStackFrames:
stack := make([]uintptr, len(e.StackFrames()))
for i, frame := range e.StackFrames() {
stack[i] = frame.ProgramCounter
}
return &Error{
Err: e,
Cause: unwrapCause(e),
stack: stack,
frames: e.StackFrames(),
}
case error:
err = e
default:
err = fmt.Errorf("%v", e)
}
stack := make([]uintptr, MaxStackDepth)
length := runtime.Callers(2+skip, stack[:])
return &Error{
Err: err,
Cause: unwrapCause(err),
stack: stack[:length],
}
}
// Errorf creates a new error with the given message. You can use it
// as a drop-in replacement for fmt.Errorf() to provide descriptive
// errors in return values.
func Errorf(format string, a ...interface{}) *Error {
return New(fmt.Errorf(format, a...), 1)
}
// Error returns the underlying error's message.
func (err *Error) Error() string {
return err.Err.Error()
}
// Callers returns the raw stack frames as returned by runtime.Callers()
func (err *Error) Callers() []uintptr {
return err.stack[:]
}
// Stack returns the callstack formatted the same way that go does
// in runtime/debug.Stack()
func (err *Error) Stack() []byte {
buf := bytes.Buffer{}
for _, frame := range err.StackFrames() {
buf.WriteString(frame.String())
}
return buf.Bytes()
}
// StackFrames returns an array of frames containing information about the
// stack.
func (err *Error) StackFrames() []StackFrame {
if err.frames == nil {
callers := runtime.CallersFrames(err.stack)
err.frames = make([]StackFrame, 0, len(err.stack))
for frame, more := callers.Next(); more; frame, more = callers.Next() {
if frame.Func == nil {
// Ignore fully inlined functions
continue
}
pkg, name := packageAndName(frame.Func)
err.frames = append(err.frames, StackFrame{
function: frame.Func,
File: frame.File,
LineNumber: frame.Line,
Name: name,
Package: pkg,
ProgramCounter: frame.PC,
})
}
}
return err.frames
}
// TypeName returns the type this error. e.g. *errors.stringError.
func (err *Error) TypeName() string {
if p, ok := err.Err.(uncaughtPanic); ok {
return p.typeName
}
if name := reflect.TypeOf(err.Err).String(); len(name) > 0 {
return name
}
return "error"
}
func unwrapCause(err interface{}) *Error {
if causer, ok := err.(errorWithCause); ok {
cause := causer.Unwrap()
if cause == nil {
return nil
} else if hasStack(cause) { // avoid generating a (duplicate) stack from the current frame
return New(cause, 0)
} else {
return &Error{
Err: cause,
Cause: unwrapCause(cause),
stack: []uintptr{},
}
}
}
return nil
}
func hasStack(err error) bool {
if _, ok := err.(errorWithStack); ok {
return true
}
if _, ok := err.(ErrorWithStackFrames); ok {
return true
}
if _, ok := err.(ErrorWithCallers); ok {
return true
}
return false
}

View File

@ -1,137 +0,0 @@
package errors
import (
"strconv"
"strings"
)
type uncaughtPanic struct {
typeName string
message string
}
func (p uncaughtPanic) Error() string {
return p.message
}
// ParsePanic allows you to get an error object from the output of a go program
// that panicked. This is particularly useful with https://github.com/mitchellh/panicwrap.
func ParsePanic(text string) (*Error, error) {
lines := strings.Split(text, "\n")
prefixes := []string{"panic:", "fatal error:"}
state := "start"
var message string
var typeName string
var stack []StackFrame
for i := 0; i < len(lines); i++ {
line := lines[i]
if state == "start" {
for _, prefix := range prefixes {
if strings.HasPrefix(line, prefix) {
message = strings.TrimSpace(strings.TrimPrefix(line, prefix))
typeName = prefix[:len(prefix) - 1]
state = "seek"
break
}
}
if state == "start" {
return nil, Errorf("bugsnag.panicParser: Invalid line (no prefix): %s", line)
}
} else if state == "seek" {
if strings.HasPrefix(line, "goroutine ") && strings.HasSuffix(line, "[running]:") {
state = "parsing"
}
} else if state == "parsing" {
if line == "" || strings.HasPrefix(line, "...") {
state = "done"
break
}
createdBy := false
if strings.HasPrefix(line, "created by ") {
line = strings.TrimPrefix(line, "created by ")
createdBy = true
}
i++
if i >= len(lines) {
return nil, Errorf("bugsnag.panicParser: Invalid line (unpaired): %s", line)
}
frame, err := parsePanicFrame(line, lines[i], createdBy)
if err != nil {
return nil, err
}
stack = append(stack, *frame)
if createdBy {
state = "done"
break
}
}
}
if state == "done" || state == "parsing" {
return &Error{Err: uncaughtPanic{typeName, message}, frames: stack}, nil
}
return nil, Errorf("could not parse panic: %v", text)
}
// The lines we're passing look like this:
//
// main.(*foo).destruct(0xc208067e98)
// /0/go/src/github.com/bugsnag/bugsnag-go/pan/main.go:22 +0x151
func parsePanicFrame(name string, line string, createdBy bool) (*StackFrame, error) {
idx := strings.LastIndex(name, "(")
if idx == -1 && !createdBy {
return nil, Errorf("bugsnag.panicParser: Invalid line (no call): %s", name)
}
if idx != -1 {
name = name[:idx]
}
pkg := ""
if lastslash := strings.LastIndex(name, "/"); lastslash >= 0 {
pkg += name[:lastslash] + "/"
name = name[lastslash+1:]
}
if period := strings.Index(name, "."); period >= 0 {
pkg += name[:period]
name = name[period+1:]
}
name = strings.Replace(name, "·", ".", -1)
if !strings.HasPrefix(line, "\t") {
return nil, Errorf("bugsnag.panicParser: Invalid line (no tab): %s", line)
}
idx = strings.LastIndex(line, ":")
if idx == -1 {
return nil, Errorf("bugsnag.panicParser: Invalid line (no line number): %s", line)
}
file := line[1:idx]
number := line[idx+1:]
if idx = strings.Index(number, " +"); idx > -1 {
number = number[:idx]
}
lno, err := strconv.ParseInt(number, 10, 32)
if err != nil {
return nil, Errorf("bugsnag.panicParser: Invalid line (bad line number): %s", line)
}
return &StackFrame{
File: file,
LineNumber: int(lno),
Package: pkg,
Name: name,
}, nil
}

View File

@ -1,95 +0,0 @@
package errors
import (
"bytes"
"fmt"
"io/ioutil"
"runtime"
"strings"
)
// A StackFrame contains all necessary information about to generate a line
// in a callstack.
type StackFrame struct {
File string
LineNumber int
Name string
Package string
ProgramCounter uintptr
function *runtime.Func
}
// NewStackFrame popoulates a stack frame object from the program counter.
func NewStackFrame(pc uintptr) (frame StackFrame) {
frame = StackFrame{ProgramCounter: pc}
if frame.Func() == nil {
return
}
frame.Package, frame.Name = packageAndName(frame.Func())
// pc -1 because the program counters we use are usually return addresses,
// and we want to show the line that corresponds to the function call
frame.File, frame.LineNumber = frame.Func().FileLine(pc - 1)
return
}
// Func returns the function that this stackframe corresponds to
func (frame *StackFrame) Func() *runtime.Func {
return frame.function
}
// String returns the stackframe formatted in the same way as go does
// in runtime/debug.Stack()
func (frame *StackFrame) String() string {
str := fmt.Sprintf("%s:%d (0x%x)\n", frame.File, frame.LineNumber, frame.ProgramCounter)
source, err := frame.SourceLine()
if err != nil {
return str
}
return str + fmt.Sprintf("\t%s: %s\n", frame.Name, source)
}
// SourceLine gets the line of code (from File and Line) of the original source if possible
func (frame *StackFrame) SourceLine() (string, error) {
data, err := ioutil.ReadFile(frame.File)
if err != nil {
return "", err
}
lines := bytes.Split(data, []byte{'\n'})
if frame.LineNumber <= 0 || frame.LineNumber >= len(lines) {
return "???", nil
}
// -1 because line-numbers are 1 based, but our array is 0 based
return string(bytes.Trim(lines[frame.LineNumber-1], " \t")), nil
}
func packageAndName(fn *runtime.Func) (string, string) {
name := fn.Name()
pkg := ""
// 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
// Since the package path might contains dots (e.g. code.google.com/...),
// we first remove the path prefix if there is one.
if lastslash := strings.LastIndex(name, "/"); lastslash >= 0 {
pkg += name[:lastslash] + "/"
name = name[lastslash+1:]
}
if period := strings.Index(name, "."); period >= 0 {
pkg += name[:period]
name = name[period+1:]
}
name = strings.Replace(name, "·", ".", -1)
return pkg, name
}

View File

@ -1,248 +0,0 @@
package bugsnag
import (
"context"
"net/http"
"strings"
"github.com/bugsnag/bugsnag-go/errors"
)
// Context is the context of the error in Bugsnag.
// This can be passed to Notify, Recover or AutoNotify as rawData.
type Context struct {
String string
}
// User represents the searchable user-data on Bugsnag. The Id is also used
// to determine the number of users affected by a bug. This can be
// passed to Notify, Recover or AutoNotify as rawData.
type User struct {
Id string `json:"id,omitempty"`
Name string `json:"name,omitempty"`
Email string `json:"email,omitempty"`
}
// ErrorClass overrides the error class in Bugsnag.
// This struct enables you to group errors as you like.
type ErrorClass struct {
Name string
}
// Sets the severity of the error on Bugsnag. These values can be
// passed to Notify, Recover or AutoNotify as rawData.
var (
SeverityError = severity{"error"}
SeverityWarning = severity{"warning"}
SeverityInfo = severity{"info"}
)
// The severity tag type, private so that people can only use Error,Warning,Info
type severity struct {
String string
}
// The form of stacktrace that Bugsnag expects
type StackFrame struct {
Method string `json:"method"`
File string `json:"file"`
LineNumber int `json:"lineNumber"`
InProject bool `json:"inProject,omitempty"`
}
type SeverityReason string
const (
SeverityReasonCallbackSpecified SeverityReason = "userCallbackSetSeverity"
SeverityReasonHandledError = "handledError"
SeverityReasonHandledPanic = "handledPanic"
SeverityReasonUnhandledError = "unhandledError"
SeverityReasonUnhandledMiddlewareError = "unhandledErrorMiddleware"
SeverityReasonUnhandledPanic = "unhandledPanic"
SeverityReasonUserSpecified = "userSpecifiedSeverity"
)
type HandledState struct {
SeverityReason SeverityReason
OriginalSeverity severity
Unhandled bool
Framework string
}
// Event represents a payload of data that gets sent to Bugsnag.
// This is passed to each OnBeforeNotify hook.
type Event struct {
// The original error that caused this event, not sent to Bugsnag.
Error *errors.Error
// The rawData affecting this error, not sent to Bugsnag.
RawData []interface{}
// The error class to be sent to Bugsnag. This defaults to the type name of the Error, for
// example *error.String
ErrorClass string
// The error message to be sent to Bugsnag. This defaults to the return value of Error.Error()
Message string
// The stacktrrace of the error to be sent to Bugsnag.
Stacktrace []StackFrame
// The context to be sent to Bugsnag. This should be set to the part of the app that was running,
// e.g. for http requests, set it to the path.
Context string
// The severity of the error. Can be SeverityError, SeverityWarning or SeverityInfo.
Severity severity
// The grouping hash is used to override Bugsnag's grouping. Set this if you'd like all errors with
// the same grouping hash to group together in the dashboard.
GroupingHash string
// User data to send to Bugsnag. This is searchable on the dashboard.
User *User
// Other MetaData to send to Bugsnag. Appears as a set of tabbed tables in the dashboard.
MetaData MetaData
// Ctx is the context of the session the event occurred in. This allows Bugsnag to associate the event with the session.
Ctx context.Context
// Request is the request information that populates the Request tab in the dashboard.
Request *RequestJSON
// The reason for the severity and original value
handledState HandledState
// True if the event was caused by an automatic event
Unhandled bool
}
func newEvent(rawData []interface{}, notifier *Notifier) (*Event, *Configuration) {
config := notifier.Config
event := &Event{
RawData: append(notifier.RawData, rawData...),
Severity: SeverityWarning,
MetaData: make(MetaData),
handledState: HandledState{
SeverityReason: SeverityReasonHandledError,
OriginalSeverity: SeverityWarning,
Unhandled: false,
Framework: "",
},
Unhandled: false,
}
var err *errors.Error
var callbacks []func(*Event)
for _, datum := range event.RawData {
switch datum := datum.(type) {
case error, errors.Error:
err = errors.New(datum.(error), 1)
event.Error = err
// Only assign automatically if not explicitly set through ErrorClass already
if event.ErrorClass == "" {
event.ErrorClass = err.TypeName()
}
event.Message = err.Error()
event.Stacktrace = make([]StackFrame, len(err.StackFrames()))
case bool:
config = config.merge(&Configuration{Synchronous: bool(datum)})
case severity:
event.Severity = datum
event.handledState.OriginalSeverity = datum
event.handledState.SeverityReason = SeverityReasonUserSpecified
case Context:
event.Context = datum.String
case context.Context:
populateEventWithContext(datum, event)
case *http.Request:
populateEventWithRequest(datum, event)
case Configuration:
config = config.merge(&datum)
case MetaData:
event.MetaData.Update(datum)
case User:
event.User = &datum
case ErrorClass:
event.ErrorClass = datum.Name
case HandledState:
event.handledState = datum
event.Severity = datum.OriginalSeverity
event.Unhandled = datum.Unhandled
case func(*Event):
callbacks = append(callbacks, datum)
}
}
event.Stacktrace = generateStacktrace(err, config)
for _, callback := range callbacks {
callback(event)
if event.Severity != event.handledState.OriginalSeverity {
event.handledState.SeverityReason = SeverityReasonCallbackSpecified
}
}
return event, config
}
func generateStacktrace(err *errors.Error, config *Configuration) []StackFrame {
stack := make([]StackFrame, len(err.StackFrames()))
for i, frame := range err.StackFrames() {
file := frame.File
inProject := config.isProjectPackage(frame.Package)
// remove $GOROOT and $GOHOME from other frames
if idx := strings.Index(file, frame.Package); idx > -1 {
file = file[idx:]
}
if inProject {
file = config.stripProjectPackages(file)
}
stack[i] = StackFrame{
Method: frame.Name,
File: file,
LineNumber: frame.LineNumber,
InProject: inProject,
}
}
return stack
}
func populateEventWithContext(ctx context.Context, event *Event) {
event.Ctx = ctx
reqJSON, req := extractRequestInfo(ctx)
if event.Request == nil {
event.Request = reqJSON
}
populateEventWithRequest(req, event)
}
func populateEventWithRequest(req *http.Request, event *Event) {
if req == nil {
return
}
event.Request = extractRequestInfoFromReq(req)
if event.Context == "" {
event.Context = req.URL.Path
}
// Default user.id to IP so that the count of users affected works.
if event.User == nil {
ip := req.RemoteAddr
if idx := strings.LastIndex(ip, ":"); idx != -1 {
ip = ip[:idx]
}
event.User = &User{Id: ip}
}
}

View File

@ -1,14 +0,0 @@
package headers
import "time"
//PrefixedHeaders returns a map of Content-Type and the 'Bugsnag-' headers for
//API key, payload version, and the time at which the request is being sent.
func PrefixedHeaders(apiKey, payloadVersion string) map[string]string {
return map[string]string{
"Content-Type": "application/json",
"Bugsnag-Api-Key": apiKey,
"Bugsnag-Payload-Version": payloadVersion,
"Bugsnag-Sent-At": time.Now().UTC().Format(time.RFC3339),
}
}

View File

@ -1,43 +0,0 @@
// The code is stripped from:
// http://golang.org/src/pkg/encoding/json/tags.go?m=text
package bugsnag
import (
"strings"
)
// tagOptions is the string following a comma in a struct field's "json"
// tag, or the empty string. It does not include the leading comma.
type tagOptions string
// parseTag splits a struct field's json tag into its name and
// comma-separated options.
func parseTag(tag string) (string, tagOptions) {
if idx := strings.Index(tag, ","); idx != -1 {
return tag[:idx], tagOptions(tag[idx+1:])
}
return tag, tagOptions("")
}
// Contains reports whether a comma-separated list of options
// contains a particular substr flag. substr must be surrounded by a
// string boundary or commas.
func (o tagOptions) Contains(optionName string) bool {
if len(o) == 0 {
return false
}
s := string(o)
for s != "" {
var next string
i := strings.Index(s, ",")
if i >= 0 {
s, next = s[:i], s[i+1:]
}
if s == optionName {
return true
}
s = next
}
return false
}

View File

@ -1,192 +0,0 @@
package bugsnag
import (
"fmt"
"reflect"
"strings"
)
// MetaData is added to the Bugsnag dashboard in tabs. Each tab is
// a map of strings -> values. You can pass MetaData to Notify, Recover
// and AutoNotify as rawData.
type MetaData map[string]map[string]interface{}
// Update the meta-data with more information. Tabs are merged together such
// that unique keys from both sides are preserved, and duplicate keys end up
// with the provided values.
func (meta MetaData) Update(other MetaData) {
for name, tab := range other {
if meta[name] == nil {
meta[name] = make(map[string]interface{})
}
for key, value := range tab {
meta[name][key] = value
}
}
}
// Add creates a tab of Bugsnag meta-data.
// If the tab doesn't yet exist it will be created.
// If the key already exists, it will be overwritten.
func (meta MetaData) Add(tab string, key string, value interface{}) {
if meta[tab] == nil {
meta[tab] = make(map[string]interface{})
}
meta[tab][key] = value
}
// AddStruct creates a tab of Bugsnag meta-data.
// The struct will be converted to an Object using the
// reflect library so any private fields will not be exported.
// As a safety measure, if you pass a non-struct the value will be
// sent to Bugsnag under the "Extra data" tab.
func (meta MetaData) AddStruct(tab string, obj interface{}) {
val := sanitizer{}.Sanitize(obj)
content, ok := val.(map[string]interface{})
if ok {
meta[tab] = content
} else {
// Wasn't a struct
meta.Add("Extra data", tab, obj)
}
}
// Remove any values from meta-data that have keys matching the filters,
// and any that are recursive data-structures
func (meta MetaData) sanitize(filters []string) interface{} {
return sanitizer{
Filters: filters,
Seen: make([]interface{}, 0),
}.Sanitize(meta)
}
// The sanitizer is used to remove filtered params and recursion from meta-data.
type sanitizer struct {
Filters []string
Seen []interface{}
}
func (s sanitizer) Sanitize(data interface{}) interface{} {
for _, s := range s.Seen {
// TODO: we don't need deep equal here, just type-ignoring equality
if reflect.DeepEqual(data, s) {
return "[RECURSION]"
}
}
// Sanitizers are passed by value, so we can modify s and it only affects
// s.Seen for nested calls.
s.Seen = append(s.Seen, data)
t := reflect.TypeOf(data)
v := reflect.ValueOf(data)
if t == nil {
return "<nil>"
}
switch t.Kind() {
case reflect.Bool,
reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64,
reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64, reflect.Uintptr,
reflect.Float32, reflect.Float64:
return data
case reflect.String:
return data
case reflect.Interface, reflect.Ptr:
if v.IsNil() {
return "<nil>"
}
return s.Sanitize(v.Elem().Interface())
case reflect.Array, reflect.Slice:
ret := make([]interface{}, v.Len())
for i := 0; i < v.Len(); i++ {
ret[i] = s.Sanitize(v.Index(i).Interface())
}
return ret
case reflect.Map:
return s.sanitizeMap(v)
case reflect.Struct:
return s.sanitizeStruct(v, t)
// Things JSON can't serialize:
// case t.Chan, t.Func, reflect.Complex64, reflect.Complex128, reflect.UnsafePointer:
default:
return "[" + t.String() + "]"
}
}
func (s sanitizer) sanitizeMap(v reflect.Value) interface{} {
ret := make(map[string]interface{})
for _, key := range v.MapKeys() {
val := s.Sanitize(v.MapIndex(key).Interface())
newKey := fmt.Sprintf("%v", key.Interface())
if s.shouldRedact(newKey) {
val = "[FILTERED]"
}
ret[newKey] = val
}
return ret
}
func (s sanitizer) sanitizeStruct(v reflect.Value, t reflect.Type) interface{} {
ret := make(map[string]interface{})
for i := 0; i < v.NumField(); i++ {
val := v.Field(i)
// Don't export private fields
if !val.CanInterface() {
continue
}
name := t.Field(i).Name
var opts tagOptions
// Parse JSON tags. Supports name and "omitempty"
if jsonTag := t.Field(i).Tag.Get("json"); len(jsonTag) != 0 {
name, opts = parseTag(jsonTag)
}
if s.shouldRedact(name) {
ret[name] = "[FILTERED]"
} else {
sanitized := s.Sanitize(val.Interface())
if str, ok := sanitized.(string); ok {
if !(opts.Contains("omitempty") && len(str) == 0) {
ret[name] = str
}
} else {
ret[name] = sanitized
}
}
}
return ret
}
func (s sanitizer) shouldRedact(key string) bool {
for _, filter := range s.Filters {
if strings.Contains(strings.ToLower(key), strings.ToLower(filter)) {
return true
}
}
return false
}

View File

@ -1,74 +0,0 @@
package bugsnag
import (
"net/http"
)
type (
beforeFunc func(*Event, *Configuration) error
// MiddlewareStacks keep middleware in the correct order. They are
// called in reverse order, so if you add a new middleware it will
// be called before all existing middleware.
middlewareStack struct {
before []beforeFunc
}
)
// AddMiddleware adds a new middleware to the outside of the existing ones,
// when the middlewareStack is Run it will be run before all middleware that
// have been added before.
func (stack *middlewareStack) OnBeforeNotify(middleware beforeFunc) {
stack.before = append(stack.before, middleware)
}
// Run causes all the middleware to be run. If they all permit it the next callback
// will be called with all the middleware on the stack.
func (stack *middlewareStack) Run(event *Event, config *Configuration, next func() error) error {
// run all the before filters in reverse order
for i := range stack.before {
before := stack.before[len(stack.before)-i-1]
severity := event.Severity
err := stack.runBeforeFilter(before, event, config)
if err != nil {
return err
}
if event.Severity != severity {
event.handledState.SeverityReason = SeverityReasonCallbackSpecified
}
}
return next()
}
func (stack *middlewareStack) runBeforeFilter(f beforeFunc, event *Event, config *Configuration) error {
defer func() {
if err := recover(); err != nil {
config.logf("bugsnag/middleware: unexpected panic: %v", err)
}
}()
return f(event, config)
}
// catchMiddlewarePanic is used to log any panics that happen inside Middleware,
// we wouldn't want to not notify Bugsnag in this case.
func catchMiddlewarePanic(event *Event, config *Configuration, next func() error) {
}
// httpRequestMiddleware is added OnBeforeNotify by default. It takes information
// from an http.Request passed in as rawData, and adds it to the Event. You can
// use this as a template for writing your own Middleware.
func httpRequestMiddleware(event *Event, config *Configuration) error {
for _, datum := range event.RawData {
if request, ok := datum.(*http.Request); ok {
event.MetaData.Update(MetaData{
"request": {
"params": request.URL.Query(),
},
})
}
}
return nil
}

View File

@ -1,151 +0,0 @@
package bugsnag
import (
"github.com/bugsnag/bugsnag-go/errors"
)
var publisher reportPublisher = new(defaultReportPublisher)
// Notifier sends errors to Bugsnag.
type Notifier struct {
Config *Configuration
RawData []interface{}
}
// New creates a new notifier.
// You can pass an instance of bugsnag.Configuration in rawData to change the configuration.
// Other values of rawData will be passed to Notify.
func New(rawData ...interface{}) *Notifier {
config := Config.clone()
for i, datum := range rawData {
if c, ok := datum.(Configuration); ok {
config.update(&c)
rawData[i] = nil
}
}
return &Notifier{
Config: config,
RawData: rawData,
}
}
// FlushSessionsOnRepanic takes a boolean that indicates whether sessions
// should be flushed when AutoNotify repanics. In the case of a fatal panic the
// sessions might not get sent to Bugsnag before the application shuts down.
// Many frameworks will have their own error handler, and for these frameworks
// there is no need to flush sessions as the application will survive the panic
// and the sessions can be sent off later. The default value is true, so this
// needs only be called if you wish to inform Bugsnag that there is an error
// handler that will take care of panics that AutoNotify will re-raise.
func (notifier *Notifier) FlushSessionsOnRepanic(shouldFlush bool) {
notifier.Config.flushSessionsOnRepanic = shouldFlush
}
// Notify sends an error to Bugsnag. Any rawData you pass here will be sent to
// Bugsnag after being converted to JSON. e.g. bugsnag.SeverityError, bugsnag.Context,
// or bugsnag.MetaData. Any bools in rawData overrides the
// notifier.Config.Synchronous flag.
func (notifier *Notifier) Notify(err error, rawData ...interface{}) (e error) {
if e := checkForEmptyError(err); e != nil {
return e
}
// Stripping one stackframe to not include this function in the stacktrace
// for a manual notification.
skipFrames := 1
return notifier.NotifySync(errors.New(err, skipFrames), notifier.Config.Synchronous, rawData...)
}
// NotifySync sends an error to Bugsnag. A boolean parameter specifies whether
// to send the report in the current context (by default false, i.e.
// asynchronous). Any other rawData you pass here will be sent to Bugsnag after
// being converted to JSON. E.g. bugsnag.SeverityError, bugsnag.Context, or
// bugsnag.MetaData.
func (notifier *Notifier) NotifySync(err error, sync bool, rawData ...interface{}) error {
if e := checkForEmptyError(err); e != nil {
return e
}
// Stripping one stackframe to not include this function in the stacktrace
// for a manual notification.
skipFrames := 1
event, config := newEvent(append(rawData, errors.New(err, skipFrames), sync), notifier)
// Never block, start throwing away errors if we have too many.
e := middleware.Run(event, config, func() error {
return publisher.publishReport(&payload{event, config})
})
if e != nil {
config.logf("bugsnag.Notify: %v", e)
}
return e
}
// AutoNotify notifies Bugsnag of any panics, then repanics.
// It sends along any rawData that gets passed in.
// Usage:
// go func() {
// defer AutoNotify()
// // (possibly crashy code)
// }()
func (notifier *Notifier) AutoNotify(rawData ...interface{}) {
if err := recover(); err != nil {
severity := notifier.getDefaultSeverity(rawData, SeverityError)
state := HandledState{SeverityReasonHandledPanic, severity, true, ""}
rawData = notifier.appendStateIfNeeded(rawData, state)
// We strip the following stackframes as they don't add much
// information but would mess with the grouping algorithm
// { "file": "github.com/bugsnag/bugsnag-go/notifier.go", "lineNumber": 116, "method": "(*Notifier).AutoNotify" },
// { "file": "runtime/asm_amd64.s", "lineNumber": 573, "method": "call32" },
skipFrames := 2
notifier.NotifySync(errors.New(err, skipFrames), true, rawData...)
panic(err)
}
}
// Recover logs any panics, then recovers.
// It sends along any rawData that gets passed in.
// Usage: defer Recover()
func (notifier *Notifier) Recover(rawData ...interface{}) {
if err := recover(); err != nil {
severity := notifier.getDefaultSeverity(rawData, SeverityWarning)
state := HandledState{SeverityReasonHandledPanic, severity, false, ""}
rawData = notifier.appendStateIfNeeded(rawData, state)
notifier.Notify(errors.New(err, 2), rawData...)
}
}
func (notifier *Notifier) dontPanic() {
if err := recover(); err != nil {
notifier.Config.logf("bugsnag/notifier.Notify: panic! %s", err)
}
}
// Get defined severity from raw data or a fallback value
func (notifier *Notifier) getDefaultSeverity(rawData []interface{}, s severity) severity {
allData := append(notifier.RawData, rawData...)
for _, datum := range allData {
if _, ok := datum.(severity); ok {
return datum.(severity)
}
}
for _, datum := range allData {
if _, ok := datum.(HandledState); ok {
return datum.(HandledState).OriginalSeverity
}
}
return s
}
func (notifier *Notifier) appendStateIfNeeded(rawData []interface{}, h HandledState) []interface{} {
for _, datum := range append(notifier.RawData, rawData...) {
if _, ok := datum.(HandledState); ok {
return rawData
}
}
return append(rawData, h)
}

View File

@ -1,32 +0,0 @@
package bugsnag
import (
"github.com/bugsnag/bugsnag-go/errors"
"github.com/bugsnag/bugsnag-go/sessions"
"github.com/bugsnag/panicwrap"
)
// Forks and re-runs your program to add panic monitoring. This function does
// not return on one process, instead listening on stderr of the other process,
// which returns nil.
//
// Related: https://godoc.org/github.com/bugsnag/panicwrap#BasicMonitor
func defaultPanicHandler() {
defer defaultNotifier.dontPanic()
ctx := sessions.SendStartupSession(&sessionTrackingConfig)
err := panicwrap.BasicMonitor(func(output string) {
toNotify, err := errors.ParsePanic(output)
if err != nil {
defaultNotifier.Config.logf("bugsnag.handleUncaughtPanic: %v", err)
}
state := HandledState{SeverityReasonUnhandledPanic, SeverityError, true, ""}
defaultNotifier.NotifySync(toNotify, true, state, ctx)
})
if err != nil {
defaultNotifier.Config.logf("bugsnag.handleUncaughtPanic: %v", err)
}
}

View File

@ -1,162 +0,0 @@
package bugsnag
import (
"bytes"
"encoding/json"
"fmt"
"net/http"
"runtime"
"sync"
"time"
"github.com/bugsnag/bugsnag-go/device"
"github.com/bugsnag/bugsnag-go/headers"
"github.com/bugsnag/bugsnag-go/sessions"
)
const notifyPayloadVersion = "4"
var sessionMutex sync.Mutex
type payload struct {
*Event
*Configuration
}
type hash map[string]interface{}
func (p *payload) deliver() error {
if len(p.APIKey) != 32 {
return fmt.Errorf("bugsnag/payload.deliver: invalid api key: '%s'", p.APIKey)
}
buf, err := p.MarshalJSON()
if err != nil {
return fmt.Errorf("bugsnag/payload.deliver: %v", err)
}
client := http.Client{
Transport: p.Transport,
}
req, err := http.NewRequest("POST", p.Endpoints.Notify, bytes.NewBuffer(buf))
if err != nil {
return fmt.Errorf("bugsnag/payload.deliver unable to create request: %v", err)
}
for k, v := range headers.PrefixedHeaders(p.APIKey, notifyPayloadVersion) {
req.Header.Add(k, v)
}
resp, err := client.Do(req)
if err != nil {
return fmt.Errorf("bugsnag/payload.deliver: %v", err)
}
defer resp.Body.Close()
if resp.StatusCode != 200 {
return fmt.Errorf("bugsnag/payload.deliver: Got HTTP %s", resp.Status)
}
return nil
}
func (p *payload) MarshalJSON() ([]byte, error) {
return json.Marshal(reportJSON{
APIKey: p.APIKey,
Events: []eventJSON{
eventJSON{
App: &appJSON{
ReleaseStage: p.ReleaseStage,
Type: p.AppType,
Version: p.AppVersion,
},
Context: p.Context,
Device: &deviceJSON{
Hostname: p.Hostname,
OsName: runtime.GOOS,
RuntimeVersions: device.GetRuntimeVersions(),
},
Request: p.Request,
Exceptions: p.exceptions(),
GroupingHash: p.GroupingHash,
Metadata: p.MetaData.sanitize(p.ParamsFilters),
PayloadVersion: notifyPayloadVersion,
Session: p.makeSession(),
Severity: p.Severity.String,
SeverityReason: p.severityReasonPayload(),
Unhandled: p.Unhandled,
User: p.User,
},
},
Notifier: notifierJSON{
Name: "Bugsnag Go",
URL: "https://github.com/bugsnag/bugsnag-go",
Version: VERSION,
},
})
}
func (p *payload) makeSession() *sessionJSON {
// If a context has not been applied to the payload then assume that no
// session has started either
if p.Ctx == nil {
return nil
}
sessionMutex.Lock()
defer sessionMutex.Unlock()
session := sessions.IncrementEventCountAndGetSession(p.Ctx, p.Unhandled)
if session != nil {
s := *session
return &sessionJSON{
ID: s.ID,
StartedAt: s.StartedAt.UTC().Format(time.RFC3339),
Events: sessions.EventCounts{
Handled: s.EventCounts.Handled,
Unhandled: s.EventCounts.Unhandled,
},
}
}
return nil
}
func (p *payload) severityReasonPayload() *severityReasonJSON {
if reason := p.handledState.SeverityReason; reason != "" {
json := &severityReasonJSON{
Type: reason,
UnhandledOverridden: p.handledState.Unhandled != p.Unhandled,
}
if p.handledState.Framework != "" {
json.Attributes = make(map[string]string, 1)
json.Attributes["framework"] = p.handledState.Framework
}
return json
}
return nil
}
func (p *payload) exceptions() []exceptionJSON {
exceptions := []exceptionJSON{
exceptionJSON{
ErrorClass: p.ErrorClass,
Message: p.Message,
Stacktrace: p.Stacktrace,
},
}
if p.Error == nil {
return exceptions
}
cause := p.Error.Cause
for cause != nil {
exceptions = append(exceptions, exceptionJSON{
ErrorClass: cause.TypeName(),
Message: cause.Error(),
Stacktrace: generateStacktrace(cause, p.Configuration),
})
cause = cause.Cause
}
return exceptions
}

View File

@ -1,75 +0,0 @@
package bugsnag
import (
"github.com/bugsnag/bugsnag-go/device"
"github.com/bugsnag/bugsnag-go/sessions"
uuid "github.com/google/uuid"
)
type reportJSON struct {
APIKey string `json:"apiKey"`
Events []eventJSON `json:"events"`
Notifier notifierJSON `json:"notifier"`
}
type notifierJSON struct {
Name string `json:"name"`
URL string `json:"url"`
Version string `json:"version"`
}
type eventJSON struct {
App *appJSON `json:"app"`
Context string `json:"context,omitempty"`
Device *deviceJSON `json:"device,omitempty"`
Request *RequestJSON `json:"request,omitempty"`
Exceptions []exceptionJSON `json:"exceptions"`
GroupingHash string `json:"groupingHash,omitempty"`
Metadata interface{} `json:"metaData"`
PayloadVersion string `json:"payloadVersion"`
Session *sessionJSON `json:"session,omitempty"`
Severity string `json:"severity"`
SeverityReason *severityReasonJSON `json:"severityReason,omitempty"`
Unhandled bool `json:"unhandled"`
User *User `json:"user,omitempty"`
}
type sessionJSON struct {
StartedAt string `json:"startedAt"`
ID uuid.UUID `json:"id"`
Events sessions.EventCounts `json:"events"`
}
type appJSON struct {
ReleaseStage string `json:"releaseStage"`
Type string `json:"type,omitempty"`
Version string `json:"version,omitempty"`
}
type exceptionJSON struct {
ErrorClass string `json:"errorClass"`
Message string `json:"message"`
Stacktrace []StackFrame `json:"stacktrace"`
}
type severityReasonJSON struct {
Type SeverityReason `json:"type,omitempty"`
Attributes map[string]string `json:"attributes,omitempty"`
UnhandledOverridden bool `json:"unhandledOverridden,omitempty"`
}
type deviceJSON struct {
Hostname string `json:"hostname,omitempty"`
OsName string `json:"osName,omitempty"`
RuntimeVersions *device.RuntimeVersions `json:"runtimeVersions,omitempty"`
}
// RequestJSON is the request information that populates the Request tab in the dashboard.
type RequestJSON struct {
ClientIP string `json:"clientIp,omitempty"`
Headers map[string]string `json:"headers,omitempty"`
HTTPMethod string `json:"httpMethod,omitempty"`
URL string `json:"url,omitempty"`
Referer string `json:"referer,omitempty"`
}

View File

@ -1,27 +0,0 @@
package bugsnag
import "fmt"
type reportPublisher interface {
publishReport(*payload) error
}
type defaultReportPublisher struct{}
func (*defaultReportPublisher) publishReport(p *payload) error {
p.logf("notifying bugsnag: %s", p.Message)
if !p.notifyInReleaseStage() {
return fmt.Errorf("not notifying in %s", p.ReleaseStage)
}
if p.Synchronous {
return p.deliver()
}
go func(p *payload) {
if err := p.deliver(); err != nil {
// Ensure that any errors are logged if they occur in a goroutine.
p.logf("bugsnag/defaultReportPublisher.publishReport: %v", err)
}
}(p)
return nil
}

View File

@ -1,115 +0,0 @@
package bugsnag
import (
"context"
"net/http"
"net/url"
"strings"
)
const requestContextKey requestKey = iota
type requestKey int
// AttachRequestData returns a child of the given context with the request
// object attached for later extraction by the notifier in order to
// automatically record request data
func AttachRequestData(ctx context.Context, r *http.Request) context.Context {
return context.WithValue(ctx, requestContextKey, r)
}
// extractRequestInfo looks for the request object that the notifier
// automatically attaches to the context when using any of the supported
// frameworks or bugsnag.HandlerFunc or bugsnag.Handler, and returns sub-object
// supported by the notify API.
func extractRequestInfo(ctx context.Context) (*RequestJSON, *http.Request) {
if req := getRequestIfPresent(ctx); req != nil {
return extractRequestInfoFromReq(req), req
}
return nil, nil
}
// extractRequestInfoFromReq extracts the request information the notify API
// understands from the given HTTP request. Returns the sub-object supported by
// the notify API.
func extractRequestInfoFromReq(req *http.Request) *RequestJSON {
return &RequestJSON{
ClientIP: req.RemoteAddr,
HTTPMethod: req.Method,
URL: sanitizeURL(req),
Referer: req.Referer(),
Headers: parseRequestHeaders(req.Header),
}
}
// sanitizeURL will build up the URL matching the request. It will filter query parameters to remove sensitive fields.
// The query part of the URL might appear differently (different order of parameters) if any filtering was done.
func sanitizeURL(req *http.Request) string {
scheme := "http"
if req.TLS != nil {
scheme = "https"
}
rawQuery := req.URL.RawQuery
parsedQuery, err := url.ParseQuery(req.URL.RawQuery)
if err != nil {
return scheme + "://" + req.Host + req.RequestURI
}
changed := false
for key, values := range parsedQuery {
if contains(Config.ParamsFilters, key) {
for i := range values {
values[i] = "BUGSNAG_URL_FILTERED"
changed = true
}
}
}
if changed {
rawQuery = parsedQuery.Encode()
rawQuery = strings.Replace(rawQuery, "BUGSNAG_URL_FILTERED", "[FILTERED]", -1)
}
u := url.URL{
Scheme: scheme,
Host: req.Host,
Path: req.URL.Path,
RawQuery: rawQuery,
}
return u.String()
}
func parseRequestHeaders(header map[string][]string) map[string]string {
headers := make(map[string]string)
for k, v := range header {
// Headers can have multiple values, in which case we report them as csv
if contains(Config.ParamsFilters, k) {
headers[k] = "[FILTERED]"
} else {
headers[k] = strings.Join(v, ",")
}
}
return headers
}
func contains(slice []string, e string) bool {
for _, s := range slice {
if strings.Contains(strings.ToLower(e), strings.ToLower(s)) {
return true
}
}
return false
}
func getRequestIfPresent(ctx context.Context) *http.Request {
if ctx == nil {
return nil
}
val := ctx.Value(requestContextKey)
if val == nil {
return nil
}
return val.(*http.Request)
}

View File

@ -1,127 +0,0 @@
package sessions
import (
"log"
"net/http"
"sync"
"time"
)
// SessionTrackingConfiguration defines the configuration options relevant for session tracking.
// These are likely a subset of the global bugsnag.Configuration. Users should
// not modify this struct directly but rather call
// `bugsnag.Configure(bugsnag.Configuration)` which will update this configuration in return.
type SessionTrackingConfiguration struct {
// PublishInterval defines how often the sessions are sent off to the session server.
PublishInterval time.Duration
// AutoCaptureSessions can be set to false to disable automatic session
// tracking. If you want control over what is deemed a session, you can
// switch off automatic session tracking with this configuration, and call
// bugsnag.StartSession() when appropriate for your application. See the
// official docs for instructions and examples of associating handled
// errors with sessions and ensuring error rate accuracy on the Bugsnag
// dashboard. This will default to true, but is stored as an interface to enable
// us to detect when this option has not been set.
AutoCaptureSessions interface{}
// APIKey defines the API key for the Bugsnag project. Same value as for reporting errors.
APIKey string
// Endpoint is the URI of the session server to receive session payloads.
Endpoint string
// Version defines the current version of the notifier.
Version string
// ReleaseStage defines the release stage, e.g. "production" or "staging",
// that this session occurred in. The release stage, in combination with
// the app version make up the release that Bugsnag tracks.
ReleaseStage string
// Hostname defines the host of the server this application is running on.
Hostname string
// AppType defines the type of the application.
AppType string
// AppVersion defines the version of the application.
AppVersion string
// Transport defines the http.RoundTripper to be used for managing HTTP requests.
Transport http.RoundTripper
// The release stages to notify about sessions in. If you set this then
// bugsnag-go will only send sessions to Bugsnag if the release stage
// is listed here.
NotifyReleaseStages []string
// Logger is the logger that Bugsnag should log to. Uses the same defaults
// as go's builtin logging package. This logger gets invoked when any error
// occurs inside the library itself.
Logger interface {
Printf(format string, v ...interface{})
}
mutex sync.Mutex
}
// Update modifies the values inside the receiver to match the non-default properties of the given config.
// Existing properties will not be cleared when given empty fields.
func (c *SessionTrackingConfiguration) Update(config *SessionTrackingConfiguration) {
c.mutex.Lock()
defer c.mutex.Unlock()
if config.PublishInterval != 0 {
c.PublishInterval = config.PublishInterval
}
if config.APIKey != "" {
c.APIKey = config.APIKey
}
if config.Endpoint != "" {
c.Endpoint = config.Endpoint
}
if config.Version != "" {
c.Version = config.Version
}
if config.ReleaseStage != "" {
c.ReleaseStage = config.ReleaseStage
}
if config.Hostname != "" {
c.Hostname = config.Hostname
}
if config.AppType != "" {
c.AppType = config.AppType
}
if config.AppVersion != "" {
c.AppVersion = config.AppVersion
}
if config.Transport != nil {
c.Transport = config.Transport
}
if config.Logger != nil {
c.Logger = config.Logger
}
if config.NotifyReleaseStages != nil {
c.NotifyReleaseStages = config.NotifyReleaseStages
}
if config.AutoCaptureSessions != nil {
c.AutoCaptureSessions = config.AutoCaptureSessions
}
}
func (c *SessionTrackingConfiguration) logf(fmt string, args ...interface{}) {
if c != nil && c.Logger != nil {
c.Logger.Printf(fmt, args...)
} else {
log.Printf(fmt, args...)
}
}
// IsAutoCaptureSessions identifies whether or not the notifier should
// automatically capture sessions as requests come in. It's a convenience
// wrapper that allows automatic session capturing to be enabled by default.
func (c *SessionTrackingConfiguration) IsAutoCaptureSessions() bool {
if c.AutoCaptureSessions == nil {
return true // enabled by default
}
if val, ok := c.AutoCaptureSessions.(bool); ok {
return val
}
// It has been configured to *something* (although not a valid value)
// assume the user wanted to disable this option.
return false
}

View File

@ -1,81 +0,0 @@
package sessions
import (
"runtime"
"time"
"github.com/bugsnag/bugsnag-go/device"
)
// notifierPayload defines the .notifier subobject of the payload
type notifierPayload struct {
Name string `json:"name"`
URL string `json:"url"`
Version string `json:"version"`
}
// appPayload defines the .app subobject of the payload
type appPayload struct {
Type string `json:"type,omitempty"`
ReleaseStage string `json:"releaseStage,omitempty"`
Version string `json:"version,omitempty"`
}
// devicePayload defines the .device subobject of the payload
type devicePayload struct {
OsName string `json:"osName,omitempty"`
Hostname string `json:"hostname,omitempty"`
RuntimeVersions *device.RuntimeVersions `json:"runtimeVersions"`
}
// sessionCountsPayload defines the .sessionCounts subobject of the payload
type sessionCountsPayload struct {
StartedAt string `json:"startedAt"`
SessionsStarted int `json:"sessionsStarted"`
}
// sessionPayload defines the top level payload object
type sessionPayload struct {
Notifier *notifierPayload `json:"notifier"`
App *appPayload `json:"app"`
Device *devicePayload `json:"device"`
SessionCounts []sessionCountsPayload `json:"sessionCounts"`
}
// makeSessionPayload creates a sessionPayload based off of the given sessions and config
func makeSessionPayload(sessions []*Session, config *SessionTrackingConfiguration) *sessionPayload {
releaseStage := config.ReleaseStage
if releaseStage == "" {
releaseStage = "production"
}
hostname := config.Hostname
if hostname == "" {
hostname = device.GetHostname()
}
return &sessionPayload{
Notifier: &notifierPayload{
Name: "Bugsnag Go",
URL: "https://github.com/bugsnag/bugsnag-go",
Version: config.Version,
},
App: &appPayload{
Type: config.AppType,
Version: config.AppVersion,
ReleaseStage: releaseStage,
},
Device: &devicePayload{
OsName: runtime.GOOS,
Hostname: hostname,
RuntimeVersions: device.GetRuntimeVersions(),
},
SessionCounts: []sessionCountsPayload{
{
//This timestamp assumes that we're sending these off once a minute
StartedAt: sessions[0].StartedAt.UTC().Format(time.RFC3339),
SessionsStarted: len(sessions),
},
},
}
}

View File

@ -1,87 +0,0 @@
package sessions
import (
"bytes"
"encoding/json"
"fmt"
"net/http"
"github.com/bugsnag/bugsnag-go/headers"
)
// sessionPayloadVersion defines the current version of the payload that's
// being sent to the session server.
const sessionPayloadVersion = "1.0"
type sessionPublisher interface {
publish(sessions []*Session) error
}
type httpClient interface {
Do(*http.Request) (*http.Response, error)
}
type publisher struct {
config *SessionTrackingConfiguration
client httpClient
}
// publish builds a payload from the given sessions and publishes them to the
// session server. Returns any errors that happened as part of publishing.
func (p *publisher) publish(sessions []*Session) error {
if p.config.Endpoint == "" {
// Session tracking is disabled, likely because the notify endpoint was
// changed without changing the sessions endpoint
// We've already logged a warning in this case, so no need to spam the
// log every minute
return nil
}
if apiKey := p.config.APIKey; len(apiKey) != 32 {
return fmt.Errorf("bugsnag/sessions/publisher.publish invalid API key: '%s'", apiKey)
}
nrs, rs := p.config.NotifyReleaseStages, p.config.ReleaseStage
if rs != "" && (nrs != nil && !contains(nrs, rs)) {
// Always send sessions if the release stage is not set, but don't send any
// sessions when notify release stages don't match the current release stage
return nil
}
if len(sessions) == 0 {
return fmt.Errorf("bugsnag/sessions/publisher.publish requested publication of 0")
}
p.config.mutex.Lock()
defer p.config.mutex.Unlock()
payload := makeSessionPayload(sessions, p.config)
buf, err := json.Marshal(payload)
if err != nil {
return fmt.Errorf("bugsnag/sessions/publisher.publish unable to marshal json: %v", err)
}
req, err := http.NewRequest("POST", p.config.Endpoint, bytes.NewBuffer(buf))
if err != nil {
return fmt.Errorf("bugsnag/sessions/publisher.publish unable to create request: %v", err)
}
for k, v := range headers.PrefixedHeaders(p.config.APIKey, sessionPayloadVersion) {
req.Header.Add(k, v)
}
res, err := p.client.Do(req)
if err != nil {
return fmt.Errorf("bugsnag/sessions/publisher.publish unable to deliver session: %v", err)
}
defer func(res *http.Response) {
if err := res.Body.Close(); err != nil {
p.config.logf("%v", err)
}
}(res)
if res.StatusCode != 202 {
return fmt.Errorf("bugsnag/session.publish expected 202 response status, got HTTP %s", res.Status)
}
return nil
}
func contains(coll []string, e string) bool {
for _, s := range coll {
if s == e {
return true
}
}
return false
}

View File

@ -1,29 +0,0 @@
package sessions
import (
"time"
uuid "github.com/google/uuid"
)
// EventCounts register how many handled/unhandled events have happened for
// this session
type EventCounts struct {
Handled int `json:"handled"`
Unhandled int `json:"unhandled"`
}
// Session represents a start time and a unique ID that identifies the session.
type Session struct {
StartedAt time.Time
ID uuid.UUID
EventCounts *EventCounts
}
func newSession() *Session {
return &Session{
StartedAt: time.Now(),
ID: uuid.New(),
EventCounts: &EventCounts{},
}
}

View File

@ -1,35 +0,0 @@
package sessions
import (
"context"
"net/http"
"os"
"github.com/bugsnag/panicwrap"
)
// SendStartupSession is called by Bugsnag on startup, which will send a
// session to Bugsnag and return a context to represent the session of the main
// goroutine. This is the session associated with any fatal panics that are
// caught by panicwrap.
func SendStartupSession(config *SessionTrackingConfiguration) context.Context {
ctx := context.Background()
session := newSession()
if !config.IsAutoCaptureSessions() || isApplicationProcess() {
return ctx
}
publisher := &publisher{
config: config,
client: &http.Client{Transport: config.Transport},
}
go publisher.publish([]*Session{session})
return context.WithValue(ctx, contextSessionKey, session)
}
// Checks to see if this is the application process, as opposed to the process
// that monitors for panics
func isApplicationProcess() bool {
// Application process is run first, and this will only have been set when
// the monitoring process runs
return "" == os.Getenv(panicwrap.DEFAULT_COOKIE_KEY)
}

View File

@ -1,158 +0,0 @@
package sessions
import (
"context"
"net/http"
"os"
"os/signal"
"sync"
"syscall"
"time"
)
const (
//contextSessionKey is a unique key for accessing and setting Bugsnag
//session data on a context.Context object
contextSessionKey ctxKey = 1
)
// ctxKey is a type alias that ensures uniqueness as a context.Context key
type ctxKey int
// SessionTracker exposes a method for starting sessions that are used for
// gauging your application's health
type SessionTracker interface {
StartSession(context.Context) context.Context
FlushSessions()
}
type sessionTracker struct {
sessionChannel chan *Session
sessions []*Session
config *SessionTrackingConfiguration
publisher sessionPublisher
sessionsMutex sync.Mutex
}
// NewSessionTracker creates a new SessionTracker based on the provided config,
func NewSessionTracker(config *SessionTrackingConfiguration) SessionTracker {
publisher := publisher{
config: config,
client: &http.Client{Transport: config.Transport},
}
st := sessionTracker{
sessionChannel: make(chan *Session, 1),
sessions: []*Session{},
config: config,
publisher: &publisher,
}
go st.processSessions()
return &st
}
// IncrementEventCountAndGetSession extracts a Bugsnag session from the given
// context and increments the event count of unhandled or handled events and
// returns the session
func IncrementEventCountAndGetSession(ctx context.Context, unhandled bool) *Session {
if s := ctx.Value(contextSessionKey); s != nil {
if session, ok := s.(*Session); ok && !session.StartedAt.IsZero() {
// It is not just getting back a default value
ec := session.EventCounts
if unhandled {
ec.Unhandled++
} else {
ec.Handled++
}
return session
}
}
return nil
}
func (s *sessionTracker) StartSession(ctx context.Context) context.Context {
session := newSession()
s.sessionChannel <- session
return context.WithValue(ctx, contextSessionKey, session)
}
func (s *sessionTracker) interval() time.Duration {
s.config.mutex.Lock()
defer s.config.mutex.Unlock()
return s.config.PublishInterval
}
func (s *sessionTracker) processSessions() {
tic := time.Tick(s.interval())
shutdown := shutdownSignals()
for {
select {
case session := <-s.sessionChannel:
s.appendSession(session)
case <-tic:
s.publishCollectedSessions()
case sig := <-shutdown:
s.flushSessionsAndRepeatSignal(shutdown, sig.(syscall.Signal))
}
}
}
func (s *sessionTracker) appendSession(session *Session) {
s.sessionsMutex.Lock()
defer s.sessionsMutex.Unlock()
s.sessions = append(s.sessions, session)
}
func (s *sessionTracker) publishCollectedSessions() {
s.sessionsMutex.Lock()
defer s.sessionsMutex.Unlock()
oldSessions := s.sessions
s.sessions = nil
if len(oldSessions) > 0 {
go func(s *sessionTracker) {
err := s.publisher.publish(oldSessions)
if err != nil {
s.config.logf("%v", err)
}
}(s)
}
}
func (s *sessionTracker) flushSessionsAndRepeatSignal(shutdown chan<- os.Signal, sig syscall.Signal) {
s.sessionsMutex.Lock()
defer s.sessionsMutex.Unlock()
signal.Stop(shutdown)
if len(s.sessions) > 0 {
err := s.publisher.publish(s.sessions)
if err != nil {
s.config.logf("%v", err)
}
}
if p, err := os.FindProcess(os.Getpid()); err != nil {
s.config.logf("%v", err)
} else {
p.Signal(sig)
}
}
func (s *sessionTracker) FlushSessions() {
s.sessionsMutex.Lock()
defer s.sessionsMutex.Unlock()
sessions := s.sessions
s.sessions = nil
if len(sessions) != 0 {
if err := s.publisher.publish(sessions); err != nil {
s.config.logf("%v", err)
}
}
}
func shutdownSignals() chan os.Signal {
c := make(chan os.Signal, 1)
signal.Notify(c, syscall.SIGTERM, syscall.SIGINT)
return c
}

View File

@ -1,76 +0,0 @@
## 1.3.4 (2021-08-10)
### Bug fixes
* Fix build support for arm64-based macOS by updating build settings for
`dup2()`
## 1.3.3 (2021-07-13)
### Bug fixes
* Fix build support for arm64 linux by updating build settings for `dup2()`
[Robert-André Mauchin](https://github.com/eclipseo)
[#21](https://github.com/bugsnag/panicwrap/pull/21)
## 1.3.2 (2021-03-25)
### Bug fixes
* Add build support from arm64-based macOS by updating build settings for
`dup2()`
## 1.3.1 (2021-01-12)
This release removes the dependency on [osext](https://github.com/kardianos/osext)
for everyone running go1.8+.
### Bug fixes
* Fix windows support by removing undefined syscall
## 1.3.0 (2021-01-05)
### Features
* Support capturing fatal errors from concurrent map writes, nil goroutines,
out of memory errors, stack exhaustion, and others which use a different panic
output format.
## 1.2.2 (2020-12-17)
### Bug fixes
* Fix compatibility with go1.7-1.8 by removing dependency on "math/bits" package
## 1.2.1 (2020-12-04)
### Bug fixes
* Fix compatibility with solaris and friends (AIX, etc)
[Till Wegmüller](https://github.com/Toasterson)
[#11](https://github.com/bugsnag/panicwrap/pull/11)
## 1.2.0 (2017-08-08)
### Bug fixes
* Fix bug where the program would relaunch without the panic wrapper
[emersion](https://github.com/emersion)
[#3](https://github.com/bugsnag/panicwrap/pull/3)
* Fix Solaris build
[Brian Meyers](https://github.com/bmeyers22)
[#4](https://github.com/bugsnag/panicwrap/pull/4)
## 1.1.0 (2016-01-18)
* Add ARM64 support
[liusdu](https://github.com/liusdu)
[#1](https://github.com/bugsnag/panicwrap/pull/1)
## 1.0.0 (2014-11-10)
### Enhancements
* Add ability to monitor a process

View File

@ -1,21 +0,0 @@
The MIT License (MIT)
Copyright (c) 2013 Mitchell Hashimoto
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.

View File

@ -1,106 +0,0 @@
# panicwrap
panicwrap is a Go library that re-executes a Go binary and monitors stderr
output from the binary for a panic. When it find a panic, it executes a
user-defined handler function. Stdout, stderr, stdin, signals, and exit
codes continue to work as normal, making the existence of panicwrap mostly
invisble to the end user until a panic actually occurs.
Since a panic is truly a bug in the program meant to crash the runtime,
globally catching panics within Go applications is not supposed to be possible.
Despite this, it is often useful to have a way to know when panics occur.
panicwrap allows you to do something with these panics, such as writing them
to a file, so that you can track when panics occur.
panicwrap is ***not a panic recovery system***. Panics indicate serious
problems with your application and _should_ crash the runtime. panicwrap
is just meant as a way to monitor for panics. If you still think this is
the worst idea ever, read the section below on why.
_This is a fork of [mitchellh/panicwrap](https://github.com/mitchellh/panicwrap)
which adds support for monitoring for panics without interrupting signal
handling on supported platforms. More information is available in the
[documentation](https://godoc.org/github.com/bugsnag/panicwrap#BasicMonitor)._
## Features
* **SIMPLE!**
* Works with all Go applications on all platforms Go supports
* Custom behavior when a panic occurs
* Stdout, stderr, stdin, exit codes, and signals continue to work as
expected.
## Usage
Using panicwrap is simple. It behaves a lot like `fork`, if you know
how that works. A basic example is shown below.
Because it would be sad to panic while capturing a panic, it is recommended
that the handler functions for panicwrap remain relatively simple and well
tested. panicwrap itself contains many tests.
```go
package main
import (
"fmt"
"github.com/mitchellh/panicwrap"
"os"
)
func main() {
exitStatus, err := panicwrap.BasicWrap(panicHandler)
if err != nil {
// Something went wrong setting up the panic wrapper. Unlikely,
// but possible.
panic(err)
}
// If exitStatus >= 0, then we're the parent process and the panicwrap
// re-executed ourselves and completed. Just exit with the proper status.
if exitStatus >= 0 {
os.Exit(exitStatus)
}
// Otherwise, exitStatus < 0 means we're the child. Continue executing as
// normal...
// Let's say we panic
panic("oh shucks")
}
func panicHandler(output string) {
// output contains the full output (including stack traces) of the
// panic. Put it in a file or something.
fmt.Printf("The child panicked:\n\n%s\n", output)
os.Exit(1)
}
```
## How Does it Work?
panicwrap works by re-executing the running program (retaining arguments,
environmental variables, etc.) and monitoring the stderr of the program.
Since Go always outputs panics in a predictable way with a predictable
exit code, panicwrap is able to reliably detect panics and allow the parent
process to handle them.
## WHY?! Panics should CRASH!
Yes, panics _should_ crash. They are 100% always indicative of bugs.
However, in some cases, such as user-facing programs (programs like
[Packer](http://github.com/mitchellh/packer) or
[Docker](http://github.com/dotcloud/docker)), it is up to the user to
report such panics. This is unreliable, at best, and it would be better if the
program could have a way to automatically report panics. panicwrap provides
a way to do this.
For backend applications, it is easier to detect crashes (since the application
exits). However, it is still nice sometimes to more intelligently log
panics in some way. For example, at [HashiCorp](http://www.hashicorp.com),
we use panicwrap to log panics to timestamped files with some additional
data (configuration settings at the time, environmental variables, etc.)
The goal of panicwrap is _not_ to hide panics. It is instead to provide
a clean mechanism for handling them before bubbling the up to the user
and ultimately crashing.

View File

@ -1,11 +0,0 @@
// +build linux,!arm64 !linux,!windows
package panicwrap
import (
"syscall"
)
func dup2(oldfd, newfd int) error {
return syscall.Dup2(oldfd, newfd)
}

View File

@ -1,11 +0,0 @@
//+build linux,arm64
package panicwrap
import (
"syscall"
)
func dup2(oldfd, newfd int) error {
return syscall.Dup3(oldfd, newfd, 0)
}

Some files were not shown because too many files have changed in this diff Show More