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
417 lines
10 KiB
Go
417 lines
10 KiB
Go
package sentry
|
|
|
|
import (
|
|
"bytes"
|
|
"io"
|
|
"net/http"
|
|
"reflect"
|
|
"sync"
|
|
"time"
|
|
)
|
|
|
|
// Scope holds contextual data for the current scope.
|
|
//
|
|
// The scope is an object that can cloned efficiently and stores data that
|
|
// is locally relevant to an event. For instance the scope will hold recorded
|
|
// breadcrumbs and similar information.
|
|
//
|
|
// The scope can be interacted with in two ways:
|
|
//
|
|
// 1. the scope is routinely updated with information by functions such as
|
|
// `AddBreadcrumb` which will modify the currently top-most scope.
|
|
// 2. the topmost scope can also be configured through the `ConfigureScope`
|
|
// method.
|
|
//
|
|
// Note that the scope can only be modified but not inspected.
|
|
// Only the client can use the scope to extract information currently.
|
|
type Scope struct {
|
|
mu sync.RWMutex
|
|
breadcrumbs []*Breadcrumb
|
|
user User
|
|
tags map[string]string
|
|
contexts map[string]interface{}
|
|
extra map[string]interface{}
|
|
fingerprint []string
|
|
level Level
|
|
transaction string
|
|
request *http.Request
|
|
// requestBody holds a reference to the original request.Body.
|
|
requestBody interface {
|
|
// Bytes returns bytes from the original body, lazily buffered as the
|
|
// original body is read.
|
|
Bytes() []byte
|
|
// Overflow returns true if the body is larger than the maximum buffer
|
|
// size.
|
|
Overflow() bool
|
|
}
|
|
eventProcessors []EventProcessor
|
|
}
|
|
|
|
func NewScope() *Scope {
|
|
scope := Scope{
|
|
breadcrumbs: make([]*Breadcrumb, 0),
|
|
tags: make(map[string]string),
|
|
contexts: make(map[string]interface{}),
|
|
extra: make(map[string]interface{}),
|
|
fingerprint: make([]string, 0),
|
|
}
|
|
|
|
return &scope
|
|
}
|
|
|
|
// AddBreadcrumb adds new breadcrumb to the current scope
|
|
// and optionally throws the old one if limit is reached.
|
|
func (scope *Scope) AddBreadcrumb(breadcrumb *Breadcrumb, limit int) {
|
|
if breadcrumb.Timestamp.IsZero() {
|
|
breadcrumb.Timestamp = time.Now().UTC()
|
|
}
|
|
|
|
scope.mu.Lock()
|
|
defer scope.mu.Unlock()
|
|
|
|
breadcrumbs := append(scope.breadcrumbs, breadcrumb)
|
|
if len(breadcrumbs) > limit {
|
|
scope.breadcrumbs = breadcrumbs[1 : limit+1]
|
|
} else {
|
|
scope.breadcrumbs = breadcrumbs
|
|
}
|
|
}
|
|
|
|
// ClearBreadcrumbs clears all breadcrumbs from the current scope.
|
|
func (scope *Scope) ClearBreadcrumbs() {
|
|
scope.mu.Lock()
|
|
defer scope.mu.Unlock()
|
|
|
|
scope.breadcrumbs = []*Breadcrumb{}
|
|
}
|
|
|
|
// SetUser sets the user for the current scope.
|
|
func (scope *Scope) SetUser(user User) {
|
|
scope.mu.Lock()
|
|
defer scope.mu.Unlock()
|
|
|
|
scope.user = user
|
|
}
|
|
|
|
// SetRequest sets the request for the current scope.
|
|
func (scope *Scope) SetRequest(r *http.Request) {
|
|
scope.mu.Lock()
|
|
defer scope.mu.Unlock()
|
|
|
|
scope.request = r
|
|
|
|
if r == nil {
|
|
return
|
|
}
|
|
|
|
// Don't buffer request body if we know it is oversized.
|
|
if r.ContentLength > maxRequestBodyBytes {
|
|
return
|
|
}
|
|
// Don't buffer if there is no body.
|
|
if r.Body == nil || r.Body == http.NoBody {
|
|
return
|
|
}
|
|
buf := &limitedBuffer{Capacity: maxRequestBodyBytes}
|
|
r.Body = readCloser{
|
|
Reader: io.TeeReader(r.Body, buf),
|
|
Closer: r.Body,
|
|
}
|
|
scope.requestBody = buf
|
|
}
|
|
|
|
// SetRequestBody sets the request body for the current scope.
|
|
//
|
|
// This method should only be called when the body bytes are already available
|
|
// in memory. Typically, the request body is buffered lazily from the
|
|
// Request.Body from SetRequest.
|
|
func (scope *Scope) SetRequestBody(b []byte) {
|
|
scope.mu.Lock()
|
|
defer scope.mu.Unlock()
|
|
|
|
capacity := maxRequestBodyBytes
|
|
overflow := false
|
|
if len(b) > capacity {
|
|
overflow = true
|
|
b = b[:capacity]
|
|
}
|
|
scope.requestBody = &limitedBuffer{
|
|
Capacity: capacity,
|
|
Buffer: *bytes.NewBuffer(b),
|
|
overflow: overflow,
|
|
}
|
|
}
|
|
|
|
// maxRequestBodyBytes is the default maximum request body size to send to
|
|
// Sentry.
|
|
const maxRequestBodyBytes = 10 * 1024
|
|
|
|
// A limitedBuffer is like a bytes.Buffer, but limited to store at most Capacity
|
|
// bytes. Any writes past the capacity are silently discarded, similar to
|
|
// ioutil.Discard.
|
|
type limitedBuffer struct {
|
|
Capacity int
|
|
|
|
bytes.Buffer
|
|
overflow bool
|
|
}
|
|
|
|
// Write implements io.Writer.
|
|
func (b *limitedBuffer) Write(p []byte) (n int, err error) {
|
|
// Silently ignore writes after overflow.
|
|
if b.overflow {
|
|
return len(p), nil
|
|
}
|
|
left := b.Capacity - b.Len()
|
|
if left < 0 {
|
|
left = 0
|
|
}
|
|
if len(p) > left {
|
|
b.overflow = true
|
|
p = p[:left]
|
|
}
|
|
return b.Buffer.Write(p)
|
|
}
|
|
|
|
// Overflow returns true if the limitedBuffer discarded bytes written to it.
|
|
func (b *limitedBuffer) Overflow() bool {
|
|
return b.overflow
|
|
}
|
|
|
|
// readCloser combines an io.Reader and an io.Closer to implement io.ReadCloser.
|
|
type readCloser struct {
|
|
io.Reader
|
|
io.Closer
|
|
}
|
|
|
|
// SetTag adds a tag to the current scope.
|
|
func (scope *Scope) SetTag(key, value string) {
|
|
scope.mu.Lock()
|
|
defer scope.mu.Unlock()
|
|
|
|
scope.tags[key] = value
|
|
}
|
|
|
|
// SetTags assigns multiple tags to the current scope.
|
|
func (scope *Scope) SetTags(tags map[string]string) {
|
|
scope.mu.Lock()
|
|
defer scope.mu.Unlock()
|
|
|
|
for k, v := range tags {
|
|
scope.tags[k] = v
|
|
}
|
|
}
|
|
|
|
// RemoveTag removes a tag from the current scope.
|
|
func (scope *Scope) RemoveTag(key string) {
|
|
scope.mu.Lock()
|
|
defer scope.mu.Unlock()
|
|
|
|
delete(scope.tags, key)
|
|
}
|
|
|
|
// SetContext adds a context to the current scope.
|
|
func (scope *Scope) SetContext(key string, value interface{}) {
|
|
scope.mu.Lock()
|
|
defer scope.mu.Unlock()
|
|
|
|
scope.contexts[key] = value
|
|
}
|
|
|
|
// SetContexts assigns multiple contexts to the current scope.
|
|
func (scope *Scope) SetContexts(contexts map[string]interface{}) {
|
|
scope.mu.Lock()
|
|
defer scope.mu.Unlock()
|
|
|
|
for k, v := range contexts {
|
|
scope.contexts[k] = v
|
|
}
|
|
}
|
|
|
|
// RemoveContext removes a context from the current scope.
|
|
func (scope *Scope) RemoveContext(key string) {
|
|
scope.mu.Lock()
|
|
defer scope.mu.Unlock()
|
|
|
|
delete(scope.contexts, key)
|
|
}
|
|
|
|
// SetExtra adds an extra to the current scope.
|
|
func (scope *Scope) SetExtra(key string, value interface{}) {
|
|
scope.mu.Lock()
|
|
defer scope.mu.Unlock()
|
|
|
|
scope.extra[key] = value
|
|
}
|
|
|
|
// SetExtras assigns multiple extras to the current scope.
|
|
func (scope *Scope) SetExtras(extra map[string]interface{}) {
|
|
scope.mu.Lock()
|
|
defer scope.mu.Unlock()
|
|
|
|
for k, v := range extra {
|
|
scope.extra[k] = v
|
|
}
|
|
}
|
|
|
|
// RemoveExtra removes a extra from the current scope.
|
|
func (scope *Scope) RemoveExtra(key string) {
|
|
scope.mu.Lock()
|
|
defer scope.mu.Unlock()
|
|
|
|
delete(scope.extra, key)
|
|
}
|
|
|
|
// SetFingerprint sets new fingerprint for the current scope.
|
|
func (scope *Scope) SetFingerprint(fingerprint []string) {
|
|
scope.mu.Lock()
|
|
defer scope.mu.Unlock()
|
|
|
|
scope.fingerprint = fingerprint
|
|
}
|
|
|
|
// SetLevel sets new level for the current scope.
|
|
func (scope *Scope) SetLevel(level Level) {
|
|
scope.mu.Lock()
|
|
defer scope.mu.Unlock()
|
|
|
|
scope.level = level
|
|
}
|
|
|
|
// SetTransaction sets new transaction name for the current transaction.
|
|
func (scope *Scope) SetTransaction(transactionName string) {
|
|
scope.mu.Lock()
|
|
defer scope.mu.Unlock()
|
|
|
|
scope.transaction = transactionName
|
|
}
|
|
|
|
// Clone returns a copy of the current scope with all data copied over.
|
|
func (scope *Scope) Clone() *Scope {
|
|
scope.mu.RLock()
|
|
defer scope.mu.RUnlock()
|
|
|
|
clone := NewScope()
|
|
clone.user = scope.user
|
|
clone.breadcrumbs = make([]*Breadcrumb, len(scope.breadcrumbs))
|
|
copy(clone.breadcrumbs, scope.breadcrumbs)
|
|
for key, value := range scope.tags {
|
|
clone.tags[key] = value
|
|
}
|
|
for key, value := range scope.contexts {
|
|
clone.contexts[key] = value
|
|
}
|
|
for key, value := range scope.extra {
|
|
clone.extra[key] = value
|
|
}
|
|
clone.fingerprint = make([]string, len(scope.fingerprint))
|
|
copy(clone.fingerprint, scope.fingerprint)
|
|
clone.level = scope.level
|
|
clone.transaction = scope.transaction
|
|
clone.request = scope.request
|
|
|
|
return clone
|
|
}
|
|
|
|
// Clear removes the data from the current scope. Not safe for concurrent use.
|
|
func (scope *Scope) Clear() {
|
|
*scope = *NewScope()
|
|
}
|
|
|
|
// AddEventProcessor adds an event processor to the current scope.
|
|
func (scope *Scope) AddEventProcessor(processor EventProcessor) {
|
|
scope.mu.Lock()
|
|
defer scope.mu.Unlock()
|
|
|
|
scope.eventProcessors = append(scope.eventProcessors, processor)
|
|
}
|
|
|
|
// ApplyToEvent takes the data from the current scope and attaches it to the event.
|
|
func (scope *Scope) ApplyToEvent(event *Event, hint *EventHint) *Event {
|
|
scope.mu.RLock()
|
|
defer scope.mu.RUnlock()
|
|
|
|
if len(scope.breadcrumbs) > 0 {
|
|
if event.Breadcrumbs == nil {
|
|
event.Breadcrumbs = []*Breadcrumb{}
|
|
}
|
|
|
|
event.Breadcrumbs = append(event.Breadcrumbs, scope.breadcrumbs...)
|
|
}
|
|
|
|
if len(scope.tags) > 0 {
|
|
if event.Tags == nil {
|
|
event.Tags = make(map[string]string)
|
|
}
|
|
|
|
for key, value := range scope.tags {
|
|
event.Tags[key] = value
|
|
}
|
|
}
|
|
|
|
if len(scope.contexts) > 0 {
|
|
if event.Contexts == nil {
|
|
event.Contexts = make(map[string]interface{})
|
|
}
|
|
|
|
for key, value := range scope.contexts {
|
|
event.Contexts[key] = value
|
|
}
|
|
}
|
|
|
|
if len(scope.extra) > 0 {
|
|
if event.Extra == nil {
|
|
event.Extra = make(map[string]interface{})
|
|
}
|
|
|
|
for key, value := range scope.extra {
|
|
event.Extra[key] = value
|
|
}
|
|
}
|
|
|
|
if (reflect.DeepEqual(event.User, User{})) {
|
|
event.User = scope.user
|
|
}
|
|
|
|
if (event.Fingerprint == nil || len(event.Fingerprint) == 0) &&
|
|
len(scope.fingerprint) > 0 {
|
|
event.Fingerprint = make([]string, len(scope.fingerprint))
|
|
copy(event.Fingerprint, scope.fingerprint)
|
|
}
|
|
|
|
if scope.level != "" {
|
|
event.Level = scope.level
|
|
}
|
|
|
|
if scope.transaction != "" {
|
|
event.Transaction = scope.transaction
|
|
}
|
|
|
|
if event.Request == nil && scope.request != nil {
|
|
event.Request = NewRequest(scope.request)
|
|
// NOTE: The SDK does not attempt to send partial request body data.
|
|
//
|
|
// The reason being that Sentry's ingest pipeline and UI are optimized
|
|
// to show structured data. Additionally, tooling around PII scrubbing
|
|
// relies on structured data; truncated request bodies would create
|
|
// invalid payloads that are more prone to leaking PII data.
|
|
//
|
|
// Users can still send more data along their events if they want to,
|
|
// for example using Event.Extra.
|
|
if scope.requestBody != nil && !scope.requestBody.Overflow() {
|
|
event.Request.Data = string(scope.requestBody.Bytes())
|
|
}
|
|
}
|
|
|
|
for _, processor := range scope.eventProcessors {
|
|
id := event.EventID
|
|
event = processor(event, hint)
|
|
if event == nil {
|
|
Logger.Printf("Event dropped by one of the Scope EventProcessors: %s\n", id)
|
|
return nil
|
|
}
|
|
}
|
|
|
|
return event
|
|
}
|