From f01cbeec10975d910a917c40d53294bb505e5206 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kamil=20Og=C3=B3rek?= Date: Wed, 19 Jun 2019 12:29:54 +0200 Subject: [PATCH] feat: HTTPSyncTransport and transport factories --- CHANGELOG.md | 4 + MIGRATION.md | 50 +-------- client.go | 4 +- example/basic/main.go | 2 +- example/multiclient/main.go | 2 +- example/synctransport/main.go | 40 +++++++ transport.go | 202 ++++++++++++++++++++++++++-------- 7 files changed, 203 insertions(+), 101 deletions(-) create mode 100644 example/synctransport/main.go diff --git a/CHANGELOG.md b/CHANGELOG.md index 1d19900db..38a210891 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,7 +2,11 @@ ## 0.0.1-beta.5 +- feat: **[breaking]** Add `NewHTTPTransport` and `NewHTTPSyncTransport` which accepts all transport options +- feat: New `HTTPSyncTransport` that blocks after each call - feat: New `Echo` integration +- ref: **[breaking]** Remove `BufferSize` option from `ClientOptions` and move it to `HTTPTransport` instead +- ref: Export default `HTTPTransport` - ref: Export `net/http` integration handler - ref: Set `Request` instantly in the package handlers, not in `recoverWithSentry` so it can be accessed later on - ci: Add craft config diff --git a/MIGRATION.md b/MIGRATION.md index a347d80e7..dac6894c8 100644 --- a/MIGRATION.md +++ b/MIGRATION.md @@ -73,55 +73,7 @@ sentry.Init(sentry.ClientOptions{ }) ``` -Available options: - -```go -// ClientOptions that configures a SDK Client -type ClientOptions struct { - // The DSN to use. If not set the client is effectively disabled. - Dsn string - // In debug mode debug information is printed to stdput to help you understand what - // sentry is doing. - Debug bool - // The sample rate for event submission (0.0 - 1.0, defaults to 1.0) - SampleRate float32 - // 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 - 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.Transport` that will be used with a default HTTPTransport. - HTTPTransport *http.Transport - // 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 optionsl CaCerts to use. - // Defaults to `gocertifi.CACerts()`. - CaCerts *x509.CertPool -} -``` +Available options: see [Configuration](https://docs.sentry.io/platforms/go/config/) section. ### Providing SSL Certificates diff --git a/client.go b/client.go index defb0d06a..088ce32db 100644 --- a/client.go +++ b/client.go @@ -74,8 +74,6 @@ type ClientOptions struct { Environment string // Maximum number of breadcrumbs. MaxBreadcrumbs int - // An optional size of the transport buffer. Defaults to 30. - BufferSize int // An optional pointer to `http.Transport` that will be used with a default HTTPTransport. HTTPTransport *http.Transport // An optional HTTP proxy to use. @@ -147,7 +145,7 @@ func (client *Client) setupTransport() { transport := client.options.Transport if transport == nil { - transport = new(httpTransport) + transport = NewHTTPTransport() } transport.Configure(client.options) diff --git a/example/basic/main.go b/example/basic/main.go index cb5735b13..8ea29deca 100644 --- a/example/basic/main.go +++ b/example/basic/main.go @@ -137,7 +137,7 @@ func main() { return breadcrumb }, SampleRate: 1, - Transport: new(devNullTransport), + Transport: &devNullTransport{}, Integrations: func(integrations []sentry.Integration) []sentry.Integration { return append(integrations, integrations[1]) }, diff --git a/example/multiclient/main.go b/example/multiclient/main.go index 65284b41c..77606490e 100644 --- a/example/multiclient/main.go +++ b/example/multiclient/main.go @@ -31,7 +31,7 @@ func main() { return nil }, Integrations: func(integrations []sentry.Integration) []sentry.Integration { - return append(integrations, new(pickleIntegration)) + return append(integrations, &pickleIntegration{}) }, }) hub1 := sentry.NewHub(client1, scope1) diff --git a/example/synctransport/main.go b/example/synctransport/main.go new file mode 100644 index 000000000..27ab1b7a5 --- /dev/null +++ b/example/synctransport/main.go @@ -0,0 +1,40 @@ +package main + +import ( + "log" + "time" + + "github.com/getsentry/sentry-go" +) + +func main() { + sentrySyncTransport := sentry.NewHTTPSyncTransport() + sentrySyncTransport.Timeout = time.Second * 3 + + _ = sentry.Init(sentry.ClientOptions{ + Dsn: "https://hello@world.io/1337", + Debug: true, + Transport: sentrySyncTransport, + }) + + go func() { + sentry.CaptureMessage("Event #1") + log.Println(1) + sentry.CaptureMessage("Event #2") + log.Println(2) + }() + + sentry.CaptureMessage("Event #3") + log.Println(3) + sentry.CaptureMessage("Event #4") + log.Println(4) + sentry.CaptureMessage("Event #5") + log.Println(5) + + go func() { + sentry.CaptureMessage("Event #6") + log.Println(6) + sentry.CaptureMessage("Event #7") + log.Println(7) + }() +} diff --git a/transport.go b/transport.go index ce0a688d2..da7806918 100644 --- a/transport.go +++ b/transport.go @@ -13,6 +13,7 @@ import ( const defaultBufferSize = 30 const defaultRetryAfter = time.Second * 60 +const defaultTimeout = time.Second * 30 // Transport is used by the `Client` to deliver events to remote server. type Transport interface { @@ -21,8 +22,54 @@ type Transport interface { SendEvent(event *Event) } -// httpTransport is a default implementation of `Transport` interface used by `Client`. -type httpTransport struct { +func getProxyConfig(options ClientOptions) func(*http.Request) (*url.URL, error) { + if options.HTTPSProxy != "" { + return func(_ *http.Request) (*url.URL, error) { + return url.Parse(options.HTTPSProxy) + } + } else if options.HTTPProxy != "" { + return func(_ *http.Request) (*url.URL, error) { + return url.Parse(options.HTTPProxy) + } + } + + return http.ProxyFromEnvironment +} + +func getTLSConfig(options ClientOptions) *tls.Config { + if options.CaCerts != nil { + return &tls.Config{ + RootCAs: options.CaCerts, + } + } + + return nil +} + +func retryAfter(now time.Time, r *http.Response) time.Duration { + retryAfterHeader := r.Header["Retry-After"] + + if retryAfterHeader == nil { + return defaultRetryAfter + } + + if date, err := time.Parse(time.RFC1123, retryAfterHeader[0]); err == nil { + return date.Sub(now) + } + + if seconds, err := strconv.Atoi(retryAfterHeader[0]); err == nil { + return time.Second * time.Duration(seconds) + } + + return defaultRetryAfter +} + +// ================================ +// HTTPTransport +// ================================ + +// HTTPTransport is a default implementation of `Transport` interface used by `Client`. +type HTTPTransport struct { dsn *Dsn client *http.Client transport *http.Transport @@ -32,34 +79,45 @@ type httpTransport struct { wg sync.WaitGroup start sync.Once + + // Size of the transport buffer. Defaults to 30. + BufferSize int + // HTTP Client request timeout. Defaults to 30 seconds. + Timeout time.Duration +} + +// NewHTTPTransport returns a new pre-configured instance of HTTPTransport +func NewHTTPTransport() *HTTPTransport { + transport := HTTPTransport{ + BufferSize: defaultBufferSize, + Timeout: defaultTimeout, + } + return &transport } // Configure is called by the `Client` itself, providing it it's own `ClientOptions`. -func (t *httpTransport) Configure(options ClientOptions) { +func (t *HTTPTransport) Configure(options ClientOptions) { dsn, err := NewDsn(options.Dsn) if err != nil { Logger.Printf("%v\n", err) return } - t.dsn = dsn - bufferSize := defaultBufferSize - if options.BufferSize != 0 { - bufferSize = options.BufferSize - } - t.buffer = make(chan *http.Request, bufferSize) + t.dsn = dsn + t.buffer = make(chan *http.Request, t.BufferSize) if options.HTTPTransport != nil { t.transport = options.HTTPTransport } else { t.transport = &http.Transport{ - Proxy: t.getProxyConfig(options), - TLSClientConfig: t.getTLSConfig(options), + Proxy: getProxyConfig(options), + TLSClientConfig: getTLSConfig(options), } } t.client = &http.Client{ Transport: t.transport, + Timeout: t.Timeout, } t.start.Do(func() { @@ -68,7 +126,7 @@ func (t *httpTransport) Configure(options ClientOptions) { } // SendEvent assembles a new packet out of `Event` and sends it to remote server. -func (t *httpTransport) SendEvent(event *Event) { +func (t *HTTPTransport) SendEvent(event *Event) { if t.dsn == nil || time.Now().Before(t.disabledUntil) { return } @@ -103,7 +161,7 @@ func (t *httpTransport) SendEvent(event *Event) { // Flush notifies when all the buffered events have been sent by returning `true` // or `false` if timeout was reached. -func (t *httpTransport) Flush(timeout time.Duration) bool { +func (t *HTTPTransport) Flush(timeout time.Duration) bool { c := make(chan struct{}) go func() { @@ -121,31 +179,7 @@ func (t *httpTransport) Flush(timeout time.Duration) bool { } } -func (t *httpTransport) getProxyConfig(options ClientOptions) func(*http.Request) (*url.URL, error) { - if options.HTTPSProxy != "" { - return func(_ *http.Request) (*url.URL, error) { - return url.Parse(options.HTTPSProxy) - } - } else if options.HTTPProxy != "" { - return func(_ *http.Request) (*url.URL, error) { - return url.Parse(options.HTTPProxy) - } - } - - return http.ProxyFromEnvironment -} - -func (t *httpTransport) getTLSConfig(options ClientOptions) *tls.Config { - if options.CaCerts != nil { - return &tls.Config{ - RootCAs: options.CaCerts, - } - } - - return nil -} - -func (t *httpTransport) worker() { +func (t *HTTPTransport) worker() { for request := range t.buffer { if time.Now().Before(t.disabledUntil) { t.wg.Done() @@ -167,20 +201,94 @@ func (t *httpTransport) worker() { } } -func retryAfter(now time.Time, r *http.Response) time.Duration { - retryAfterHeader := r.Header["Retry-After"] +// ================================ +// HTTPSyncTransport +// ================================ - if retryAfterHeader == nil { - return defaultRetryAfter +// HTTPSyncTransport is an implementation of `Transport` interface which blocks after each captured event. +type HTTPSyncTransport struct { + dsn *Dsn + client *http.Client + transport *http.Transport + disabledUntil time.Time + + // HTTP Client request timeout. Defaults to 30 seconds. + Timeout time.Duration +} + +// NewHTTPSyncTransport returns a new pre-configured instance of HTTPSyncTransport +func NewHTTPSyncTransport() *HTTPSyncTransport { + transport := HTTPSyncTransport{ + Timeout: defaultTimeout, } - if date, err := time.Parse(time.RFC1123, retryAfterHeader[0]); err == nil { - return date.Sub(now) + return &transport +} + +// Configure is called by the `Client` itself, providing it it's own `ClientOptions`. +func (t *HTTPSyncTransport) Configure(options ClientOptions) { + dsn, err := NewDsn(options.Dsn) + if err != nil { + Logger.Printf("%v\n", err) + return } + t.dsn = dsn - if seconds, err := strconv.Atoi(retryAfterHeader[0]); err == nil { - return time.Second * time.Duration(seconds) + if options.HTTPTransport != nil { + t.transport = options.HTTPTransport + } else { + t.transport = &http.Transport{ + Proxy: getProxyConfig(options), + TLSClientConfig: getTLSConfig(options), + } } - return defaultRetryAfter + t.client = &http.Client{ + Transport: t.transport, + Timeout: t.Timeout, + } +} + +// SendEvent assembles a new packet out of `Event` and sends it to remote server. +func (t *HTTPSyncTransport) SendEvent(event *Event) { + if t.dsn == nil || time.Now().Before(t.disabledUntil) { + return + } + + body, _ := json.Marshal(event) + + request, _ := http.NewRequest( + http.MethodPost, + t.dsn.StoreAPIURL().String(), + bytes.NewBuffer(body), + ) + + for headerKey, headerValue := range t.dsn.RequestHeaders() { + request.Header.Set(headerKey, headerValue) + } + + Logger.Printf( + "Sending %s event [%s] to %s project: %d\n", + event.Level, + event.EventID, + t.dsn.host, + t.dsn.projectID, + ) + + response, err := t.client.Do(request) + + if err != nil { + Logger.Printf("There was an issue with sending an event: %v", err) + } + + if response != nil && response.StatusCode == http.StatusTooManyRequests { + t.disabledUntil = time.Now().Add(retryAfter(time.Now(), response)) + Logger.Printf("Too many requests, backing off till: %s\n", t.disabledUntil) + } +} + +// Flush notifies when all the buffered events have been sent by returning `true` +// or `false` if timeout was reached. No-op for HTTPSyncTransport. +func (t *HTTPSyncTransport) Flush(_ time.Duration) bool { + return true }