composer: send error log messages to sentry

This commit is contained in:
Diaa Sami 2024-02-06 20:26:49 +01:00 committed by Ondřej Budai
parent d0caac9d69
commit f08d1f6068
3 changed files with 237 additions and 0 deletions

View file

@ -0,0 +1,229 @@
// 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"
)
// The identifier of the Logrus SDK.
const sdkIdentifier = "sentry.go.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
}
client.SetSDKIdentifier(sdkIdentifier)
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)
}