diff options
Diffstat (limited to 'vendor/github.com/heroku/rollrus/rollrus.go')
-rw-r--r-- | vendor/github.com/heroku/rollrus/rollrus.go | 287 |
1 files changed, 287 insertions, 0 deletions
diff --git a/vendor/github.com/heroku/rollrus/rollrus.go b/vendor/github.com/heroku/rollrus/rollrus.go new file mode 100644 index 000000000..50dd225da --- /dev/null +++ b/vendor/github.com/heroku/rollrus/rollrus.go @@ -0,0 +1,287 @@ +// 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 map[error]struct{} + 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. +func WithIgnoredErrors(errors ...error) OptionFunc { + return func(h *Hook) { + for _, e := range errors { + h.ignoredErrors[e] = struct{}{} + } + } +} + +// 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(map[error]struct{}), + 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) + if _, ok := r.ignoredErrors[cause]; ok { + 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 +} |