reg/vendor/gopkg.in/airbrake/gobrake.v2/notifier.go
Jess Frazelle 46640512e7
update deps
Signed-off-by: Jess Frazelle <acidburn@google.com>
2017-03-04 22:10:04 -08:00

281 lines
5.9 KiB
Go

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)
}
}