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: https://kolaente.dev/vikunja/api/pulls/591
487 lines
14 KiB
Go
487 lines
14 KiB
Go
package sentry
|
|
|
|
import (
|
|
"context"
|
|
"crypto/x509"
|
|
"fmt"
|
|
"io"
|
|
"io/ioutil"
|
|
"log"
|
|
"math/rand"
|
|
"net/http"
|
|
"os"
|
|
"reflect"
|
|
"sort"
|
|
"time"
|
|
)
|
|
|
|
// maxErrorDepth is the maximum number of errors reported in a chain of errors.
|
|
// This protects the SDK from an arbitrarily long chain of wrapped errors.
|
|
//
|
|
// An additional consideration is that arguably reporting a long chain of errors
|
|
// is of little use when debugging production errors with Sentry. The Sentry UI
|
|
// is not optimized for long chains either. The top-level error together with a
|
|
// stack trace is often the most useful information.
|
|
const maxErrorDepth = 10
|
|
|
|
// usageError is used to report to Sentry an SDK usage error.
|
|
//
|
|
// It is not exported because it is never returned by any function or method in
|
|
// the exported API.
|
|
type usageError struct {
|
|
error
|
|
}
|
|
|
|
// Logger is an instance of log.Logger that is use to provide debug information about running Sentry Client
|
|
// can be enabled by either using `Logger.SetOutput` directly or with `Debug` client option
|
|
var Logger = log.New(ioutil.Discard, "[Sentry] ", log.LstdFlags) //nolint: gochecknoglobals
|
|
|
|
type EventProcessor func(event *Event, hint *EventHint) *Event
|
|
|
|
type EventModifier interface {
|
|
ApplyToEvent(event *Event, hint *EventHint) *Event
|
|
}
|
|
|
|
var globalEventProcessors []EventProcessor //nolint: gochecknoglobals
|
|
|
|
func AddGlobalEventProcessor(processor EventProcessor) {
|
|
globalEventProcessors = append(globalEventProcessors, processor)
|
|
}
|
|
|
|
// Integration allows for registering a functions that modify or discard captured events.
|
|
type Integration interface {
|
|
Name() string
|
|
SetupOnce(client *Client)
|
|
}
|
|
|
|
// ClientOptions that configures a SDK Client
|
|
type ClientOptions struct {
|
|
// The DSN to use. If the DSN is not set, the client is effectively disabled.
|
|
Dsn string
|
|
// In debug mode, the debug information is printed to stdout to help you understand what
|
|
// sentry is doing.
|
|
Debug bool
|
|
// Configures whether SDK should generate and attach stacktraces to pure capture message calls.
|
|
AttachStacktrace bool
|
|
// The sample rate for event submission (0.0 - 1.0, defaults to 1.0).
|
|
SampleRate float64
|
|
// List of regexp strings that will be used to match against event's message
|
|
// and if applicable, caught errors type and value.
|
|
// If the match is found, then a whole event will be dropped.
|
|
IgnoreErrors []string
|
|
// Before send callback.
|
|
BeforeSend func(event *Event, hint *EventHint) *Event
|
|
// Before breadcrumb add callback.
|
|
BeforeBreadcrumb func(breadcrumb *Breadcrumb, hint *BreadcrumbHint) *Breadcrumb
|
|
// Integrations to be installed on the current Client, receives default integrations
|
|
Integrations func([]Integration) []Integration
|
|
// io.Writer implementation that should be used with the `Debug` mode
|
|
DebugWriter io.Writer
|
|
// The transport to use.
|
|
// This is an instance of a struct implementing `Transport` interface.
|
|
// Defaults to `httpTransport` from `transport.go`
|
|
Transport Transport
|
|
// The server name to be reported.
|
|
ServerName string
|
|
// The release to be sent with events.
|
|
Release string
|
|
// The dist to be sent with events.
|
|
Dist string
|
|
// The environment to be sent with events.
|
|
Environment string
|
|
// Maximum number of breadcrumbs.
|
|
MaxBreadcrumbs int
|
|
// An optional pointer to `http.Client` that will be used with a default HTTPTransport.
|
|
// Using your own client will make HTTPTransport, HTTPProxy, HTTPSProxy and CaCerts options ignored.
|
|
HTTPClient *http.Client
|
|
// An optional pointer to `http.Transport` that will be used with a default HTTPTransport.
|
|
// Using your own transport will make HTTPProxy, HTTPSProxy and CaCerts options ignored.
|
|
HTTPTransport http.RoundTripper
|
|
// An optional HTTP proxy to use.
|
|
// This will default to the `http_proxy` environment variable.
|
|
// or `https_proxy` if that one exists.
|
|
HTTPProxy string
|
|
// An optional HTTPS proxy to use.
|
|
// This will default to the `HTTPS_PROXY` environment variable
|
|
// or `http_proxy` if that one exists.
|
|
HTTPSProxy string
|
|
// An optional CaCerts to use.
|
|
// Defaults to `gocertifi.CACerts()`.
|
|
CaCerts *x509.CertPool
|
|
}
|
|
|
|
// Client is the underlying processor that's used by the main API and `Hub` instances.
|
|
type Client struct {
|
|
options ClientOptions
|
|
dsn *Dsn
|
|
eventProcessors []EventProcessor
|
|
integrations []Integration
|
|
Transport Transport
|
|
}
|
|
|
|
// NewClient creates and returns an instance of `Client` configured using `ClientOptions`.
|
|
func NewClient(options ClientOptions) (*Client, error) {
|
|
if options.Debug {
|
|
debugWriter := options.DebugWriter
|
|
if debugWriter == nil {
|
|
debugWriter = os.Stdout
|
|
}
|
|
Logger.SetOutput(debugWriter)
|
|
}
|
|
|
|
if options.Dsn == "" {
|
|
options.Dsn = os.Getenv("SENTRY_DSN")
|
|
}
|
|
|
|
if options.Release == "" {
|
|
options.Release = os.Getenv("SENTRY_RELEASE")
|
|
}
|
|
|
|
if options.Environment == "" {
|
|
options.Environment = os.Getenv("SENTRY_ENVIRONMENT")
|
|
}
|
|
|
|
var dsn *Dsn
|
|
if options.Dsn != "" {
|
|
var err error
|
|
dsn, err = NewDsn(options.Dsn)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
}
|
|
|
|
client := Client{
|
|
options: options,
|
|
dsn: dsn,
|
|
}
|
|
|
|
client.setupTransport()
|
|
client.setupIntegrations()
|
|
|
|
return &client, nil
|
|
}
|
|
|
|
func (client *Client) setupTransport() {
|
|
transport := client.options.Transport
|
|
|
|
if transport == nil {
|
|
if client.options.Dsn == "" {
|
|
transport = new(noopTransport)
|
|
} else {
|
|
transport = NewHTTPTransport()
|
|
}
|
|
}
|
|
|
|
transport.Configure(client.options)
|
|
client.Transport = transport
|
|
}
|
|
|
|
func (client *Client) setupIntegrations() {
|
|
integrations := []Integration{
|
|
new(contextifyFramesIntegration),
|
|
new(environmentIntegration),
|
|
new(modulesIntegration),
|
|
new(ignoreErrorsIntegration),
|
|
}
|
|
|
|
if client.options.Integrations != nil {
|
|
integrations = client.options.Integrations(integrations)
|
|
}
|
|
|
|
for _, integration := range integrations {
|
|
if client.integrationAlreadyInstalled(integration.Name()) {
|
|
Logger.Printf("Integration %s is already installed\n", integration.Name())
|
|
continue
|
|
}
|
|
client.integrations = append(client.integrations, integration)
|
|
integration.SetupOnce(client)
|
|
Logger.Printf("Integration installed: %s\n", integration.Name())
|
|
}
|
|
}
|
|
|
|
// AddEventProcessor adds an event processor to the client.
|
|
func (client *Client) AddEventProcessor(processor EventProcessor) {
|
|
client.eventProcessors = append(client.eventProcessors, processor)
|
|
}
|
|
|
|
// Options return `ClientOptions` for the current `Client`.
|
|
func (client Client) Options() ClientOptions {
|
|
return client.options
|
|
}
|
|
|
|
// CaptureMessage captures an arbitrary message.
|
|
func (client *Client) CaptureMessage(message string, hint *EventHint, scope EventModifier) *EventID {
|
|
event := client.eventFromMessage(message, LevelInfo)
|
|
return client.CaptureEvent(event, hint, scope)
|
|
}
|
|
|
|
// CaptureException captures an error.
|
|
func (client *Client) CaptureException(exception error, hint *EventHint, scope EventModifier) *EventID {
|
|
event := client.eventFromException(exception, LevelError)
|
|
return client.CaptureEvent(event, hint, scope)
|
|
}
|
|
|
|
// CaptureEvent captures an event on the currently active client if any.
|
|
//
|
|
// The event must already be assembled. Typically code would instead use
|
|
// the utility methods like `CaptureException`. The return value is the
|
|
// event ID. In case Sentry is disabled or event was dropped, the return value will be nil.
|
|
func (client *Client) CaptureEvent(event *Event, hint *EventHint, scope EventModifier) *EventID {
|
|
return client.processEvent(event, hint, scope)
|
|
}
|
|
|
|
// Recover captures a panic.
|
|
// Returns `EventID` if successfully, or `nil` if there's no error to recover from.
|
|
func (client *Client) Recover(err interface{}, hint *EventHint, scope EventModifier) *EventID {
|
|
if err == nil {
|
|
err = recover()
|
|
}
|
|
|
|
if err != nil {
|
|
if err, ok := err.(error); ok {
|
|
event := client.eventFromException(err, LevelFatal)
|
|
return client.CaptureEvent(event, hint, scope)
|
|
}
|
|
|
|
if err, ok := err.(string); ok {
|
|
event := client.eventFromMessage(err, LevelFatal)
|
|
return client.CaptureEvent(event, hint, scope)
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// Recover captures a panic and passes relevant context object.
|
|
// Returns `EventID` if successfully, or `nil` if there's no error to recover from.
|
|
func (client *Client) RecoverWithContext(
|
|
ctx context.Context,
|
|
err interface{},
|
|
hint *EventHint,
|
|
scope EventModifier,
|
|
) *EventID {
|
|
if err == nil {
|
|
err = recover()
|
|
}
|
|
|
|
if err != nil {
|
|
if hint.Context == nil && ctx != nil {
|
|
hint.Context = ctx
|
|
}
|
|
|
|
if err, ok := err.(error); ok {
|
|
event := client.eventFromException(err, LevelFatal)
|
|
return client.CaptureEvent(event, hint, scope)
|
|
}
|
|
|
|
if err, ok := err.(string); ok {
|
|
event := client.eventFromMessage(err, LevelFatal)
|
|
return client.CaptureEvent(event, hint, scope)
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// 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 (client *Client) Flush(timeout time.Duration) bool {
|
|
return client.Transport.Flush(timeout)
|
|
}
|
|
|
|
func (client *Client) eventFromMessage(message string, level Level) *Event {
|
|
event := NewEvent()
|
|
event.Level = level
|
|
event.Message = message
|
|
|
|
if client.Options().AttachStacktrace {
|
|
event.Threads = []Thread{{
|
|
Stacktrace: NewStacktrace(),
|
|
Crashed: false,
|
|
Current: true,
|
|
}}
|
|
}
|
|
|
|
return event
|
|
}
|
|
|
|
func (client *Client) eventFromException(exception error, level Level) *Event {
|
|
err := exception
|
|
if err == nil {
|
|
err = usageError{fmt.Errorf("%s called with nil error", callerFunctionName())}
|
|
}
|
|
|
|
event := NewEvent()
|
|
event.Level = level
|
|
|
|
for i := 0; i < maxErrorDepth && err != nil; i++ {
|
|
event.Exception = append(event.Exception, Exception{
|
|
Value: err.Error(),
|
|
Type: reflect.TypeOf(err).String(),
|
|
Stacktrace: ExtractStacktrace(err),
|
|
})
|
|
switch previous := err.(type) {
|
|
case interface{ Unwrap() error }:
|
|
err = previous.Unwrap()
|
|
case interface{ Cause() error }:
|
|
err = previous.Cause()
|
|
default:
|
|
err = nil
|
|
}
|
|
}
|
|
|
|
// Add a trace of the current stack to the most recent error in a chain if
|
|
// it doesn't have a stack trace yet.
|
|
// We only add to the most recent error to avoid duplication and because the
|
|
// current stack is most likely unrelated to errors deeper in the chain.
|
|
if event.Exception[0].Stacktrace == nil {
|
|
event.Exception[0].Stacktrace = NewStacktrace()
|
|
}
|
|
|
|
// event.Exception should be sorted such that the most recent error is last.
|
|
reverse(event.Exception)
|
|
|
|
return event
|
|
}
|
|
|
|
// reverse reverses the slice a in place.
|
|
func reverse(a []Exception) {
|
|
for i := len(a)/2 - 1; i >= 0; i-- {
|
|
opp := len(a) - 1 - i
|
|
a[i], a[opp] = a[opp], a[i]
|
|
}
|
|
}
|
|
|
|
func (client *Client) processEvent(event *Event, hint *EventHint, scope EventModifier) *EventID {
|
|
options := client.Options()
|
|
|
|
// TODO: Reconsider if its worth going away from default implementation
|
|
// of other SDKs. In Go zero value (default) for float32 is 0.0,
|
|
// which means that if someone uses ClientOptions{} struct directly
|
|
// and we would not check for 0 here, we'd skip all events by default
|
|
if options.SampleRate != 0.0 {
|
|
randomFloat := rand.New(rand.NewSource(time.Now().UnixNano())).Float64()
|
|
if randomFloat > options.SampleRate {
|
|
Logger.Println("Event dropped due to SampleRate hit.")
|
|
return nil
|
|
}
|
|
}
|
|
|
|
if event = client.prepareEvent(event, hint, scope); event == nil {
|
|
return nil
|
|
}
|
|
|
|
if options.BeforeSend != nil {
|
|
h := &EventHint{}
|
|
if hint != nil {
|
|
h = hint
|
|
}
|
|
if event = options.BeforeSend(event, h); event == nil {
|
|
Logger.Println("Event dropped due to BeforeSend callback.")
|
|
return nil
|
|
}
|
|
}
|
|
|
|
client.Transport.SendEvent(event)
|
|
|
|
return &event.EventID
|
|
}
|
|
|
|
func (client *Client) prepareEvent(event *Event, hint *EventHint, scope EventModifier) *Event {
|
|
if event.EventID == "" {
|
|
event.EventID = EventID(uuid())
|
|
}
|
|
|
|
if event.Timestamp.IsZero() {
|
|
event.Timestamp = time.Now().UTC()
|
|
}
|
|
|
|
if event.Level == "" {
|
|
event.Level = LevelInfo
|
|
}
|
|
|
|
if event.ServerName == "" {
|
|
if client.Options().ServerName != "" {
|
|
event.ServerName = client.Options().ServerName
|
|
} else if hostname, err := os.Hostname(); err == nil {
|
|
event.ServerName = hostname
|
|
}
|
|
}
|
|
|
|
if event.Release == "" && client.Options().Release != "" {
|
|
event.Release = client.Options().Release
|
|
}
|
|
|
|
if event.Dist == "" && client.Options().Dist != "" {
|
|
event.Dist = client.Options().Dist
|
|
}
|
|
|
|
if event.Environment == "" && client.Options().Environment != "" {
|
|
event.Environment = client.Options().Environment
|
|
}
|
|
|
|
event.Platform = "go"
|
|
event.Sdk = SdkInfo{
|
|
Name: "sentry.go",
|
|
Version: Version,
|
|
Integrations: client.listIntegrations(),
|
|
Packages: []SdkPackage{{
|
|
Name: "sentry-go",
|
|
Version: Version,
|
|
}},
|
|
}
|
|
|
|
if scope != nil {
|
|
event = scope.ApplyToEvent(event, hint)
|
|
if event == nil {
|
|
return nil
|
|
}
|
|
}
|
|
|
|
for _, processor := range client.eventProcessors {
|
|
id := event.EventID
|
|
event = processor(event, hint)
|
|
if event == nil {
|
|
Logger.Printf("Event dropped by one of the Client EventProcessors: %s\n", id)
|
|
return nil
|
|
}
|
|
}
|
|
|
|
for _, processor := range globalEventProcessors {
|
|
id := event.EventID
|
|
event = processor(event, hint)
|
|
if event == nil {
|
|
Logger.Printf("Event dropped by one of the Global EventProcessors: %s\n", id)
|
|
return nil
|
|
}
|
|
}
|
|
|
|
return event
|
|
}
|
|
|
|
func (client Client) listIntegrations() []string {
|
|
integrations := make([]string, 0, len(client.integrations))
|
|
for _, integration := range client.integrations {
|
|
integrations = append(integrations, integration.Name())
|
|
}
|
|
sort.Strings(integrations)
|
|
return integrations
|
|
}
|
|
|
|
func (client Client) integrationAlreadyInstalled(name string) bool {
|
|
for _, integration := range client.integrations {
|
|
if integration.Name() == name {
|
|
return true
|
|
}
|
|
}
|
|
return false
|
|
}
|