diff options
Diffstat (limited to 'vendor/gopkg.in/airbrake/gobrake.v2/notifier.go')
-rw-r--r-- | vendor/gopkg.in/airbrake/gobrake.v2/notifier.go | 280 |
1 files changed, 280 insertions, 0 deletions
diff --git a/vendor/gopkg.in/airbrake/gobrake.v2/notifier.go b/vendor/gopkg.in/airbrake/gobrake.v2/notifier.go new file mode 100644 index 00000000..e409321f --- /dev/null +++ b/vendor/gopkg.in/airbrake/gobrake.v2/notifier.go @@ -0,0 +1,280 @@ +package gobrake // import "gopkg.in/airbrake/gobrake.v2" + +import ( + "bytes" + "crypto/tls" + "encoding/json" + "errors" + "fmt" + "net" + "net/http" + "path/filepath" + "strings" + "sync" + "time" +) + +const defaultAirbrakeHost = "https://airbrake.io" +const waitTimeout = 5 * time.Second +const httpStatusTooManyRequests = 429 + +var ( + errClosed = errors.New("gobrake: notifier is closed") + errRateLimited = errors.New("gobrake: rate limited") +) + +var httpClient = &http.Client{ + Transport: &http.Transport{ + Proxy: http.ProxyFromEnvironment, + Dial: (&net.Dialer{ + Timeout: 15 * time.Second, + KeepAlive: 30 * time.Second, + }).Dial, + TLSHandshakeTimeout: 10 * time.Second, + TLSClientConfig: &tls.Config{ + ClientSessionCache: tls.NewLRUClientSessionCache(1024), + }, + MaxIdleConnsPerHost: 10, + ResponseHeaderTimeout: 10 * time.Second, + }, + Timeout: 10 * time.Second, +} + +var buffers = sync.Pool{ + New: func() interface{} { + return new(bytes.Buffer) + }, +} + +type filter func(*Notice) *Notice + +type Notifier struct { + // http.Client that is used to interact with Airbrake API. + Client *http.Client + + projectId int64 + projectKey string + createNoticeURL string + + filters []filter + + wg sync.WaitGroup + noticeCh chan *Notice + closed chan struct{} +} + +func NewNotifier(projectId int64, projectKey string) *Notifier { + n := &Notifier{ + Client: httpClient, + + projectId: projectId, + projectKey: projectKey, + createNoticeURL: getCreateNoticeURL(defaultAirbrakeHost, projectId, projectKey), + + filters: []filter{noticeBacktraceFilter}, + + noticeCh: make(chan *Notice, 1000), + closed: make(chan struct{}), + } + for i := 0; i < 10; i++ { + go n.worker() + } + return n +} + +// Sets Airbrake host name. Default is https://airbrake.io. +func (n *Notifier) SetHost(h string) { + n.createNoticeURL = getCreateNoticeURL(h, n.projectId, n.projectKey) +} + +// AddFilter adds filter that can modify or ignore notice. +func (n *Notifier) AddFilter(fn filter) { + n.filters = append(n.filters, fn) +} + +// Notify notifies Airbrake about the error. +func (n *Notifier) Notify(e interface{}, req *http.Request) { + notice := n.Notice(e, req, 1) + n.SendNoticeAsync(notice) +} + +// Notice returns Aibrake notice created from error and request. depth +// determines which call frame to use when constructing backtrace. +func (n *Notifier) Notice(err interface{}, req *http.Request, depth int) *Notice { + return NewNotice(err, req, depth+3) +} + +type sendResponse struct { + Id string `json:"id"` +} + +// SendNotice sends notice to Airbrake. +func (n *Notifier) SendNotice(notice *Notice) (string, error) { + for _, fn := range n.filters { + notice = fn(notice) + if notice == nil { + // Notice is ignored. + return "", nil + } + } + + buf := buffers.Get().(*bytes.Buffer) + defer buffers.Put(buf) + + buf.Reset() + if err := json.NewEncoder(buf).Encode(notice); err != nil { + return "", err + } + + resp, err := n.Client.Post(n.createNoticeURL, "application/json", buf) + if err != nil { + return "", err + } + defer resp.Body.Close() + + buf.Reset() + _, err = buf.ReadFrom(resp.Body) + if err != nil { + return "", err + } + + if resp.StatusCode != http.StatusCreated { + if resp.StatusCode == httpStatusTooManyRequests { + return "", errRateLimited + } + err := fmt.Errorf("gobrake: got response status=%q, wanted 201 CREATED", resp.Status) + return "", err + } + + var sendResp sendResponse + err = json.NewDecoder(buf).Decode(&sendResp) + if err != nil { + return "", err + } + + return sendResp.Id, nil +} + +func (n *Notifier) sendNotice(notice *Notice) { + if _, err := n.SendNotice(notice); err != nil && err != errRateLimited { + logger.Printf("gobrake failed reporting notice=%q: %s", notice, err) + } + n.wg.Done() +} + +// SendNoticeAsync acts as SendNotice, but sends notice asynchronously +// and pending notices can be flushed with Flush. +func (n *Notifier) SendNoticeAsync(notice *Notice) { + select { + case <-n.closed: + return + default: + } + + n.wg.Add(1) + select { + case n.noticeCh <- notice: + default: + n.wg.Done() + logger.Printf( + "notice=%q is ignored, because queue is full (len=%d)", + notice, len(n.noticeCh), + ) + } +} + +func (n *Notifier) worker() { + for { + select { + case notice := <-n.noticeCh: + n.sendNotice(notice) + case <-n.closed: + select { + case notice := <-n.noticeCh: + n.sendNotice(notice) + default: + return + } + } + } +} + +// NotifyOnPanic notifies Airbrake about the panic and should be used +// with defer statement. +func (n *Notifier) NotifyOnPanic() { + if v := recover(); v != nil { + notice := n.Notice(v, nil, 3) + n.SendNotice(notice) + panic(v) + } +} + +// Flush waits for pending requests to finish. +func (n *Notifier) Flush() { + n.waitTimeout(waitTimeout) +} + +// Deprecated. Use CloseTimeout instead. +func (n *Notifier) WaitAndClose(timeout time.Duration) error { + return n.CloseTimeout(timeout) +} + +// CloseTimeout waits for pending requests to finish and then closes the notifier. +func (n *Notifier) CloseTimeout(timeout time.Duration) error { + select { + case <-n.closed: + default: + close(n.closed) + } + return n.waitTimeout(timeout) +} + +func (n *Notifier) waitTimeout(timeout time.Duration) error { + done := make(chan struct{}) + go func() { + n.wg.Wait() + close(done) + }() + + select { + case <-done: + return nil + case <-time.After(timeout): + return fmt.Errorf("Wait timed out after %s", timeout) + } +} + +func (n *Notifier) Close() error { + return n.CloseTimeout(waitTimeout) +} + +func getCreateNoticeURL(host string, projectId int64, key string) string { + return fmt.Sprintf( + "%s/api/v3/projects/%d/notices?key=%s", + host, projectId, key, + ) +} + +func noticeBacktraceFilter(notice *Notice) *Notice { + v, ok := notice.Context["rootDirectory"] + if !ok { + return notice + } + + dir, ok := v.(string) + if !ok { + return notice + } + + dir = filepath.Join(dir, "src") + for i := range notice.Errors { + replaceRootDirectory(notice.Errors[i].Backtrace, dir) + } + return notice +} + +func replaceRootDirectory(backtrace []StackFrame, rootDir string) { + for i := range backtrace { + backtrace[i].File = strings.Replace(backtrace[i].File, rootDir, "[PROJECT_ROOT]", 1) + } +} |