Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add logrus support #471

Merged
merged 12 commits into from Nov 9, 2022
Merged
51 changes: 51 additions & 0 deletions example/logrus/main.go
@@ -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!")
}
2 changes: 1 addition & 1 deletion go.mod
Expand Up @@ -11,6 +11,7 @@ require (
github.com/labstack/echo/v4 v4.9.0
github.com/pingcap/errors v0.11.4
github.com/pkg/errors v0.9.1
github.com/sirupsen/logrus v1.9.0
github.com/urfave/negroni v1.0.0
github.com/valyala/fasthttp v1.40.0
golang.org/x/sys v0.0.0-20220928140112-f11e5e49a4ec
Expand Down Expand Up @@ -59,7 +60,6 @@ require (
github.com/pelletier/go-toml/v2 v2.0.5 // indirect
github.com/russross/blackfriday/v2 v2.1.0 // indirect
github.com/schollz/closestmatch v2.1.0+incompatible // indirect
github.com/sirupsen/logrus v1.9.0 // indirect
github.com/tdewolff/minify/v2 v2.12.4 // indirect
github.com/tdewolff/parse/v2 v2.6.4 // indirect
github.com/ugorji/go/codec v1.2.7 // indirect
Expand Down
223 changes: 223 additions & 0 deletions logrus/logrusentry.go
@@ -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
flimzy marked this conversation as resolved.
Show resolved Hide resolved
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)
}