1package sentry 2 3import ( 4 "context" 5 "crypto/x509" 6 "errors" 7 "fmt" 8 "io" 9 "io/ioutil" 10 "log" 11 "math/rand" 12 "net/http" 13 "os" 14 "reflect" 15 "sort" 16 "strings" 17 "sync" 18 "time" 19 20 "github.com/getsentry/sentry-go/internal/debug" 21) 22 23// maxErrorDepth is the maximum number of errors reported in a chain of errors. 24// This protects the SDK from an arbitrarily long chain of wrapped errors. 25// 26// An additional consideration is that arguably reporting a long chain of errors 27// is of little use when debugging production errors with Sentry. The Sentry UI 28// is not optimized for long chains either. The top-level error together with a 29// stack trace is often the most useful information. 30const maxErrorDepth = 10 31 32// hostname is the host name reported by the kernel. It is precomputed once to 33// avoid syscalls when capturing events. 34// 35// The error is ignored because retrieving the host name is best-effort. If the 36// error is non-nil, there is nothing to do other than retrying. We choose not 37// to retry for now. 38var hostname, _ = os.Hostname() 39 40// lockedRand is a random number generator safe for concurrent use. Its API is 41// intentionally limited and it is not meant as a full replacement for a 42// rand.Rand. 43type lockedRand struct { 44 mu sync.Mutex 45 r *rand.Rand 46} 47 48// Float64 returns a pseudo-random number in [0.0,1.0). 49func (r *lockedRand) Float64() float64 { 50 r.mu.Lock() 51 defer r.mu.Unlock() 52 return r.r.Float64() 53} 54 55// rng is the internal random number generator. 56// 57// We do not use the global functions from math/rand because, while they are 58// safe for concurrent use, any package in a build could change the seed and 59// affect the generated numbers, for instance making them deterministic. On the 60// other hand, the source returned from rand.NewSource is not safe for 61// concurrent use, so we need to couple its use with a sync.Mutex. 62var rng = &lockedRand{ 63 r: rand.New(rand.NewSource(time.Now().UnixNano())), 64} 65 66// usageError is used to report to Sentry an SDK usage error. 67// 68// It is not exported because it is never returned by any function or method in 69// the exported API. 70type usageError struct { 71 error 72} 73 74// Logger is an instance of log.Logger that is use to provide debug information about running Sentry Client 75// can be enabled by either using Logger.SetOutput directly or with Debug client option. 76var Logger = log.New(ioutil.Discard, "[Sentry] ", log.LstdFlags) 77 78// EventProcessor is a function that processes an event. 79// Event processors are used to change an event before it is sent to Sentry. 80type EventProcessor func(event *Event, hint *EventHint) *Event 81 82// EventModifier is the interface that wraps the ApplyToEvent method. 83// 84// ApplyToEvent changes an event based on external data and/or 85// an event hint. 86type EventModifier interface { 87 ApplyToEvent(event *Event, hint *EventHint) *Event 88} 89 90var globalEventProcessors []EventProcessor 91 92// AddGlobalEventProcessor adds processor to the global list of event 93// processors. Global event processors apply to all events. 94// 95// AddGlobalEventProcessor is deprecated. Most users will prefer to initialize 96// the SDK with Init and provide a ClientOptions.BeforeSend function or use 97// Scope.AddEventProcessor instead. 98func AddGlobalEventProcessor(processor EventProcessor) { 99 globalEventProcessors = append(globalEventProcessors, processor) 100} 101 102// Integration allows for registering a functions that modify or discard captured events. 103type Integration interface { 104 Name() string 105 SetupOnce(client *Client) 106} 107 108// ClientOptions that configures a SDK Client. 109type ClientOptions struct { 110 // The DSN to use. If the DSN is not set, the client is effectively 111 // disabled. 112 Dsn string 113 // In debug mode, the debug information is printed to stdout to help you 114 // understand what sentry is doing. 115 Debug bool 116 // Configures whether SDK should generate and attach stacktraces to pure 117 // capture message calls. 118 AttachStacktrace bool 119 // The sample rate for event submission in the range [0.0, 1.0]. By default, 120 // all events are sent. Thus, as a historical special case, the sample rate 121 // 0.0 is treated as if it was 1.0. To drop all events, set the DSN to the 122 // empty string. 123 SampleRate float64 124 // The sample rate for sampling traces in the range [0.0, 1.0]. 125 TracesSampleRate float64 126 // Used to customize the sampling of traces, overrides TracesSampleRate. 127 TracesSampler TracesSampler 128 // List of regexp strings that will be used to match against event's message 129 // and if applicable, caught errors type and value. 130 // If the match is found, then a whole event will be dropped. 131 IgnoreErrors []string 132 // Before send callback. 133 BeforeSend func(event *Event, hint *EventHint) *Event 134 // Before breadcrumb add callback. 135 BeforeBreadcrumb func(breadcrumb *Breadcrumb, hint *BreadcrumbHint) *Breadcrumb 136 // Integrations to be installed on the current Client, receives default 137 // integrations. 138 Integrations func([]Integration) []Integration 139 // io.Writer implementation that should be used with the Debug mode. 140 DebugWriter io.Writer 141 // The transport to use. Defaults to HTTPTransport. 142 Transport Transport 143 // The server name to be reported. 144 ServerName string 145 // The release to be sent with events. 146 Release string 147 // The dist to be sent with events. 148 Dist string 149 // The environment to be sent with events. 150 Environment string 151 // Maximum number of breadcrumbs. 152 MaxBreadcrumbs int 153 // An optional pointer to http.Client that will be used with a default 154 // HTTPTransport. Using your own client will make HTTPTransport, HTTPProxy, 155 // HTTPSProxy and CaCerts options ignored. 156 HTTPClient *http.Client 157 // An optional pointer to http.Transport that will be used with a default 158 // HTTPTransport. Using your own transport will make HTTPProxy, HTTPSProxy 159 // and CaCerts options ignored. 160 HTTPTransport http.RoundTripper 161 // An optional HTTP proxy to use. 162 // This will default to the HTTP_PROXY environment variable. 163 HTTPProxy string 164 // An optional HTTPS proxy to use. 165 // This will default to the HTTPS_PROXY environment variable. 166 // HTTPS_PROXY takes precedence over HTTP_PROXY for https requests. 167 HTTPSProxy string 168 // An optional set of SSL certificates to use. 169 CaCerts *x509.CertPool 170} 171 172// Client is the underlying processor that is used by the main API and Hub 173// instances. It must be created with NewClient. 174type Client struct { 175 options ClientOptions 176 dsn *Dsn 177 eventProcessors []EventProcessor 178 integrations []Integration 179 // Transport is read-only. Replacing the transport of an existing client is 180 // not supported, create a new client instead. 181 Transport Transport 182} 183 184// NewClient creates and returns an instance of Client configured using 185// ClientOptions. 186// 187// Most users will not create clients directly. Instead, initialize the SDK with 188// Init and use the package-level functions (for simple programs that run on a 189// single goroutine) or hub methods (for concurrent programs, for example web 190// servers). 191func NewClient(options ClientOptions) (*Client, error) { 192 if options.TracesSampleRate != 0.0 && options.TracesSampler != nil { 193 return nil, errors.New("TracesSampleRate and TracesSampler are mutually exclusive") 194 } 195 196 if options.Debug { 197 debugWriter := options.DebugWriter 198 if debugWriter == nil { 199 debugWriter = os.Stderr 200 } 201 Logger.SetOutput(debugWriter) 202 } 203 204 if options.Dsn == "" { 205 options.Dsn = os.Getenv("SENTRY_DSN") 206 } 207 208 if options.Release == "" { 209 options.Release = os.Getenv("SENTRY_RELEASE") 210 } 211 212 if options.Environment == "" { 213 options.Environment = os.Getenv("SENTRY_ENVIRONMENT") 214 } 215 216 // SENTRYGODEBUG is a comma-separated list of key=value pairs (similar 217 // to GODEBUG). It is not a supported feature: recognized debug options 218 // may change any time. 219 // 220 // The intended public is SDK developers. It is orthogonal to 221 // options.Debug, which is also available for SDK users. 222 dbg := strings.Split(os.Getenv("SENTRYGODEBUG"), ",") 223 sort.Strings(dbg) 224 // dbgOpt returns true when the given debug option is enabled, for 225 // example SENTRYGODEBUG=someopt=1. 226 dbgOpt := func(opt string) bool { 227 s := opt + "=1" 228 return dbg[sort.SearchStrings(dbg, s)%len(dbg)] == s 229 } 230 if dbgOpt("httpdump") || dbgOpt("httptrace") { 231 options.HTTPTransport = &debug.Transport{ 232 RoundTripper: http.DefaultTransport, 233 Output: os.Stderr, 234 Dump: dbgOpt("httpdump"), 235 Trace: dbgOpt("httptrace"), 236 } 237 } 238 239 var dsn *Dsn 240 if options.Dsn != "" { 241 var err error 242 dsn, err = NewDsn(options.Dsn) 243 if err != nil { 244 return nil, err 245 } 246 } 247 248 client := Client{ 249 options: options, 250 dsn: dsn, 251 } 252 253 client.setupTransport() 254 client.setupIntegrations() 255 256 return &client, nil 257} 258 259func (client *Client) setupTransport() { 260 opts := client.options 261 transport := opts.Transport 262 263 if transport == nil { 264 if opts.Dsn == "" { 265 transport = new(noopTransport) 266 } else { 267 httpTransport := NewHTTPTransport() 268 // When tracing is enabled, use larger buffer to 269 // accommodate more concurrent events. 270 // TODO(tracing): consider using separate buffers per 271 // event type. 272 if opts.TracesSampleRate != 0 || opts.TracesSampler != nil { 273 httpTransport.BufferSize = 1000 274 } 275 transport = httpTransport 276 } 277 } 278 279 transport.Configure(opts) 280 client.Transport = transport 281} 282 283func (client *Client) setupIntegrations() { 284 integrations := []Integration{ 285 new(contextifyFramesIntegration), 286 new(environmentIntegration), 287 new(modulesIntegration), 288 new(ignoreErrorsIntegration), 289 } 290 291 if client.options.Integrations != nil { 292 integrations = client.options.Integrations(integrations) 293 } 294 295 for _, integration := range integrations { 296 if client.integrationAlreadyInstalled(integration.Name()) { 297 Logger.Printf("Integration %s is already installed\n", integration.Name()) 298 continue 299 } 300 client.integrations = append(client.integrations, integration) 301 integration.SetupOnce(client) 302 Logger.Printf("Integration installed: %s\n", integration.Name()) 303 } 304} 305 306// AddEventProcessor adds an event processor to the client. It must not be 307// called from concurrent goroutines. Most users will prefer to use 308// ClientOptions.BeforeSend or Scope.AddEventProcessor instead. 309// 310// Note that typical programs have only a single client created by Init and the 311// client is shared among multiple hubs, one per goroutine, such that adding an 312// event processor to the client affects all hubs that share the client. 313func (client *Client) AddEventProcessor(processor EventProcessor) { 314 client.eventProcessors = append(client.eventProcessors, processor) 315} 316 317// Options return ClientOptions for the current Client. 318func (client Client) Options() ClientOptions { 319 return client.options 320} 321 322// CaptureMessage captures an arbitrary message. 323func (client *Client) CaptureMessage(message string, hint *EventHint, scope EventModifier) *EventID { 324 event := client.eventFromMessage(message, LevelInfo) 325 return client.CaptureEvent(event, hint, scope) 326} 327 328// CaptureException captures an error. 329func (client *Client) CaptureException(exception error, hint *EventHint, scope EventModifier) *EventID { 330 event := client.eventFromException(exception, LevelError) 331 return client.CaptureEvent(event, hint, scope) 332} 333 334// CaptureEvent captures an event on the currently active client if any. 335// 336// The event must already be assembled. Typically code would instead use 337// the utility methods like CaptureException. The return value is the 338// event ID. In case Sentry is disabled or event was dropped, the return value will be nil. 339func (client *Client) CaptureEvent(event *Event, hint *EventHint, scope EventModifier) *EventID { 340 return client.processEvent(event, hint, scope) 341} 342 343// Recover captures a panic. 344// Returns EventID if successfully, or nil if there's no error to recover from. 345func (client *Client) Recover(err interface{}, hint *EventHint, scope EventModifier) *EventID { 346 if err == nil { 347 err = recover() 348 } 349 350 // Normally we would not pass a nil Context, but RecoverWithContext doesn't 351 // use the Context for communicating deadline nor cancelation. All it does 352 // is store the Context in the EventHint and there nil means the Context is 353 // not available. 354 //nolint: staticcheck 355 return client.RecoverWithContext(nil, err, hint, scope) 356} 357 358// RecoverWithContext captures a panic and passes relevant context object. 359// Returns EventID if successfully, or nil if there's no error to recover from. 360func (client *Client) RecoverWithContext( 361 ctx context.Context, 362 err interface{}, 363 hint *EventHint, 364 scope EventModifier, 365) *EventID { 366 if err == nil { 367 err = recover() 368 } 369 if err == nil { 370 return nil 371 } 372 373 if ctx != nil { 374 if hint == nil { 375 hint = &EventHint{} 376 } 377 if hint.Context == nil { 378 hint.Context = ctx 379 } 380 } 381 382 var event *Event 383 switch err := err.(type) { 384 case error: 385 event = client.eventFromException(err, LevelFatal) 386 case string: 387 event = client.eventFromMessage(err, LevelFatal) 388 default: 389 event = client.eventFromMessage(fmt.Sprintf("%#v", err), LevelFatal) 390 } 391 return client.CaptureEvent(event, hint, scope) 392} 393 394// Flush waits until the underlying Transport sends any buffered events to the 395// Sentry server, blocking for at most the given timeout. It returns false if 396// the timeout was reached. In that case, some events may not have been sent. 397// 398// Flush should be called before terminating the program to avoid 399// unintentionally dropping events. 400// 401// Do not call Flush indiscriminately after every call to CaptureEvent, 402// CaptureException or CaptureMessage. Instead, to have the SDK send events over 403// the network synchronously, configure it to use the HTTPSyncTransport in the 404// call to Init. 405func (client *Client) Flush(timeout time.Duration) bool { 406 return client.Transport.Flush(timeout) 407} 408 409func (client *Client) eventFromMessage(message string, level Level) *Event { 410 if message == "" { 411 err := usageError{fmt.Errorf("%s called with empty message", callerFunctionName())} 412 return client.eventFromException(err, level) 413 } 414 event := NewEvent() 415 event.Level = level 416 event.Message = message 417 418 if client.Options().AttachStacktrace { 419 event.Threads = []Thread{{ 420 Stacktrace: NewStacktrace(), 421 Crashed: false, 422 Current: true, 423 }} 424 } 425 426 return event 427} 428 429func (client *Client) eventFromException(exception error, level Level) *Event { 430 err := exception 431 if err == nil { 432 err = usageError{fmt.Errorf("%s called with nil error", callerFunctionName())} 433 } 434 435 event := NewEvent() 436 event.Level = level 437 438 for i := 0; i < maxErrorDepth && err != nil; i++ { 439 event.Exception = append(event.Exception, Exception{ 440 Value: err.Error(), 441 Type: reflect.TypeOf(err).String(), 442 Stacktrace: ExtractStacktrace(err), 443 }) 444 switch previous := err.(type) { 445 case interface{ Unwrap() error }: 446 err = previous.Unwrap() 447 case interface{ Cause() error }: 448 err = previous.Cause() 449 default: 450 err = nil 451 } 452 } 453 454 // Add a trace of the current stack to the most recent error in a chain if 455 // it doesn't have a stack trace yet. 456 // We only add to the most recent error to avoid duplication and because the 457 // current stack is most likely unrelated to errors deeper in the chain. 458 if event.Exception[0].Stacktrace == nil { 459 event.Exception[0].Stacktrace = NewStacktrace() 460 } 461 462 // event.Exception should be sorted such that the most recent error is last. 463 reverse(event.Exception) 464 465 return event 466} 467 468// reverse reverses the slice a in place. 469func reverse(a []Exception) { 470 for i := len(a)/2 - 1; i >= 0; i-- { 471 opp := len(a) - 1 - i 472 a[i], a[opp] = a[opp], a[i] 473 } 474} 475 476func (client *Client) processEvent(event *Event, hint *EventHint, scope EventModifier) *EventID { 477 if event == nil { 478 err := usageError{fmt.Errorf("%s called with nil event", callerFunctionName())} 479 return client.CaptureException(err, hint, scope) 480 } 481 482 options := client.Options() 483 484 // The default error event sample rate for all SDKs is 1.0 (send all). 485 // 486 // In Go, the zero value (default) for float64 is 0.0, which means that 487 // constructing a client with NewClient(ClientOptions{}), or, equivalently, 488 // initializing the SDK with Init(ClientOptions{}) without an explicit 489 // SampleRate would drop all events. 490 // 491 // To retain the desired default behavior, we exceptionally flip SampleRate 492 // from 0.0 to 1.0 here. Setting the sample rate to 0.0 is not very useful 493 // anyway, and the same end result can be achieved in many other ways like 494 // not initializing the SDK, setting the DSN to the empty string or using an 495 // event processor that always returns nil. 496 // 497 // An alternative API could be such that default options don't need to be 498 // the same as Go's zero values, for example using the Functional Options 499 // pattern. That would either require a breaking change if we want to reuse 500 // the obvious NewClient name, or a new function as an alternative 501 // constructor. 502 if options.SampleRate == 0.0 { 503 options.SampleRate = 1.0 504 } 505 506 // Transactions are sampled by options.TracesSampleRate or 507 // options.TracesSampler when they are started. All other events 508 // (errors, messages) are sampled here. 509 if event.Type != transactionType && !sample(options.SampleRate) { 510 Logger.Println("Event dropped due to SampleRate hit.") 511 return nil 512 } 513 514 if event = client.prepareEvent(event, hint, scope); event == nil { 515 return nil 516 } 517 518 // As per spec, transactions do not go through BeforeSend. 519 if event.Type != transactionType && options.BeforeSend != nil { 520 if hint == nil { 521 hint = &EventHint{} 522 } 523 if event = options.BeforeSend(event, hint); event == nil { 524 Logger.Println("Event dropped due to BeforeSend callback.") 525 return nil 526 } 527 } 528 529 client.Transport.SendEvent(event) 530 531 return &event.EventID 532} 533 534func (client *Client) prepareEvent(event *Event, hint *EventHint, scope EventModifier) *Event { 535 if event.EventID == "" { 536 event.EventID = EventID(uuid()) 537 } 538 539 if event.Timestamp.IsZero() { 540 event.Timestamp = time.Now() 541 } 542 543 if event.Level == "" { 544 event.Level = LevelInfo 545 } 546 547 if event.ServerName == "" { 548 if client.Options().ServerName != "" { 549 event.ServerName = client.Options().ServerName 550 } else { 551 event.ServerName = hostname 552 } 553 } 554 555 if event.Release == "" && client.Options().Release != "" { 556 event.Release = client.Options().Release 557 } 558 559 if event.Dist == "" && client.Options().Dist != "" { 560 event.Dist = client.Options().Dist 561 } 562 563 if event.Environment == "" && client.Options().Environment != "" { 564 event.Environment = client.Options().Environment 565 } 566 567 event.Platform = "go" 568 event.Sdk = SdkInfo{ 569 Name: "sentry.go", 570 Version: Version, 571 Integrations: client.listIntegrations(), 572 Packages: []SdkPackage{{ 573 Name: "sentry-go", 574 Version: Version, 575 }}, 576 } 577 578 if scope != nil { 579 event = scope.ApplyToEvent(event, hint) 580 if event == nil { 581 return nil 582 } 583 } 584 585 for _, processor := range client.eventProcessors { 586 id := event.EventID 587 event = processor(event, hint) 588 if event == nil { 589 Logger.Printf("Event dropped by one of the Client EventProcessors: %s\n", id) 590 return nil 591 } 592 } 593 594 for _, processor := range globalEventProcessors { 595 id := event.EventID 596 event = processor(event, hint) 597 if event == nil { 598 Logger.Printf("Event dropped by one of the Global EventProcessors: %s\n", id) 599 return nil 600 } 601 } 602 603 return event 604} 605 606func (client Client) listIntegrations() []string { 607 integrations := make([]string, 0, len(client.integrations)) 608 for _, integration := range client.integrations { 609 integrations = append(integrations, integration.Name()) 610 } 611 sort.Strings(integrations) 612 return integrations 613} 614 615func (client Client) integrationAlreadyInstalled(name string) bool { 616 for _, integration := range client.integrations { 617 if integration.Name() == name { 618 return true 619 } 620 } 621 return false 622} 623 624// sample returns true with the given probability, which must be in the range 625// [0.0, 1.0]. 626func sample(probability float64) bool { 627 return rng.Float64() < probability 628} 629