├── worker.go ├── .github ├── dependabot.yml ├── ISSUE_TEMPLATE.md ├── workflows │ ├── tests.yml │ ├── check-pr-title.yml │ └── release.yml └── PULL_REQUEST_TEMPLATE.md ├── .travis.yml ├── context.go ├── context_test.go ├── go.mod ├── .gitignore ├── context_sync.go ├── Makefile ├── context_sync_test.go ├── null_backend.go ├── buffered_worker.go ├── LICENSE ├── configuration_test.go ├── error.go ├── server.go ├── notice_test.go ├── error_test.go ├── go.sum ├── configuration.go ├── CHANGELOG.md ├── honeybadger.go ├── client_test.go ├── client.go ├── notice.go ├── honeybadger_test.go └── README.md /worker.go: -------------------------------------------------------------------------------- 1 | package honeybadger 2 | 3 | type envelope func() error 4 | 5 | type worker interface { 6 | Push(envelope) error 7 | Flush() 8 | } 9 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: "gomod" 4 | directory: "/" 5 | schedule: 6 | interval: "weekly" 7 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: go 2 | install: go get -t ./... 3 | go: 4 | - 1.9.x 5 | - 1.10.x 6 | - 1.11.x 7 | - 1.12.x 8 | - stable 9 | - tip 10 | 11 | env: 12 | global: 13 | - GO111MODULE=on 14 | -------------------------------------------------------------------------------- /context.go: -------------------------------------------------------------------------------- 1 | package honeybadger 2 | 3 | // Context is used to send extra data to Honeybadger. 4 | type Context hash 5 | 6 | // Update applies the values in other Context to context. 7 | func (context Context) Update(other Context) { 8 | for k, v := range other { 9 | context[k] = v 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /context_test.go: -------------------------------------------------------------------------------- 1 | package honeybadger 2 | 3 | import "testing" 4 | 5 | func TestContextUpdate(t *testing.T) { 6 | c := Context{"foo": "bar"} 7 | c.Update(Context{"foo": "baz"}) 8 | if c["foo"] != "baz" { 9 | t.Errorf("Context should update values. expected=%#v actual=%#v", "baz", c["foo"]) 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/honeybadger-io/honeybadger-go 2 | 3 | go 1.12 4 | 5 | require ( 6 | github.com/pborman/uuid v1.2.1 7 | github.com/shirou/gopsutil v3.21.11+incompatible 8 | github.com/stretchr/testify v1.8.4 9 | github.com/yusufpapurcu/wmi v1.2.3 // indirect 10 | golang.org/x/sys v0.1.0 // indirect 11 | ) 12 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Compiled Object files, Static and Dynamic libs (Shared Objects) 2 | *.o 3 | *.a 4 | *.so 5 | 6 | # Folders 7 | _obj 8 | _test 9 | 10 | # Architecture specific extensions/prefixes 11 | *.[568vq] 12 | [568vq].out 13 | 14 | *.cgo1.go 15 | *.cgo2.c 16 | _cgo_defun.c 17 | _cgo_gotypes.go 18 | _cgo_export.* 19 | 20 | _testmain.go 21 | 22 | *.exe 23 | *.test 24 | *.prof 25 | 26 | cover.out 27 | -------------------------------------------------------------------------------- /context_sync.go: -------------------------------------------------------------------------------- 1 | package honeybadger 2 | 3 | import "sync" 4 | 5 | type contextSync struct { 6 | sync.RWMutex 7 | internal Context 8 | } 9 | 10 | func (context *contextSync) Update(other Context) { 11 | context.Lock() 12 | context.internal.Update(other) 13 | context.Unlock() 14 | } 15 | 16 | func newContextSync() *contextSync { 17 | instance := contextSync{ 18 | internal: Context{}, 19 | } 20 | 21 | return &instance 22 | } 23 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | ## What are the steps to reproduce this issue? 4 | 1. … 5 | 2. … 6 | 3. … 7 | 8 | ## What happens? 9 | … 10 | 11 | ## What were you expecting to happen? 12 | … 13 | 14 | ## Any logs, error output, etc? 15 | … 16 | 17 | ## Any other comments? 18 | … 19 | 20 | ## What versions are you using? 21 | **Operating System:** … 22 | **Package Version:** … 23 | **Go Version:** … 24 | -------------------------------------------------------------------------------- /.github/workflows/tests.yml: -------------------------------------------------------------------------------- 1 | # This workflow will build a golang project 2 | # For more information see: https://docs.github.com/en/actions/automating-builds-and-tests/building-and-testing-go 3 | 4 | name: Test 5 | 6 | on: 7 | push: 8 | branches: [ "master" ] 9 | pull_request: 10 | branches: [ "master" ] 11 | 12 | jobs: 13 | 14 | build: 15 | runs-on: ubuntu-latest 16 | steps: 17 | - uses: actions/checkout@v3 18 | 19 | - name: Set up Go 20 | uses: actions/setup-go@v4 21 | with: 22 | go-version: '1.20' 23 | 24 | - name: Build 25 | run: go build -v ./... 26 | 27 | - name: Test 28 | run: go test -v ./... 29 | -------------------------------------------------------------------------------- /.github/PULL_REQUEST_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | ## Status 2 | 3 | 4 | **READY/WIP/HOLD** 5 | 6 | ## Description 7 | A few sentences describing the overall goals of the pull request's commits. 8 | 9 | ## Related PRs 10 | List related PRs against other branches: 11 | 12 | branch | PR 13 | ------ | ------ 14 | other_pr_production | [link]() 15 | other_pr_master | [link]() 16 | 17 | ## Todos 18 | - [ ] Tests 19 | - [ ] Documentation 20 | - [ ] Changelog Entry (unreleased) 21 | 22 | ## Steps to Test or Reproduce 23 | Outline the steps to test or reproduce the PR here. 24 | ```bash 25 | > git pull --prune 26 | > git checkout 27 | > make test 28 | ``` 29 | 1. 30 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | all: test 2 | 3 | prepare: 4 | # needed for `make fmt` 5 | go get golang.org/x/tools/cmd/goimports 6 | # linters 7 | go get github.com/alecthomas/gometalinter 8 | gometalinter --install 9 | # needed for `make cover` 10 | go get golang.org/x/tools/cmd/cover 11 | @echo Now you should be ready to run "make" 12 | 13 | test: 14 | @go test -parallel 4 -race ./... 15 | 16 | # goimports produces slightly different formatted code from go fmt 17 | fmt: 18 | find . -name "*.go" -exec goimports -w {} \; 19 | 20 | lint: 21 | gometalinter 22 | 23 | cover: 24 | go test -cover -coverprofile cover.out 25 | go tool cover -html=cover.out 26 | 27 | .PHONY: all prepare test fmt lint cover 28 | -------------------------------------------------------------------------------- /context_sync_test.go: -------------------------------------------------------------------------------- 1 | package honeybadger 2 | 3 | import ( 4 | "testing" 5 | "sync" 6 | ) 7 | 8 | func TestContextSync(t *testing.T) { 9 | var wg sync.WaitGroup 10 | 11 | instance := newContextSync() 12 | newContext := Context{"foo":"bar"} 13 | 14 | wg.Add(2) 15 | 16 | go update(&wg, instance, newContext) 17 | go update(&wg, instance, newContext) 18 | 19 | wg.Wait() 20 | 21 | context := instance.internal 22 | 23 | if context["foo"] != "bar" { 24 | t.Errorf("Expected context value. expected=%#v result=%#v", "bar", context["foo"]) 25 | } 26 | } 27 | 28 | func update(wg *sync.WaitGroup, instance *contextSync, context Context) { 29 | instance.Update(context) 30 | wg.Done() 31 | } 32 | -------------------------------------------------------------------------------- /null_backend.go: -------------------------------------------------------------------------------- 1 | package honeybadger 2 | 3 | // nullBackend implements the Backend interface but swallows errors and does not 4 | // send them to Honeybadger. 5 | type nullBackend struct{} 6 | 7 | // Ensure nullBackend implements Backend. 8 | var _ Backend = &nullBackend{} 9 | 10 | // NewNullBackend creates a backend which swallows all errors and does not send 11 | // them to Honeybadger. This is useful for development and testing to disable 12 | // sending unnecessary errors. 13 | func NewNullBackend() Backend { 14 | return &nullBackend{} 15 | } 16 | 17 | // Notify swallows error reports, does nothing, and returns no error. 18 | func (*nullBackend) Notify(_ Feature, _ Payload) error { 19 | return nil 20 | } 21 | -------------------------------------------------------------------------------- /.github/workflows/check-pr-title.yml: -------------------------------------------------------------------------------- 1 | name: Check PR Title 2 | 3 | on: 4 | pull_request: 5 | branches: [ master ] 6 | types: [opened, edited, synchronize, reopened] 7 | 8 | jobs: 9 | commitlint: 10 | name: Check PR title 11 | runs-on: ubuntu-latest 12 | steps: 13 | - name: Setup Node.js 14 | uses: actions/setup-node@v4 15 | with: 16 | node-version: '18.x' 17 | 18 | - name: Setup 19 | run: | 20 | npm install -g @commitlint/cli @commitlint/config-conventional 21 | echo "module.exports = {extends: ['@commitlint/config-conventional']}" > commitlint.config.js 22 | 23 | - name: Verify PR title is in the correct format 24 | env: 25 | TITLE: ${{ github.event.pull_request.title }} 26 | run: | 27 | echo $TITLE | npx commitlint -V 28 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: Release package 2 | 3 | on: 4 | workflow_run: 5 | workflows: [Test] 6 | types: [completed] 7 | branches: [master] 8 | 9 | permissions: 10 | contents: write 11 | issues: write 12 | pull-requests: write 13 | repository-projects: read # this is required by release-please-action to set the auto-release tags 14 | 15 | # Note for release-please-action: 16 | # The action will set the auto-release tags for the release PR. If these tags do not already exist, the action will fail to create them. 17 | # Therefore, make sure that the tags are created in the repository before running the release workflow: `autorelease: pending`, `autorelease: tagged` 18 | 19 | jobs: 20 | release-if-needed: 21 | if: ${{ github.event.workflow_run.conclusion == 'success' }} 22 | runs-on: ubuntu-latest 23 | steps: 24 | - name: Create Release PR 25 | uses: googleapis/release-please-action@v4 26 | id: release 27 | with: 28 | token: ${{ secrets.GITHUB_TOKEN }} 29 | release-type: go 30 | -------------------------------------------------------------------------------- /buffered_worker.go: -------------------------------------------------------------------------------- 1 | package honeybadger 2 | 3 | import "fmt" 4 | 5 | var ( 6 | errWorkerOverflow = fmt.Errorf("The worker is full; this envelope will be dropped.") 7 | ) 8 | 9 | func newBufferedWorker(config *Configuration) *bufferedWorker { 10 | worker := &bufferedWorker{ch: make(chan envelope, 100)} 11 | go func() { 12 | for w := range worker.ch { 13 | work := func() error { 14 | defer func() { 15 | if err := recover(); err != nil { 16 | config.Logger.Printf("worker recovered from panic: %v\n", err) 17 | } 18 | }() 19 | return w() 20 | } 21 | if err := work(); err != nil { 22 | config.Logger.Printf("worker processing error: %v\n", err) 23 | } 24 | } 25 | }() 26 | return worker 27 | } 28 | 29 | type bufferedWorker struct { 30 | ch chan envelope 31 | } 32 | 33 | func (w *bufferedWorker) Push(work envelope) error { 34 | select { 35 | case w.ch <- work: 36 | return nil 37 | default: 38 | return errWorkerOverflow 39 | } 40 | } 41 | 42 | func (w *bufferedWorker) Flush() { 43 | ch := make(chan bool) 44 | w.ch <- func() error { 45 | ch <- true 46 | return nil 47 | } 48 | <-ch 49 | } 50 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2015 Honeybadger Industries LLC 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | 23 | -------------------------------------------------------------------------------- /configuration_test.go: -------------------------------------------------------------------------------- 1 | package honeybadger 2 | 3 | import "testing" 4 | 5 | type TestLogger struct{} 6 | 7 | func (l *TestLogger) Printf(format string, v ...interface{}) {} 8 | 9 | type TestBackend struct{} 10 | 11 | func (l *TestBackend) Notify(f Feature, p Payload) (err error) { 12 | return 13 | } 14 | 15 | func TestUpdateConfig(t *testing.T) { 16 | config := &Configuration{} 17 | logger := &TestLogger{} 18 | backend := &TestBackend{} 19 | config.update(&Configuration{ 20 | Logger: logger, 21 | Backend: backend, 22 | Root: "/tmp/foo", 23 | }) 24 | 25 | if config.Logger != logger { 26 | t.Errorf("Expected config to update logger expected=%#v actual=%#v", logger, config.Logger) 27 | } 28 | if config.Backend != backend { 29 | t.Errorf("Expected config to update backend expected=%#v actual=%#v", backend, config.Backend) 30 | } 31 | if config.Root != "/tmp/foo" { 32 | t.Errorf("Expected config to update root expected=%#v actual=%#v", "/tmp/foo", config.Root) 33 | } 34 | } 35 | 36 | func TestReplaceConfigPointer(t *testing.T) { 37 | config := Configuration{Root: "/tmp/foo"} 38 | root := &config.Root 39 | config = Configuration{Root: "/tmp/bar"} 40 | if *root != "/tmp/bar" { 41 | t.Errorf("Expected updated config to update pointer expected=%#v actual=%#v", "/tmp/bar", *root) 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /error.go: -------------------------------------------------------------------------------- 1 | package honeybadger 2 | 3 | import ( 4 | "errors" 5 | "fmt" 6 | "reflect" 7 | "runtime" 8 | "strconv" 9 | ) 10 | 11 | const maxFrames = 20 12 | 13 | // Frame represent a stack frame inside of a Honeybadger backtrace. 14 | type Frame struct { 15 | Number string `json:"number"` 16 | File string `json:"file"` 17 | Method string `json:"method"` 18 | } 19 | 20 | // Error provides more structured information about a Go error. 21 | type Error struct { 22 | err error 23 | Message string 24 | Class string 25 | Stack []*Frame 26 | } 27 | 28 | func (e Error) Unwrap() error { 29 | return e.err 30 | } 31 | 32 | func (e Error) Error() string { 33 | return e.Message 34 | } 35 | 36 | type stacked interface { 37 | Callers() []uintptr 38 | } 39 | 40 | func NewError(msg interface{}) Error { 41 | return newError(msg, 2) 42 | } 43 | 44 | func newError(thing interface{}, stackOffset int) Error { 45 | var err error 46 | 47 | switch t := thing.(type) { 48 | case Error: 49 | return t 50 | case error: 51 | err = t 52 | default: 53 | err = fmt.Errorf("%v", t) 54 | } 55 | 56 | return Error{ 57 | err: err, 58 | Message: err.Error(), 59 | Class: reflect.TypeOf(err).String(), 60 | Stack: generateStack(autostack(err, stackOffset)), 61 | } 62 | } 63 | 64 | func autostack(err error, offset int) []uintptr { 65 | var s stacked 66 | 67 | if errors.As(err, &s) { 68 | return s.Callers() 69 | } 70 | 71 | stack := make([]uintptr, maxFrames) 72 | length := runtime.Callers(2+offset, stack[:]) 73 | return stack[:length] 74 | } 75 | 76 | func generateStack(stack []uintptr) []*Frame { 77 | frames := runtime.CallersFrames(stack) 78 | result := make([]*Frame, 0, len(stack)) 79 | 80 | for { 81 | frame, more := frames.Next() 82 | 83 | result = append(result, &Frame{ 84 | File: frame.File, 85 | Number: strconv.Itoa(frame.Line), 86 | Method: frame.Function, 87 | }) 88 | 89 | if !more { 90 | break 91 | } 92 | } 93 | 94 | return result 95 | } 96 | -------------------------------------------------------------------------------- /server.go: -------------------------------------------------------------------------------- 1 | package honeybadger 2 | 3 | import ( 4 | "bytes" 5 | "errors" 6 | "fmt" 7 | "io/ioutil" 8 | "net/http" 9 | "net/url" 10 | "time" 11 | ) 12 | 13 | // Errors returned by the backend when unable to successfully handle payload. 14 | var ( 15 | ErrRateExceeded = errors.New("Rate exceeded: slow down!") 16 | ErrPaymentRequired = errors.New("Payment required: expired trial or credit card?") 17 | ErrUnauthorized = errors.New("Unauthorized: bad API key?") 18 | ) 19 | 20 | func newServerBackend(config *Configuration) *server { 21 | return &server{ 22 | URL: &config.Endpoint, 23 | APIKey: &config.APIKey, 24 | Client: &http.Client{ 25 | Transport: http.DefaultTransport, 26 | Timeout: config.Timeout, 27 | }, 28 | Timeout: &config.Timeout, 29 | } 30 | } 31 | 32 | type server struct { 33 | APIKey *string 34 | URL *string 35 | Timeout *time.Duration 36 | Client *http.Client 37 | } 38 | 39 | func (s *server) Notify(feature Feature, payload Payload) error { 40 | // Copy the value from the pointer in case it has changed in the 41 | // configuration. 42 | s.Client.Timeout = *s.Timeout 43 | 44 | url, err := url.Parse(*s.URL) 45 | if err != nil { 46 | return err 47 | } 48 | url.Path = "v1/" + feature.Endpoint 49 | req, err := http.NewRequest("POST", url.String(), bytes.NewReader(payload.toJSON())) 50 | if err != nil { 51 | return err 52 | } 53 | 54 | req.Header.Set("X-API-Key", *s.APIKey) 55 | req.Header.Set("Content-Type", "application/json") 56 | req.Header.Set("Accept", "application/json") 57 | 58 | resp, err := s.Client.Do(req) 59 | if err != nil { 60 | return err 61 | } 62 | defer func() { 63 | ioutil.ReadAll(resp.Body) 64 | resp.Body.Close() 65 | }() 66 | 67 | switch resp.StatusCode { 68 | case 201: 69 | return nil 70 | case 429, 503: 71 | return ErrRateExceeded 72 | case 402: 73 | return ErrPaymentRequired 74 | case 403: 75 | return ErrUnauthorized 76 | default: 77 | return fmt.Errorf( 78 | "request failed status=%d expected=%d", 79 | resp.StatusCode, 80 | http.StatusCreated) 81 | } 82 | } 83 | -------------------------------------------------------------------------------- /notice_test.go: -------------------------------------------------------------------------------- 1 | package honeybadger 2 | 3 | import ( 4 | "encoding/json" 5 | "errors" 6 | "testing" 7 | ) 8 | 9 | func newTestError() Error { 10 | var frames []*Frame 11 | frames = append(frames, &Frame{ 12 | File: "/path/to/root/badgers.go", 13 | Number: "1", 14 | Method: "badgers", 15 | }) 16 | frames = append(frames, &Frame{ 17 | File: "/foo/bar/baz.go", 18 | Number: "2", 19 | Method: "baz", 20 | }) 21 | return Error{ 22 | err: errors.New("Cobras!"), 23 | Message: "Cobras!", 24 | Class: "honeybadger", 25 | Stack: frames, 26 | } 27 | } 28 | 29 | func TestNewNotice(t *testing.T) { 30 | err := newTestError() 31 | var notice *Notice 32 | 33 | notice = newNotice(&Configuration{Root: "/path/to/root"}, err) 34 | 35 | if notice.ErrorMessage != "Cobras!" { 36 | t.Errorf("Unexpected value for notice.ErrorMessage. expected=%#v result=%#v", "Cobras!", notice.ErrorMessage) 37 | } 38 | 39 | if notice.Error.err != err.err { 40 | t.Errorf("Unexpected value for notice.Error. expected=%#v result=%#v", err.err, notice.Error.err) 41 | } 42 | 43 | if notice.Backtrace[0].File != "[PROJECT_ROOT]/badgers.go" { 44 | t.Errorf("Expected notice to substitute project root. expected=%#v result=%#v", "[PROJECT_ROOT]/badgers.go", notice.Backtrace[0].File) 45 | } 46 | 47 | if notice.Backtrace[1].File != "/foo/bar/baz.go" { 48 | t.Errorf("Expected notice not to trash non-project file. expected=%#v result=%#v", "/foo/bar/baz.go", notice.Backtrace[1].File) 49 | } 50 | 51 | notice = newNotice(&Configuration{Root: ""}, err) 52 | if notice.Backtrace[0].File != "/path/to/root/badgers.go" { 53 | t.Errorf("Expected notice not to trash project root. expected=%#v result=%#v", "/path/to/root/badgers.go", notice.Backtrace[0].File) 54 | } 55 | 56 | notice = newNotice(&Configuration{}, err, Context{"foo": "bar"}) 57 | if notice.Context["foo"] != "bar" { 58 | t.Errorf("Expected notice to contain context. expected=%#v result=%#v", "bar", notice.Context["foo"]) 59 | } 60 | } 61 | 62 | func TestToJSON(t *testing.T) { 63 | notice := newNotice(Config, newError(errors.New("Cobras!"), 0)) 64 | raw := notice.toJSON() 65 | 66 | var payload hash 67 | err := json.Unmarshal(raw, &payload) 68 | if err != nil { 69 | t.Errorf("Got error while parsing notice JSON err=%#v json=%#v", err, raw) 70 | return 71 | } 72 | 73 | testNoticePayload(t, payload) 74 | } 75 | -------------------------------------------------------------------------------- /error_test.go: -------------------------------------------------------------------------------- 1 | package honeybadger 2 | 3 | import ( 4 | "fmt" 5 | "runtime" 6 | "strings" 7 | "testing" 8 | ) 9 | 10 | func TestNewErrorTrace(t *testing.T) { 11 | fn := func() Error { 12 | return NewError("Error msg") 13 | } 14 | 15 | err := fn() 16 | 17 | // The stack should look like this: 18 | // github.com/honeybadger-io/honeybadger-go.TestNewErrorTrace.func1 19 | // github.com/honeybadger-io/honeybadger-go.TestNewErrorTrace 20 | // testing.tRunner 21 | // runtime.goexit 22 | if len(err.Stack) < 3 { 23 | t.Errorf("Expected to generate full trace") 24 | } 25 | 26 | // Checks that the top top methods are the (inlined) fn and the test Method 27 | expected := []string{ 28 | ".TestNewErrorTrace.func1", 29 | ".TestNewErrorTrace", 30 | } 31 | 32 | for i, suffix := range expected { 33 | method := err.Stack[i].Method 34 | 35 | if !strings.HasSuffix(method, suffix) { 36 | // Logs the stack to give some context about the error 37 | for j, stack := range err.Stack { 38 | t.Logf("%d: %s", j, stack.Method) 39 | } 40 | 41 | t.Fatalf("stack[%d].Method expected_suffix=%q actual=%q", i, suffix, method) 42 | } 43 | } 44 | } 45 | 46 | type customerror struct { 47 | error 48 | callers []uintptr 49 | } 50 | 51 | func (t customerror) Callers() []uintptr { 52 | return t.callers 53 | } 54 | 55 | func newcustomerror() customerror { 56 | stack := make([]uintptr, maxFrames) 57 | length := runtime.Callers(1, stack[:]) 58 | return customerror{ 59 | error: fmt.Errorf("hello world"), 60 | callers: stack[:length], 61 | } 62 | } 63 | 64 | func TestNewErrorCustomTrace(t *testing.T) { 65 | err := NewError(newcustomerror()) 66 | 67 | // The stack should look like this: 68 | // github.com/honeybadger-io/honeybadger-go.newcustomerror 69 | // github.com/honeybadger-io/honeybadger-go.TestNewErrorCustomTrace 70 | // testing.tRunner 71 | // runtime.goexit 72 | if len(err.Stack) < 3 { 73 | t.Errorf("Expected to generate full trace") 74 | } 75 | 76 | // Checks that the top top methods are the (inlined) fn and the test Method 77 | expected := []string{ 78 | ".newcustomerror", 79 | ".TestNewErrorCustomTrace", 80 | } 81 | 82 | for i, suffix := range expected { 83 | method := err.Stack[i].Method 84 | if !strings.HasSuffix(method, suffix) { 85 | // Logs the stack to give some context about the error 86 | for j, stack := range err.Stack { 87 | t.Logf("%d: %s", j, stack.Method) 88 | } 89 | 90 | t.Fatalf("stack[%d].Method expected_suffix=%q actual=%q", i, suffix, method) 91 | } 92 | } 93 | } 94 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 2 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= 3 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 4 | github.com/go-ole/go-ole v1.2.6 h1:/Fpf6oFPoeFik9ty7siob0G6Ke8QvQEuVcuChpwXzpY= 5 | github.com/go-ole/go-ole v1.2.6/go.mod h1:pprOEPIfldk/42T2oK7lQ4v4JSDwmV0As9GaiUsvbm0= 6 | github.com/google/uuid v1.0.0 h1:b4Gk+7WdP/d3HZH8EJsZpvV7EtDOgaZLtnaNGIu1adA= 7 | github.com/google/uuid v1.0.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= 8 | github.com/pborman/uuid v1.2.1 h1:+ZZIw58t/ozdjRaXh/3awHfmWRbzYxJoAdNJxe/3pvw= 9 | github.com/pborman/uuid v1.2.1/go.mod h1:X/NO0urCmaxf9VXbdlT7C2Yzkj2IKimNn4k+gtPdI/k= 10 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 11 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 12 | github.com/shirou/gopsutil v3.21.11+incompatible h1:+1+c1VGhc88SSonWP6foOcLhvnKlUeu/erjjvaPEYiI= 13 | github.com/shirou/gopsutil v3.21.11+incompatible/go.mod h1:5b4v6he4MtMOwMlS0TUMTu2PcXUg8+E1lC7eC3UO/RA= 14 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 15 | github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= 16 | github.com/stretchr/objx v0.5.0 h1:1zr/of2m5FGMsad5YfcqgdqdWrIhu+EBEJRhR1U7z/c= 17 | github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= 18 | github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= 19 | github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= 20 | github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk= 21 | github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= 22 | github.com/yusufpapurcu/wmi v1.2.3 h1:E1ctvB7uKFMOJw3fdOW32DwGE9I7t++CRUEMKvFoFiw= 23 | github.com/yusufpapurcu/wmi v1.2.3/go.mod h1:SBZ9tNy3G9/m5Oi98Zks0QjeHVDvuK0qfxQmPyzfmi0= 24 | golang.org/x/sys v0.0.0-20190916202348-b4ddaad3f8a3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 25 | golang.org/x/sys v0.1.0 h1:kunALQeHf1/185U1i0GOB/fy1IPRDDpuoOOqRReG57U= 26 | golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 27 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= 28 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 29 | gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 30 | gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= 31 | gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 32 | -------------------------------------------------------------------------------- /configuration.go: -------------------------------------------------------------------------------- 1 | package honeybadger 2 | 3 | import ( 4 | "log" 5 | "os" 6 | "strconv" 7 | "time" 8 | ) 9 | 10 | // The Logger interface is implemented by the standard log package and requires 11 | // a limited subset of the interface implemented by log.Logger. 12 | type Logger interface { 13 | Printf(format string, v ...interface{}) 14 | } 15 | 16 | // Configuration manages the configuration for the client. 17 | type Configuration struct { 18 | APIKey string 19 | Root string 20 | Env string 21 | Hostname string 22 | Endpoint string 23 | Sync bool 24 | Timeout time.Duration 25 | Logger Logger 26 | Backend Backend 27 | } 28 | 29 | func (c1 *Configuration) update(c2 *Configuration) *Configuration { 30 | if c2.APIKey != "" { 31 | c1.APIKey = c2.APIKey 32 | } 33 | if c2.Root != "" { 34 | c1.Root = c2.Root 35 | } 36 | if c2.Env != "" { 37 | c1.Env = c2.Env 38 | } 39 | if c2.Hostname != "" { 40 | c1.Hostname = c2.Hostname 41 | } 42 | if c2.Endpoint != "" { 43 | c1.Endpoint = c2.Endpoint 44 | } 45 | if c2.Timeout > 0 { 46 | c1.Timeout = c2.Timeout 47 | } 48 | if c2.Logger != nil { 49 | c1.Logger = c2.Logger 50 | } 51 | if c2.Backend != nil { 52 | c1.Backend = c2.Backend 53 | } 54 | 55 | c1.Sync = c2.Sync 56 | return c1 57 | } 58 | 59 | func newConfig(c Configuration) *Configuration { 60 | config := &Configuration{ 61 | APIKey: getEnv("HONEYBADGER_API_KEY"), 62 | Root: getPWD(), 63 | Env: getEnv("HONEYBADGER_ENV"), 64 | Hostname: getHostname(), 65 | Endpoint: getEnv("HONEYBADGER_ENDPOINT", "https://api.honeybadger.io"), 66 | Timeout: getTimeout(), 67 | Logger: log.New(os.Stderr, "[honeybadger] ", log.Flags()), 68 | Sync: getSync(), 69 | } 70 | config.update(&c) 71 | 72 | if config.Backend == nil { 73 | config.Backend = newServerBackend(config) 74 | } 75 | 76 | return config 77 | } 78 | 79 | func getTimeout() time.Duration { 80 | if env := getEnv("HONEYBADGER_TIMEOUT"); env != "" { 81 | if ns, err := strconv.ParseInt(env, 10, 64); err == nil { 82 | return time.Duration(ns) 83 | } 84 | } 85 | return 3 * time.Second 86 | } 87 | 88 | func getEnv(key string, fallback ...string) (val string) { 89 | val = os.Getenv(key) 90 | if val == "" && len(fallback) > 0 { 91 | return fallback[0] 92 | } 93 | return 94 | } 95 | 96 | func getHostname() (hostname string) { 97 | if val, err := os.Hostname(); err == nil { 98 | hostname = val 99 | } 100 | return getEnv("HONEYBADGER_HOSTNAME", hostname) 101 | } 102 | 103 | func getPWD() (pwd string) { 104 | if val, err := os.Getwd(); err == nil { 105 | pwd = val 106 | } 107 | return getEnv("HONEYBADGER_ROOT", pwd) 108 | } 109 | 110 | func getSync() bool { 111 | if getEnv("HONEYBADGER_SYNC") != "" { 112 | return true 113 | } 114 | return false 115 | } 116 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Change Log 2 | 3 | All notable changes to this project will be documented in this file. See [Keep a 4 | CHANGELOG](http://keepachangelog.com/) for how to update this file. This project 5 | adheres to [Semantic Versioning](http://semver.org/). 6 | 7 | ## [0.8.0](https://github.com/honeybadger-io/honeybadger-go/compare/v0.7.0...v0.8.0) (2024-09-16) 8 | 9 | 10 | ### Features 11 | 12 | * allow Notify to receive *http.Request ([#54](https://github.com/honeybadger-io/honeybadger-go/issues/54)) ([b0d2af0](https://github.com/honeybadger-io/honeybadger-go/commit/b0d2af07a031075d86b497c882f2649f25ca1404)) 13 | 14 | ## [0.7.0](https://github.com/honeybadger-io/honeybadger-go/compare/v0.6.1...v0.7.0) (2024-04-25) 15 | 16 | 17 | ### Features 18 | 19 | * allow errors to provide their own stack traces ([b8d3e83](https://github.com/honeybadger-io/honeybadger-go/commit/b8d3e83e6a36f7dac1b72e3ff7d1bf9cde4382da)) 20 | 21 | ## [Unreleased][unreleased] 22 | 23 | ## [0.6.1] - 2024-04-12 24 | 25 | ### Fixed 26 | 27 | - Implement error chains Unwrap method 28 | 29 | ## [0.6.0] - 2024-01-19 30 | 31 | ### Changed 32 | 33 | - Updated dependencies 34 | 35 | ## [0.5.0] - 2019-10-17 36 | 37 | ### Added 38 | 39 | - Added Sync mode 40 | 41 | ## [0.4.0] - 2018-07-18 42 | 43 | ### Added 44 | 45 | - Ability to tag errors. -@izumin5210 46 | 47 | ## [0.3.0] - 2018-07-03 48 | 49 | ### Changed 50 | 51 | - Remove deprecated metrics methods. 52 | 53 | ### Fixed 54 | 55 | - Fixed concurrent map writes bug when calling `honeybadger.SetContext` from 56 | concurrent goroutines. 57 | 58 | ## [0.2.1] - 2017-09-14 59 | 60 | ### Fixed 61 | 62 | - Previously, if you put `honeybadger.Monitor()` in your main func, the app 63 | could finish and exit before the error was sent to honeybadger. We now Flush 64 | notices before re-panicking. 65 | 66 | ## [0.2.0] - 2016-10-14 67 | 68 | ### Changed 69 | 70 | - Sunset performance metrics. See 71 | http://blog.honeybadger.io/sunsetting-performance-metrics/ 72 | 73 | ## [0.1.0] - 2016-05-12 74 | 75 | ### Added 76 | 77 | - Use `honeybadger.MetricsHandler` to send us request metrics! 78 | 79 | ## [0.0.3] - 2016-04-13 80 | 81 | ### Added 82 | 83 | - `honeybadger.NewNullBackend()`: creates a backend which swallows all errors 84 | and does not send them to Honeybadger. This is useful for development and 85 | testing to disable sending unnecessary errors. -@gaffneyc 86 | - Tested against Go 1.5 and 1.6. -@gaffneyc 87 | 88 | ### Fixed 89 | 90 | - Export Fingerprint fields. -@smeriwether 91 | - Fix HB due to changes in shirou/gopsutil. -@kostyantyn 92 | 93 | ## [0.0.2] - 2016-03-28 94 | 95 | ### Added 96 | 97 | - Make newError function public (#6). -@kostyantyn 98 | - Add public access to default client (#5). -@kostyantyn 99 | - Support default server mux in Handler. 100 | - Allow error class to be customized from `honeybadger.Notify`. 101 | - Support sending fingerprint in `honeybadger.Notify`. 102 | - Added BeforeNotify callback. 103 | 104 | ### Fixed 105 | 106 | - Drain the body of a response before closing it (#4). -@kostyantyn 107 | - Update config at pointer rather than dereferencing. (#2). 108 | 109 | ## [0.0.1] - 2015-06-25 110 | 111 | ### Added 112 | 113 | - Go client for Honeybadger.io. 114 | -------------------------------------------------------------------------------- /honeybadger.go: -------------------------------------------------------------------------------- 1 | package honeybadger 2 | 3 | import ( 4 | "encoding/json" 5 | "net/http" 6 | "net/url" 7 | ) 8 | 9 | // VERSION defines the version of the honeybadger package. 10 | const VERSION = "0.7.0" 11 | 12 | var ( 13 | // client is a pre-defined "global" client. 14 | DefaultClient = New(Configuration{}) 15 | 16 | // Config is a pointer to the global client's Config. 17 | Config = DefaultClient.Config 18 | 19 | // Notices is the feature for sending error reports. 20 | Notices = Feature{"notices"} 21 | ) 22 | 23 | // Feature references a resource provided by the API service. Its Endpoint maps 24 | // to the collection endpoint of the /v1 API. 25 | type Feature struct { 26 | Endpoint string 27 | } 28 | 29 | // CGIData stores variables from the server/request environment indexed by key. 30 | // Header keys should be converted to upercase, all non-alphanumeric characters 31 | // replaced with underscores, and prefixed with HTTP_. For example, the header 32 | // "Content-Type" would become "HTTP_CONTENT_TYPE". 33 | type CGIData hash 34 | 35 | // Params stores the form or url values from an HTTP request. 36 | type Params url.Values 37 | 38 | // Tags represents tags of the error which is classified errors in Honeybadger. 39 | type Tags []string 40 | 41 | // hash is used internally to construct JSON payloads. 42 | type hash map[string]interface{} 43 | 44 | func (h *hash) toJSON() []byte { 45 | out, err := json.Marshal(h) 46 | if err == nil { 47 | return out 48 | } 49 | panic(err) 50 | } 51 | 52 | // Configure updates configuration of the global client. 53 | func Configure(c Configuration) { 54 | DefaultClient.Configure(c) 55 | } 56 | 57 | // SetContext merges c Context into the Context of the global client. 58 | func SetContext(c Context) { 59 | DefaultClient.SetContext(c) 60 | } 61 | 62 | // Notify reports the error err to the Honeybadger service. 63 | // 64 | // The first argument err may be an error, a string, or any other type in which 65 | // case its formatted value will be used. 66 | // 67 | // It returns a string UUID which can be used to reference the error from the 68 | // Honeybadger service, and an error as a second argument. 69 | func Notify(err interface{}, extra ...interface{}) (string, error) { 70 | return DefaultClient.Notify(newError(err, 2), extra...) 71 | } 72 | 73 | // Monitor is used to automatically notify Honeybadger service of panics which 74 | // happen inside the current function. In order to monitor for panics, defer a 75 | // call to Monitor. For example: 76 | // func main { 77 | // defer honeybadger.Monitor() 78 | // // Do risky stuff... 79 | // } 80 | // The Monitor function re-panics after the notification has been sent, so it's 81 | // still up to the user to recover from panics if desired. 82 | func Monitor() { 83 | if err := recover(); err != nil { 84 | DefaultClient.Notify(newError(err, 2)) 85 | DefaultClient.Flush() 86 | panic(err) 87 | } 88 | } 89 | 90 | // Flush blocks until all data (normally sent in the background) has been sent 91 | // to the Honeybadger service. 92 | func Flush() { 93 | DefaultClient.Flush() 94 | } 95 | 96 | // Handler returns an http.Handler function which automatically reports panics 97 | // to Honeybadger and then re-panics. 98 | func Handler(h http.Handler) http.Handler { 99 | return DefaultClient.Handler(h) 100 | } 101 | 102 | // BeforeNotify adds a callback function which is run before a notice is 103 | // reported to Honeybadger. If any function returns an error the notification 104 | // will be skipped, otherwise it will be sent. 105 | func BeforeNotify(handler func(notice *Notice) error) { 106 | DefaultClient.BeforeNotify(handler) 107 | } 108 | -------------------------------------------------------------------------------- /client_test.go: -------------------------------------------------------------------------------- 1 | package honeybadger 2 | 3 | import ( 4 | "sync" 5 | "testing" 6 | ) 7 | 8 | func TestNewConfig(t *testing.T) { 9 | client := New(Configuration{APIKey: "lemmings"}) 10 | if client.Config.APIKey != "lemmings" { 11 | t.Errorf("Expected New to configure APIKey. expected=%#v actual=%#v", "lemmings", client.Config.APIKey) 12 | } 13 | } 14 | 15 | func TestConfigureClient(t *testing.T) { 16 | client := New(Configuration{}) 17 | client.Configure(Configuration{APIKey: "badgers"}) 18 | if client.Config.APIKey != "badgers" { 19 | t.Errorf("Expected Configure to override config.APIKey. expected=%#v actual=%#v", "badgers", client.Config.APIKey) 20 | } 21 | } 22 | 23 | func TestConfigureClientEndpoint(t *testing.T) { 24 | client := New(Configuration{}) 25 | backend := client.Config.Backend.(*server) 26 | client.Configure(Configuration{Endpoint: "http://localhost:3000"}) 27 | if *backend.URL != "http://localhost:3000" { 28 | t.Errorf("Expected Configure to update backend. expected=%#v actual=%#v", "http://localhost:3000", backend.URL) 29 | } 30 | } 31 | 32 | func TestClientContext(t *testing.T) { 33 | client := New(Configuration{}) 34 | 35 | client.SetContext(Context{"foo": "bar"}) 36 | client.SetContext(Context{"bar": "baz"}) 37 | 38 | context := client.context.internal 39 | 40 | if context["foo"] != "bar" { 41 | t.Errorf("Expected client to merge global context. expected=%#v actual=%#v", "bar", context["foo"]) 42 | } 43 | 44 | if context["bar"] != "baz" { 45 | t.Errorf("Expected client to merge global context. expected=%#v actual=%#v", "baz", context["bar"]) 46 | } 47 | } 48 | 49 | func TestClientConcurrentContext(t *testing.T) { 50 | var wg sync.WaitGroup 51 | 52 | client := New(Configuration{}) 53 | newContext := Context{"foo": "bar"} 54 | 55 | wg.Add(2) 56 | 57 | go updateContext(&wg, client, newContext) 58 | go updateContext(&wg, client, newContext) 59 | 60 | wg.Wait() 61 | 62 | context := client.context.internal 63 | 64 | if context["foo"] != "bar" { 65 | t.Errorf("Expected context value. expected=%#v result=%#v", "bar", context["foo"]) 66 | } 67 | } 68 | 69 | func updateContext(wg *sync.WaitGroup, client *Client, context Context) { 70 | client.SetContext(context) 71 | wg.Done() 72 | } 73 | 74 | func TestNotifyPushesTheEnvelope(t *testing.T) { 75 | client, worker, _ := mockClient(Configuration{}) 76 | 77 | client.Notify("test") 78 | 79 | if worker.receivedEnvelope == false { 80 | t.Errorf("Expected client to push envelope") 81 | } 82 | } 83 | 84 | func TestNotifySyncMode(t *testing.T) { 85 | client, worker, backend := mockClient(Configuration{Sync: true}) 86 | 87 | token, _ := client.Notify("test") 88 | 89 | if worker.receivedEnvelope == true { 90 | t.Errorf("Expected client to not push envelope") 91 | } 92 | if backend.notice.Token != token { 93 | t.Errorf("Notice should have been called on backend") 94 | } 95 | } 96 | 97 | type mockWorker struct { 98 | receivedEnvelope bool 99 | } 100 | 101 | func (w *mockWorker) Push(work envelope) error { 102 | w.receivedEnvelope = true 103 | return nil 104 | } 105 | 106 | func (w *mockWorker) Flush() {} 107 | 108 | type mockBackend struct { 109 | notice *Notice 110 | } 111 | 112 | func (b *mockBackend) Notify(_ Feature, n Payload) error { 113 | b.notice = n.(*Notice) 114 | return nil 115 | } 116 | 117 | func mockClient(c Configuration) (Client, *mockWorker, *mockBackend) { 118 | worker := &mockWorker{} 119 | backend := &mockBackend{} 120 | backendConfig := &Configuration{Backend: backend} 121 | backendConfig.update(&c) 122 | 123 | client := Client{ 124 | Config: newConfig(*backendConfig), 125 | worker: worker, 126 | context: newContextSync(), 127 | } 128 | 129 | return client, worker, backend 130 | } 131 | -------------------------------------------------------------------------------- /client.go: -------------------------------------------------------------------------------- 1 | package honeybadger 2 | 3 | import ( 4 | "net/http" 5 | ) 6 | 7 | // The Payload interface is implemented by any type which can be handled by the 8 | // Backend interface. 9 | type Payload interface { 10 | toJSON() []byte 11 | } 12 | 13 | // The Backend interface is implemented by the server type by default, but a 14 | // custom implementation may be configured by the user. 15 | type Backend interface { 16 | Notify(feature Feature, payload Payload) error 17 | } 18 | 19 | type noticeHandler func(*Notice) error 20 | 21 | // Client is the manager for interacting with the Honeybadger service. It holds 22 | // the configuration and implements the public API. 23 | type Client struct { 24 | Config *Configuration 25 | context *contextSync 26 | worker worker 27 | beforeNotifyHandlers []noticeHandler 28 | } 29 | 30 | // Configure updates the client configuration with the supplied config. 31 | func (client *Client) Configure(config Configuration) { 32 | client.Config.update(&config) 33 | } 34 | 35 | // SetContext updates the client context with supplied context. 36 | func (client *Client) SetContext(context Context) { 37 | client.context.Update(context) 38 | } 39 | 40 | // Flush blocks until the worker has processed its queue. 41 | func (client *Client) Flush() { 42 | client.worker.Flush() 43 | } 44 | 45 | // BeforeNotify adds a callback function which is run before a notice is 46 | // reported to Honeybadger. If any function returns an error the notification 47 | // will be skipped, otherwise it will be sent. 48 | func (client *Client) BeforeNotify(handler func(notice *Notice) error) { 49 | client.beforeNotifyHandlers = append(client.beforeNotifyHandlers, handler) 50 | } 51 | 52 | // Notify reports the error err to the Honeybadger service. 53 | func (client *Client) Notify(err interface{}, extra ...interface{}) (string, error) { 54 | extra = append([]interface{}{client.context.internal}, extra...) 55 | notice := newNotice(client.Config, newError(err, 2), extra...) 56 | for _, handler := range client.beforeNotifyHandlers { 57 | if err := handler(notice); err != nil { 58 | return "", err 59 | } 60 | } 61 | 62 | notifyFn := func() error { 63 | return client.Config.Backend.Notify(Notices, notice) 64 | } 65 | 66 | if client.Config.Sync { 67 | if notifyErr := notifyFn(); notifyErr != nil { 68 | client.Config.Logger.Printf("notify error: %v\n", notifyErr) 69 | return "", notifyErr 70 | } 71 | } else { 72 | if workerPushErr := client.worker.Push(notifyFn); workerPushErr != nil { 73 | client.Config.Logger.Printf("worker error: %v\n", workerPushErr) 74 | return "", workerPushErr 75 | } 76 | } 77 | 78 | return notice.Token, nil 79 | } 80 | 81 | // Monitor automatically reports panics which occur in the function it's called 82 | // from. Must be deferred. 83 | func (client *Client) Monitor() { 84 | if err := recover(); err != nil { 85 | client.Notify(newError(err, 2)) 86 | client.Flush() 87 | panic(err) 88 | } 89 | } 90 | 91 | // Handler returns an http.Handler function which automatically reports panics 92 | // to Honeybadger and then re-panics. 93 | func (client *Client) Handler(h http.Handler) http.Handler { 94 | if h == nil { 95 | h = http.DefaultServeMux 96 | } 97 | fn := func(w http.ResponseWriter, r *http.Request) { 98 | defer func() { 99 | if err := recover(); err != nil { 100 | client.Notify(newError(err, 2), r) 101 | panic(err) 102 | } 103 | }() 104 | h.ServeHTTP(w, r) 105 | } 106 | return http.HandlerFunc(fn) 107 | } 108 | 109 | // New returns a new instance of Client. 110 | func New(c Configuration) *Client { 111 | config := newConfig(c) 112 | worker := newBufferedWorker(config) 113 | 114 | client := Client{ 115 | Config: config, 116 | worker: worker, 117 | context: newContextSync(), 118 | } 119 | 120 | return &client 121 | } 122 | -------------------------------------------------------------------------------- /notice.go: -------------------------------------------------------------------------------- 1 | package honeybadger 2 | 3 | import ( 4 | "encoding/json" 5 | "net/http" 6 | "net/url" 7 | "os" 8 | "regexp" 9 | "strings" 10 | "time" 11 | 12 | "github.com/pborman/uuid" 13 | "github.com/shirou/gopsutil/load" 14 | "github.com/shirou/gopsutil/mem" 15 | ) 16 | 17 | // ErrorClass represents the class name of the error which is sent to 18 | // Honeybadger. 19 | type ErrorClass struct { 20 | Name string 21 | } 22 | 23 | // Fingerprint represents the fingerprint of the error, which controls grouping 24 | // in Honeybadger. 25 | type Fingerprint struct { 26 | Content string 27 | } 28 | 29 | func (f *Fingerprint) String() string { 30 | return f.Content 31 | } 32 | 33 | // Notice is a representation of the error which is sent to Honeybadger, and 34 | // implements the Payload interface. 35 | type Notice struct { 36 | APIKey string 37 | Error Error 38 | Token string 39 | ErrorMessage string 40 | ErrorClass string 41 | Tags []string 42 | Hostname string 43 | Env string 44 | Backtrace []*Frame 45 | ProjectRoot string 46 | Context Context 47 | Params Params 48 | CGIData CGIData 49 | URL string 50 | Fingerprint string 51 | } 52 | 53 | func (n *Notice) asJSON() *hash { 54 | return &hash{ 55 | "api_key": n.APIKey, 56 | "notifier": &hash{ 57 | "name": "honeybadger", 58 | "url": "https://github.com/honeybadger-io/honeybadger-go", 59 | "version": VERSION, 60 | }, 61 | "error": &hash{ 62 | "token": n.Token, 63 | "message": n.ErrorMessage, 64 | "class": n.ErrorClass, 65 | "tags": n.Tags, 66 | "backtrace": n.Backtrace, 67 | "fingerprint": n.Fingerprint, 68 | }, 69 | "request": &hash{ 70 | "context": n.Context, 71 | "params": n.Params, 72 | "cgi_data": n.CGIData, 73 | "url": n.URL, 74 | }, 75 | "server": &hash{ 76 | "project_root": n.ProjectRoot, 77 | "environment_name": n.Env, 78 | "hostname": n.Hostname, 79 | "time": time.Now().UTC(), 80 | "pid": os.Getpid(), 81 | "stats": getStats(), 82 | }, 83 | } 84 | } 85 | 86 | func bytesToKB(bytes uint64) float64 { 87 | return float64(bytes) / 1024.0 88 | } 89 | 90 | func getStats() *hash { 91 | var m, l *hash 92 | 93 | if stat, err := mem.VirtualMemory(); err == nil { 94 | m = &hash{ 95 | "total": bytesToKB(stat.Total), 96 | "free": bytesToKB(stat.Free), 97 | "buffers": bytesToKB(stat.Buffers), 98 | "cached": bytesToKB(stat.Cached), 99 | "free_total": bytesToKB(stat.Free + stat.Buffers + stat.Cached), 100 | } 101 | } 102 | 103 | if stat, err := load.Avg(); err == nil { 104 | l = &hash{ 105 | "one": stat.Load1, 106 | "five": stat.Load5, 107 | "fifteen": stat.Load15, 108 | } 109 | } 110 | 111 | return &hash{"mem": m, "load": l} 112 | } 113 | 114 | func (n *Notice) toJSON() []byte { 115 | out, err := json.Marshal(n.asJSON()) 116 | if err == nil { 117 | return out 118 | } 119 | panic(err) 120 | } 121 | 122 | func (n *Notice) setContext(context Context) { 123 | n.Context.Update(context) 124 | } 125 | 126 | func composeStack(stack []*Frame, root string) (frames []*Frame) { 127 | if root == "" { 128 | return stack 129 | } 130 | 131 | re, err := regexp.Compile("^" + regexp.QuoteMeta(root)) 132 | if err != nil { 133 | return stack 134 | } 135 | 136 | for _, frame := range stack { 137 | file := re.ReplaceAllString(frame.File, "[PROJECT_ROOT]") 138 | frames = append(frames, &Frame{ 139 | File: file, 140 | Number: frame.Number, 141 | Method: frame.Method, 142 | }) 143 | } 144 | return 145 | } 146 | 147 | func newNotice(config *Configuration, err Error, extra ...interface{}) *Notice { 148 | notice := Notice{ 149 | APIKey: config.APIKey, 150 | Error: err, 151 | Token: uuid.NewRandom().String(), 152 | ErrorMessage: err.Message, 153 | ErrorClass: err.Class, 154 | Env: config.Env, 155 | Hostname: config.Hostname, 156 | Backtrace: composeStack(err.Stack, config.Root), 157 | ProjectRoot: config.Root, 158 | Context: Context{}, 159 | } 160 | 161 | for _, thing := range extra { 162 | switch t := thing.(type) { 163 | case Context: 164 | notice.setContext(t) 165 | case ErrorClass: 166 | notice.ErrorClass = t.Name 167 | case Tags: 168 | for _, tag := range t { 169 | notice.Tags = append(notice.Tags, tag) 170 | } 171 | case Fingerprint: 172 | notice.Fingerprint = t.String() 173 | case Params: 174 | notice.Params = t 175 | case CGIData: 176 | notice.CGIData = t 177 | case url.URL: 178 | notice.URL = t.String() 179 | case *http.Request: 180 | setHttpRequest(¬ice, t) 181 | } 182 | } 183 | 184 | return ¬ice 185 | } 186 | 187 | func setHttpRequest(notice *Notice, r *http.Request) { 188 | if r == nil { 189 | return 190 | } 191 | 192 | notice.URL = r.URL.String() 193 | notice.CGIData = CGIData{} 194 | 195 | replacer := strings.NewReplacer("-", "_") 196 | for k, v := range r.Header { 197 | key := "HTTP_" + replacer.Replace(strings.ToUpper(k)) 198 | notice.CGIData[key] = v[0] 199 | } 200 | 201 | // Form is only populated if ParseForm() is called on the request and it 202 | // will include URL query parameters. So if it's empty, then it's possible 203 | // that ParseForm wasn't called, and we will miss reporting URL params. 204 | if form := r.Form; len(form) == 0 { 205 | notice.Params = Params(r.URL.Query()) 206 | } else { 207 | notice.Params = Params(form) 208 | } 209 | } 210 | -------------------------------------------------------------------------------- /honeybadger_test.go: -------------------------------------------------------------------------------- 1 | package honeybadger 2 | 3 | import ( 4 | "encoding/json" 5 | "errors" 6 | "fmt" 7 | "io/ioutil" 8 | "net/http" 9 | "net/http/httptest" 10 | "net/url" 11 | "reflect" 12 | "testing" 13 | 14 | "github.com/pborman/uuid" 15 | "github.com/stretchr/testify/mock" 16 | ) 17 | 18 | var ( 19 | mux *http.ServeMux 20 | ts *httptest.Server 21 | requests []*HTTPRequest 22 | defaultConfig = *Config 23 | ) 24 | 25 | type MockedHandler struct { 26 | mock.Mock 27 | } 28 | 29 | func (h *MockedHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { 30 | h.Called() 31 | } 32 | 33 | type HTTPRequest struct { 34 | Request *http.Request 35 | Body []byte 36 | } 37 | 38 | func (h *HTTPRequest) decodeJSON() hash { 39 | var dat hash 40 | err := json.Unmarshal(h.Body, &dat) 41 | if err != nil { 42 | panic(err) 43 | } 44 | return dat 45 | } 46 | 47 | func newHTTPRequest(r *http.Request) *HTTPRequest { 48 | body, _ := ioutil.ReadAll(r.Body) 49 | return &HTTPRequest{r, body} 50 | } 51 | 52 | func setup(t *testing.T) { 53 | mux = http.NewServeMux() 54 | ts = httptest.NewServer(mux) 55 | requests = []*HTTPRequest{} 56 | mux.HandleFunc("/v1/notices", 57 | func(w http.ResponseWriter, r *http.Request) { 58 | assertMethod(t, r, "POST") 59 | requests = append(requests, newHTTPRequest(r)) 60 | w.WriteHeader(201) 61 | fmt.Fprint(w, `{"id":"87ded4b4-63cc-480a-b50c-8abe1376d972"}`) 62 | }, 63 | ) 64 | 65 | *DefaultClient.Config = *newConfig(Configuration{APIKey: "badgers", Endpoint: ts.URL}) 66 | } 67 | 68 | func teardown() { 69 | *DefaultClient.Config = defaultConfig 70 | } 71 | 72 | func TestDefaultConfig(t *testing.T) { 73 | if Config.APIKey != "" { 74 | t.Errorf("Expected Config.APIKey to be empty by default. expected=%#v result=%#v", "", Config.APIKey) 75 | } 76 | } 77 | 78 | func TestConfigure(t *testing.T) { 79 | Configure(Configuration{APIKey: "badgers"}) 80 | if Config.APIKey != "badgers" { 81 | t.Errorf("Expected Configure to override config.APIKey. expected=%#v actual=%#v", "badgers", Config.APIKey) 82 | } 83 | } 84 | 85 | func TestNotify(t *testing.T) { 86 | setup(t) 87 | defer teardown() 88 | 89 | res, _ := Notify(errors.New("Cobras!")) 90 | 91 | if uuid.Parse(res) == nil { 92 | t.Errorf("Expected Notify() to return a UUID. actual=%#v", res) 93 | } 94 | 95 | Flush() 96 | 97 | if !testRequestCount(t, 1) { 98 | return 99 | } 100 | 101 | testNoticePayload(t, requests[0].decodeJSON()) 102 | } 103 | 104 | func TestNotifyWithContext(t *testing.T) { 105 | setup(t) 106 | defer teardown() 107 | 108 | context := Context{"foo": "bar"} 109 | Notify("Cobras!", context) 110 | Flush() 111 | 112 | if !testRequestCount(t, 1) { 113 | return 114 | } 115 | 116 | payload := requests[0].decodeJSON() 117 | if !testNoticePayload(t, payload) { 118 | return 119 | } 120 | 121 | assertContext(t, payload, context) 122 | } 123 | 124 | func TestNotifyWithErrorClass(t *testing.T) { 125 | setup(t) 126 | defer teardown() 127 | 128 | Notify("Cobras!", ErrorClass{"Badgers"}) 129 | Flush() 130 | 131 | if !testRequestCount(t, 1) { 132 | return 133 | } 134 | 135 | payload := requests[0].decodeJSON() 136 | error_payload, _ := payload["error"].(map[string]interface{}) 137 | sent_klass, _ := error_payload["class"].(string) 138 | 139 | if !testNoticePayload(t, payload) { 140 | return 141 | } 142 | 143 | if sent_klass != "Badgers" { 144 | t.Errorf("Custom error class should override default. expected=%v actual=%#v.", "Badgers", sent_klass) 145 | return 146 | } 147 | } 148 | 149 | func TestNotifyWithTags(t *testing.T) { 150 | setup(t) 151 | defer teardown() 152 | 153 | Notify("Cobras!", Tags{"timeout", "http"}) 154 | Flush() 155 | 156 | if !testRequestCount(t, 1) { 157 | return 158 | } 159 | 160 | payload := requests[0].decodeJSON() 161 | error_payload, _ := payload["error"].(map[string]interface{}) 162 | sent_tags, _ := error_payload["tags"].([]interface{}) 163 | 164 | if !testNoticePayload(t, payload) { 165 | return 166 | } 167 | 168 | if got, want := sent_tags, []interface{}{"timeout", "http"}; !reflect.DeepEqual(got, want) { 169 | t.Errorf("Custom error class should override default. expected=%#v actual=%#v.", want, got) 170 | return 171 | } 172 | } 173 | 174 | func TestNotifyWithFingerprint(t *testing.T) { 175 | setup(t) 176 | defer teardown() 177 | 178 | Notify("Cobras!", Fingerprint{"Badgers"}) 179 | Flush() 180 | 181 | if !testRequestCount(t, 1) { 182 | return 183 | } 184 | 185 | payload := requests[0].decodeJSON() 186 | error_payload, _ := payload["error"].(map[string]interface{}) 187 | sent_fingerprint, _ := error_payload["fingerprint"].(string) 188 | 189 | if !testNoticePayload(t, payload) { 190 | return 191 | } 192 | 193 | if sent_fingerprint != "Badgers" { 194 | t.Errorf("Custom fingerprint should override default. expected=%v actual=%#v.", "Badgers", sent_fingerprint) 195 | return 196 | } 197 | } 198 | 199 | func TestNotifyWithRequest(t *testing.T) { 200 | setup(t) 201 | defer teardown() 202 | 203 | reqUrl := "/reqPath?qKey=qValue" 204 | var req *http.Request 205 | 206 | // Make sure nil request doesn't panic 207 | Notify("Cobras!", req) 208 | 209 | // Test a request with query data without form 210 | req = httptest.NewRequest("GET", reqUrl, nil) 211 | Notify("Cobras!", req) 212 | Flush() 213 | 214 | // Test a request with form and query data 215 | req = httptest.NewRequest("GET", reqUrl, nil) 216 | req.Header.Set("Accept", "application/test-data") 217 | req.Form = url.Values{"fKey": {"fValue"}} 218 | Notify("Cobras!", req) 219 | Flush() 220 | 221 | if !testRequestCount(t, 3) { 222 | return 223 | } 224 | 225 | // Request[0] - Valid error means we properly handled a nil value 226 | if error := requests[0].decodeJSON()["error"]; error == nil { 227 | t.Errorf("Request error should be populated.") 228 | } 229 | 230 | // Request[1] - Checks URL & query extraction 231 | payload := requests[1].decodeJSON() 232 | request_payload, _ := payload["request"].(map[string]interface{}) 233 | 234 | if url, _ := request_payload["url"].(string); url != reqUrl { 235 | t.Errorf("Request URL should be extracted. expected=%v actual=%#v.", "/fail", url) 236 | return 237 | } 238 | 239 | params, _ := request_payload["params"].(map[string]interface{}) 240 | values, _ := params["qKey"].([]interface{}) 241 | if len(params) != 1 || len(values) != 1 || values[0] != "qValue" { 242 | t.Errorf("Request params should be extracted. expected=%v actual=%#v.", req.Form, params) 243 | } 244 | 245 | // Request[2] - Checks header & form extraction 246 | payload = requests[2].decodeJSON() 247 | request_payload, _ = payload["request"].(map[string]interface{}) 248 | 249 | if !testNoticePayload(t, payload) { 250 | return 251 | } 252 | 253 | cgi, _ := request_payload["cgi_data"].(map[string]interface{}) 254 | if len(cgi) != 1 || cgi["HTTP_ACCEPT"] != "application/test-data" { 255 | t.Errorf("Request cgi_data should be extracted. expected=%v actual=%#v.", req.Header, cgi) 256 | } 257 | 258 | params, _ = request_payload["params"].(map[string]interface{}) 259 | values, _ = params["fKey"].([]interface{}) 260 | if len(params) != 1 || len(values) != 1 || values[0] != "fValue" { 261 | t.Errorf("Request params should be extracted. expected=%v actual=%#v.", req.Form, params) 262 | } 263 | } 264 | 265 | func TestMonitor(t *testing.T) { 266 | setup(t) 267 | defer teardown() 268 | 269 | defer func() { 270 | _ = recover() 271 | 272 | if !testRequestCount(t, 1) { 273 | return 274 | } 275 | 276 | testNoticePayload(t, requests[0].decodeJSON()) 277 | }() 278 | 279 | defer Monitor() 280 | 281 | panic("Cobras!") 282 | } 283 | 284 | func TestNotifyWithHandler(t *testing.T) { 285 | setup(t) 286 | defer teardown() 287 | 288 | BeforeNotify(func(n *Notice) error { 289 | n.Fingerprint = "foo bar baz" 290 | return nil 291 | }) 292 | Notify(errors.New("Cobras!")) 293 | Flush() 294 | 295 | payload := requests[0].decodeJSON() 296 | error_payload, _ := payload["error"].(map[string]interface{}) 297 | sent_fingerprint, _ := error_payload["fingerprint"].(string) 298 | 299 | if !testRequestCount(t, 1) { 300 | return 301 | } 302 | 303 | if sent_fingerprint != "foo bar baz" { 304 | t.Errorf("Handler fingerprint should override default. expected=%v actual=%#v.", "foo bar baz", sent_fingerprint) 305 | return 306 | } 307 | } 308 | 309 | func TestNotifyWithHandlerError(t *testing.T) { 310 | setup(t) 311 | defer teardown() 312 | 313 | err := fmt.Errorf("Skipping this notification") 314 | 315 | BeforeNotify(func(n *Notice) error { 316 | return err 317 | }) 318 | _, notifyErr := Notify(errors.New("Cobras!")) 319 | Flush() 320 | 321 | if !testRequestCount(t, 0) { 322 | return 323 | } 324 | 325 | if notifyErr != err { 326 | t.Errorf("Notify should return error from handler. expected=%v actual=%#v.", err, notifyErr) 327 | return 328 | } 329 | } 330 | 331 | // Helper functions. 332 | 333 | func assertContext(t *testing.T, payload hash, expected Context) { 334 | var request, context hash 335 | var ok bool 336 | 337 | request, ok = payload["request"].(map[string]interface{}) 338 | if !ok { 339 | t.Errorf("Missing request in payload actual=%#v.", payload) 340 | return 341 | } 342 | 343 | context, ok = request["context"].(map[string]interface{}) 344 | if !ok { 345 | t.Errorf("Missing context in request payload actual=%#v.", request) 346 | return 347 | } 348 | 349 | for k, v := range expected { 350 | if context[k] != v { 351 | t.Errorf("Expected context to include hash. expected=%#v actual=%#v", expected, context) 352 | return 353 | } 354 | } 355 | } 356 | 357 | func testRequestCount(t *testing.T, num int) bool { 358 | if len(requests) != num { 359 | t.Errorf("Expected %v request to have been made. expected=%#v actual=%#v", num, num, len(requests)) 360 | return false 361 | } 362 | return true 363 | } 364 | 365 | func testNoticePayload(t *testing.T, payload hash) bool { 366 | for _, key := range []string{"notifier", "error", "request", "server"} { 367 | switch payload[key].(type) { 368 | case map[string]interface{}: 369 | // OK 370 | default: 371 | t.Errorf("Expected payload to include %v hash. expected=%#v actual=%#v", key, key, payload) 372 | return false 373 | } 374 | } 375 | return true 376 | } 377 | 378 | func TestHandlerCallsHandler(t *testing.T) { 379 | mockHandler := &MockedHandler{} 380 | mockHandler.On("ServeHTTP").Return() 381 | 382 | handler := Handler(mockHandler) 383 | req, _ := http.NewRequest("GET", "", nil) 384 | w := httptest.NewRecorder() 385 | handler.ServeHTTP(w, req) 386 | 387 | mockHandler.AssertCalled(t, "ServeHTTP") 388 | } 389 | 390 | func assertMethod(t *testing.T, r *http.Request, method string) { 391 | if r.Method != method { 392 | t.Errorf("Unexpected request method. actual=%#v expected=%#v", r.Method, method) 393 | } 394 | } 395 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Honeybadger for Go 2 | 3 | [![Build Status](https://travis-ci.org/honeybadger-io/honeybadger-go.svg?branch=master)](https://travis-ci.org/honeybadger-io/honeybadger-go) 4 | 5 | Go (golang) support for the :zap: [Honeybadger error 6 | notifier](https://www.honeybadger.io/). Receive instant notification of panics 7 | and errors in your Go applications. 8 | 9 | ## Getting Started 10 | 11 | 12 | ### 1. Install the library 13 | 14 | To install, grab the package from GitHub: 15 | 16 | ```sh 17 | go get github.com/honeybadger-io/honeybadger-go 18 | ``` 19 | 20 | Then add an import to your application code: 21 | 22 | ```go 23 | import "github.com/honeybadger-io/honeybadger-go" 24 | ``` 25 | 26 | ### 2. Set your API key 27 | 28 | Finally, configure your API key: 29 | 30 | ```go 31 | honeybadger.Configure(honeybadger.Configuration{APIKey: "{{PROJECT_API_KEY}}"}) 32 | ``` 33 | 34 | You can also configure Honeybadger via environment variables. See 35 | [Configuration](#configuration) for more information. 36 | 37 | ### 3. Enable automatic panic reporting 38 | 39 | #### Panics during HTTP requests 40 | 41 | To automatically report panics which happen during an HTTP request, wrap your 42 | `http.Handler` function with `honeybadger.Handler`: 43 | 44 | ```go 45 | log.Fatal(http.ListenAndServe(":8080", honeybadger.Handler(handler))) 46 | ``` 47 | 48 | Request data such as cookies and params will automatically be reported with 49 | errors which happen inside `honeybadger.Handler`. Make sure you recover from 50 | panics after honeybadger's Handler has been executed to ensure all panics are 51 | reported. 52 | 53 | #### Unhandled Panics 54 | 55 | 56 | To report all unhandled panics which happen in your application 57 | the following can be added to `main()`: 58 | 59 | ```go 60 | func main() { 61 | defer honeybadger.Monitor() 62 | // application code... 63 | } 64 | ``` 65 | 66 | #### Manually Reporting Errors 67 | 68 | To report an error manually, use `honeybadger.Notify`: 69 | 70 | ```go 71 | if err != nil { 72 | honeybadger.Notify(err) 73 | } 74 | ``` 75 | 76 | 77 | ## Sample Application 78 | 79 | If you'd like to see the library in action before you integrate it with your apps, check out our [sample application](https://github.com/honeybadger-io/crywolf-go). 80 | 81 | You can deploy the sample app to your Heroku account by clicking this button: 82 | 83 | [![Deploy](https://www.herokucdn.com/deploy/button.png)](https://heroku.com/deploy?template=https://github.com/honeybadger-io/crywolf-go) 84 | 85 | Don't forget to destroy the Heroku app after you're done so that you aren't charged for usage. 86 | 87 | The code for the sample app is [available on Github](https://github.com/honeybadger-io/crywolf-go), in case you'd like to read through it, or run it locally. 88 | 89 | 90 | ## Configuration 91 | 92 | To set configuration options, use the `honeybadger.Configuration` method, like so: 93 | 94 | ```go 95 | honeybadger.Configure(honeybadger.Configuration{ 96 | APIKey: "{{PROJECT_API_KEY}}", 97 | Env: "staging" 98 | }) 99 | ``` 100 | The following options are available to you: 101 | 102 | | Name | Type | Default | Example | Environment variable | 103 | | ----- | ---- | ------- | ------- | -------------------- | 104 | | APIKey | `string` | `""` | `"badger01"` | `HONEYBADGER_API_KEY` | 105 | | Root | `string` | The current working directory | `"/path/to/project"` | `HONEYBADGER_ROOT` | 106 | | Env | `string` | `""` | `"production"` | `HONEYBADGER_ENV` | 107 | | Hostname | `string` | The hostname of the current server. | `"badger01"` | `HONEYBADGER_HOSTNAME` | 108 | | Endpoint | `string` | `"https://api.honeybadger.io"` | `"https://honeybadger.example.com/"` | `HONEYBADGER_ENDPOINT` | 109 | | Sync | `bool` | false | `true` | `HONEYBADGER_SYNC` | 110 | | Timeout | `time.Duration` | 3 seconds | `10 * time.Second` | `HONEYBADGER_TIMEOUT` (nanoseconds) | 111 | | Logger | `honeybadger.Logger` | Logs to stderr | `CustomLogger{}` | n/a | 112 | | Backend | `honeybadger.Backend` | HTTP backend | `CustomBackend{}` | n/a | 113 | 114 | 115 | ## Public Interface 116 | 117 | ### `honeybadger.Notify()`: Send an error to Honeybadger. 118 | 119 | If you've handled a panic in your code, but would still like to report the error to Honeybadger, this is the method for you. 120 | 121 | #### Examples: 122 | 123 | ```go 124 | if err != nil { 125 | honeybadger.Notify(err) 126 | } 127 | ``` 128 | 129 | You can also add local context using an optional second argument when calling 130 | `honeybadger.Notify`: 131 | 132 | ```go 133 | honeybadger.Notify(err, honeybadger.Context{"user_id": 2}) 134 | ``` 135 | 136 | Honeybadger uses the error's class name to group similar errors together. If 137 | your error classes are often generic (such as `errors.errorString`), you can 138 | improve grouping by overriding the default with something more unique: 139 | 140 | ```go 141 | honeybadger.Notify(err, honeybadger.ErrorClass{"CustomClassName"}) 142 | ``` 143 | 144 | To override grouping entirely, you can send a custom fingerprint. All errors 145 | with the same fingerprint will be grouped together: 146 | 147 | ```go 148 | honeybadger.Notify(err, honeybadger.Fingerprint{"A unique string"}) 149 | ``` 150 | 151 | To tag errors in Honeybadger: 152 | 153 | ```go 154 | honeybadger.Notify(err, honeybadger.Tags{"timeout", "http"}) 155 | ``` 156 | 157 | --- 158 | 159 | 160 | ### `honeybadger.SetContext()`: Set metadata to be sent if an error occurs 161 | 162 | This method lets you set context data that will be sent if an error should occur. 163 | 164 | For example, it's often useful to record the current user's ID when an error occurs in a web app. To do that, just use `SetContext` to set the user id on each request. If an error occurs, the id will be reported with it. 165 | 166 | **Note**: This method is currently shared across goroutines, and therefore may not be optimal for use in highly concurrent use cases, such as HTTP requests. See [issue #35](https://github.com/honeybadger-io/honeybadger-go/issues/35). 167 | 168 | #### Examples: 169 | 170 | ```go 171 | honeybadger.SetContext(honeybadger.Context{ 172 | "user_id": 1, 173 | }) 174 | ``` 175 | 176 | --- 177 | 178 | ### ``defer honeybadger.Monitor()``: Automatically report panics from your functions 179 | 180 | To automatically report panics in your functions or methods, add 181 | `defer honeybadger.Monitor()` to the beginning of the function or method you wish to monitor. 182 | 183 | 184 | #### Examples: 185 | 186 | ```go 187 | func risky() { 188 | defer honeybadger.Monitor() 189 | // risky business logic... 190 | } 191 | ``` 192 | 193 | __Important:__ `honeybadger.Monitor()` will re-panic after it reports the error, so make sure that it is only called once before recovering from the panic (or allowing the process to crash). 194 | 195 | --- 196 | 197 | ### ``honeybadger.BeforeNotify()``: Add a callback to skip or modify error notification. 198 | 199 | Sometimes you may want to modify the data sent to Honeybadger right before an 200 | error notification is sent, or skip the notification entirely. To do so, add a 201 | callback using `honeybadger.BeforeNotify()`. 202 | 203 | #### Examples: 204 | 205 | ```go 206 | honeybadger.BeforeNotify( 207 | func(notice *honeybadger.Notice) error { 208 | if notice.ErrorClass == "SkippedError" { 209 | return fmt.Errorf("Skipping this notification") 210 | } 211 | // Return nil to send notification for all other classes. 212 | return nil 213 | } 214 | ) 215 | ``` 216 | 217 | To modify information: 218 | 219 | ```go 220 | honeybadger.BeforeNotify( 221 | func(notice *honeybadger.Notice) error { 222 | // Errors in Honeybadger will always have the class name "GenericError". 223 | notice.ErrorClass = "GenericError" 224 | return nil 225 | } 226 | ) 227 | ``` 228 | 229 | --- 230 | 231 | ### ``honeybadger.NewNullBackend()``: Disable data reporting. 232 | 233 | `NewNullBackend` creates a backend which swallows all errors and does not send them to Honeybadger. This is useful for development and testing to disable sending unnecessary errors. 234 | 235 | #### Examples: 236 | 237 | ```go 238 | honeybadger.Configure(honeybadger.Configuration{Backend: honeybadger.NewNullBackend()}) 239 | ``` 240 | 241 | --- 242 | 243 | ## Creating a new client 244 | 245 | In the same way that the log library provides a predefined "standard" logger, honeybadger defines a standard client which may be accessed directly via `honeybadger`. A new client may also be created by calling `honeybadger.New`: 246 | 247 | ```go 248 | hb := honeybadger.New(honeybadger.Configuration{APIKey: "some other api key"}) 249 | hb.Notify("This error was reported by an alternate client.") 250 | ``` 251 | 252 | ## Grouping 253 | 254 | Honeybadger groups by the error class and the first line of the backtrace by 255 | default. In some cases it may be desirable to provide your own grouping 256 | algorithm. One use case for this is `errors.errorString`. Because that type is 257 | used for many different types of errors in Go, Honeybadger will appear to group 258 | unrelated errors together. Here's an example of providing a custom fingerprint 259 | which will group `errors.errorString` by message instead: 260 | 261 | ```go 262 | honeybadger.BeforeNotify( 263 | func(notice *honeybadger.Notice) error { 264 | if notice.ErrorClass == "errors.errorString" { 265 | notice.Fingerprint = notice.Message 266 | } 267 | return nil 268 | } 269 | ) 270 | ``` 271 | 272 | Note that in this example, the backtrace is ignored. If you want to group by 273 | message *and* backtrace, you could append data from `notice.Backtrace` to the 274 | fingerprint string. 275 | 276 | An alternate approach would be to override `notice.ErrorClass` with a more 277 | specific class name that may be inferred from the message. 278 | 279 | ## Sync Mode 280 | 281 | By default, we send out all notices via a separate worker goroutine. This is 282 | awesome for long running applications as it keeps Honeybadger from blocking 283 | during execution. However, this can be a problem for short running applications 284 | (lambdas, for example) as the program might terminate before all messages are 285 | processed. To combat this, you can configure Honeybadger to work in 286 | "Sync" mode which blocks until notices are sent when `honeybadger.Notify` is 287 | executed. You can enable sync mode by setting the `HONEYBADGER_SYNC` environment 288 | variable or updating the config: 289 | 290 | ```go 291 | honeybadger.Configure(honeybadger.Configuration{Sync: true}) 292 | ``` 293 | 294 | "Sync" mode is most useful for situations when you are not sending the notice 295 | directly. If you *are* sending them directly and you want the same 296 | functionality, you can call `honeybadger.Flush` after sending the Notice to 297 | block until the worker has completed processing. 298 | 299 | ```go 300 | honeybadger.Notify("I errored.") 301 | honeybadger.Flush() 302 | ``` 303 | 304 | ## Versioning 305 | 306 | We use [Semantic Versioning](http://semver.org/) to version releases of 307 | honeybadger-go. Because there is no official method to specify version 308 | dependencies in Go, we will do our best never to introduce a breaking change on 309 | the master branch of this repo after reaching version 1. Until we reach version 310 | 1 there is a small chance that we may introduce a breaking change (changing the 311 | signature of a function or method, for example), but we'll always tag a new 312 | minor release and broadcast that we made the change. 313 | 314 | If you're concerned about versioning, there are two options: 315 | 316 | ### Vendor your dependencies 317 | 318 | If you're really concerned about changes to this library, then copy it into your 319 | source control management system so that you can perform upgrades on your own 320 | time. 321 | 322 | ### Use gopkg.in 323 | 324 | Rather than importing directly from GitHub, [gopkg.in](http://gopkg.in/) allows 325 | you to use their special URL format to transparently import a branch or tag from 326 | GitHub. Because we tag each release, using gopkg.in can enable you to depend 327 | explicitly on a certain version of this library. Importing from gopkg.in instead 328 | of directly from GitHub is as easy as: 329 | 330 | ```go 331 | import "gopkg.in/honeybadger-io/honeybadger-go.v0" 332 | ``` 333 | 334 | Check out the [gopkg.in](http://gopkg.in/) homepage for more information on how 335 | to request versions. 336 | 337 | ## Changelog 338 | 339 | See https://github.com/honeybadger-io/honeybadger-go/blob/master/CHANGELOG.md 340 | 341 | ## Contributing 342 | 343 | If you're adding a new feature, please [submit an issue](https://github.com/honeybadger-io/honeybadger-go/issues/new) as a preliminary step; that way you can be (moderately) sure that your pull request will be accepted. 344 | 345 | ### To contribute your code: 346 | 347 | 1. Fork it. 348 | 2. Create a topic branch `git checkout -b my_branch` 349 | 3. Commit your changes `git commit -am "Boom"` 350 | 3. Push to your branch `git push origin my_branch` 351 | 4. Send a [pull request](https://github.com/honeybadger-io/honeybadger-go/pulls) 352 | 353 | ## Releasing 354 | 355 | 1. Update `VERSION` in `honeybadger.go` 356 | 2. Include version in `CHANGELOG.md` 357 | 3. Commit release and push tag with release version 358 | 359 | ### License 360 | 361 | This library is MIT licensed. See the [LICENSE](https://raw.github.com/honeybadger-io/honeybadger-go/master/LICENSE) file in this repository for details. 362 | --------------------------------------------------------------------------------