Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
Showing
4 changed files
with
760 additions
and
1 deletion.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,51 @@ | ||
package main | ||
|
||
import ( | ||
"fmt" | ||
"net/http" | ||
"os" | ||
"time" | ||
|
||
"github.com/sirupsen/logrus" | ||
|
||
"github.com/getsentry/sentry-go" | ||
sentrylogrus "github.com/getsentry/sentry-go/logrus" | ||
) | ||
|
||
func main() { | ||
logger := logrus.New() | ||
|
||
// Log DEBUG and higher level logs to STDERR | ||
logger.Level = logrus.DebugLevel | ||
logger.Out = os.Stderr | ||
|
||
// Send only ERROR and higher level logs to Sentry | ||
sentryLevels := []logrus.Level{logrus.ErrorLevel, logrus.FatalLevel, logrus.PanicLevel} | ||
|
||
sentryHook, err := sentrylogrus.New(sentryLevels, sentry.ClientOptions{ | ||
Dsn: "", | ||
BeforeSend: func(event *sentry.Event, hint *sentry.EventHint) *sentry.Event { | ||
if hint.Context != nil { | ||
if req, ok := hint.Context.Value(sentry.RequestContextKey).(*http.Request); ok { | ||
// You have access to the original Request | ||
fmt.Println(req) | ||
} | ||
} | ||
fmt.Println(event) | ||
return event | ||
}, | ||
Debug: true, | ||
AttachStacktrace: true, | ||
}) | ||
if err != nil { | ||
panic(err) | ||
} | ||
defer sentryHook.Flush(5 * time.Second) | ||
logger.AddHook(sentryHook) | ||
|
||
// The following line is logged to STDERR, but not to Sentry | ||
logger.Infof("Application has started") | ||
|
||
// The following line is logged to STDERR and also sent to Sentry | ||
logger.Errorf("oh no!") | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,223 @@ | ||
// Package sentrylogrus provides a simple Logrus hook for Sentry. | ||
package sentrylogrus | ||
|
||
import ( | ||
"errors" | ||
"net/http" | ||
"time" | ||
|
||
sentry "github.com/getsentry/sentry-go" | ||
"github.com/sirupsen/logrus" | ||
) | ||
|
||
// These default log field keys are used to pass specific metadata in a way that | ||
// Sentry understands. If they are found in the log fields, and the value is of | ||
// the expected datatype, it will be converted from a generic field, into Sentry | ||
// metadata. | ||
// | ||
// These keys may be overridden by calling SetKey on the hook object. | ||
const ( | ||
// FieldRequest holds an *http.Request. | ||
FieldRequest = "request" | ||
// FieldUser holds a User or *User value. | ||
FieldUser = "user" | ||
// FieldTransaction holds a transaction ID as a string. | ||
FieldTransaction = "transaction" | ||
// FieldFingerprint holds a string slice ([]string), used to dictate the | ||
// grouping of this event. | ||
FieldFingerprint = "fingerprint" | ||
|
||
// These fields are simply omitted, as they are duplicated by the Sentry SDK. | ||
FieldGoVersion = "go_version" | ||
FieldMaxProcs = "go_maxprocs" | ||
) | ||
|
||
// Hook is the logrus hook for Sentry. | ||
// | ||
// It is not safe to configure the hook while logging is happening. Please | ||
// perform all configuration before using it. | ||
type Hook struct { | ||
hub *sentry.Hub | ||
fallback FallbackFunc | ||
keys map[string]string | ||
levels []logrus.Level | ||
} | ||
|
||
var _ logrus.Hook = &Hook{} | ||
|
||
// New initializes a new Logrus hook which sends logs to a new Sentry client | ||
// configured according to opts. | ||
func New(levels []logrus.Level, opts sentry.ClientOptions) (*Hook, error) { | ||
client, err := sentry.NewClient(opts) | ||
if err != nil { | ||
return nil, err | ||
} | ||
return NewFromClient(levels, client), nil | ||
} | ||
|
||
// NewFromClient initializes a new Logrus hook which sends logs to the provided | ||
// sentry client. | ||
func NewFromClient(levels []logrus.Level, client *sentry.Client) *Hook { | ||
h := &Hook{ | ||
levels: levels, | ||
hub: sentry.NewHub(client, sentry.NewScope()), | ||
keys: make(map[string]string), | ||
} | ||
return h | ||
} | ||
|
||
// AddTags adds tags to the hook's scope. | ||
func (h *Hook) AddTags(tags map[string]string) { | ||
h.hub.Scope().SetTags(tags) | ||
} | ||
|
||
// A FallbackFunc can be used to attempt to handle any errors in logging, before | ||
// resorting to Logrus's standard error reporting. | ||
type FallbackFunc func(*logrus.Entry) error | ||
|
||
// SetFallback sets a fallback function, which will be called in case logging to | ||
// sentry fails. In case of a logging failure in the Fire() method, the | ||
// fallback function is called with the original logrus entry. If the | ||
// fallback function returns nil, the error is considered handled. If it returns | ||
// an error, that error is passed along to logrus as the return value from the | ||
// Fire() call. If no fallback function is defined, a default error message is | ||
// returned to Logrus in case of failure to send to Sentry. | ||
func (h *Hook) SetFallback(fb FallbackFunc) { | ||
h.fallback = fb | ||
} | ||
|
||
// SetKey sets an alternate field key. Use this if the default values conflict | ||
// with other loggers, for instance. You may pass "" for new, to unset an | ||
// existing alternate. | ||
func (h *Hook) SetKey(oldKey, newKey string) { | ||
if oldKey == "" { | ||
return | ||
} | ||
if newKey == "" { | ||
delete(h.keys, oldKey) | ||
return | ||
} | ||
delete(h.keys, newKey) | ||
h.keys[oldKey] = newKey | ||
} | ||
|
||
func (h *Hook) key(key string) string { | ||
if val := h.keys[key]; val != "" { | ||
return val | ||
} | ||
return key | ||
} | ||
|
||
// Levels returns the list of logging levels that will be sent to | ||
// Sentry. | ||
func (h *Hook) Levels() []logrus.Level { | ||
return h.levels | ||
} | ||
|
||
// Fire sends entry to Sentry. | ||
func (h *Hook) Fire(entry *logrus.Entry) error { | ||
event := h.entryToEvent(entry) | ||
if id := h.hub.CaptureEvent(event); id == nil { | ||
if h.fallback != nil { | ||
return h.fallback(entry) | ||
} | ||
return errors.New("failed to send to sentry") | ||
} | ||
return nil | ||
} | ||
|
||
var levelMap = map[logrus.Level]sentry.Level{ | ||
logrus.TraceLevel: sentry.LevelDebug, | ||
logrus.DebugLevel: sentry.LevelDebug, | ||
logrus.InfoLevel: sentry.LevelInfo, | ||
logrus.WarnLevel: sentry.LevelWarning, | ||
logrus.ErrorLevel: sentry.LevelError, | ||
logrus.FatalLevel: sentry.LevelFatal, | ||
logrus.PanicLevel: sentry.LevelFatal, | ||
} | ||
|
||
func (h *Hook) entryToEvent(l *logrus.Entry) *sentry.Event { | ||
data := make(logrus.Fields, len(l.Data)) | ||
for k, v := range l.Data { | ||
data[k] = v | ||
} | ||
s := &sentry.Event{ | ||
Level: levelMap[l.Level], | ||
Extra: data, | ||
Message: l.Message, | ||
Timestamp: l.Time, | ||
} | ||
key := h.key(FieldRequest) | ||
if req, ok := s.Extra[key].(*http.Request); ok { | ||
delete(s.Extra, key) | ||
s.Request = sentry.NewRequest(req) | ||
} | ||
if err, ok := s.Extra[logrus.ErrorKey].(error); ok { | ||
delete(s.Extra, logrus.ErrorKey) | ||
ex := h.exceptions(err) | ||
s.Exception = ex | ||
} | ||
key = h.key(FieldUser) | ||
if user, ok := s.Extra[key].(sentry.User); ok { | ||
delete(s.Extra, key) | ||
s.User = user | ||
} | ||
if user, ok := s.Extra[key].(*sentry.User); ok { | ||
delete(s.Extra, key) | ||
s.User = *user | ||
} | ||
key = h.key(FieldTransaction) | ||
if txn, ok := s.Extra[key].(string); ok { | ||
delete(s.Extra, key) | ||
s.Transaction = txn | ||
} | ||
key = h.key(FieldFingerprint) | ||
if fp, ok := s.Extra[key].([]string); ok { | ||
delete(s.Extra, key) | ||
s.Fingerprint = fp | ||
} | ||
delete(s.Extra, FieldGoVersion) | ||
delete(s.Extra, FieldMaxProcs) | ||
return s | ||
} | ||
|
||
func (h *Hook) exceptions(err error) []sentry.Exception { | ||
if !h.hub.Client().Options().AttachStacktrace { | ||
return []sentry.Exception{{ | ||
Type: "error", | ||
Value: err.Error(), | ||
}} | ||
} | ||
excs := []sentry.Exception{} | ||
var last *sentry.Exception | ||
for ; err != nil; err = errors.Unwrap(err) { | ||
exc := sentry.Exception{ | ||
Type: "error", | ||
Value: err.Error(), | ||
Stacktrace: sentry.ExtractStacktrace(err), | ||
} | ||
if last != nil && exc.Value == last.Value { | ||
if last.Stacktrace == nil { | ||
last.Stacktrace = exc.Stacktrace | ||
continue | ||
} | ||
if exc.Stacktrace == nil { | ||
continue | ||
} | ||
} | ||
excs = append(excs, exc) | ||
last = &excs[len(excs)-1] | ||
} | ||
// reverse | ||
for i, j := 0, len(excs)-1; i < j; i, j = i+1, j-1 { | ||
excs[i], excs[j] = excs[j], excs[i] | ||
} | ||
return excs | ||
} | ||
|
||
// Flush waits until the underlying Sentry transport sends any buffered events, | ||
// blocking for at most the given timeout. It returns false if the timeout was | ||
// reached, in which case some events may not have been sent. | ||
func (h *Hook) Flush(timeout time.Duration) bool { | ||
return h.hub.Client().Flush(timeout) | ||
} |
Oops, something went wrong.