debian-forge-composer/vendor/github.com/getsentry/sentry-go/logrus/logrusentry.go
2025-08-05 18:55:32 +02:00

424 lines
12 KiB
Go

// Package sentrylogrus provides a simple Logrus hook for Sentry.
package sentrylogrus
import (
"context"
"errors"
"fmt"
"math"
"net/http"
"reflect"
"strconv"
"time"
"github.com/getsentry/sentry-go"
"github.com/getsentry/sentry-go/attribute"
"github.com/sirupsen/logrus"
)
const (
// sdkIdentifier is the identifier of the Logrus SDK.
sdkIdentifier = "sentry.go.logrus"
// the name of the logger.
name = "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"
LogrusOrigin = "auto.logger.logrus"
)
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,
}
// 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 interface {
// SetHubProvider sets a function to provide a hub for each log entry.
SetHubProvider(provider func() *sentry.Hub)
// AddTags adds tags to the hook's scope.
AddTags(tags map[string]string)
// SetFallback sets a fallback function for the eventHook.
SetFallback(fb FallbackFunc)
// SetKey sets an alternate field key for the eventHook.
SetKey(oldKey, newKey string)
// Levels returns the list of logging levels that will be sent to Sentry as events.
Levels() []logrus.Level
// Fire sends entry to Sentry as an event.
Fire(entry *logrus.Entry) error
// Flush waits until the underlying Sentry transport sends any buffered events.
Flush(timeout time.Duration) bool
// FlushWithContext waits for the underlying Sentry transport to send any buffered
// events, blocking until the context's deadline is reached or the context is canceled.
// It returns false if the context is canceled or its deadline expires before the events
// are sent, meaning some events may not have been sent.
FlushWithContext(ctx context.Context) bool
}
// Deprecated: New just makes an underlying call to NewEventHook.
func New(levels []logrus.Level, opts sentry.ClientOptions) (Hook, error) {
return NewEventHook(levels, opts)
}
// Deprecated: NewFromClient just makes an underlying call to NewEventHookFromClient.
func NewFromClient(levels []logrus.Level, client *sentry.Client) Hook {
return NewEventHookFromClient(levels, client)
}
// 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
type eventHook struct {
hubProvider func() *sentry.Hub
fallback FallbackFunc
keys map[string]string
levels []logrus.Level
}
var _ Hook = &eventHook{}
var _ logrus.Hook = &eventHook{} // eventHook still needs to be a logrus.Hook
func (h *eventHook) SetHubProvider(provider func() *sentry.Hub) {
h.hubProvider = provider
}
func (h *eventHook) AddTags(tags map[string]string) {
h.hubProvider().Scope().SetTags(tags)
}
func (h *eventHook) SetFallback(fb FallbackFunc) {
h.fallback = fb
}
func (h *eventHook) SetKey(oldKey, newKey string) {
if oldKey == "" {
return
}
if newKey == "" {
delete(h.keys, oldKey)
return
}
delete(h.keys, newKey)
h.keys[oldKey] = newKey
}
func (h *eventHook) key(key string) string {
if val := h.keys[key]; val != "" {
return val
}
return key
}
func (h *eventHook) Levels() []logrus.Level {
return h.levels
}
func (h *eventHook) Fire(entry *logrus.Entry) error {
hub := h.hubProvider()
event := h.entryToEvent(entry)
if id := hub.CaptureEvent(event); id == nil {
if h.fallback != nil {
return h.fallback(entry)
}
return errors.New("failed to send to sentry")
}
return nil
}
func (h *eventHook) 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,
Logger: name,
}
key := h.key(FieldRequest)
switch request := s.Extra[key].(type) {
case *http.Request:
delete(s.Extra, key)
s.Request = sentry.NewRequest(request)
case sentry.Request:
delete(s.Extra, key)
s.Request = &request
case *sentry.Request:
delete(s.Extra, key)
s.Request = request
}
if err, ok := s.Extra[logrus.ErrorKey].(error); ok {
delete(s.Extra, logrus.ErrorKey)
s.SetException(err, -1)
}
key = h.key(FieldUser)
switch user := s.Extra[key].(type) {
case sentry.User:
delete(s.Extra, key)
s.User = user
case *sentry.User:
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 *eventHook) Flush(timeout time.Duration) bool {
return h.hubProvider().Client().Flush(timeout)
}
func (h *eventHook) FlushWithContext(ctx context.Context) bool {
return h.hubProvider().Client().FlushWithContext(ctx)
}
// NewEventHook initializes a new Logrus hook which sends events to a new Sentry client
// configured according to opts.
func NewEventHook(levels []logrus.Level, opts sentry.ClientOptions) (Hook, error) {
client, err := sentry.NewClient(opts)
if err != nil {
return nil, err
}
client.SetSDKIdentifier(sdkIdentifier)
return NewEventHookFromClient(levels, client), nil
}
// NewEventHookFromClient initializes a new Logrus hook which sends events to the provided
// sentry client.
func NewEventHookFromClient(levels []logrus.Level, client *sentry.Client) Hook {
defaultHub := sentry.NewHub(client, sentry.NewScope())
return &eventHook{
levels: levels,
hubProvider: func() *sentry.Hub {
// Default to using the same hub if no specific provider is set
return defaultHub
},
keys: make(map[string]string),
}
}
type logHook struct {
hubProvider func() *sentry.Hub
fallback FallbackFunc
keys map[string]string
levels []logrus.Level
logger sentry.Logger
}
var _ Hook = &logHook{}
var _ logrus.Hook = &logHook{} // logHook also needs to be a logrus.Hook
func (h *logHook) SetHubProvider(provider func() *sentry.Hub) {
h.hubProvider = provider
}
func (h *logHook) AddTags(tags map[string]string) {
// for logs convert tags to attributes
for k, v := range tags {
h.logger.SetAttributes(attribute.String(k, v))
}
}
func (h *logHook) SetFallback(fb FallbackFunc) {
h.fallback = fb
}
func (h *logHook) SetKey(oldKey, newKey string) {
if oldKey == "" {
return
}
if newKey == "" {
delete(h.keys, oldKey)
return
}
delete(h.keys, newKey)
h.keys[oldKey] = newKey
}
func (h *logHook) key(key string) string {
if val := h.keys[key]; val != "" {
return val
}
return key
}
func logrusFieldToLogEntry(logEntry sentry.LogEntry, key string, value interface{}) sentry.LogEntry {
switch val := value.(type) {
case int8:
return logEntry.Int64(key, int64(val))
case int16:
return logEntry.Int64(key, int64(val))
case int32:
return logEntry.Int64(key, int64(val))
case int64:
return logEntry.Int64(key, val)
case int:
return logEntry.Int64(key, int64(val))
case uint, uint8, uint16, uint32, uint64:
uval := reflect.ValueOf(val).Convert(reflect.TypeOf(uint64(0))).Uint()
if uval <= math.MaxInt64 {
return logEntry.Int64(key, int64(uval))
} else {
// For values larger than int64 can handle, we use string
return logEntry.String(key, strconv.FormatUint(uval, 10))
}
case string:
return logEntry.String(key, val)
case float32:
return logEntry.Float64(key, float64(val))
case float64:
return logEntry.Float64(key, val)
case bool:
return logEntry.Bool(key, val)
case time.Time:
return logEntry.String(key, val.Format(time.RFC3339))
case time.Duration:
return logEntry.String(key, val.String())
default:
// Fallback to string conversion for unknown types
return logEntry.String(key, fmt.Sprint(value))
}
}
func (h *logHook) Fire(entry *logrus.Entry) error {
ctx := context.Background()
if entry.Context != nil {
ctx = entry.Context
}
// Create the base log entry for the appropriate level
var logEntry sentry.LogEntry
switch entry.Level {
case logrus.TraceLevel:
logEntry = h.logger.Trace().WithCtx(ctx)
case logrus.DebugLevel:
logEntry = h.logger.Debug().WithCtx(ctx)
case logrus.InfoLevel:
logEntry = h.logger.Info().WithCtx(ctx)
case logrus.WarnLevel:
logEntry = h.logger.Warn().WithCtx(ctx)
case logrus.ErrorLevel:
logEntry = h.logger.Error().WithCtx(ctx)
case logrus.FatalLevel:
logEntry = h.logger.Fatal().WithCtx(ctx)
case logrus.PanicLevel:
logEntry = h.logger.Panic().WithCtx(ctx)
default:
sentry.DebugLogger.Printf("Invalid logrus logging level: %v. Dropping log.", entry.Level)
if h.fallback != nil {
return h.fallback(entry)
}
return errors.New("invalid log level")
}
// Add all the fields as attributes to this specific log entry
for k, v := range entry.Data {
// Skip specific fields that might be handled separately
if k == h.key(FieldRequest) || k == h.key(FieldUser) ||
k == h.key(FieldFingerprint) || k == FieldGoVersion ||
k == FieldMaxProcs || k == logrus.ErrorKey {
continue
}
logEntry = logrusFieldToLogEntry(logEntry, k, v)
}
// Emit the log entry with the message
logEntry.Emit(entry.Message)
return nil
}
func (h *logHook) Levels() []logrus.Level {
return h.levels
}
func (h *logHook) Flush(timeout time.Duration) bool {
return h.hubProvider().Client().Flush(timeout)
}
func (h *logHook) FlushWithContext(ctx context.Context) bool {
return h.hubProvider().Client().FlushWithContext(ctx)
}
// NewLogHook initializes a new Logrus hook which sends logs to a new Sentry client
// configured according to opts.
func NewLogHook(levels []logrus.Level, opts sentry.ClientOptions) (Hook, error) {
if !opts.EnableLogs {
return nil, errors.New("cannot create log hook, EnableLogs is set to false")
}
client, err := sentry.NewClient(opts)
if err != nil {
return nil, err
}
client.SetSDKIdentifier(sdkIdentifier)
return NewLogHookFromClient(levels, client), nil
}
// NewLogHookFromClient initializes a new Logrus hook which sends logs to the provided
// sentry client.
func NewLogHookFromClient(levels []logrus.Level, client *sentry.Client) Hook {
defaultHub := sentry.NewHub(client, sentry.NewScope())
ctx := sentry.SetHubOnContext(context.Background(), defaultHub)
logger := sentry.NewLogger(ctx)
logger.SetAttributes(attribute.String("sentry.origin", LogrusOrigin))
return &logHook{
logger: logger,
levels: levels,
hubProvider: func() *sentry.Hub {
// Default to using the same hub if no specific provider is set
return defaultHub
},
keys: make(map[string]string),
}
}