pax_global_header00006660000000000000000000000064134257227640014526gustar00rootroot0000000000000052 comment=ce87e9f95e2e85eb20786d08605b23a10caf60ba logrus_sentry-0.8.1/000077500000000000000000000000001342572276400144535ustar00rootroot00000000000000logrus_sentry-0.8.1/.travis.yml000066400000000000000000000005721342572276400165700ustar00rootroot00000000000000sudo: false language: go go: - 1.10.x - 1.x - tip matrix: allow_failures: - go: tip before_install: - go get github.com/axw/gocov/gocov - go get github.com/mattn/goveralls - go get golang.org/x/tools/cmd/cover - test -z "$(gofmt -s -l . | tee /dev/stderr)" - go tool vet -all -structtags -shadow . script: - $HOME/gopath/bin/goveralls -service=travis-ci logrus_sentry-0.8.1/LICENSE000066400000000000000000000021001342572276400154510ustar00rootroot00000000000000The MIT License (MIT) Copyright (c) 2016 logrus_sentry Authors 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. logrus_sentry-0.8.1/README.md000066400000000000000000000143101342572276400157310ustar00rootroot00000000000000Sentry Hook for Logrus :walrus: ---- [![GoDoc][1]][2] [![Release][5]][6] [![Build Status][7]][8] [![Coverage Status][9]][10] [![Go Report Card][13]][14] [![Code Climate][19]][20] [![BCH compliance][21]][22] [1]: https://godoc.org/github.com/evalphobia/logrus_sentry?status.svg [2]: https://godoc.org/github.com/evalphobia/logrus_sentry [4]: LICENSE.md [5]: https://img.shields.io/github/release/evalphobia/logrus_sentry.svg [6]: https://github.com/evalphobia/logrus_sentry/releases/latest [7]: https://travis-ci.org/evalphobia/logrus_sentry.svg?branch=master [8]: https://travis-ci.org/evalphobia/logrus_sentry [9]: https://coveralls.io/repos/evalphobia/logrus_sentry/badge.svg?branch=master&service=github [10]: https://coveralls.io/github/evalphobia/logrus_sentry?branch=master [11]: https://codecov.io/github/evalphobia/logrus_sentry/coverage.svg?branch=master [12]: https://codecov.io/github/evalphobia/logrus_sentry?branch=master [13]: https://goreportcard.com/badge/github.com/evalphobia/logrus_sentry [14]: https://goreportcard.com/report/github.com/evalphobia/logrus_sentry [15]: https://img.shields.io/github/downloads/evalphobia/logrus_sentry/total.svg?maxAge=1800 [16]: https://github.com/evalphobia/logrus_sentry/releases [17]: https://img.shields.io/github/stars/evalphobia/logrus_sentry.svg [18]: https://github.com/evalphobia/logrus_sentry/stargazers [19]: https://codeclimate.com/github/evalphobia/logrus_sentry/badges/gpa.svg [20]: https://codeclimate.com/github/evalphobia/logrus_sentry [21]: https://bettercodehub.com/edge/badge/evalphobia/logrus_sentry?branch=master [22]: https://bettercodehub.com/ [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/evalphobia/logrus_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) } } ``` If you wish to initialize a SentryHook with tags, you can use the `NewWithTagsSentryHook` constructor to provide default tags: ```go tags := map[string]string{ "site": "example.com", } levels := []logrus.Level{ logrus.PanicLevel, logrus.FatalLevel, logrus.ErrorLevel, } hook, err := logrus_sentry.NewWithTagsSentryHook(YOUR_DSN, tags, levels) ``` If you wish to initialize a SentryHook with an already initialized raven client, you can use the `NewWithClientSentryHook` constructor: ```go import ( "github.com/sirupsen/logrus" "github.com/evalphobia/logrus_sentry" "github.com/getsentry/raven-go" ) func main() { log := logrus.New() client, err := raven.New(YOUR_DSN) if err != nil { log.Fatal(err) } hook, err := logrus_sentry.NewWithClientSentryHook(client, []logrus.Level{ logrus.PanicLevel, logrus.FatalLevel, logrus.ErrorLevel, }) if err == nil { log.Hooks.Add(hook) } } hook, err := NewWithClientSentryHook(client, []logrus.Level{ logrus.ErrorLevel, }) ``` ## Special fields Some logrus fields have a special meaning in this hook, and they will be especially processed by Sentry. | Field key | Description | | ------------- | ------------- | | `event_id` | Each logged event is identified by the `event_id`, which is hexadecimal string representing a UUID4 value. You can manually specify the identifier of a log event by supplying this field. The `event_id` string should be in one of the following UUID format: `xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx` `xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx` and `urn:uuid:xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx`)| | `user_name` | Name of the user who is in the context of the event | | `user_email` | Email of the user who is in the context of the event | | `user_id` | ID of the user who is in the context of the event | | `user_ip` | IP of the user who is in the context of the event | | `server_name` | Also known as hostname, it is the name of the server which is logging the event (hostname.example.com) | | `tags` | `tags` are `raven.Tags` struct from `github.com/getsentry/raven-go` and override default tags data | | `fingerprint` | `fingerprint` is an string array, that allows you to affect sentry's grouping of events as detailed in the [sentry documentation](https://docs.sentry.io/learn/rollups/#customize-grouping-with-fingerprints) | | `logger` | `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. | | `http_request` | `http_request` is the in-coming request(*http.Request). The detailed request data are sent to Sentry. | ## 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 ``` ## Enabling Stacktraces By default the hook will not send any stacktraces. However, this can be enabled with: ```go hook, _ := logrus_sentry.NewSentryHook(...) hook.StacktraceConfiguration.Enable = true ``` Subsequent calls to `logger.Error` and above will create a stacktrace. Other configuration options are: - `StacktraceConfiguration.Level` the logrus level at which to start capturing stacktraces. - `StacktraceConfiguration.Skip` how many stack frames to skip before stacktrace starts recording. - `StacktraceConfiguration.Context` the number of lines to include around a stack frame for context. - `StacktraceConfiguration.InAppPrefixes` the prefixes that will be matched against the stack frame to identify it as in_app logrus_sentry-0.8.1/async_test.go000066400000000000000000000026521342572276400171630ustar00rootroot00000000000000package logrus_sentry import ( "net/http" "sync" "testing" "time" "github.com/sirupsen/logrus" ) func TestParallelLogging(t *testing.T) { WithTestDSN(t, func(dsn string, pch <-chan *resultPacket) { logger := getTestLogger() hook, err := NewAsyncSentryHook(dsn, []logrus.Level{ logrus.ErrorLevel, }) if err != nil { t.Fatal(err.Error()) } logger.Hooks.Add(hook) wg := &sync.WaitGroup{} // start draining messages var logsReceived int const logCount = 10 go func() { for i := 0; i < logCount; i++ { timeoutCh := time.After(hook.Timeout * 2) var packet *resultPacket select { case packet = <-pch: case <-timeoutCh: t.Fatalf("Waited %s without a response", hook.Timeout*2) } 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) } logsReceived++ wg.Done() } }() req, _ := http.NewRequest("GET", "url", nil) log := logger.WithFields(logrus.Fields{ "server_name": server_name, "logger": logger_name, "http_request": req, }) for i := 0; i < logCount; i++ { wg.Add(1) go func() { log.Error(message) }() } wg.Wait() if logCount != logsReceived { t.Errorf("Sent %d logs, received %d", logCount, logsReceived) } }) } logrus_sentry-0.8.1/data_field.go000066400000000000000000000056731342572276400170710ustar00rootroot00000000000000package logrus_sentry import ( "net/http" "github.com/getsentry/raven-go" "github.com/sirupsen/logrus" ) const ( fieldEventID = "event_id" fieldFingerprint = "fingerprint" fieldLogger = "logger" fieldServerName = "server_name" fieldTags = "tags" fieldHTTPRequest = "http_request" fieldUser = "user" ) type dataField struct { data logrus.Fields omitList map[string]struct{} } func newDataField(data logrus.Fields) *dataField { return &dataField{ data: data, omitList: make(map[string]struct{}), } } func (d *dataField) len() int { return len(d.data) } func (d *dataField) isOmit(key string) bool { _, ok := d.omitList[key] return ok } func (d *dataField) getLogger() (string, bool) { if logger, ok := d.data[fieldLogger].(string); ok { d.omitList[fieldLogger] = struct{}{} return logger, true } return "", false } func (d *dataField) getServerName() (string, bool) { if serverName, ok := d.data[fieldServerName].(string); ok { d.omitList[fieldServerName] = struct{}{} return serverName, true } return "", false } func (d *dataField) getTags() (raven.Tags, bool) { if tags, ok := d.data[fieldTags].(raven.Tags); ok { d.omitList[fieldTags] = struct{}{} return tags, true } return nil, false } func (d *dataField) getFingerprint() ([]string, bool) { if fingerprint, ok := d.data[fieldFingerprint].([]string); ok { d.omitList[fieldFingerprint] = struct{}{} return fingerprint, true } return nil, false } func (d *dataField) getError() (error, bool) { if err, ok := d.data[logrus.ErrorKey].(error); ok { d.omitList[logrus.ErrorKey] = struct{}{} return err, true } return nil, false } func (d *dataField) getHTTPRequest() (*raven.Http, bool) { if req, ok := d.data[fieldHTTPRequest].(*http.Request); ok { d.omitList[fieldHTTPRequest] = struct{}{} return raven.NewHttp(req), true } if req, ok := d.data[fieldHTTPRequest].(*raven.Http); ok { d.omitList[fieldHTTPRequest] = struct{}{} return req, true } return nil, false } func (d *dataField) getEventID() (string, bool) { eventID, ok := d.data[fieldEventID].(string) if !ok { return "", false } //verify eventID is 32 characters hexadecimal string (UUID4) uuid := parseUUID(eventID) if uuid == nil { return "", false } d.omitList[fieldEventID] = struct{}{} return uuid.noDashString(), true } func (d *dataField) getUser() (*raven.User, bool) { data := d.data if v, ok := data[fieldUser]; ok { switch val := v.(type) { case *raven.User: d.omitList[fieldUser] = struct{}{} return val, true case raven.User: d.omitList[fieldUser] = struct{}{} return &val, true } } username, _ := data["user_name"].(string) email, _ := data["user_email"].(string) id, _ := data["user_id"].(string) ip, _ := data["user_ip"].(string) if username == "" && email == "" && id == "" && ip == "" { return nil, false } return &raven.User{ ID: id, Username: username, Email: email, IP: ip, }, true } logrus_sentry-0.8.1/data_field_test.go000066400000000000000000000256571342572276400201340ustar00rootroot00000000000000package logrus_sentry import ( "errors" "fmt" "net/http" "strconv" "testing" "github.com/getsentry/raven-go" "github.com/sirupsen/logrus" "github.com/stretchr/testify/assert" ) func TestLen(t *testing.T) { a := assert.New(t) tests := []struct { fieldSize int }{ {0}, // empty fields {1}, // "0" {2}, // "0", "1" {9}, // "0", "1", "2" ... "8" {100}, // "0", "1", "2" ... "99" } for _, tt := range tests { target := fmt.Sprintf("%+v", tt) fields := logrus.Fields{} for i, max := 0, tt.fieldSize; i < max; i++ { fields[strconv.Itoa(i)] = struct{}{} } df := dataField{ data: fields, } a.Equal(tt.fieldSize, df.len(), "dataField.Len() should equal fieldSize", target) } } func TestIsOmit(t *testing.T) { a := assert.New(t) omitList := map[string]struct{}{ "key_1": {}, "key_2": {}, "key_3": {}, "key_4": {}, } tests := []struct { key string expected bool }{ {"key_1", true}, {"key_2", true}, {"key_3", true}, {"key_4", true}, {"not_key", false}, {"foo", false}, {"bar", false}, {"_key_1", false}, {"key_1_", false}, } for _, tt := range tests { target := fmt.Sprintf("%+v", tt) df := dataField{ omitList: omitList, } a.Equal(tt.expected, df.isOmit(tt.key), target) } } func TestGetLogger(t *testing.T) { a := assert.New(t) tests := []struct { key string value interface{} expected bool description string }{ {"logger", "test_logger", true, "valid logger"}, {"logger", "", true, "valid logger"}, {"not_logger", "test_logger", false, "invalid key"}, {"logger", 1, false, "invalid value type"}, {"logger", true, false, "invalid value type"}, {"logger", struct{}{}, false, "invalid value type"}, } for _, tt := range tests { target := fmt.Sprintf("%+v", tt) fields := logrus.Fields{} fields[tt.key] = tt.value df := newDataField(fields) logger, ok := df.getLogger() a.Equal(tt.expected, ok, target) if ok { a.Equal(tt.value, logger, target) a.True(df.isOmit("logger"), "`logger` should be in omitList") } else { a.False(df.isOmit("logger"), "`logger` should not be in omitList") } } } func TestGetServerName(t *testing.T) { a := assert.New(t) tests := []struct { key string value interface{} expected bool description string }{ {"server_name", "test_server_name", true, "valid server name"}, {"server_name", "", true, "valid server name"}, {"not_server_name", "test_server_name", false, "invalid key"}, {"server_name", 1, false, "invalid value type"}, {"server_name", true, false, "invalid value type"}, {"server_name", struct{}{}, false, "invalid value type"}, } for _, tt := range tests { target := fmt.Sprintf("%+v", tt) fields := logrus.Fields{} fields[tt.key] = tt.value df := newDataField(fields) serverName, ok := df.getServerName() a.Equal(tt.expected, ok, target) if ok { a.Equal(tt.value, serverName, target) a.True(df.isOmit("server_name"), "`server_name` should be in omitList") } else { a.False(df.isOmit("server_name"), "`server_name` should not be in omitList") } } } func TestGetTags(t *testing.T) { a := assert.New(t) tests := []struct { key string value interface{} expected bool description string }{ {"tags", raven.Tags{{Key: "key", Value: "value"}}, true, "valid tags"}, {"tags", raven.Tags{}, true, "valid tags"}, {"not_tags", raven.Tags{{Key: "key", Value: "value"}}, false, "invalid key"}, {"tags", &raven.Tags{}, false, "invalid value type"}, {"tags", "test_tags", false, "invalid value type"}, {"tags", 1, false, "invalid value type"}, {"tags", true, false, "invalid value type"}, {"tags", struct{}{}, false, "invalid value type"}, } for _, tt := range tests { target := fmt.Sprintf("%+v", tt) fields := logrus.Fields{} fields[tt.key] = tt.value df := newDataField(fields) tags, ok := df.getTags() a.Equal(tt.expected, ok, target) if ok { a.Equal(tt.value, tags, target) a.True(df.isOmit("tags"), "`tags` should be in omitList") } else { a.False(df.isOmit("tags"), "`tags` should not be in omitList") } } } func TestGetFingerprint(t *testing.T) { a := assert.New(t) tests := []struct { key string value interface{} expected bool description string }{ {"fingerprint", []string{"a", "fingerprint"}, true, "valid fingerprint"}, {"fingerprint", []string{}, true, "valid fingerprint"}, {"not_fingerprint", []string{"a", "fingerprint"}, false, "invalid key"}, {"fingerprint", []int{}, false, "invalid value type"}, {"fingerprint", "test_fingerprint", false, "invalid value type"}, {"fingerprint", 1, false, "invalid value type"}, {"fingerprint", true, false, "invalid value type"}, {"fingerprint", struct{}{}, false, "invalid value type"}, } for _, tt := range tests { target := fmt.Sprintf("%+v", tt) fields := logrus.Fields{} fields[tt.key] = tt.value df := newDataField(fields) fingerprint, ok := df.getFingerprint() a.Equal(tt.expected, ok, target) if ok { a.Equal(tt.value, fingerprint, target) a.True(df.isOmit("fingerprint"), "`fingerprint` should be in omitList") } else { a.False(df.isOmit("fingerprint"), "`fingerprint` should not be in omitList") } } } func TestGetError(t *testing.T) { a := assert.New(t) tests := []struct { key string value interface{} expected bool description string }{ {"error", errors.New("error type"), true, "valid error"}, {"error", errors.New(""), true, "valid error"}, {"not_error", errors.New("error type"), false, "invalid key"}, {"error", "test_error", false, "invalid value type"}, {"error", 1, false, "invalid value type"}, {"error", true, false, "invalid value type"}, {"error", struct{}{}, false, "invalid value type"}, } for _, tt := range tests { target := fmt.Sprintf("%+v", tt) fields := logrus.Fields{} fields[tt.key] = tt.value df := newDataField(fields) err, ok := df.getError() a.Equal(tt.expected, ok, target) if ok { a.Equal(tt.value, err, target) a.True(df.isOmit("error"), "`error` should be in omitList") } else { a.False(df.isOmit("error"), "`error` should not be in omitList") } } } func TestGetHTTPRequest(t *testing.T) { a := assert.New(t) httpReq, _ := http.NewRequest("GET", "/", nil) ravenReq := raven.NewHttp(httpReq) tests := []struct { key string value interface{} expected bool description string }{ {"http_request", httpReq, true, "valid http_request"}, {"not_http_request", httpReq, false, "invalid key"}, {"http_request", http.Request{}, false, "invalid value type"}, {"http_request", "test_http_request", false, "invalid value type"}, {"http_request", 1, false, "invalid value type"}, {"http_request", true, false, "invalid value type"}, {"http_request", struct{}{}, false, "invalid value type"}, {"http_request", raven.NewHttp(httpReq), true, "valid raven http_request"}, {"http_request", raven.Http{}, false, "invalid raven http_request"}, } for _, tt := range tests { target := fmt.Sprintf("%+v", tt) fields := logrus.Fields{} fields[tt.key] = tt.value df := newDataField(fields) req, ok := df.getHTTPRequest() a.Equal(tt.expected, ok, target) if ok { a.Equal(ravenReq, req, target) a.True(df.isOmit("http_request"), "`http_request` should be in omitList") } else { a.False(df.isOmit("http_request"), "`http_request` should not be in omitList") } } } func TestGetEventID(t *testing.T) { a := assert.New(t) tests := []struct { key string value interface{} expected bool description string }{ {"event_id", "ffffffff-ffff-ffff-ffff-ffffffffffff", true, "valid event id"}, {"event_id", "ffffffffffffffffffffffffffffffff", true, "valid event id"}, {"event_id", "urn:uuid:ffffffff-ffff-ffff-ffff-ffffffffffff", true, "valid event id"}, {"not_event_id", "ffffffff-ffff-ffff-ffff-ffffffffffff", false, "invalid key"}, {"event_id", "test_event_id", false, "invalid uuid format"}, {"event_id", "ffffffff-ffff-ffff-ffff-ffffffffffffZ", false, "invalid uuid format"}, {"event_id", "Zffffffff-ffff-ffff-ffff-ffffffffffff", false, "invalid uuid format"}, {"event_id", 1, false, "invalid value type"}, {"event_id", true, false, "invalid value type"}, {"event_id", struct{}{}, false, "invalid value type"}, } for _, tt := range tests { target := fmt.Sprintf("%+v", tt) fields := logrus.Fields{} fields[tt.key] = tt.value df := newDataField(fields) eventID, ok := df.getEventID() a.Equal(tt.expected, ok, target) if ok { a.Equal("ffffffffffffffffffffffffffffffff", eventID, target) a.True(df.isOmit("event_id"), "`event_id` should be in omitList") } else { a.False(df.isOmit("event_id"), "`event_id` should not be in omitList") } } } func TestGetUser(t *testing.T) { a := assert.New(t) tests := []struct { key string value interface{} expected bool description string }{ {"user", &raven.User{}, true, "valid user"}, {"user", raven.User{}, true, "valid user"}, {"not_user", &raven.User{}, false, "invalid key"}, {"user", "test_user", false, "invalid value type"}, {"user", 1, false, "invalid value type"}, {"user", true, false, "invalid value type"}, {"user", struct{}{}, false, "invalid value type"}, } for _, tt := range tests { target := fmt.Sprintf("%+v", tt) fields := logrus.Fields{} fields[tt.key] = tt.value df := newDataField(fields) user, ok := df.getUser() a.Equal(tt.expected, ok, target) if ok { a.IsType(&raven.User{}, user, target) a.True(df.isOmit("user"), "`user` should be in omitList") } else { a.False(df.isOmit("user"), "`user` should not be in omitList") } } } func TestGetUserFromString(t *testing.T) { a := assert.New(t) tests := []struct { data map[string]interface{} expected bool description string }{ {map[string]interface{}{ "user_name": "name", "user_email": "example@example.com", "user_id": "A0001", "user_ip": "0.0.0.0", }, true, "valid user"}, {map[string]interface{}{"user_name": "name"}, true, "valid user"}, {map[string]interface{}{"user_email": "example@example.com"}, true, "valid user"}, {map[string]interface{}{"user_id": "A0001"}, true, "valid user"}, {map[string]interface{}{"user_ip": "0.0.0.0"}, true, "valid user"}, {map[string]interface{}{"user_name": ""}, false, "invalid user: empty user_name"}, {map[string]interface{}{"user_email": ""}, false, "invalid user: empty user_email"}, {map[string]interface{}{"user_id": ""}, false, "invalid user: empty user_id"}, {map[string]interface{}{"user_ip": ""}, false, "invalid user: empty user_ip"}, {map[string]interface{}{ "user_name": 1, "user_email": true, "user_id": errors.New("user_id"), "user_ip": "", }, false, "invalid types"}, } for _, tt := range tests { target := fmt.Sprintf("%+v", tt) fields := logrus.Fields(tt.data) df := newDataField(fields) user, ok := df.getUser() a.Equal(tt.expected, ok, target) if ok { a.IsType(&raven.User{}, user, target) } } } logrus_sentry-0.8.1/sentry.go000066400000000000000000000264271342572276400163410ustar00rootroot00000000000000package logrus_sentry import ( "encoding/json" "fmt" "runtime" "sync" "time" "github.com/getsentry/raven-go" "github.com/pkg/errors" "github.com/sirupsen/logrus" ) var ( severityMap = map[logrus.Level]raven.Severity{ logrus.TraceLevel: raven.DEBUG, logrus.DebugLevel: raven.DEBUG, logrus.InfoLevel: raven.INFO, logrus.WarnLevel: raven.WARNING, logrus.ErrorLevel: raven.ERROR, logrus.FatalLevel: raven.FATAL, logrus.PanicLevel: raven.FATAL, } ) // 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. // // This is ignored for asynchronous hooks. If you want to set a timeout when // using an async hook (to bound the length of time that hook.Flush can take), // you probably want to create your own raven.Client and set // ravenClient.Transport.(*raven.HTTPTransport).Client.Timeout to set a // timeout on the underlying HTTP request instead. Timeout time.Duration StacktraceConfiguration StackTraceConfiguration client *raven.Client levels []logrus.Level serverName string ignoreFields map[string]struct{} extraFilters map[string]func(interface{}) interface{} errorHandlers []func(entry *logrus.Entry, err error) asynchronous bool mu sync.RWMutex wg sync.WaitGroup } // The Stacktracer interface allows an error type to return a raven.Stacktrace. type Stacktracer interface { GetStacktrace() *raven.Stacktrace } type causer interface { Cause() error } type pkgErrorStackTracer interface { StackTrace() errors.StackTrace } // StackTraceConfiguration allows for configuring stacktraces type StackTraceConfiguration struct { // whether stacktraces should be enabled Enable bool // the level at which to start capturing stacktraces Level logrus.Level // how many stack frames to skip before stacktrace starts recording Skip int // the number of lines to include around a stack frame for context Context int // the prefixes that will be matched against the stack frame. // if the stack frame's package matches one of these prefixes // sentry will identify the stack frame as "in_app" InAppPrefixes []string // whether sending exception type should be enabled. SendExceptionType bool // whether the exception type and message should be switched. SwitchExceptionTypeAndMessage bool } // 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.New(DSN) if err != nil { return nil, err } return NewWithClientSentryHook(client, levels) } // NewWithTagsSentryHook creates a hook with tags to be added to an instance // of logger and initializes the raven client. This method sets the timeout to // 100 milliseconds. func NewWithTagsSentryHook(DSN string, tags map[string]string, levels []logrus.Level) (*SentryHook, error) { client, err := raven.NewWithTags(DSN, tags) if err != nil { return nil, err } return NewWithClientSentryHook(client, levels) } // NewWithClientSentryHook creates a hook using an initialized raven client. // This method sets the timeout to 100 milliseconds. func NewWithClientSentryHook(client *raven.Client, levels []logrus.Level) (*SentryHook, error) { return &SentryHook{ Timeout: 100 * time.Millisecond, StacktraceConfiguration: StackTraceConfiguration{ Enable: false, Level: logrus.ErrorLevel, Skip: 6, Context: 0, InAppPrefixes: nil, SendExceptionType: true, }, client: client, levels: levels, ignoreFields: make(map[string]struct{}), extraFilters: make(map[string]func(interface{}) interface{}), }, nil } // NewAsyncSentryHook creates a hook same as NewSentryHook, but in asynchronous // mode. func NewAsyncSentryHook(DSN string, levels []logrus.Level) (*SentryHook, error) { hook, err := NewSentryHook(DSN, levels) return setAsync(hook), err } // NewAsyncWithTagsSentryHook creates a hook same as NewWithTagsSentryHook, but // in asynchronous mode. func NewAsyncWithTagsSentryHook(DSN string, tags map[string]string, levels []logrus.Level) (*SentryHook, error) { hook, err := NewWithTagsSentryHook(DSN, tags, levels) return setAsync(hook), err } // NewAsyncWithClientSentryHook creates a hook same as NewWithClientSentryHook, // but in asynchronous mode. func NewAsyncWithClientSentryHook(client *raven.Client, levels []logrus.Level) (*SentryHook, error) { hook, err := NewWithClientSentryHook(client, levels) return setAsync(hook), err } func setAsync(hook *SentryHook) *SentryHook { if hook == nil { return nil } hook.asynchronous = true return hook } // Fire is 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: error, logger, server_name, http_request, tags func (hook *SentryHook) Fire(entry *logrus.Entry) error { hook.mu.RLock() // Allow multiple go routines to log simultaneously defer hook.mu.RUnlock() packet := raven.NewPacket(entry.Message) packet.Timestamp = raven.Timestamp(entry.Time) packet.Level = severityMap[entry.Level] packet.Platform = "go" df := newDataField(entry.Data) // set special fields if hook.serverName != "" { packet.ServerName = hook.serverName } if logger, ok := df.getLogger(); ok { packet.Logger = logger } if serverName, ok := df.getServerName(); ok { packet.ServerName = serverName } if eventID, ok := df.getEventID(); ok { packet.EventID = eventID } if tags, ok := df.getTags(); ok { packet.Tags = tags } if fingerprint, ok := df.getFingerprint(); ok { packet.Fingerprint = fingerprint } if req, ok := df.getHTTPRequest(); ok { packet.Interfaces = append(packet.Interfaces, req) } if user, ok := df.getUser(); ok { packet.Interfaces = append(packet.Interfaces, user) } // set stacktrace data stConfig := &hook.StacktraceConfiguration if stConfig.Enable && entry.Level <= stConfig.Level { if err, ok := df.getError(); ok { var currentStacktrace *raven.Stacktrace currentStacktrace = hook.findStacktrace(err) if currentStacktrace == nil { currentStacktrace = raven.NewStacktrace(stConfig.Skip, stConfig.Context, stConfig.InAppPrefixes) } cause := errors.Cause(err) if cause == nil { cause = err } exc := raven.NewException(cause, currentStacktrace) if !stConfig.SendExceptionType { exc.Type = "" } if stConfig.SwitchExceptionTypeAndMessage { packet.Interfaces = append(packet.Interfaces, currentStacktrace) packet.Culprit = exc.Type + ": " + currentStacktrace.Culprit() } else { packet.Interfaces = append(packet.Interfaces, exc) packet.Culprit = err.Error() } } else { currentStacktrace := raven.NewStacktrace(stConfig.Skip, stConfig.Context, stConfig.InAppPrefixes) if currentStacktrace != nil { packet.Interfaces = append(packet.Interfaces, currentStacktrace) } } } else { // set the culprit even when the stack trace is disabled, as long as we have an error if err, ok := df.getError(); ok { packet.Culprit = err.Error() } } // set other fields dataExtra := hook.formatExtraData(df) if packet.Extra == nil { packet.Extra = dataExtra } else { for k, v := range dataExtra { packet.Extra[k] = v } } _, errCh := hook.client.Capture(packet, nil) switch { case hook.asynchronous: // Our use of hook.mu guarantees that we are following the WaitGroup rule of // not calling Add in parallel with Wait. hook.wg.Add(1) go func() { if err := <-errCh; err != nil { for _, handlerFn := range hook.errorHandlers { handlerFn(entry, err) } } hook.wg.Done() }() return nil case hook.Timeout == 0: return nil default: timeout := hook.Timeout timeoutCh := time.After(timeout) select { case err := <-errCh: for _, handlerFn := range hook.errorHandlers { handlerFn(entry, err) } return err case <-timeoutCh: return fmt.Errorf("no response from sentry server in %s", timeout) } } } // Flush waits for the log queue to empty. This function only does anything in // asynchronous mode. func (hook *SentryHook) Flush() { if !hook.asynchronous { return } hook.mu.Lock() // Claim exclusive access; any logging goroutines will block until the flush completes defer hook.mu.Unlock() hook.wg.Wait() } func (hook *SentryHook) findStacktrace(err error) *raven.Stacktrace { var stacktrace *raven.Stacktrace var stackErr errors.StackTrace for err != nil { // Find the earliest *raven.Stacktrace, or error.StackTrace if tracer, ok := err.(Stacktracer); ok { stacktrace = tracer.GetStacktrace() stackErr = nil } else if tracer, ok := err.(pkgErrorStackTracer); ok { stacktrace = nil stackErr = tracer.StackTrace() } if cause, ok := err.(causer); ok { err = cause.Cause() } else { break } } if stackErr != nil { stacktrace = hook.convertStackTrace(stackErr) } return stacktrace } // convertStackTrace converts an errors.StackTrace into a natively consumable // *raven.Stacktrace func (hook *SentryHook) convertStackTrace(st errors.StackTrace) *raven.Stacktrace { stConfig := &hook.StacktraceConfiguration stFrames := []errors.Frame(st) frames := make([]*raven.StacktraceFrame, 0, len(stFrames)) for i := range stFrames { pc := uintptr(stFrames[i]) fn := runtime.FuncForPC(pc) file, line := fn.FileLine(pc) frame := raven.NewStacktraceFrame(pc, fn.Name(), file, line, stConfig.Context, stConfig.InAppPrefixes) if frame != nil { frames = append(frames, frame) } } // Sentry wants the frames with the oldest first, so reverse them for i, j := 0, len(frames)-1; i < j; i, j = i+1, j-1 { frames[i], frames[j] = frames[j], frames[i] } return &raven.Stacktrace{Frames: frames} } // Levels returns the available logging levels. func (hook *SentryHook) Levels() []logrus.Level { return hook.levels } // AddIgnore adds field name to ignore. func (hook *SentryHook) AddIgnore(name string) { hook.ignoreFields[name] = struct{}{} } // AddExtraFilter adds a custom filter function. func (hook *SentryHook) AddExtraFilter(name string, fn func(interface{}) interface{}) { hook.extraFilters[name] = fn } // AddErrorHandler adds a error handler function used when Sentry returns error. func (hook *SentryHook) AddErrorHandler(fn func(entry *logrus.Entry, err error)) { hook.errorHandlers = append(hook.errorHandlers, fn) } func (hook *SentryHook) formatExtraData(df *dataField) (result map[string]interface{}) { // create a map for passing to Sentry's extra data result = make(map[string]interface{}, df.len()) for k, v := range df.data { if df.isOmit(k) { continue // skip already used special fields } if _, ok := hook.ignoreFields[k]; ok { continue } if fn, ok := hook.extraFilters[k]; ok { v = fn(v) // apply custom filter } else { v = formatData(v) // use default formatter } result[k] = v } return result } // formatData returns value as a suitable format. func formatData(value interface{}) (formatted interface{}) { switch value := value.(type) { case json.Marshaler: return value case error: return value.Error() case fmt.Stringer: return value.String() default: return value } } logrus_sentry-0.8.1/sentry_setter.go000066400000000000000000000026031342572276400177150ustar00rootroot00000000000000package logrus_sentry import ( "github.com/getsentry/raven-go" ) // SetDefaultLoggerName sets default logger name tag. func (hook *SentryHook) SetDefaultLoggerName(name string) { hook.client.SetDefaultLoggerName(name) } // SetEnvironment sets environment tag. func (hook *SentryHook) SetEnvironment(environment string) { hook.client.SetEnvironment(environment) } // SetHttpContext sets http client. func (hook *SentryHook) SetHttpContext(h *raven.Http) { hook.client.SetHttpContext(h) } // SetIgnoreErrors sets ignoreErrorsRegexp. func (hook *SentryHook) SetIgnoreErrors(errs ...string) error { return hook.client.SetIgnoreErrors(errs) } // SetIncludePaths sets includePaths. func (hook *SentryHook) SetIncludePaths(p []string) { hook.client.SetIncludePaths(p) } // SetRelease sets release tag. func (hook *SentryHook) SetRelease(release string) { hook.client.SetRelease(release) } // SetSampleRate sets sampling rate. func (hook *SentryHook) SetSampleRate(rate float32) error { return hook.client.SetSampleRate(rate) } // SetTagsContext sets tags. func (hook *SentryHook) SetTagsContext(t map[string]string) { hook.client.SetTagsContext(t) } // SetUserContext sets user. func (hook *SentryHook) SetUserContext(u *raven.User) { hook.client.SetUserContext(u) } // SetServerName sets server_name tag. func (hook *SentryHook) SetServerName(serverName string) { hook.serverName = serverName } logrus_sentry-0.8.1/sentry_setter_test.go000066400000000000000000000073151342572276400207610ustar00rootroot00000000000000package logrus_sentry import ( "fmt" "testing" "github.com/sirupsen/logrus" "github.com/stretchr/testify/assert" ) func TestSetDefaultLoggerName(t *testing.T) { const name = "my_logger" a := assert.New(t) WithTestDSN(t, func(dsn string, pch <-chan *resultPacket) { logger := getTestLogger() hook, err := NewSentryHook(dsn, []logrus.Level{ logrus.ErrorLevel, }) a.NoError(err, "NewSentryHook should be NoError") hook.SetDefaultLoggerName(name) logger.Hooks.Add(hook) logger.Error(message) packet := <-pch a.Equal(name, packet.Logger, "logger must be set") }) } func TestSetEnvironment(t *testing.T) { const env = "test" a := assert.New(t) WithTestDSN(t, func(dsn string, pch <-chan *resultPacket) { logger := getTestLogger() hook, err := NewSentryHook(dsn, []logrus.Level{ logrus.ErrorLevel, }) a.NoError(err, "NewSentryHook should be NoError") hook.SetEnvironment(env) logger.Hooks.Add(hook) logger.Error(message) packet := <-pch a.Equal(env, packet.Environment, "environment must be set") }) } func TestSetIgnoreErrors(t *testing.T) { a := assert.New(t) tests := []struct { errString string isSuccess bool }{ {"", true}, {"aaa", true}, {"jskljdasidjiaoklzmxcasifjiklmzx9eijodfsklcmzx", true}, {"[0-9]+", true}, {"[0-9", false}, {"+", false}, } WithTestDSN(t, func(dsn string, pch <-chan *resultPacket) { hook, err := NewSentryHook(dsn, []logrus.Level{ logrus.ErrorLevel, }) a.NoError(err, "NewSentryHook should be NoError") for _, tt := range tests { target := fmt.Sprintf("%+v", tt) err := hook.SetIgnoreErrors(tt.errString) switch { case !tt.isSuccess: a.Error(err, target) case tt.isSuccess: a.NoError(err, target) } } }) } func TestSetIncludePaths(t *testing.T) { a := assert.New(t) paths := []string{ "aaa", "bbb", "ccc", } WithTestDSN(t, func(dsn string, pch <-chan *resultPacket) { hook, err := NewSentryHook(dsn, []logrus.Level{ logrus.ErrorLevel, }) a.NoError(err, "NewSentryHook should be NoError") hook.SetIncludePaths(paths) a.Equal(paths, hook.client.IncludePaths(), "includePaths must be set") }) } func TestSetRelease(t *testing.T) { const releaseVer = "v0.1.0" a := assert.New(t) WithTestDSN(t, func(dsn string, pch <-chan *resultPacket) { logger := getTestLogger() hook, err := NewSentryHook(dsn, []logrus.Level{ logrus.ErrorLevel, }) a.NoError(err, "NewSentryHook should be NoError") hook.SetRelease(releaseVer) logger.Hooks.Add(hook) logger.Error(message) packet := <-pch a.Equal(releaseVer, packet.Release, "release version must be set") }) } func TestSetSampleRate(t *testing.T) { a := assert.New(t) tests := []struct { rate float32 isSuccess bool }{ {0.0, true}, {0.1, true}, {0.5, true}, {0.9, true}, {1.0, true}, {-0.1, false}, {-2, false}, {1.1, false}, {2.0, false}, } WithTestDSN(t, func(dsn string, pch <-chan *resultPacket) { hook, err := NewSentryHook(dsn, []logrus.Level{ logrus.ErrorLevel, }) a.NoError(err, "NewSentryHook should be NoError") for _, tt := range tests { target := fmt.Sprintf("%+v", tt) err := hook.SetSampleRate(tt.rate) switch { case !tt.isSuccess: a.Error(err, target) case tt.isSuccess: a.NoError(err, target) } } }) } func TestSetServerName(t *testing.T) { a := assert.New(t) WithTestDSN(t, func(dsn string, pch <-chan *resultPacket) { logger := getTestLogger() hook, err := NewSentryHook(dsn, []logrus.Level{ logrus.ErrorLevel, }) a.NoError(err, "NewSentryHook should be NoError") hook.SetServerName(server_name) logger.Hooks.Add(hook) logger.Error(message) packet := <-pch a.Equal(server_name, packet.ServerName, "server name must be set") }) } logrus_sentry-0.8.1/sentry_stacktrace_test.go000066400000000000000000000101711342572276400215710ustar00rootroot00000000000000package logrus_sentry import ( "strings" "testing" "github.com/getsentry/raven-go" pkgerrors "github.com/pkg/errors" "github.com/sirupsen/logrus" ) func TestSentryStacktrace(t *testing.T) { WithTestDSN(t, func(dsn string, pch <-chan *resultPacket) { logger := getTestLogger() hook, err := NewSentryHook(dsn, []logrus.Level{ logrus.ErrorLevel, logrus.InfoLevel, }) if err != nil { t.Fatal(err.Error()) } logger.Hooks.Add(hook) logger.Error(message) packet := <-pch stacktraceSize := len(packet.Stacktrace.Frames) if stacktraceSize != 0 { t.Error("Stacktrace should be empty as it is not enabled") } hook.StacktraceConfiguration.Enable = true logger.Error(message) // this is the call that the last frame of stacktrace should capture expectedLineno := 33 //this should be the line number of the previous line packet = <-pch stacktraceSize = len(packet.Stacktrace.Frames) if stacktraceSize == 0 { t.Error("Stacktrace should not be empty") } lastFrame := packet.Stacktrace.Frames[stacktraceSize-2] expectedSuffix := "logrus_sentry/sentry_stacktrace_test.go" if !strings.HasSuffix(lastFrame.Filename, expectedSuffix) { t.Errorf("File name should have ended with %s, was %s", expectedSuffix, lastFrame.Filename) } if lastFrame.Lineno != expectedLineno { t.Errorf("Line number should have been %d, was %d", expectedLineno, lastFrame.Lineno) } if lastFrame.InApp { t.Error("Frame should not be identified as in_app without prefixes") } hook.StacktraceConfiguration.InAppPrefixes = []string{"github.com/sirupsen/logrus"} hook.StacktraceConfiguration.Context = 2 hook.StacktraceConfiguration.Skip = 2 logger.Error(message) packet = <-pch stacktraceSize = len(packet.Stacktrace.Frames) if stacktraceSize == 0 { t.Error("Stacktrace should not be empty") } lastFrame = packet.Stacktrace.Frames[stacktraceSize-1] expectedFilename := "github.com/sirupsen/logrus/entry.go" if lastFrame.Filename != expectedFilename { t.Errorf("File name should have been %s, was %s", expectedFilename, lastFrame.Filename) } if !lastFrame.InApp { t.Error("Frame should be identified as in_app") } logger.WithError(myStacktracerError{}).Error(message) // use an error that implements Stacktracer packet = <-pch var frames []*raven.StacktraceFrame if packet.Exception.Stacktrace != nil { frames = packet.Exception.Stacktrace.Frames } if len(frames) != 1 || frames[0].Filename != expectedStackFrameFilename { t.Error("Stacktrace should be taken from err if it implements the Stacktracer interface") } logger.WithError(pkgerrors.Wrap(myStacktracerError{}, "wrapped")).Error(message) // use an error that wraps a Stacktracer packet = <-pch if packet.Exception.Stacktrace != nil { frames = packet.Exception.Stacktrace.Frames } expectedCulprit := "wrapped: myStacktracerError!" if packet.Culprit != expectedCulprit { t.Errorf("Expected culprit of '%s', got '%s'", expectedCulprit, packet.Culprit) } if len(frames) != 1 || frames[0].Filename != expectedStackFrameFilename { t.Error("Stacktrace should be taken from err if it implements the Stacktracer interface") } logger.WithError(pkgerrors.New("errorX")).Error(message) // use an error that implements pkgErrorStackTracer packet = <-pch if packet.Exception.Stacktrace != nil { frames = packet.Exception.Stacktrace.Frames } expectedPkgErrorsStackTraceFilename := "testing/testing.go" expectedFrameCount := 4 expectedCulprit = "errorX" if packet.Culprit != expectedCulprit { t.Errorf("Expected culprit of '%s', got '%s'", expectedCulprit, packet.Culprit) } if len(frames) != expectedFrameCount { t.Errorf("Expected %d frames, got %d", expectedFrameCount, len(frames)) } if !strings.HasSuffix(frames[0].Filename, expectedPkgErrorsStackTraceFilename) { t.Error("Stacktrace should be taken from err if it implements the pkgErrorStackTracer interface") } // zero stack frames defer func() { if err := recover(); err != nil { t.Error("Zero stack frames should not cause panic") } }() hook.StacktraceConfiguration.Skip = 1000 logger.Error(message) <-pch // check panic }) } logrus_sentry-0.8.1/sentry_test.go000066400000000000000000000261141342572276400173710ustar00rootroot00000000000000package logrus_sentry import ( "compress/zlib" "encoding/base64" "encoding/json" "errors" "fmt" "io" "io/ioutil" "net/http" "net/http/httptest" "reflect" "strings" "testing" "time" "github.com/getsentry/raven-go" pkgerrors "github.com/pkg/errors" "github.com/sirupsen/logrus" "github.com/stretchr/testify/assert" ) 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 } // raven.Packet does not have a json directive for deserializing stacktrace // so need to explicitly construct one for purpose of test type resultPacket struct { raven.Packet Stacktrace raven.Stacktrace `json:"stacktrace"` Exception raven.Exception `json:"exception"` } func WithTestDSN(t *testing.T, tf func(string, <-chan *resultPacket)) { pch := make(chan *resultPacket, 1) s := httptest.NewServer(http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) { defer req.Body.Close() contentType := req.Header.Get("Content-Type") var bodyReader io.Reader = req.Body // underlying client will compress and encode payload above certain size if contentType == "application/octet-stream" { bodyReader = base64.NewDecoder(base64.StdEncoding, bodyReader) bodyReader, _ = zlib.NewReader(bodyReader) } d := json.NewDecoder(bodyReader) p := &resultPacket{} 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 *resultPacket) { logger := getTestLogger() hook, err := NewSentryHook(dsn, []logrus.Level{ logrus.ErrorLevel, }) if err != nil { t.Fatal(err.Error()) } logger.Hooks.Add(hook) req, _ := http.NewRequest("GET", "url", nil) logger.WithFields(logrus.Fields{ "logger": logger_name, "server_name": server_name, "http_request": req, }).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 *resultPacket) { 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) } }) } func TestSentryWithClient(t *testing.T) { WithTestDSN(t, func(dsn string, pch <-chan *resultPacket) { logger := getTestLogger() client, _ := raven.New(dsn) hook, err := NewWithClientSentryHook(client, []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) } }) } func TestSentryWithClientAndError(t *testing.T) { WithTestDSN(t, func(dsn string, pch <-chan *resultPacket) { logger := getTestLogger() client, _ := raven.New(dsn) hook, err := NewWithClientSentryHook(client, []logrus.Level{ logrus.ErrorLevel, }) if err != nil { t.Fatal(err.Error()) } logger.Hooks.Add(hook) errorMsg := "error message" logger.WithError(errors.New(errorMsg)).Error(message) packet := <-pch if packet.Message != message { t.Errorf("message should have been %s, was %s", message, packet.Message) } if packet.Culprit != errorMsg { t.Errorf("culprit should have been %s, was %s", errorMsg, packet.Culprit) } }) } func TestSentryTags(t *testing.T) { WithTestDSN(t, func(dsn string, pch <-chan *resultPacket) { logger := getTestLogger() tags := map[string]string{ "site": "test", } levels := []logrus.Level{ logrus.ErrorLevel, } hook, err := NewWithTagsSentryHook(dsn, tags, levels) if err != nil { t.Fatal(err.Error()) } logger.Hooks.Add(hook) logger.Error(message) packet := <-pch expected := raven.Tags{ raven.Tag{ Key: "site", Value: "test", }, } if !reflect.DeepEqual(packet.Tags, expected) { t.Errorf("tags should have been %+v, was %+v", expected, packet.Tags) } }) } func TestSentryFingerprint(t *testing.T) { WithTestDSN(t, func(dsn string, pch <-chan *resultPacket) { logger := getTestLogger() levels := []logrus.Level{ logrus.ErrorLevel, } fingerprint := []string{"fingerprint"} hook, err := NewSentryHook(dsn, levels) if err != nil { t.Fatal(err.Error()) } logger.Hooks.Add(hook) logger.WithFields(logrus.Fields{ "fingerprint": fingerprint, }).Error(message) packet := <-pch if !reflect.DeepEqual(packet.Fingerprint, fingerprint) { t.Errorf("fingerprint should have been %v, was %v", fingerprint, packet.Fingerprint) } }) } func TestAddIgnore(t *testing.T) { hook := SentryHook{ ignoreFields: make(map[string]struct{}), } list := []string{"foo", "bar", "baz"} for i, key := range list { if len(hook.ignoreFields) != i { t.Errorf("hook.ignoreFields has %d length, but %d", i, len(hook.ignoreFields)) continue } hook.AddIgnore(key) if len(hook.ignoreFields) != i+1 { t.Errorf("hook.ignoreFields should be added") continue } for j := 0; j <= i; j++ { k := list[j] if _, ok := hook.ignoreFields[k]; !ok { t.Errorf("%s should be added into hook.ignoreFields", k) continue } } } } func TestAddExtraFilter(t *testing.T) { hook := SentryHook{ extraFilters: make(map[string]func(interface{}) interface{}), } list := []string{"foo", "bar", "baz"} for i, key := range list { if len(hook.extraFilters) != i { t.Errorf("hook.extraFilters has %d length, but %d", i, len(hook.extraFilters)) continue } hook.AddExtraFilter(key, nil) if len(hook.extraFilters) != i+1 { t.Errorf("hook.extraFilters should be added") continue } for j := 0; j <= i; j++ { k := list[j] if _, ok := hook.extraFilters[k]; !ok { t.Errorf("%s should be added into hook.extraFilters", k) continue } } } } func TestFormatExtraData(t *testing.T) { hook := SentryHook{ ignoreFields: make(map[string]struct{}), extraFilters: make(map[string]func(interface{}) interface{}), } hook.AddIgnore("ignore1") hook.AddIgnore("ignore2") hook.AddIgnore("ignore3") hook.AddExtraFilter("filter1", func(v interface{}) interface{} { return "filter1 value" }) tests := []struct { isExist bool key string value interface{} expected interface{} }{ {true, "integer", 13, 13}, {true, "string", "foo", "foo"}, {true, "bool", true, true}, {true, "time.Time", time.Time{}, "0001-01-01 00:00:00 +0000 UTC"}, {true, "myStringer", myStringer{}, "myStringer!"}, {true, "myStringer_ptr", &myStringer{}, "myStringer!"}, {true, "notStringer", notStringer{}, notStringer{}}, {true, "notStringer_ptr", ¬Stringer{}, ¬Stringer{}}, {false, "ignore1", 13, false}, {false, "ignore2", "foo", false}, {false, "ignore3", time.Time{}, false}, {true, "filter1", "filter1", "filter1 value"}, {true, "filter1", time.Time{}, "filter1 value"}, } for _, tt := range tests { target := fmt.Sprintf("%+v", tt) fields := logrus.Fields{ "time_stamp": time.Now(), // implements JSON marshaler "time_duration": time.Hour, // implements .String() "err": errors.New("this is a test error"), "order": 13, tt.key: tt.value, } df := newDataField(fields) result := hook.formatExtraData(df) value, ok := result[tt.key] if !tt.isExist { if ok { t.Errorf("%s should not be exist. data=%s", tt.key, target) } continue } if fmt.Sprint(tt.expected) != fmt.Sprint(value) { t.Errorf("%s should be %v, but %v. data=%s", tt.key, tt.expected, value, target) } } } func TestFormatData(t *testing.T) { // assertion types var ( assertTypeInt int assertTypeString string assertTypeTime time.Time ) tests := []struct { name string value interface{} expectedType interface{} }{ {"int", 13, assertTypeInt}, {"string", "foo", assertTypeString}, {"error", errors.New("this is a test error"), assertTypeString}, {"time_stamp", time.Now(), assertTypeTime}, // implements JSON marshaler {"time_duration", time.Hour, assertTypeString}, // implements .String() {"stringer", myStringer{}, assertTypeString}, // implements .String() {"stringer_ptr", &myStringer{}, assertTypeString}, // implements .String() {"not_stringer", notStringer{}, notStringer{}}, {"not_stringer_ptr", ¬Stringer{}, ¬Stringer{}}, } for _, tt := range tests { target := fmt.Sprintf("%+v", tt) result := formatData(tt.value) resultType := reflect.TypeOf(result).String() expectedType := reflect.TypeOf(tt.expectedType).String() if resultType != expectedType { t.Errorf("invalid type: type should be %s, but %s. data=%s", resultType, expectedType, target) } } } type myStringer struct{} func (myStringer) String() string { return "myStringer!" } type notStringer struct{} func (notStringer) String() {} type myStacktracerError struct{} func (myStacktracerError) Error() string { return "myStacktracerError!" } const expectedStackFrameFilename = "errorFile.go" func (myStacktracerError) GetStacktrace() *raven.Stacktrace { return &raven.Stacktrace{ Frames: []*raven.StacktraceFrame{ {Filename: expectedStackFrameFilename}, }, } } func TestConvertStackTrace(t *testing.T) { hook := SentryHook{} expected := raven.NewStacktrace(0, 0, nil) st := pkgerrors.New("-").(pkgErrorStackTracer).StackTrace() ravenSt := hook.convertStackTrace(st) // Obscure the line numbes, so DeepEqual doesn't fail erroneously for _, frame := range append(expected.Frames, ravenSt.Frames...) { frame.Lineno = 999 } if !reflect.DeepEqual(ravenSt, expected) { t.Error("stack traces differ") } } func TestErrorHandler(t *testing.T) { a := assert.New(t) s, dsn := httptestNewServer(func(rw http.ResponseWriter, req *http.Request) { defer req.Body.Close() rw.WriteHeader(400) }) defer s.Close() hook, err := NewSentryHook(dsn, []logrus.Level{ logrus.ErrorLevel, }) a.NoError(err, "NewSentryHook should be no error") logger := getTestLogger() logger.Hooks.Add(hook) hook.AddErrorHandler(func(e *logrus.Entry, err error) { a.Error(err, "ErrorHandler should capture error") a.Contains(err.Error(), "raven: got http status 400") }) err = hook.Fire(&logrus.Entry{}) a.Error(err, "hook.Fire should have error") } // create http test server func httptestNewServer(handler func(http.ResponseWriter, *http.Request)) (server *httptest.Server, dsn string) { server = httptest.NewServer(http.HandlerFunc(handler)) fragments := strings.SplitN(server.URL, "://", 2) dsn = fmt.Sprintf( "%s://public:secret@%s/sentry/project-id", fragments[0], fragments[1], ) return server, dsn } logrus_sentry-0.8.1/utils.go000066400000000000000000000111461342572276400161450ustar00rootroot00000000000000package logrus_sentry import ( "fmt" "strings" ) /* Copyright (c) 2009,2014 Google Inc. All rights reserved. Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: * Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. * Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. * Neither the name of Google Inc. nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission. THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */ // A UUID is a 128 bit (16 byte) Universal Unique IDentifier as defined in RFC // 4122. type uuid []byte // parseUUID decodes s into a UUID or returns nil. Both the UUID form of // xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx and // xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx and // urn:uuid:xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx are decoded. func parseUUID(s string) uuid { //If it is in no dash format "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx" if len(s) == 32 { uuid := make([]byte, 16) for i, x := range []int{ 0, 2, 4, 6, 8, 10, 12, 14, 16, 18, 20, 22, 24, 26, 28, 30} { if v, ok := xtob(s[x:]); !ok { return nil } else { uuid[i] = v } } return uuid } if len(s) == 36+9 { if strings.ToLower(s[:9]) != "urn:uuid:" { return nil } s = s[9:] } else if len(s) != 36 { return nil } if s[8] != '-' || s[13] != '-' || s[18] != '-' || s[23] != '-' { return nil } uuid := make([]byte, 16) for i, x := range []int{ 0, 2, 4, 6, 9, 11, 14, 16, 19, 21, 24, 26, 28, 30, 32, 34} { if v, ok := xtob(s[x:]); !ok { return nil } else { uuid[i] = v } } return uuid } // String returns the string form of uuid, xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx // , or "" if uuid is invalid. func (uuid uuid) string() string { if uuid == nil || len(uuid) != 16 { return "" } b := []byte(uuid) return fmt.Sprintf("%08x-%04x-%04x-%04x-%012x", b[:4], b[4:6], b[6:8], b[8:10], b[10:]) } func (uuid uuid) noDashString() string { if uuid == nil || len(uuid) != 16 { return "" } b := []byte(uuid) return fmt.Sprintf("%08x%04x%04x%04x%012x", b[:4], b[4:6], b[6:8], b[8:10], b[10:]) } // xvalues returns the value of a byte as a hexadecimal digit or 255. var xvalues = []byte{ 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 255, 255, 255, 255, 255, 255, 255, 10, 11, 12, 13, 14, 15, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 10, 11, 12, 13, 14, 15, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, } // xtob converts the the first two hex bytes of x into a byte. func xtob(x string) (byte, bool) { b1 := xvalues[x[0]] b2 := xvalues[x[1]] return (b1 << 4) | b2, b1 != 255 && b2 != 255 }