You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
424 lines
12 KiB
424 lines
12 KiB
package logrus_sentry
|
|
|
|
import (
|
|
"encoding/json"
|
|
"fmt"
|
|
"runtime"
|
|
"sync"
|
|
"time"
|
|
|
|
raven "github.com/getsentry/raven-go"
|
|
"github.com/pkg/errors"
|
|
"github.com/sirupsen/logrus"
|
|
)
|
|
|
|
var (
|
|
severityMap = map[logrus.Level]raven.Severity{
|
|
logrus.TraceLevel: raven.DEBUG,
|
|
logrus.DebugLevel: raven.DEBUG,
|
|
logrus.InfoLevel: raven.INFO,
|
|
logrus.WarnLevel: raven.WARNING,
|
|
logrus.ErrorLevel: raven.ERROR,
|
|
logrus.FatalLevel: raven.FATAL,
|
|
logrus.PanicLevel: raven.FATAL,
|
|
}
|
|
)
|
|
|
|
// SentryHook delivers logs to a sentry server.
|
|
type SentryHook struct {
|
|
// Timeout sets the time to wait for a delivery error from the sentry server.
|
|
// If this is set to zero the server will not wait for any response and will
|
|
// consider the message correctly sent.
|
|
//
|
|
// This is ignored for asynchronous hooks. If you want to set a timeout when
|
|
// using an async hook (to bound the length of time that hook.Flush can take),
|
|
// you probably want to create your own raven.Client and set
|
|
// ravenClient.Transport.(*raven.HTTPTransport).Client.Timeout to set a
|
|
// timeout on the underlying HTTP request instead.
|
|
Timeout time.Duration
|
|
StacktraceConfiguration StackTraceConfiguration
|
|
|
|
client *raven.Client
|
|
levels []logrus.Level
|
|
|
|
serverName string
|
|
ignoreFields map[string]struct{}
|
|
extraFilters map[string]func(interface{}) interface{}
|
|
errorHandlers []func(entry *logrus.Entry, err error)
|
|
|
|
asynchronous bool
|
|
|
|
mu sync.RWMutex
|
|
wg sync.WaitGroup
|
|
}
|
|
|
|
// The Stacktracer interface allows an error type to return a raven.Stacktrace.
|
|
type Stacktracer interface {
|
|
GetStacktrace() *raven.Stacktrace
|
|
}
|
|
|
|
type causer interface {
|
|
Cause() error
|
|
}
|
|
|
|
type pkgErrorStackTracer interface {
|
|
StackTrace() errors.StackTrace
|
|
}
|
|
|
|
// StackTraceConfiguration allows for configuring stacktraces
|
|
type StackTraceConfiguration struct {
|
|
// whether stacktraces should be enabled
|
|
Enable bool
|
|
// the level at which to start capturing stacktraces
|
|
Level logrus.Level
|
|
// how many stack frames to skip before stacktrace starts recording
|
|
Skip int
|
|
// the number of lines to include around a stack frame for context
|
|
Context int
|
|
// the prefixes that will be matched against the stack frame.
|
|
// if the stack frame's package matches one of these prefixes
|
|
// sentry will identify the stack frame as "in_app"
|
|
InAppPrefixes []string
|
|
// whether sending exception type should be enabled.
|
|
SendExceptionType bool
|
|
// whether the exception type and message should be switched.
|
|
SwitchExceptionTypeAndMessage bool
|
|
// whether to include a breadcrumb with the full error stack
|
|
IncludeErrorBreadcrumb bool
|
|
}
|
|
|
|
// NewSentryHook creates a hook to be added to an instance of logger
|
|
// and initializes the raven client.
|
|
// This method sets the timeout to 100 milliseconds.
|
|
func NewSentryHook(DSN string, levels []logrus.Level) (*SentryHook, error) {
|
|
client, err := raven.New(DSN)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return NewWithClientSentryHook(client, levels)
|
|
}
|
|
|
|
// NewWithTagsSentryHook creates a hook with tags to be added to an instance
|
|
// of logger and initializes the raven client. This method sets the timeout to
|
|
// 100 milliseconds.
|
|
func NewWithTagsSentryHook(DSN string, tags map[string]string, levels []logrus.Level) (*SentryHook, error) {
|
|
client, err := raven.NewWithTags(DSN, tags)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return NewWithClientSentryHook(client, levels)
|
|
}
|
|
|
|
// NewWithClientSentryHook creates a hook using an initialized raven client.
|
|
// This method sets the timeout to 100 milliseconds.
|
|
func NewWithClientSentryHook(client *raven.Client, levels []logrus.Level) (*SentryHook, error) {
|
|
return &SentryHook{
|
|
Timeout: 100 * time.Millisecond,
|
|
StacktraceConfiguration: StackTraceConfiguration{
|
|
Enable: false,
|
|
Level: logrus.ErrorLevel,
|
|
Skip: 6,
|
|
Context: 0,
|
|
InAppPrefixes: nil,
|
|
SendExceptionType: true,
|
|
},
|
|
client: client,
|
|
levels: levels,
|
|
ignoreFields: make(map[string]struct{}),
|
|
extraFilters: make(map[string]func(interface{}) interface{}),
|
|
}, nil
|
|
}
|
|
|
|
// NewAsyncSentryHook creates a hook same as NewSentryHook, but in asynchronous
|
|
// mode.
|
|
func NewAsyncSentryHook(DSN string, levels []logrus.Level) (*SentryHook, error) {
|
|
hook, err := NewSentryHook(DSN, levels)
|
|
return setAsync(hook), err
|
|
}
|
|
|
|
// NewAsyncWithTagsSentryHook creates a hook same as NewWithTagsSentryHook, but
|
|
// in asynchronous mode.
|
|
func NewAsyncWithTagsSentryHook(DSN string, tags map[string]string, levels []logrus.Level) (*SentryHook, error) {
|
|
hook, err := NewWithTagsSentryHook(DSN, tags, levels)
|
|
return setAsync(hook), err
|
|
}
|
|
|
|
// NewAsyncWithClientSentryHook creates a hook same as NewWithClientSentryHook,
|
|
// but in asynchronous mode.
|
|
func NewAsyncWithClientSentryHook(client *raven.Client, levels []logrus.Level) (*SentryHook, error) {
|
|
hook, err := NewWithClientSentryHook(client, levels)
|
|
return setAsync(hook), err
|
|
}
|
|
|
|
func setAsync(hook *SentryHook) *SentryHook {
|
|
if hook == nil {
|
|
return nil
|
|
}
|
|
hook.asynchronous = true
|
|
return hook
|
|
}
|
|
|
|
// Fire is called when an event should be sent to sentry
|
|
// Special fields that sentry uses to give more information to the server
|
|
// are extracted from entry.Data (if they are found)
|
|
// These fields are: error, logger, server_name, http_request, tags
|
|
func (hook *SentryHook) Fire(entry *logrus.Entry) error {
|
|
hook.mu.RLock() // Allow multiple go routines to log simultaneously
|
|
defer hook.mu.RUnlock()
|
|
|
|
df := newDataField(entry.Data)
|
|
|
|
err, hasError := df.getError()
|
|
var crumbs *Breadcrumbs
|
|
if hasError && hook.StacktraceConfiguration.IncludeErrorBreadcrumb {
|
|
crumbs = &Breadcrumbs{
|
|
Values: []Value{{
|
|
Timestamp: int64(time.Now().Unix()),
|
|
Type: "error",
|
|
Message: fmt.Sprintf("%+v", err),
|
|
}},
|
|
}
|
|
}
|
|
|
|
packet := raven.NewPacketWithExtra(entry.Message, nil, crumbs)
|
|
packet.Timestamp = raven.Timestamp(entry.Time)
|
|
packet.Level = severityMap[entry.Level]
|
|
packet.Platform = "go"
|
|
|
|
// set special fields
|
|
if hook.serverName != "" {
|
|
packet.ServerName = hook.serverName
|
|
}
|
|
if logger, ok := df.getLogger(); ok {
|
|
packet.Logger = logger
|
|
}
|
|
if serverName, ok := df.getServerName(); ok {
|
|
packet.ServerName = serverName
|
|
}
|
|
if eventID, ok := df.getEventID(); ok {
|
|
packet.EventID = eventID
|
|
}
|
|
if tags, ok := df.getTags(); ok {
|
|
packet.Tags = tags
|
|
}
|
|
if fingerprint, ok := df.getFingerprint(); ok {
|
|
packet.Fingerprint = fingerprint
|
|
}
|
|
if req, ok := df.getHTTPRequest(); ok {
|
|
packet.Interfaces = append(packet.Interfaces, req)
|
|
}
|
|
if user, ok := df.getUser(); ok {
|
|
packet.Interfaces = append(packet.Interfaces, user)
|
|
}
|
|
|
|
// set stacktrace data
|
|
stConfig := &hook.StacktraceConfiguration
|
|
if stConfig.Enable && entry.Level <= stConfig.Level {
|
|
if err, ok := df.getError(); ok {
|
|
var currentStacktrace *raven.Stacktrace
|
|
currentStacktrace = hook.findStacktrace(err)
|
|
if currentStacktrace == nil {
|
|
currentStacktrace = raven.NewStacktrace(stConfig.Skip, stConfig.Context, stConfig.InAppPrefixes)
|
|
}
|
|
cause := errors.Cause(err)
|
|
if cause == nil {
|
|
cause = err
|
|
}
|
|
exc := raven.NewException(cause, currentStacktrace)
|
|
if !stConfig.SendExceptionType {
|
|
exc.Type = ""
|
|
}
|
|
if stConfig.SwitchExceptionTypeAndMessage {
|
|
packet.Interfaces = append(packet.Interfaces, currentStacktrace)
|
|
packet.Culprit = exc.Type + ": " + currentStacktrace.Culprit()
|
|
} else {
|
|
packet.Interfaces = append(packet.Interfaces, exc)
|
|
packet.Culprit = err.Error()
|
|
}
|
|
} else {
|
|
currentStacktrace := raven.NewStacktrace(stConfig.Skip, stConfig.Context, stConfig.InAppPrefixes)
|
|
if currentStacktrace != nil {
|
|
packet.Interfaces = append(packet.Interfaces, currentStacktrace)
|
|
}
|
|
}
|
|
} else {
|
|
// set the culprit even when the stack trace is disabled, as long as we have an error
|
|
if err, ok := df.getError(); ok {
|
|
packet.Culprit = err.Error()
|
|
}
|
|
}
|
|
|
|
// set other fields
|
|
dataExtra := hook.formatExtraData(df)
|
|
if packet.Extra == nil {
|
|
packet.Extra = dataExtra
|
|
} else {
|
|
for k, v := range dataExtra {
|
|
packet.Extra[k] = v
|
|
}
|
|
}
|
|
|
|
_, errCh := hook.client.Capture(packet, nil)
|
|
|
|
switch {
|
|
case hook.asynchronous:
|
|
// Our use of hook.mu guarantees that we are following the WaitGroup rule of
|
|
// not calling Add in parallel with Wait.
|
|
hook.wg.Add(1)
|
|
go func() {
|
|
if err := <-errCh; err != nil {
|
|
for _, handlerFn := range hook.errorHandlers {
|
|
handlerFn(entry, err)
|
|
}
|
|
}
|
|
hook.wg.Done()
|
|
}()
|
|
return nil
|
|
case hook.Timeout == 0:
|
|
return nil
|
|
default:
|
|
timeout := hook.Timeout
|
|
timeoutCh := time.After(timeout)
|
|
select {
|
|
case err := <-errCh:
|
|
for _, handlerFn := range hook.errorHandlers {
|
|
handlerFn(entry, err)
|
|
}
|
|
return err
|
|
case <-timeoutCh:
|
|
return fmt.Errorf("no response from sentry server in %s", timeout)
|
|
}
|
|
}
|
|
}
|
|
|
|
// Flush waits for the log queue to empty. This function only does anything in
|
|
// asynchronous mode.
|
|
func (hook *SentryHook) Flush() {
|
|
if !hook.asynchronous {
|
|
return
|
|
}
|
|
hook.mu.Lock() // Claim exclusive access; any logging goroutines will block until the flush completes
|
|
defer hook.mu.Unlock()
|
|
|
|
hook.wg.Wait()
|
|
}
|
|
|
|
func (hook *SentryHook) findStacktrace(err error) *raven.Stacktrace {
|
|
var stacktrace *raven.Stacktrace
|
|
var stackErr errors.StackTrace
|
|
for err != nil {
|
|
// Find the earliest *raven.Stacktrace, or error.StackTrace
|
|
if tracer, ok := err.(Stacktracer); ok {
|
|
stacktrace = tracer.GetStacktrace()
|
|
stackErr = nil
|
|
} else if tracer, ok := err.(pkgErrorStackTracer); ok {
|
|
stacktrace = nil
|
|
stackErr = tracer.StackTrace()
|
|
}
|
|
if cause, ok := err.(causer); ok {
|
|
err = cause.Cause()
|
|
} else {
|
|
break
|
|
}
|
|
}
|
|
if stackErr != nil {
|
|
stacktrace = hook.convertStackTrace(stackErr)
|
|
}
|
|
return stacktrace
|
|
}
|
|
|
|
// convertStackTrace converts an errors.StackTrace into a natively consumable
|
|
// *raven.Stacktrace
|
|
func (hook *SentryHook) convertStackTrace(st errors.StackTrace) *raven.Stacktrace {
|
|
stConfig := &hook.StacktraceConfiguration
|
|
stFrames := []errors.Frame(st)
|
|
frames := make([]*raven.StacktraceFrame, 0, len(stFrames))
|
|
for i := range stFrames {
|
|
pc := uintptr(stFrames[i])
|
|
fn := runtime.FuncForPC(pc)
|
|
file, line := fn.FileLine(pc)
|
|
frame := raven.NewStacktraceFrame(pc, fn.Name(), file, line, stConfig.Context, stConfig.InAppPrefixes)
|
|
if frame != nil {
|
|
frames = append(frames, frame)
|
|
}
|
|
}
|
|
|
|
// Sentry wants the frames with the oldest first, so reverse them
|
|
for i, j := 0, len(frames)-1; i < j; i, j = i+1, j-1 {
|
|
frames[i], frames[j] = frames[j], frames[i]
|
|
}
|
|
return &raven.Stacktrace{Frames: frames}
|
|
}
|
|
|
|
// Levels returns the available logging levels.
|
|
func (hook *SentryHook) Levels() []logrus.Level {
|
|
return hook.levels
|
|
}
|
|
|
|
// AddIgnore adds field name to ignore.
|
|
func (hook *SentryHook) AddIgnore(name string) {
|
|
hook.ignoreFields[name] = struct{}{}
|
|
}
|
|
|
|
// AddExtraFilter adds a custom filter function.
|
|
func (hook *SentryHook) AddExtraFilter(name string, fn func(interface{}) interface{}) {
|
|
hook.extraFilters[name] = fn
|
|
}
|
|
|
|
// AddErrorHandler adds a error handler function used when Sentry returns error.
|
|
func (hook *SentryHook) AddErrorHandler(fn func(entry *logrus.Entry, err error)) {
|
|
hook.errorHandlers = append(hook.errorHandlers, fn)
|
|
}
|
|
|
|
func (hook *SentryHook) formatExtraData(df *dataField) (result map[string]interface{}) {
|
|
// create a map for passing to Sentry's extra data
|
|
result = make(map[string]interface{}, df.len())
|
|
for k, v := range df.data {
|
|
if df.isOmit(k) {
|
|
continue // skip already used special fields
|
|
}
|
|
if _, ok := hook.ignoreFields[k]; ok {
|
|
continue
|
|
}
|
|
|
|
if fn, ok := hook.extraFilters[k]; ok {
|
|
v = fn(v) // apply custom filter
|
|
} else {
|
|
v = formatData(v) // use default formatter
|
|
}
|
|
result[k] = v
|
|
}
|
|
return result
|
|
}
|
|
|
|
// formatData returns value as a suitable format.
|
|
func formatData(value interface{}) (formatted interface{}) {
|
|
switch value := value.(type) {
|
|
case json.Marshaler:
|
|
return value
|
|
case error:
|
|
return value.Error()
|
|
case fmt.Stringer:
|
|
return value.String()
|
|
default:
|
|
return value
|
|
}
|
|
}
|
|
|
|
// utility classes for breadcrumb support
|
|
type Breadcrumbs struct {
|
|
Values []Value `json:"values"`
|
|
}
|
|
|
|
type Value struct {
|
|
Timestamp int64 `json:"timestamp"`
|
|
Type string `json:"type"`
|
|
Message string `json:"message"`
|
|
Category string `json:"category"`
|
|
Level string `json:"string"`
|
|
Data interface{} `json:"data"`
|
|
}
|
|
|
|
func (b *Breadcrumbs) Class() string {
|
|
return "breadcrumbs"
|
|
}
|
|
|