package sentry import ( "context" "crypto/x509" "fmt" "io" "log" "math/rand" "net/http" "os" "reflect" "sort" "strings" "sync" "time" "github.com/getsentry/sentry-go/internal/debug" ) // 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 // defaultMaxSpans limits the default number of recorded spans per transaction. The limit is // meant to bound memory usage and prevent too large transaction events that // would be rejected by Sentry. const defaultMaxSpans = 1000 // hostname is the host name reported by the kernel. It is precomputed once to // avoid syscalls when capturing events. // // The error is ignored because retrieving the host name is best-effort. If the // error is non-nil, there is nothing to do other than retrying. We choose not // to retry for now. var hostname, _ = os.Hostname() // lockedRand is a random number generator safe for concurrent use. Its API is // intentionally limited and it is not meant as a full replacement for a // rand.Rand. type lockedRand struct { mu sync.Mutex r *rand.Rand } // Float64 returns a pseudo-random number in [0.0,1.0). func (r *lockedRand) Float64() float64 { r.mu.Lock() defer r.mu.Unlock() return r.r.Float64() } // rng is the internal random number generator. // // We do not use the global functions from math/rand because, while they are // safe for concurrent use, any package in a build could change the seed and // affect the generated numbers, for instance making them deterministic. On the // other hand, the source returned from rand.NewSource is not safe for // concurrent use, so we need to couple its use with a sync.Mutex. var rng = &lockedRand{ // #nosec G404 -- We are fine using transparent, non-secure value here. r: rand.New(rand.NewSource(time.Now().UnixNano())), } // 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(io.Discard, "[Sentry] ", log.LstdFlags) // EventProcessor is a function that processes an event. // Event processors are used to change an event before it is sent to Sentry. type EventProcessor func(event *Event, hint *EventHint) *Event // EventModifier is the interface that wraps the ApplyToEvent method. // // ApplyToEvent changes an event based on external data and/or // an event hint. type EventModifier interface { ApplyToEvent(event *Event, hint *EventHint) *Event } var globalEventProcessors []EventProcessor // AddGlobalEventProcessor adds processor to the global list of event // processors. Global event processors apply to all events. // // AddGlobalEventProcessor is deprecated. Most users will prefer to initialize // the SDK with Init and provide a ClientOptions.BeforeSend function or use // Scope.AddEventProcessor instead. 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 in the range [0.0, 1.0]. By default, // all events are sent. Thus, as a historical special case, the sample rate // 0.0 is treated as if it was 1.0. To drop all events, set the DSN to the // empty string. SampleRate float64 // Enable performance tracing. EnableTracing bool // The sample rate for sampling traces in the range [0.0, 1.0]. TracesSampleRate float64 // Used to customize the sampling of traces, overrides TracesSampleRate. TracesSampler TracesSampler // 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 // If this flag is enabled, certain personally identifiable information (PII) is added by active integrations. // By default, no such data is sent. SendDefaultPII bool // BeforeSend is called before error events are sent to Sentry. // Use it to mutate the event or return nil to discard the event. // See EventProcessor if you need to mutate transactions. 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. Defaults to HTTPTransport. Transport Transport // The server name to be reported. ServerName string // The release to be sent with events. // // Some Sentry features are built around releases, and, thus, reporting // events with a non-empty release improves the product experience. See // https://docs.sentry.io/product/releases/. // // If Release is not set, the SDK will try to derive a default value // from environment variables or the Git repository in the working // directory. // // If you distribute a compiled binary, it is recommended to set the // Release value explicitly at build time. As an example, you can use: // // go build -ldflags='-X main.release=VALUE' // // That will set the value of a predeclared variable 'release' in the // 'main' package to 'VALUE'. Then, use that variable when initializing // the SDK: // // sentry.Init(ClientOptions{Release: release}) // // See https://golang.org/cmd/go/ and https://golang.org/cmd/link/ for // the official documentation of -ldflags and -X, respectively. Release string // The dist to be sent with events. Dist string // The environment to be sent with events. Environment string // Maximum number of breadcrumbs // when MaxBreadcrumbs is negative then ignore breadcrumbs. MaxBreadcrumbs int // Maximum number of spans. // // See https://develop.sentry.dev/sdk/envelopes/#size-limits for size limits // applied during event ingestion. Events that exceed these limits might get dropped. MaxSpans 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. HTTPProxy string // An optional HTTPS proxy to use. // This will default to the HTTPS_PROXY environment variable. // HTTPS_PROXY takes precedence over HTTP_PROXY for https requests. HTTPSProxy string // An optional set of SSL certificates to use. CaCerts *x509.CertPool // 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. MaxErrorDepth int } // Client is the underlying processor that is used by the main API and Hub // instances. It must be created with NewClient. type Client struct { options ClientOptions dsn *Dsn eventProcessors []EventProcessor integrations []Integration // Transport is read-only. Replacing the transport of an existing client is // not supported, create a new client instead. Transport Transport } // NewClient creates and returns an instance of Client configured using // ClientOptions. // // Most users will not create clients directly. Instead, initialize the SDK with // Init and use the package-level functions (for simple programs that run on a // single goroutine) or hub methods (for concurrent programs, for example web // servers). func NewClient(options ClientOptions) (*Client, error) { if options.Debug { debugWriter := options.DebugWriter if debugWriter == nil { debugWriter = os.Stderr } Logger.SetOutput(debugWriter) } if options.Dsn == "" { options.Dsn = os.Getenv("SENTRY_DSN") } if options.Release == "" { options.Release = defaultRelease() } if options.Environment == "" { options.Environment = os.Getenv("SENTRY_ENVIRONMENT") } if options.MaxErrorDepth == 0 { options.MaxErrorDepth = maxErrorDepth } if options.MaxSpans == 0 { options.MaxSpans = defaultMaxSpans } // SENTRYGODEBUG is a comma-separated list of key=value pairs (similar // to GODEBUG). It is not a supported feature: recognized debug options // may change any time. // // The intended public is SDK developers. It is orthogonal to // options.Debug, which is also available for SDK users. dbg := strings.Split(os.Getenv("SENTRYGODEBUG"), ",") sort.Strings(dbg) // dbgOpt returns true when the given debug option is enabled, for // example SENTRYGODEBUG=someopt=1. dbgOpt := func(opt string) bool { s := opt + "=1" return dbg[sort.SearchStrings(dbg, s)%len(dbg)] == s } if dbgOpt("httpdump") || dbgOpt("httptrace") { options.HTTPTransport = &debug.Transport{ RoundTripper: http.DefaultTransport, Output: os.Stderr, Dump: dbgOpt("httpdump"), Trace: dbgOpt("httptrace"), } } 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() { opts := client.options transport := opts.Transport if transport == nil { if opts.Dsn == "" { transport = new(noopTransport) } else { httpTransport := NewHTTPTransport() // When tracing is enabled, use larger buffer to // accommodate more concurrent events. // TODO(tracing): consider using separate buffers per // event type. if opts.EnableTracing { httpTransport.BufferSize = 1000 } transport = httpTransport } } transport.Configure(opts) 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()) } sort.Slice(client.integrations, func(i, j int) bool { return client.integrations[i].Name() < client.integrations[j].Name() }) } // AddEventProcessor adds an event processor to the client. It must not be // called from concurrent goroutines. Most users will prefer to use // ClientOptions.BeforeSend or Scope.AddEventProcessor instead. // // Note that typical programs have only a single client created by Init and the // client is shared among multiple hubs, one per goroutine, such that adding an // event processor to the client affects all hubs that share 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() } // Normally we would not pass a nil Context, but RecoverWithContext doesn't // use the Context for communicating deadline nor cancelation. All it does // is store the Context in the EventHint and there nil means the Context is // not available. // nolint: staticcheck return client.RecoverWithContext(nil, err, hint, scope) } // RecoverWithContext 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 { return nil } if ctx != nil { if hint == nil { hint = &EventHint{} } if hint.Context == nil { hint.Context = ctx } } var event *Event switch err := err.(type) { case error: event = client.eventFromException(err, LevelFatal) case string: event = client.eventFromMessage(err, LevelFatal) default: event = client.eventFromMessage(fmt.Sprintf("%#v", err), LevelFatal) } return client.CaptureEvent(event, hint, 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 (client *Client) Flush(timeout time.Duration) bool { return client.Transport.Flush(timeout) } func (client *Client) eventFromMessage(message string, level Level) *Event { if message == "" { err := usageError{fmt.Errorf("%s called with empty message", callerFunctionName())} return client.eventFromException(err, level) } 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 < client.options.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 { if event == nil { err := usageError{fmt.Errorf("%s called with nil event", callerFunctionName())} return client.CaptureException(err, hint, scope) } options := client.Options() // The default error event sample rate for all SDKs is 1.0 (send all). // // In Go, the zero value (default) for float64 is 0.0, which means that // constructing a client with NewClient(ClientOptions{}), or, equivalently, // initializing the SDK with Init(ClientOptions{}) without an explicit // SampleRate would drop all events. // // To retain the desired default behavior, we exceptionally flip SampleRate // from 0.0 to 1.0 here. Setting the sample rate to 0.0 is not very useful // anyway, and the same end result can be achieved in many other ways like // not initializing the SDK, setting the DSN to the empty string or using an // event processor that always returns nil. // // An alternative API could be such that default options don't need to be // the same as Go's zero values, for example using the Functional Options // pattern. That would either require a breaking change if we want to reuse // the obvious NewClient name, or a new function as an alternative // constructor. if options.SampleRate == 0.0 { options.SampleRate = 1.0 } // Transactions are sampled by options.TracesSampleRate or // options.TracesSampler when they are started. All other events // (errors, messages) are sampled here. if event.Type != transactionType && !sample(options.SampleRate) { Logger.Println("Event dropped due to SampleRate hit.") return nil } if event = client.prepareEvent(event, hint, scope); event == nil { return nil } // As per spec, transactions do not go through BeforeSend. if event.Type != transactionType && options.BeforeSend != nil { if hint == nil { hint = &EventHint{} } if event = options.BeforeSend(event, hint); 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() } if event.Level == "" { event.Level = LevelInfo } if event.ServerName == "" { event.ServerName = client.Options().ServerName if event.ServerName == "" { event.ServerName = hostname } } if event.Release == "" { event.Release = client.Options().Release } if event.Dist == "" { event.Dist = client.Options().Dist } if event.Environment == "" { event.Environment = client.Options().Environment } event.Platform = "go" event.Sdk = SdkInfo{ Name: SDKIdentifier, Version: SDKVersion, Integrations: client.listIntegrations(), Packages: []SdkPackage{{ Name: "sentry-go", Version: SDKVersion, }}, } 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, len(client.integrations)) for i, integration := range client.integrations { integrations[i] = integration.Name() } return integrations } func (client Client) integrationAlreadyInstalled(name string) bool { for _, integration := range client.integrations { if integration.Name() == name { return true } } return false } // sample returns true with the given probability, which must be in the range // [0.0, 1.0]. func sample(probability float64) bool { return rng.Float64() < probability }