From 34f6c0a08f5ed64846228e30caa911319b92083a Mon Sep 17 00:00:00 2001 From: Marijn Suijten Date: Fri, 28 Jan 2022 12:06:32 +0100 Subject: [PATCH] feat: Add `ureq` transport support (#419) Ureq is easy to use, smaller than the other http crates and doesn't pull in as many crate dependencies either, which is why [we] are actively switching to it and in need of an `ureq` transport in Sentry too. [we]: https://github.com/Traverse-Research --- sentry/Cargo.toml | 3 + sentry/README.md | 44 ++++++++------- sentry/src/lib.rs | 44 ++++++++------- sentry/src/transports/mod.rs | 64 ++++++++++++++++++--- sentry/src/transports/ureq.rs | 102 ++++++++++++++++++++++++++++++++++ 5 files changed, 209 insertions(+), 48 deletions(-) create mode 100644 sentry/src/transports/ureq.rs diff --git a/sentry/Cargo.toml b/sentry/Cargo.toml index fe2fd7b8..76f89ee1 100644 --- a/sentry/Cargo.toml +++ b/sentry/Cargo.toml @@ -44,6 +44,8 @@ surf-h1 = ["surf_/h1-client", "httpdate"] surf = ["surf_/curl-client", "httpdate", "tokio"] native-tls = ["reqwest_/default-tls"] rustls = ["reqwest_/rustls-tls"] +ureq = ["ureq_/tls", "httpdate", "tokio"] +ureq-native-tls = ["ureq_/native-tls", "httpdate", "tokio"] [dependencies] sentry-core = { version = "0.24.2", path = "../sentry-core", features = ["client"] } @@ -63,6 +65,7 @@ httpdate = { version = "1.0.0", optional = true } surf_ = { package = "surf", version = "2.0.0", optional = true, default-features = false } serde_json = { version = "1.0.48", optional = true } tokio = { version = "1.0", features = ["rt"], optional = true } +ureq_ = { package = "ureq", version = "2.3.0", optional = true, default-features = false } [dev-dependencies] sentry-anyhow = { path = "../sentry-anyhow" } diff --git a/sentry/README.md b/sentry/README.md index fda637ee..589a250d 100644 --- a/sentry/README.md +++ b/sentry/README.md @@ -64,24 +64,26 @@ one should use the [`sentry-core`] crate instead. Additional functionality and integrations are enabled via feature flags. Some features require extra setup to function properly. -| Feature | Default | Is Integration | Deprecated | Additional notes | -| -------------- | ------- | -------------- | ---------- | ---------------------------------------------------------------------------------------- | -| `backtrace` | ✅ | 🔌 | | | -| `contexts` | ✅ | 🔌 | | | -| `panic` | ✅ | 🔌 | | | -| `transport` | ✅ | | | | -| `anyhow` | | 🔌 | | | -| `test` | | | | | -| `debug-images` | | 🔌 | | | -| `log` | | 🔌 | | Requires extra setup; See [`sentry-log`]'s documentation. | -| `debug-logs` | | | ❗ | Requires extra setup; See [`sentry-log`]'s documentation. | -| `slog` | | 🔌 | | Requires extra setup; See [`sentry-slog`]'s documentation. | -| `reqwest` | ✅ | | | | -| `native-tls` | ✅ | | | `reqwest` must be enabled. | -| `rustls` | | | | `reqwest` must be enabled. `native-tls` must be disabled via `default-features = false`. | -| `curl` | | | | | -| `surf` | | | | | -| `tower` | | 🔌 | | Requires extra setup; See [`sentry-tower`]'s documentation. | +| Feature | Default | Is Integration | Deprecated | Additional notes | +| -------------- | ------- | -------------- | ---------- | ---------------------------------------------------------------------------------------- | +| `backtrace` | ✅ | 🔌 | | | +| `contexts` | ✅ | 🔌 | | | +| `panic` | ✅ | 🔌 | | | +| `transport` | ✅ | | | | +| `anyhow` | | 🔌 | | | +| `test` | | | | | +| `debug-images` | | 🔌 | | | +| `log` | | 🔌 | | Requires extra setup; See [`sentry-log`]'s documentation. | +| `debug-logs` | | | ❗ | Requires extra setup; See [`sentry-log`]'s documentation. | +| `slog` | | 🔌 | | Requires extra setup; See [`sentry-slog`]'s documentation. | +| `reqwest` | ✅ | | | | +| `native-tls` | ✅ | | | `reqwest` must be enabled. | +| `rustls` | | | | `reqwest` must be enabled. `native-tls` must be disabled via `default-features = false`. | +| `curl` | | | | | +| `surf` | | | | | +| `tower` | | 🔌 | | Requires extra setup; See [`sentry-tower`]'s documentation. | +| `ureq` | | | | `ureq` transport support using `rustls` by default | +| `ureq-native-tls` | | | | | [`sentry-log`]: https://crates.io/crates/sentry-log [`sentry-slog`]: https://crates.io/crates/sentry-slog @@ -109,8 +111,10 @@ extra setup to function properly. - `rustls`: Enables `rustls` support for `reqwest`. Please note that `native-tls` is a default feature, and `default-features = false` must be set to completely disable building `native-tls` dependencies. -- `curl`: Enables the curl transport. -- `surf`: Enables the surf transport. +- `curl`: Enables the `curl` transport. +- `surf`: Enables the `surf` transport. +- `ureq`: Enables the `ureq` transport using `rustls`. +- `ureq-native-tls`: Enables the `ureq` transport using `native-tls`. ### Integrations - `tower`: Enables support for the `tower` crate and those using it. diff --git a/sentry/src/lib.rs b/sentry/src/lib.rs index 51c29c24..3e81ede6 100644 --- a/sentry/src/lib.rs +++ b/sentry/src/lib.rs @@ -56,24 +56,26 @@ //! Additional functionality and integrations are enabled via feature flags. Some features require //! extra setup to function properly. //! -//! | Feature | Default | Is Integration | Deprecated | Additional notes | -//! | -------------- | ------- | -------------- | ---------- | ---------------------------------------------------------------------------------------- | -//! | `backtrace` | ✅ | 🔌 | | | -//! | `contexts` | ✅ | 🔌 | | | -//! | `panic` | ✅ | 🔌 | | | -//! | `transport` | ✅ | | | | -//! | `anyhow` | | 🔌 | | | -//! | `test` | | | | | -//! | `debug-images` | | 🔌 | | | -//! | `log` | | 🔌 | | Requires extra setup; See [`sentry-log`]'s documentation. | -//! | `debug-logs` | | | ❗ | Requires extra setup; See [`sentry-log`]'s documentation. | -//! | `slog` | | 🔌 | | Requires extra setup; See [`sentry-slog`]'s documentation. | -//! | `reqwest` | ✅ | | | | -//! | `native-tls` | ✅ | | | `reqwest` must be enabled. | -//! | `rustls` | | | | `reqwest` must be enabled. `native-tls` must be disabled via `default-features = false`. | -//! | `curl` | | | | | -//! | `surf` | | | | | -//! | `tower` | | 🔌 | | Requires extra setup; See [`sentry-tower`]'s documentation. | +//! | Feature | Default | Is Integration | Deprecated | Additional notes | +//! | -------------- | ------- | -------------- | ---------- | ---------------------------------------------------------------------------------------- | +//! | `backtrace` | ✅ | 🔌 | | | +//! | `contexts` | ✅ | 🔌 | | | +//! | `panic` | ✅ | 🔌 | | | +//! | `transport` | ✅ | | | | +//! | `anyhow` | | 🔌 | | | +//! | `test` | | | | | +//! | `debug-images` | | 🔌 | | | +//! | `log` | | 🔌 | | Requires extra setup; See [`sentry-log`]'s documentation. | +//! | `debug-logs` | | | ❗ | Requires extra setup; See [`sentry-log`]'s documentation. | +//! | `slog` | | 🔌 | | Requires extra setup; See [`sentry-slog`]'s documentation. | +//! | `reqwest` | ✅ | | | | +//! | `native-tls` | ✅ | | | `reqwest` must be enabled. | +//! | `rustls` | | | | `reqwest` must be enabled. `native-tls` must be disabled via `default-features = false`. | +//! | `curl` | | | | | +//! | `surf` | | | | | +//! | `tower` | | 🔌 | | Requires extra setup; See [`sentry-tower`]'s documentation. | +//! | `ureq` | | | | `ureq` transport support using `rustls` by default | +//! | `ureq-native-tls` | | | | | //! //! [`sentry-log`]: https://crates.io/crates/sentry-log //! [`sentry-slog`]: https://crates.io/crates/sentry-slog @@ -101,8 +103,10 @@ //! - `rustls`: Enables `rustls` support for `reqwest`. Please note that `native-tls` is a default //! feature, and `default-features = false` must be set to completely disable building `native-tls` //! dependencies. -//! - `curl`: Enables the curl transport. -//! - `surf`: Enables the surf transport. +//! - `curl`: Enables the `curl` transport. +//! - `surf`: Enables the `surf` transport. +//! - `ureq`: Enables the `ureq` transport using `rustls`. +//! - `ureq-native-tls`: Enables the `ureq` transport using `native-tls`. //! //! ## Integrations //! - `tower`: Enables support for the `tower` crate and those using it. diff --git a/sentry/src/transports/mod.rs b/sentry/src/transports/mod.rs index 7aa09bd8..053730a8 100644 --- a/sentry/src/transports/mod.rs +++ b/sentry/src/transports/mod.rs @@ -1,14 +1,24 @@ //! The provided transports. //! //! This module exposes all transports that are compiled into the sentry -//! library. The `reqwest`, `curl` and `surf` features turn on these transports. +//! library. The `reqwest`, `curl`, `surf` and `ureq` features turn on these transports. use crate::{ClientOptions, Transport, TransportFactory}; use std::sync::Arc; -#[cfg(any(feature = "reqwest", feature = "curl", feature = "surf"))] +#[cfg(any( + feature = "reqwest", + feature = "curl", + feature = "surf", + feature = "ureq" +))] mod ratelimit; -#[cfg(any(feature = "reqwest", feature = "curl", feature = "surf"))] +#[cfg(any( + feature = "reqwest", + feature = "curl", + feature = "surf", + feature = "ureq" +))] mod thread; #[cfg(feature = "reqwest")] @@ -26,17 +36,45 @@ mod surf; #[cfg(feature = "surf")] pub use surf::SurfHttpTransport; +#[cfg(feature = "ureq")] +mod ureq; +#[cfg(feature = "ureq")] +pub use ureq::UreqHttpTransport; + #[cfg(feature = "reqwest")] type DefaultTransport = ReqwestHttpTransport; -#[cfg(all(feature = "curl", not(feature = "reqwest"), not(feature = "surf")))] +#[cfg(all( + feature = "curl", + not(feature = "reqwest"), + not(feature = "surf"), + not(feature = "ureq") +))] type DefaultTransport = CurlHttpTransport; -#[cfg(all(feature = "surf", not(feature = "reqwest"), not(feature = "curl")))] +#[cfg(all( + feature = "surf", + not(feature = "reqwest"), + not(feature = "curl"), + not(feature = "ureq") +))] type DefaultTransport = SurfHttpTransport; +#[cfg(all( + feature = "ureq", + not(feature = "reqwest"), + not(feature = "curl"), + not(feature = "surf") +))] +type DefaultTransport = UreqHttpTransport; + /// The default http transport. -#[cfg(any(feature = "reqwest", feature = "curl", feature = "surf"))] +#[cfg(any( + feature = "reqwest", + feature = "curl", + feature = "surf", + feature = "ureq" +))] pub type HttpTransport = DefaultTransport; /// Creates the default HTTP transport. @@ -49,11 +87,21 @@ pub struct DefaultTransportFactory; impl TransportFactory for DefaultTransportFactory { fn create_transport(&self, options: &ClientOptions) -> Arc { - #[cfg(any(feature = "reqwest", feature = "curl", feature = "surf"))] + #[cfg(any( + feature = "reqwest", + feature = "curl", + feature = "surf", + feature = "ureq" + ))] { Arc::new(HttpTransport::new(options)) } - #[cfg(not(any(feature = "reqwest", feature = "curl", feature = "surf")))] + #[cfg(not(any( + feature = "reqwest", + feature = "curl", + feature = "surf", + feature = "ureq" + )))] { let _ = options; panic!("sentry crate was compiled without transport") diff --git a/sentry/src/transports/ureq.rs b/sentry/src/transports/ureq.rs new file mode 100644 index 00000000..ccaab56e --- /dev/null +++ b/sentry/src/transports/ureq.rs @@ -0,0 +1,102 @@ +use std::time::Duration; + +#[cfg(fdoc)] +use ureq_ as ureq; +use ureq_::{Agent, AgentBuilder, Proxy}; + +use super::thread::TransportThread; + +use crate::{sentry_debug, types::Scheme, ClientOptions, Envelope, Transport}; + +/// A [`Transport`] that sends events via the [`ureq`] library. +/// +/// This is enabled by the `ureq` feature flag. +/// +/// [`ureq`]: https://crates.io/crates/ureq +#[cfg_attr(doc_cfg, doc(cfg(feature = "ureq")))] +pub struct UreqHttpTransport { + thread: TransportThread, +} + +impl UreqHttpTransport { + /// Creates a new Transport. + pub fn new(options: &ClientOptions) -> Self { + Self::new_internal(options, None) + } + + /// Creates a new Transport that uses the specified [`ureq::Agent`]. + pub fn with_agent(options: &ClientOptions, agent: Agent) -> Self { + Self::new_internal(options, Some(agent)) + } + + fn new_internal(options: &ClientOptions, agent: Option) -> Self { + let dsn = options.dsn.as_ref().unwrap(); + let scheme = dsn.scheme(); + let agent = agent.unwrap_or_else(|| { + let mut builder = AgentBuilder::new(); + + match (scheme, &options.http_proxy, &options.https_proxy) { + (Scheme::Https, _, &Some(ref proxy)) => { + builder = builder.proxy(Proxy::new(proxy).unwrap()); + } + (_, &Some(ref proxy), _) => { + builder = builder.proxy(Proxy::new(proxy).unwrap()); + } + _ => {} + } + + builder.build() + }); + let user_agent = options.user_agent.to_owned(); + let auth = dsn.to_auth(Some(&user_agent)).to_string(); + let url = dsn.envelope_api_url().to_string(); + + let thread = TransportThread::new(move |envelope, mut rl| { + let mut body = Vec::new(); + envelope.to_writer(&mut body).unwrap(); + let request = agent + .post(&url) + .set("X-Sentry-Auth", &auth) + .send_bytes(&body); + + match request { + Ok(response) => { + if let Some(sentry_header) = response.header("x-sentry-rate-limits") { + rl.update_from_sentry_header(sentry_header); + } else if let Some(retry_after) = response.header("retry-after") { + rl.update_from_retry_after(retry_after); + } else if response.status() == 429 { + rl.update_from_429(); + } + + match response.into_string() { + Err(err) => { + sentry_debug!("Failed to read sentry response: {}", err); + } + Ok(text) => { + sentry_debug!("Get response: `{}`", text); + } + } + } + Err(err) => { + sentry_debug!("Failed to send envelope: {}", err); + } + } + async move { rl } + }); + Self { thread } + } +} + +impl Transport for UreqHttpTransport { + fn send_envelope(&self, envelope: Envelope) { + self.thread.send(envelope) + } + fn flush(&self, timeout: Duration) -> bool { + self.thread.flush(timeout) + } + + fn shutdown(&self, timeout: Duration) -> bool { + self.flush(timeout) + } +}