pax_global_header00006660000000000000000000000064134547270450014525gustar00rootroot0000000000000052 comment=edccebed60f9b6ebebd627af73295d0658da6e33 golang-github-heroku-rollrus-0.0~git20190402.fde2a6b/000077500000000000000000000000001345472704500220425ustar00rootroot00000000000000golang-github-heroku-rollrus-0.0~git20190402.fde2a6b/.travis.yml000066400000000000000000000001151345472704500241500ustar00rootroot00000000000000language: go go: - 1.10.8 - 1.11.6 - 1.12.1 script: - go test -v -race ./... golang-github-heroku-rollrus-0.0~git20190402.fde2a6b/LICENSE000066400000000000000000000020671345472704500230540ustar00rootroot00000000000000The MIT License (MIT) Copyright © Heroku 2014 - 2015 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. golang-github-heroku-rollrus-0.0~git20190402.fde2a6b/README.md000066400000000000000000000015011345472704500233160ustar00rootroot00000000000000[![Build Status](https://travis-ci.org/heroku/rollrus.svg?branch=master)](https://travis-ci.org/heroku/rollrus) [![GoDoc](https://godoc.org/github.com/heroku/rollrus?status.svg)](https://godoc.org/github.com/heroku/rollrus) # What Rollrus is what happens when [Logrus](https://github.com/sirupsen/logrus) meets [Roll](https://github.com/stvp/roll). When a .Error, .Fatal or .Panic logging function is called, report the details to rollbar via a Logrus hook. Delivery is synchronous to help ensure that logs are delivered. If the error includes a [`StackTrace`](https://godoc.org/github.com/pkg/errors#StackTrace), that `StackTrace` is reported to rollbar. # Usage Examples available in the [tests](https://github.com/heroku/rollrus/blob/master/rollrus_test.go) or on [GoDoc](https://godoc.org/github.com/heroku/rollrus). golang-github-heroku-rollrus-0.0~git20190402.fde2a6b/go.mod000066400000000000000000000002501345472704500231450ustar00rootroot00000000000000module github.com/heroku/rollrus require ( github.com/pkg/errors v0.8.1 github.com/sirupsen/logrus v1.3.0 github.com/stvp/roll v0.0.0-20170522205222-3627a5cbeaea ) golang-github-heroku-rollrus-0.0~git20190402.fde2a6b/go.sum000066400000000000000000000034021345472704500231740ustar00rootroot00000000000000github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/konsorten/go-windows-terminal-sequences v1.0.1 h1:mweAR1A6xJ3oS2pRaGiHgQ4OO8tzTaLawm8vnODuwDk= github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= github.com/pkg/errors v0.8.1 h1:iURUrRGxPUNPdy5/HRSm+Yj6okJ6UtLINN0Q9M4+h3I= github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/sirupsen/logrus v1.3.0 h1:hI/7Q+DtNZ2kINb6qt/lS+IyXnHQe9e90POfeewL/ME= github.com/sirupsen/logrus v1.3.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo= github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/testify v1.2.2 h1:bSDNvY7ZPG5RlJ8otE/7V6gMiyenm9RtJ7IUVIAoJ1w= github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= github.com/stvp/roll v0.0.0-20170522205222-3627a5cbeaea h1:jysxIKov/4GJ33wI2aRvuIK7yBwB28E5almlgDLPRpM= github.com/stvp/roll v0.0.0-20170522205222-3627a5cbeaea/go.mod h1:Ffmqrj3nXIMIjeA4uW3Qjj0Ud9eDoTG0fu4JxyAr/tE= golang.org/x/crypto v0.0.0-20180904163835-0709b304e793 h1:u+LnwYTOOW7Ukr/fppxEb1Nwz0AtPflrblfvUudpo+I= golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33 h1:I6FyU15t786LL7oL/hn43zqTuEGr4PN7F4XJ1p4E3Y8= golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang-github-heroku-rollrus-0.0~git20190402.fde2a6b/rollrus.go000066400000000000000000000175211345472704500241010ustar00rootroot00000000000000// Package rollrus combines github.com/stvp/roll with github.com/sirupsen/logrus // via logrus.Hook mechanism, so that whenever logrus' logger.Error/f(), // logger.Fatal/f() or logger.Panic/f() are used the messages are // intercepted and sent to rollbar. // // Using SetupLogging should suffice for basic use cases that use the logrus // singleton logger. // // More custom uses are supported by creating a new Hook with NewHook and // registering that hook with the logrus Logger of choice. // // The levels can be customized with the WithLevels OptionFunc. // // Specific errors can be ignored with the WithIgnoredErrors OptionFunc. This is // useful for ignoring errors such as context.Canceled. // // See the Examples in the tests for more usage. package rollrus import ( "fmt" "os" "time" "github.com/pkg/errors" "github.com/sirupsen/logrus" "github.com/stvp/roll" ) var defaultTriggerLevels = []logrus.Level{ logrus.ErrorLevel, logrus.FatalLevel, logrus.PanicLevel, } // Hook is a wrapper for the rollbar Client and is usable as a logrus.Hook. type Hook struct { roll.Client triggers []logrus.Level ignoredErrors []error ignoreErrorFunc func(error) bool ignoreFunc func(error, map[string]string) bool // only used for tests to verify whether or not a report happened. reported bool } // OptionFunc that can be passed to NewHook. type OptionFunc func(*Hook) // wellKnownErrorFields are the names of the fields to be checked for values of // type `error`, in priority order. var wellKnownErrorFields = []string{ logrus.ErrorKey, "err", } // WithLevels is an OptionFunc that customizes the log.Levels the hook will // report on. func WithLevels(levels ...logrus.Level) OptionFunc { return func(h *Hook) { h.triggers = levels } } // WithMinLevel is an OptionFunc that customizes the log.Levels the hook will // report on by selecting all levels more severe than the one provided. func WithMinLevel(level logrus.Level) OptionFunc { var levels []logrus.Level for _, l := range logrus.AllLevels { if l <= level { levels = append(levels, l) } } return func(h *Hook) { h.triggers = levels } } // WithIgnoredErrors is an OptionFunc that whitelists certain errors to prevent // them from firing. See https://golang.org/ref/spec#Comparison_operators func WithIgnoredErrors(errors ...error) OptionFunc { return func(h *Hook) { h.ignoredErrors = append(h.ignoredErrors, errors...) } } // WithIgnoreErrorFunc is an OptionFunc that receives the error that is about // to be logged and returns true/false if it wants to fire a rollbar alert for. func WithIgnoreErrorFunc(fn func(error) bool) OptionFunc { return func(h *Hook) { h.ignoreErrorFunc = fn } } // WithIgnoreFunc is an OptionFunc that receives the error and custom fields that are about // to be logged and returns true/false if it wants to fire a rollbar alert for. func WithIgnoreFunc(fn func(err error, fields map[string]string) bool) OptionFunc { return func(h *Hook) { h.ignoreFunc = fn } } // NewHook creates a hook that is intended for use with your own logrus.Logger // instance. Uses the defualt report levels defined in wellKnownErrorFields. func NewHook(token string, env string, opts ...OptionFunc) *Hook { h := NewHookForLevels(token, env, defaultTriggerLevels) for _, o := range opts { o(h) } return h } // NewHookForLevels provided by the caller. Otherwise works like NewHook. func NewHookForLevels(token string, env string, levels []logrus.Level) *Hook { return &Hook{ Client: roll.New(token, env), triggers: levels, ignoredErrors: make([]error, 0), ignoreErrorFunc: func(error) bool { return false }, ignoreFunc: func(error, map[string]string) bool { return false }, } } // SetupLogging for use on Heroku. If token is not an empty string a rollbar // hook is added with the environment set to env. The log formatter is set to a // TextFormatter with timestamps disabled. func SetupLogging(token, env string) { setupLogging(token, env, defaultTriggerLevels) } // SetupLoggingForLevels works like SetupLogging, but allows you to // set the levels on which to trigger this hook. func SetupLoggingForLevels(token, env string, levels []logrus.Level) { setupLogging(token, env, levels) } func setupLogging(token, env string, levels []logrus.Level) { logrus.SetFormatter(&logrus.TextFormatter{DisableTimestamp: true}) if token != "" { logrus.AddHook(NewHookForLevels(token, env, levels)) } } // ReportPanic attempts to report the panic to rollbar using the provided // client and then re-panic. If it can't report the panic it will print an // error to stderr. func (r *Hook) ReportPanic() { if p := recover(); p != nil { if _, err := r.Client.Critical(fmt.Errorf("panic: %q", p), nil); err != nil { fmt.Fprintf(os.Stderr, "reporting_panic=false err=%q\n", err) } panic(p) } } // ReportPanic attempts to report the panic to rollbar if the token is set func ReportPanic(token, env string) { if token != "" { h := &Hook{Client: roll.New(token, env)} h.ReportPanic() } } // Levels returns the logrus log.Levels that this hook handles func (r *Hook) Levels() []logrus.Level { if r.triggers == nil { return defaultTriggerLevels } return r.triggers } // Fire the hook. This is called by Logrus for entries that match the levels // returned by Levels(). func (r *Hook) Fire(entry *logrus.Entry) error { trace, cause := extractError(entry) for _, ie := range r.ignoredErrors { if ie == cause { return nil } } if r.ignoreErrorFunc(cause) { return nil } m := convertFields(entry.Data) if _, exists := m["time"]; !exists { m["time"] = entry.Time.Format(time.RFC3339) } if r.ignoreFunc(cause, m) { return nil } return r.report(entry, cause, m, trace) } func (r *Hook) report(entry *logrus.Entry, cause error, m map[string]string, trace []uintptr) (err error) { hasTrace := len(trace) > 0 level := entry.Level r.reported = true switch { case hasTrace && level == logrus.FatalLevel: _, err = r.Client.CriticalStack(cause, trace, m) case hasTrace && level == logrus.PanicLevel: _, err = r.Client.CriticalStack(cause, trace, m) case hasTrace && level == logrus.ErrorLevel: _, err = r.Client.ErrorStack(cause, trace, m) case hasTrace && level == logrus.WarnLevel: _, err = r.Client.WarningStack(cause, trace, m) case level == logrus.FatalLevel || level == logrus.PanicLevel: _, err = r.Client.Critical(cause, m) case level == logrus.ErrorLevel: _, err = r.Client.Error(cause, m) case level == logrus.WarnLevel: _, err = r.Client.Warning(cause, m) case level == logrus.InfoLevel: _, err = r.Client.Info(entry.Message, m) case level == logrus.DebugLevel: _, err = r.Client.Debug(entry.Message, m) } return err } // convertFields converts from log.Fields to map[string]string so that we can // report extra fields to Rollbar func convertFields(fields logrus.Fields) map[string]string { m := make(map[string]string) for k, v := range fields { switch t := v.(type) { case time.Time: m[k] = t.Format(time.RFC3339) default: if s, ok := v.(fmt.Stringer); ok { m[k] = s.String() } else { m[k] = fmt.Sprintf("%+v", t) } } } return m } // extractError attempts to extract an error from a well known field, err or error func extractError(entry *logrus.Entry) ([]uintptr, error) { var trace []uintptr fields := entry.Data type stackTracer interface { StackTrace() errors.StackTrace } for _, f := range wellKnownErrorFields { e, ok := fields[f] if !ok { continue } err, ok := e.(error) if !ok { continue } cause := errors.Cause(err) tracer, ok := err.(stackTracer) if ok { return copyStackTrace(tracer.StackTrace()), cause } return trace, cause } // when no error found, default to the logged message. return trace, fmt.Errorf(entry.Message) } func copyStackTrace(trace errors.StackTrace) (out []uintptr) { for _, frame := range trace { out = append(out, uintptr(frame)) } return } golang-github-heroku-rollrus-0.0~git20190402.fde2a6b/rollrus_test.go000066400000000000000000000224731345472704500251420ustar00rootroot00000000000000package rollrus import ( "fmt" "io" "net/http" "net/http/httptest" "reflect" "testing" "time" "github.com/pkg/errors" "github.com/sirupsen/logrus" "github.com/stvp/roll" ) func ExampleSetupLogging() { SetupLogging("some-long-token", "staging") // This will not be reported to Rollbar logrus.Info("OHAI") // This will be reported to Rollbar logrus.WithFields(logrus.Fields{"hi": "there"}).Fatal("The end.") } func ExampleNewHook() { log := logrus.New() hook := NewHook("my-secret-token", "production") log.Hooks.Add(hook) // This will not be reported to Rollbar log.WithFields(logrus.Fields{"power_level": "9001"}).Debug("It's over 9000!") // This will be reported to Rollbar log.Panic("Boom.") } func TestLogrusHookInterface(t *testing.T) { var hook interface{} = NewHook("", "foo") if _, ok := hook.(logrus.Hook); !ok { t.Fatal("expected NewHook's return value to implement logrus.Hook") } } func TestIntConversion(t *testing.T) { i := make(logrus.Fields) i["test"] = 5 r := convertFields(i) v, ok := r["test"] if !ok { t.Fatal("Expected test key, but did not find it") } if v != "5" { t.Fatal("Expected value to equal 5, but instead it is: ", v) } } func TestErrConversion(t *testing.T) { i := make(logrus.Fields) i["test"] = fmt.Errorf("This is an error") r := convertFields(i) v, ok := r["test"] if !ok { t.Fatal("Expected test key, but did not find it") } if v != "This is an error" { t.Fatal("Expected value to be a string of the error but instead it is: ", v) } } func TestStringConversion(t *testing.T) { i := make(logrus.Fields) i["test"] = "This is a string" r := convertFields(i) v, ok := r["test"] if !ok { t.Fatal("Expected test key, but did not find it") } if v != "This is a string" { t.Fatal("Expected value to equal a certain string, but instead it is: ", v) } } func TestTimeConversion(t *testing.T) { now := time.Now() i := make(logrus.Fields) i["test"] = now r := convertFields(i) v, ok := r["test"] if !ok { t.Fatal("Expected test key, but did not find it") } if v != now.Format(time.RFC3339) { t.Fatal("Expected value to equal, but instead it is: ", v) } } func TestExtractError(t *testing.T) { entry := logrus.NewEntry(nil) entry.Data["err"] = fmt.Errorf("foo bar baz") trace, cause := extractError(entry) if len(trace) != 0 { t.Fatal("Expected length of trace to be equal to 0, but instead is: ", len(trace)) } if cause.Error() != "foo bar baz" { t.Fatalf("Expected error as string to be 'foo bar baz', but was instead: %q", cause) } } func TestExtractErrorDefault(t *testing.T) { entry := logrus.NewEntry(nil) entry.Data["no-err"] = fmt.Errorf("foo bar baz") entry.Message = "message error" trace, cause := extractError(entry) if len(trace) != 0 { t.Fatal("Expected length of trace to be equal to 0, but instead is: ", len(trace)) } if cause.Error() != "message error" { t.Fatalf("Expected error as string to be 'message error', but was instead: %q", cause) } } func TestExtractErrorFromStackTracer(t *testing.T) { entry := logrus.NewEntry(nil) entry.Data["err"] = errors.Errorf("foo bar baz") trace, cause := extractError(entry) if len(trace) != 3 { t.Fatal("Expected length of trace to be == 3, but instead is: ", len(trace)) } if cause.Error() != "foo bar baz" { t.Fatalf("Expected error as string to be 'foo bar baz', but was instead: %q", cause.Error()) } } func TestTriggerLevels(t *testing.T) { client := roll.New("", "testing") underTest := &Hook{Client: client} if !reflect.DeepEqual(underTest.Levels(), defaultTriggerLevels) { t.Fatal("Expected Levels() to return defaultTriggerLevels") } newLevels := []logrus.Level{logrus.InfoLevel} underTest.triggers = newLevels if !reflect.DeepEqual(underTest.Levels(), newLevels) { t.Fatal("Expected Levels() to return newLevels") } } func TestWithMinLevelInfo(t *testing.T) { h := NewHook("", "testing", WithMinLevel(logrus.InfoLevel)) expectedLevels := []logrus.Level{ logrus.PanicLevel, logrus.FatalLevel, logrus.ErrorLevel, logrus.WarnLevel, logrus.InfoLevel, } if !reflect.DeepEqual(h.Levels(), expectedLevels) { t.Fatal("Expected Levels() to return all levels above Info") } } func TestWithMinLevelFatal(t *testing.T) { h := NewHook("", "testing", WithMinLevel(logrus.FatalLevel)) expectedLevels := []logrus.Level{ logrus.PanicLevel, logrus.FatalLevel, } if !reflect.DeepEqual(h.Levels(), expectedLevels) { t.Fatal("Expected Levels() to return all levels above Fatal") } } func TestLoggingBelowTheMinimumLevelDoesNotFire(t *testing.T) { h := NewHook("", "testing", WithMinLevel(logrus.FatalLevel)) l := logrus.New() l.AddHook(h) l.Error("This is a test") if h.reported { t.Fatal("expected no report to have happened") } } func TestLoggingAboveTheMinimumLevelDoesFire(t *testing.T) { h := NewHook("", "testing", WithMinLevel(logrus.WarnLevel)) l := logrus.New() l.AddHook(h) l.Warn("This is a test") if !h.reported { t.Fatal("expected report to have happened") } } func TestWithIgnoredErrors(t *testing.T) { h := NewHook("", "testing", WithIgnoredErrors(io.EOF)) entry := logrus.NewEntry(nil) entry.Message = "This is a test" // Exact error is skipped. entry.Data["err"] = io.EOF if err := h.Fire(entry); err != nil { t.Fatal("unexpected error ", err) } if h.reported { t.Fatal("expected no report to have happened") } // Wrapped error is also skipped. entry.Data["err"] = errors.Wrap(io.EOF, "hello") if err := h.Fire(entry); err != nil { t.Fatal("unexpected error ", err) } if h.reported { t.Fatal("expected no report to have happened") } // Non blacklisted errors get reported. entry.Data["err"] = errors.New("hello") if err := h.Fire(entry); err != nil { t.Fatal("unexpected error ", err) } if !h.reported { t.Fatal("expected a report to have happened") } // no err gets reported. delete(entry.Data, "err") if err := h.Fire(entry); err != nil { t.Fatal("unexpected error ", err) } if !h.reported { t.Fatal("expected a report to have happened") } } type isTemporary interface { Temporary() bool } // https://github.com/go-pg/pg/blob/2ebb4d1d9b890619de2dc9e1dc0085b86f93fc91/internal/error.go#L22 type PGError struct { m map[byte]string } func (err PGError) Error() string { return "error" } // https://github.com/heroku/rollrus/issues/26 func TestWithErrorHandlesUnhashableErrors(t *testing.T) { _ = NewHook("", "", WithIgnoredErrors(PGError{m: make(map[byte]string)})) entry := logrus.NewEntry(nil) entry.Message = "This is a test" entry.Data["err"] = PGError{m: make(map[byte]string)} h := NewHook("", "testing") // actually panics if err := h.Fire(entry); err != nil { t.Fatal("unexpected error ", err) } } func TestWithIgnoreErrorFunc(t *testing.T) { h := NewHook("", "testing", WithIgnoreErrorFunc(func(err error) bool { if err == io.EOF { return true } if e, ok := err.(isTemporary); ok && e.Temporary() { return true } return false })) entry := logrus.NewEntry(nil) entry.Message = "This is a test" // Exact error is skipped. entry.Data["err"] = io.EOF if err := h.Fire(entry); err != nil { t.Fatal("unexpected error ", err) } if h.reported { t.Fatal("expected no report to have happened") } // Wrapped error is also skipped. entry.Data["err"] = errors.Wrap(io.EOF, "hello") if err := h.Fire(entry); err != nil { t.Fatal("unexpected error ", err) } if h.reported { t.Fatal("expected no report to have happened") } srv := httptest.NewServer(nil) srv.Close() // Temporary error skipped _, err := http.Get(srv.URL) entry.Data["err"] = err if err := h.Fire(entry); err != nil { t.Fatal("unexpected error ", err) } // Non blacklisted errors get reported. entry.Data["err"] = errors.New("hello") if err := h.Fire(entry); err != nil { t.Fatal("unexpected error ", err) } if !h.reported { t.Fatal("expected a report to have happened") } // no err gets reported. delete(entry.Data, "err") if err := h.Fire(entry); err != nil { t.Fatal("unexpected error ", err) } if !h.reported { t.Fatal("expected a report to have happened") } } func TestWithIgnoreFunc(t *testing.T) { cases := []struct { name string fields logrus.Fields skipReport bool }{ { name: "extract error is skipped", fields: map[string]interface{}{"err": io.EOF}, skipReport: true, }, { name: "wrapped error is skipped", fields: map[string]interface{}{"err": errors.Wrap(io.EOF, "hello")}, skipReport: true, }, { name: "ignored field is skipped", fields: map[string]interface{}{"ignore": "true"}, skipReport: true, }, { name: "error is not skipped", fields: map[string]interface{}{}, skipReport: false, }, } for _, c := range cases { c := c // capture local var for parallel tests t.Run(c.name, func(t *testing.T) { t.Parallel() h := NewHook("", "testing", WithIgnoreFunc(func(err error, m map[string]string) bool { if err == io.EOF { return true } if m["ignore"] == "true" { return true } return false })) entry := logrus.NewEntry(nil) entry.Message = "This is a test" entry.Data = c.fields if err := h.Fire(entry); err != nil { t.Errorf("unexpected error %s", err) } if c.skipReport && h.reported { t.Errorf("expected report to be skipped") } if !c.skipReport && !h.reported { t.Errorf("expected report to be fired") } }) } }