mirror of
https://github.com/jesseduffield/lazygit.git
synced 2025-03-19 21:28:28 +02:00
288 lines
7.8 KiB
Go
288 lines
7.8 KiB
Go
// 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
|
|
}
|