forked from vikunja/vikunja
konrad
d02d413c5e
Use sentry echo integration to send errors Only capture errors not already handled by echo Add sentry panic handler Add sentry library Add sentry init Add sentry config Co-authored-by: kolaente <k@knt.li> Reviewed-on: vikunja/api#591
365 lines
9.5 KiB
Go
365 lines
9.5 KiB
Go
package sentry
|
|
|
|
import (
|
|
"context"
|
|
"sync"
|
|
"time"
|
|
)
|
|
|
|
type contextKey int
|
|
|
|
// HubContextKey is a context key used to store Hub on any context.Context type
|
|
const HubContextKey = contextKey(1)
|
|
|
|
// RequestContextKey is a context key used to store http.Request on the context passed to RecoverWithContext
|
|
const RequestContextKey = contextKey(2)
|
|
|
|
// Default maximum number of breadcrumbs added to an event. Can be overwritten `maxBreadcrumbs` option.
|
|
const defaultMaxBreadcrumbs = 30
|
|
|
|
// Absolute maximum number of breadcrumbs added to an event.
|
|
// The `maxBreadcrumbs` option cannot be higher than this value.
|
|
const maxBreadcrumbs = 100
|
|
|
|
// Initial instance of the Hub that has no `Client` bound and an empty `Scope`
|
|
var currentHub = NewHub(nil, NewScope()) //nolint: gochecknoglobals
|
|
|
|
// Hub is the central object that manages scopes and clients.
|
|
//
|
|
// This can be used to capture events and manage the scope.
|
|
// The default hub that is available automatically.
|
|
//
|
|
// In most situations developers do not need to interface the hub. Instead
|
|
// toplevel convenience functions are exposed that will automatically dispatch
|
|
// to global (`CurrentHub`) hub. In some situations this might not be
|
|
// possible in which case it might become necessary to manually work with the
|
|
// hub. This is for instance the case when working with async code.
|
|
type Hub struct {
|
|
mu sync.RWMutex
|
|
stack *stack
|
|
lastEventID EventID
|
|
}
|
|
|
|
type layer struct {
|
|
// mu protects concurrent reads and writes to client.
|
|
mu sync.RWMutex
|
|
client *Client
|
|
// scope is read-only, not protected by mu.
|
|
scope *Scope
|
|
}
|
|
|
|
// Client returns the layer's client. Safe for concurrent use.
|
|
func (l *layer) Client() *Client {
|
|
l.mu.RLock()
|
|
defer l.mu.RUnlock()
|
|
return l.client
|
|
}
|
|
|
|
// SetClient sets the layer's client. Safe for concurrent use.
|
|
func (l *layer) SetClient(c *Client) {
|
|
l.mu.Lock()
|
|
defer l.mu.Unlock()
|
|
l.client = c
|
|
}
|
|
|
|
type stack []*layer
|
|
|
|
// NewHub returns an instance of a `Hub` with provided `Client` and `Scope` bound.
|
|
func NewHub(client *Client, scope *Scope) *Hub {
|
|
hub := Hub{
|
|
stack: &stack{{
|
|
client: client,
|
|
scope: scope,
|
|
}},
|
|
}
|
|
return &hub
|
|
}
|
|
|
|
// CurrentHub returns an instance of previously initialized `Hub` stored in the global namespace.
|
|
func CurrentHub() *Hub {
|
|
return currentHub
|
|
}
|
|
|
|
// LastEventID returns an ID of last captured event for the current `Hub`.
|
|
func (hub *Hub) LastEventID() EventID {
|
|
return hub.lastEventID
|
|
}
|
|
|
|
func (hub *Hub) stackTop() *layer {
|
|
hub.mu.RLock()
|
|
defer hub.mu.RUnlock()
|
|
|
|
stack := hub.stack
|
|
if stack == nil {
|
|
return nil
|
|
}
|
|
|
|
stackLen := len(*stack)
|
|
if stackLen == 0 {
|
|
return nil
|
|
}
|
|
top := (*stack)[stackLen-1]
|
|
|
|
return top
|
|
}
|
|
|
|
// Clone returns a copy of the current Hub with top-most scope and client copied over.
|
|
func (hub *Hub) Clone() *Hub {
|
|
top := hub.stackTop()
|
|
if top == nil {
|
|
return nil
|
|
}
|
|
scope := top.scope
|
|
if scope != nil {
|
|
scope = scope.Clone()
|
|
}
|
|
return NewHub(top.Client(), scope)
|
|
}
|
|
|
|
// Scope returns top-level `Scope` of the current `Hub` or `nil` if no `Scope` is bound.
|
|
func (hub *Hub) Scope() *Scope {
|
|
top := hub.stackTop()
|
|
if top == nil {
|
|
return nil
|
|
}
|
|
return top.scope
|
|
}
|
|
|
|
// Scope returns top-level `Client` of the current `Hub` or `nil` if no `Client` is bound.
|
|
func (hub *Hub) Client() *Client {
|
|
top := hub.stackTop()
|
|
if top == nil {
|
|
return nil
|
|
}
|
|
return top.Client()
|
|
}
|
|
|
|
// PushScope pushes a new scope for the current `Hub` and reuses previously bound `Client`.
|
|
func (hub *Hub) PushScope() *Scope {
|
|
top := hub.stackTop()
|
|
|
|
var client *Client
|
|
if top != nil {
|
|
client = top.Client()
|
|
}
|
|
|
|
var scope *Scope
|
|
if top != nil && top.scope != nil {
|
|
scope = top.scope.Clone()
|
|
} else {
|
|
scope = NewScope()
|
|
}
|
|
|
|
hub.mu.Lock()
|
|
defer hub.mu.Unlock()
|
|
|
|
*hub.stack = append(*hub.stack, &layer{
|
|
client: client,
|
|
scope: scope,
|
|
})
|
|
|
|
return scope
|
|
}
|
|
|
|
// PushScope pops the most recent scope for the current `Hub`.
|
|
func (hub *Hub) PopScope() {
|
|
hub.mu.Lock()
|
|
defer hub.mu.Unlock()
|
|
|
|
stack := *hub.stack
|
|
stackLen := len(stack)
|
|
if stackLen > 0 {
|
|
*hub.stack = stack[0 : stackLen-1]
|
|
}
|
|
}
|
|
|
|
// BindClient binds a new `Client` for the current `Hub`.
|
|
func (hub *Hub) BindClient(client *Client) {
|
|
top := hub.stackTop()
|
|
if top != nil {
|
|
top.SetClient(client)
|
|
}
|
|
}
|
|
|
|
// WithScope temporarily pushes a scope for a single call.
|
|
//
|
|
// A shorthand for:
|
|
// PushScope()
|
|
// f(scope)
|
|
// PopScope()
|
|
func (hub *Hub) WithScope(f func(scope *Scope)) {
|
|
scope := hub.PushScope()
|
|
defer hub.PopScope()
|
|
f(scope)
|
|
}
|
|
|
|
// ConfigureScope invokes a function that can modify the current scope.
|
|
//
|
|
// The function is passed a mutable reference to the `Scope` so that modifications
|
|
// can be performed.
|
|
func (hub *Hub) ConfigureScope(f func(scope *Scope)) {
|
|
scope := hub.Scope()
|
|
f(scope)
|
|
}
|
|
|
|
// CaptureEvent calls the method of a same name on currently bound `Client` instance
|
|
// passing it a top-level `Scope`.
|
|
// Returns `EventID` if successfully, or `nil` if there's no `Scope` or `Client` available.
|
|
func (hub *Hub) CaptureEvent(event *Event) *EventID {
|
|
client, scope := hub.Client(), hub.Scope()
|
|
if client == nil || scope == nil {
|
|
return nil
|
|
}
|
|
eventID := client.CaptureEvent(event, nil, scope)
|
|
if eventID != nil {
|
|
hub.lastEventID = *eventID
|
|
} else {
|
|
hub.lastEventID = ""
|
|
}
|
|
return eventID
|
|
}
|
|
|
|
// CaptureMessage calls the method of a same name on currently bound `Client` instance
|
|
// passing it a top-level `Scope`.
|
|
// Returns `EventID` if successfully, or `nil` if there's no `Scope` or `Client` available.
|
|
func (hub *Hub) CaptureMessage(message string) *EventID {
|
|
client, scope := hub.Client(), hub.Scope()
|
|
if client == nil || scope == nil {
|
|
return nil
|
|
}
|
|
eventID := client.CaptureMessage(message, nil, scope)
|
|
if eventID != nil {
|
|
hub.lastEventID = *eventID
|
|
} else {
|
|
hub.lastEventID = ""
|
|
}
|
|
return eventID
|
|
}
|
|
|
|
// CaptureException calls the method of a same name on currently bound `Client` instance
|
|
// passing it a top-level `Scope`.
|
|
// Returns `EventID` if successfully, or `nil` if there's no `Scope` or `Client` available.
|
|
func (hub *Hub) CaptureException(exception error) *EventID {
|
|
client, scope := hub.Client(), hub.Scope()
|
|
if client == nil || scope == nil {
|
|
return nil
|
|
}
|
|
eventID := client.CaptureException(exception, &EventHint{OriginalException: exception}, scope)
|
|
if eventID != nil {
|
|
hub.lastEventID = *eventID
|
|
} else {
|
|
hub.lastEventID = ""
|
|
}
|
|
return eventID
|
|
}
|
|
|
|
// AddBreadcrumb records a new breadcrumb.
|
|
//
|
|
// The total number of breadcrumbs that can be recorded are limited by the
|
|
// configuration on the client.
|
|
func (hub *Hub) AddBreadcrumb(breadcrumb *Breadcrumb, hint *BreadcrumbHint) {
|
|
client := hub.Client()
|
|
|
|
// If there's no client, just store it on the scope straight away
|
|
if client == nil {
|
|
hub.Scope().AddBreadcrumb(breadcrumb, maxBreadcrumbs)
|
|
return
|
|
}
|
|
|
|
options := client.Options()
|
|
max := defaultMaxBreadcrumbs
|
|
|
|
if options.MaxBreadcrumbs != 0 {
|
|
max = options.MaxBreadcrumbs
|
|
}
|
|
|
|
if max < 0 {
|
|
return
|
|
}
|
|
|
|
if options.BeforeBreadcrumb != nil {
|
|
h := &BreadcrumbHint{}
|
|
if hint != nil {
|
|
h = hint
|
|
}
|
|
if breadcrumb = options.BeforeBreadcrumb(breadcrumb, h); breadcrumb == nil {
|
|
Logger.Println("breadcrumb dropped due to BeforeBreadcrumb callback.")
|
|
return
|
|
}
|
|
}
|
|
|
|
if max > maxBreadcrumbs {
|
|
max = maxBreadcrumbs
|
|
}
|
|
hub.Scope().AddBreadcrumb(breadcrumb, max)
|
|
}
|
|
|
|
// Recover calls the method of a same name on currently bound `Client` instance
|
|
// passing it a top-level `Scope`.
|
|
// Returns `EventID` if successfully, or `nil` if there's no `Scope` or `Client` available.
|
|
func (hub *Hub) Recover(err interface{}) *EventID {
|
|
if err == nil {
|
|
err = recover()
|
|
}
|
|
client, scope := hub.Client(), hub.Scope()
|
|
if client == nil || scope == nil {
|
|
return nil
|
|
}
|
|
return client.Recover(err, &EventHint{RecoveredException: err}, scope)
|
|
}
|
|
|
|
// RecoverWithContext calls the method of a same name on currently bound `Client` instance
|
|
// passing it a top-level `Scope`.
|
|
// Returns `EventID` if successfully, or `nil` if there's no `Scope` or `Client` available.
|
|
func (hub *Hub) RecoverWithContext(ctx context.Context, err interface{}) *EventID {
|
|
if err == nil {
|
|
err = recover()
|
|
}
|
|
client, scope := hub.Client(), hub.Scope()
|
|
if client == nil || scope == nil {
|
|
return nil
|
|
}
|
|
return client.RecoverWithContext(ctx, err, &EventHint{RecoveredException: err}, scope)
|
|
}
|
|
|
|
// Flush waits until the underlying Transport sends any buffered events to the
|
|
// Sentry server, blocking for at most the given timeout. It returns false if
|
|
// the timeout was reached. In that case, some events may not have been sent.
|
|
//
|
|
// Flush should be called before terminating the program to avoid
|
|
// unintentionally dropping events.
|
|
//
|
|
// Do not call Flush indiscriminately after every call to CaptureEvent,
|
|
// CaptureException or CaptureMessage. Instead, to have the SDK send events over
|
|
// the network synchronously, configure it to use the HTTPSyncTransport in the
|
|
// call to Init.
|
|
func (hub *Hub) Flush(timeout time.Duration) bool {
|
|
client := hub.Client()
|
|
|
|
if client == nil {
|
|
return false
|
|
}
|
|
|
|
return client.Flush(timeout)
|
|
}
|
|
|
|
// HasHubOnContext checks whether `Hub` instance is bound to a given `Context` struct.
|
|
func HasHubOnContext(ctx context.Context) bool {
|
|
_, ok := ctx.Value(HubContextKey).(*Hub)
|
|
return ok
|
|
}
|
|
|
|
// GetHubFromContext tries to retrieve `Hub` instance from the given `Context` struct
|
|
// or return `nil` if one is not found.
|
|
func GetHubFromContext(ctx context.Context) *Hub {
|
|
if hub, ok := ctx.Value(HubContextKey).(*Hub); ok {
|
|
return hub
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// SetHubOnContext stores given `Hub` instance on the `Context` struct and returns a new `Context`.
|
|
func SetHubOnContext(ctx context.Context, hub *Hub) context.Context {
|
|
return context.WithValue(ctx, HubContextKey, hub)
|
|
}
|