diff --git a/Cargo.toml b/Cargo.toml index 413c38171..8564f60e2 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -18,8 +18,11 @@ autoexamples = true all-features = true [features] -default = ["with_client_implementation", "with_panic", "with_failure", "with_log", "with_env_logger", "with_device_info", "with_rust_info"] -with_client_implementation = ["reqwest", "im", "url", "with_backtrace"] +default = ["with_client_implementation", "with_default_transport", "with_panic", "with_failure", "with_log", "with_env_logger", "with_device_info", "with_rust_info"] +with_reqwest_transport = ["reqwest", "httpdate", "with_client_implementation"] +with_curl_transport = ["curl", "httpdate", "serde_json", "with_client_implementation"] +with_default_transport = ["with_reqwest_transport"] +with_client_implementation = ["im", "url", "with_backtrace"] with_backtrace = ["backtrace", "regex"] with_panic = ["with_backtrace"] with_failure = ["failure", "with_backtrace"] @@ -48,7 +51,9 @@ libc = { version = "0.2.48", optional = true } hostname = { version = "0.1.5", optional = true } findshlibs = { version = "0.4.1", optional = true } rand = "0.6.5" -httpdate = "0.3.2" +httpdate = { version = "0.3.2", optional = true } +curl = { version = "0.4.19", optional = true } +serde_json = { version = "1.0.38", optional = true } [target."cfg(not(windows))".dependencies] uname = { version = "0.1.1", optional = true } diff --git a/Makefile b/Makefile index 965da1c20..e8bd15737 100644 --- a/Makefile +++ b/Makefile @@ -53,6 +53,13 @@ check-all-impls: @RUSTFLAGS=-Dwarnings cargo check --no-default-features --features 'with_failure,with_log,with_panic,with_error_chain' .PHONY: check-all-impls +check-curl-transport: + @echo 'CURL TRANSPORT' + @RUSTFLAGS=-Dwarnings cargo check --features with_curl_transport + @echo 'CURL TRANSPORT ONLY' + @RUSTFLAGS=-Dwarnings cargo check --no-default-features --features 'with_curl_transport,with_client_implementation,with_panic' +.PHONY: check-curl-transport + check-actix: @echo 'ACTIX INTEGRATION' @RUSTFLAGS=-Dwarnings cargo check --manifest-path integrations/sentry-actix/Cargo.toml @@ -61,7 +68,7 @@ check-actix: check: check-no-default-features check-default-features .PHONY: check-all-features -checkall: check-all-features check-no-default-features check-default-features check-failure check-log check-panic check-error-chain check-all-impls check-actix +checkall: check-all-features check-no-default-features check-default-features check-failure check-log check-panic check-error-chain check-all-impls check-curl-transport check-actix .PHONY: checkall cargotest: diff --git a/src/lib.rs b/src/lib.rs index d80096f41..7bef80100 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -89,6 +89,7 @@ //! default flags: //! //! * `with_client_implementation`: turns on the real client implementation. +//! * `with_default_transport`: compiles in the default HTTP transport. //! * `with_backtrace`: enables backtrace support (automatically turned on in a few cases) //! * `with_panic`: enables the panic integration //! * `with_failure`: enables the `failure` integration @@ -104,6 +105,9 @@ //! //! * `with_error_chain`: enables the error-chain integration //! * `with_test_support`: enables the test support module +//! * `with_reqwest_transport`: enables the reqwest transport explicitly. This +//! is currently the default transport. +//! * `with_curl_transport`: enables the curl transport. #![warn(missing_docs)] #[macro_use] @@ -142,7 +146,7 @@ pub mod internals { #[cfg(feature = "with_client_implementation")] pub use crate::{ client::{ClientInitGuard, IntoDsn}, - transport::{DefaultTransportFactory, HttpTransport, Transport, TransportFactory}, + transport::{Transport, TransportFactory}, }; pub use sentry_types::{ @@ -151,6 +155,22 @@ pub mod internals { }; } +/// The provided transports. +/// +/// This module exposes all transports that are compiled into the sentry +/// library. The `with_reqwest_transport` and `with_curl_transport` flags +/// turn on these transports. +pub mod transports { + #[cfg(any(feature = "with_reqwest_transport", feature = "with_curl_transport"))] + pub use crate::transport::{DefaultTransportFactory, HttpTransport}; + + #[cfg(feature = "with_reqwest_transport")] + pub use crate::transport::ReqwestHttpTransport; + + #[cfg(feature = "with_curl_transport")] + pub use crate::transport::CurlHttpTransport; +} + // public api or exports from this crate pub use crate::api::*; pub use crate::hub::Hub; diff --git a/src/transport.rs b/src/transport.rs index ee00a454f..0eec3cce6 100644 --- a/src/transport.rs +++ b/src/transport.rs @@ -4,11 +4,16 @@ use std::sync::{Arc, Condvar, Mutex}; use std::thread::{self, JoinHandle}; use std::time::{Duration, SystemTime}; +#[cfg(any(feature = "with_reqwest_transport", feature = "with_curl_transport"))] use httpdate::parse_http_date; + +#[cfg(feature = "with_reqwest_transport")] use reqwest::{header::RETRY_AFTER, Client, Proxy}; +#[cfg(feature = "with_curl_transport")] +use {crate::internals::Scheme, curl, std::io::Read}; + use crate::client::ClientOptions; -use crate::internals::Dsn; use crate::protocol::Event; /// The trait for transports. @@ -88,27 +93,25 @@ impl TransportFactory for Arc { /// Creates the default HTTP transport. /// /// This is the default value for `transport` on the client options. It -/// creates a `HttpTransport`. +/// creates a `HttpTransport`. If no http transport was compiled into the +/// library it will panic on transport creation. #[derive(Clone)] pub struct DefaultTransportFactory; impl TransportFactory for DefaultTransportFactory { fn create_transport(&self, options: &ClientOptions) -> Box { - Box::new(HttpTransport::new(options)) + #[cfg(any(feature = "with_reqwest_transport", feature = "with_curl_transport"))] + { + Box::new(HttpTransport::new(options)) + } + #[cfg(not(any(feature = "with_reqwest_transport", feature = "with_curl_transport")))] + { + panic!("sentry crate was compiled without transport") + } } } -/// A transport can send events via HTTP to sentry. -#[derive(Debug)] -pub struct HttpTransport { - dsn: Dsn, - sender: Mutex>>>, - shutdown_signal: Arc, - shutdown_immediately: Arc, - queue_size: Arc>, - _handle: Option>, -} - +#[cfg(any(feature = "with_reqwest_transport", feature = "with_curl_transport"))] fn parse_retry_after(s: &str) -> Option { if let Ok(value) = s.parse::() { Some(SystemTime::now() + Duration::from_secs(value.ceil() as u64)) @@ -119,23 +122,198 @@ fn parse_retry_after(s: &str) -> Option { } } -/// # Panics -/// -/// Panics if the OS fails to create thread. -fn spawn_http_sender( - client: Client, - receiver: Receiver>>, - dsn: Dsn, - signal: Arc, - shutdown_immediately: Arc, - queue_size: Arc>, - user_agent: String, -) -> JoinHandle<()> { - let mut disabled = SystemTime::now(); - - thread::Builder::new() - .name("sentry-transport".to_string()) - .spawn(move || { +macro_rules! implement_http_transport { + ( + $(#[$attr:meta])* + pub struct $typename:ident; + fn spawn($($argname:ident: $argty:ty,)*) $body:block + ) => { + $(#[$attr])* + pub struct $typename { + sender: Mutex>>>, + shutdown_signal: Arc, + shutdown_immediately: Arc, + queue_size: Arc>, + _handle: Option>, + } + + impl $typename { + /// Creates a new transport. + pub fn new(options: &ClientOptions) -> $typename { + fn spawn($($argname: $argty,)*) -> JoinHandle<()> { $body } + + let (sender, receiver) = sync_channel(30); + let shutdown_signal = Arc::new(Condvar::new()); + let shutdown_immediately = Arc::new(AtomicBool::new(false)); + #[allow(clippy::mutex_atomic)] + let queue_size = Arc::new(Mutex::new(0)); + let _handle = Some(spawn( + options, + receiver, + shutdown_signal.clone(), + shutdown_immediately.clone(), + queue_size.clone(), + )); + $typename { + sender: Mutex::new(sender), + shutdown_signal, + shutdown_immediately, + queue_size, + _handle, + } + } + } + + impl Transport for $typename { + fn send_event(&self, event: Event<'static>) { + // we count up before we put the item on the queue and in case the + // queue is filled with too many items or we shut down, we decrement + // the count again as there is nobody that can pick it up. + *self.queue_size.lock().unwrap() += 1; + if self.sender.lock().unwrap().try_send(Some(event)).is_err() { + *self.queue_size.lock().unwrap() -= 1; + } + } + + fn shutdown(&self, timeout: Duration) -> bool { + sentry_debug!("shutting down http transport"); + let guard = self.queue_size.lock().unwrap(); + if *guard == 0 { + true + } else { + if let Ok(sender) = self.sender.lock() { + sender.send(None).ok(); + } + self.shutdown_signal.wait_timeout(guard, timeout).is_ok() + } + } + } + + impl Drop for $typename { + fn drop(&mut self) { + sentry_debug!("dropping http transport"); + self.shutdown_immediately.store(true, Ordering::SeqCst); + if let Ok(sender) = self.sender.lock() { + sender.send(None).ok(); + } + } + } + } +} + +#[cfg(feature = "with_reqwest_transport")] +implement_http_transport! { + /// A transport can send events via HTTP to sentry via `reqwest`. + /// + /// When the `with_default_transport` feature is enabled this will currently + /// be the default transport. This is separately enabled by the + /// `with_reqwest_transport` flag. + pub struct ReqwestHttpTransport; + + fn spawn( + options: &ClientOptions, + receiver: Receiver>>, + signal: Arc, + shutdown_immediately: Arc, + queue_size: Arc>, + ) { + let dsn = options.dsn.clone().unwrap(); + let user_agent = options.user_agent.to_string(); + let http_proxy = options.http_proxy.as_ref().map(|x| x.to_string()); + let https_proxy = options.https_proxy.as_ref().map(|x| x.to_string()); + let mut client = Client::builder(); + if let Some(url) = http_proxy { + client = client.proxy(Proxy::http(&url).unwrap()); + }; + if let Some(url) = https_proxy { + client = client.proxy(Proxy::https(&url).unwrap()); + }; + let client = client.build().unwrap(); + + let mut disabled = SystemTime::now(); + + thread::Builder::new() + .name("sentry-transport".to_string()) + .spawn(move || { + sentry_debug!("spawning reqwest transport"); + let url = dsn.store_api_url().to_string(); + + while let Some(event) = receiver.recv().unwrap_or(None) { + // on drop we want to not continue processing the queue. + if shutdown_immediately.load(Ordering::SeqCst) { + let mut size = queue_size.lock().unwrap(); + *size = 0; + signal.notify_all(); + break; + } + + // while we are disabled due to rate limits, skip + let now = SystemTime::now(); + if let Ok(time_left) = disabled.duration_since(now) { + sentry_debug!( + "Skipping event send because we're disabled due to rate limits for {}s", + time_left.as_secs() + ); + continue; + } + + match client + .post(url.as_str()) + .json(&event) + .header("X-Sentry-Auth", dsn.to_auth(Some(&user_agent)).to_string()) + .send() + { + Ok(resp) => { + if resp.status() == 429 { + if let Some(retry_after) = resp + .headers() + .get(RETRY_AFTER) + .and_then(|x| x.to_str().ok()) + .and_then(parse_retry_after) + { + disabled = retry_after; + } + } + } + Err(err) => { + sentry_debug!("Failed to send event: {}", err); + } + } + + let mut size = queue_size.lock().unwrap(); + *size -= 1; + if *size == 0 { + signal.notify_all(); + } + } + }).unwrap() + } +} + +#[cfg(feature = "with_curl_transport")] +implement_http_transport! { + /// A transport can send events via HTTP to sentry via `curl`. + /// + /// This is enabled by the `with_curl_transport` flag. + pub struct CurlHttpTransport; + + fn spawn( + options: &ClientOptions, + receiver: Receiver>>, + signal: Arc, + shutdown_immediately: Arc, + queue_size: Arc>, + ) { + let dsn = options.dsn.clone().unwrap(); + let user_agent = options.user_agent.to_string(); + let http_proxy = options.http_proxy.as_ref().map(|x| x.to_string()); + let https_proxy = options.https_proxy.as_ref().map(|x| x.to_string()); + + let mut disabled = SystemTime::now(); + let mut handle = curl::easy::Easy::new(); + + thread::spawn(move || { + sentry_debug!("spawning curl transport"); let url = dsn.store_api_url().to_string(); while let Some(event) = receiver.recv().unwrap_or(None) { @@ -157,26 +335,71 @@ fn spawn_http_sender( continue; } - match client - .post(url.as_str()) - .json(&event) - .header("X-Sentry-Auth", dsn.to_auth(Some(&user_agent)).to_string()) - .send() + handle.reset(); + handle.url(&url).unwrap(); + handle.custom_request("POST").unwrap(); + + match (dsn.scheme(), &http_proxy, &https_proxy) { + (Scheme::Https, _, &Some(ref proxy)) => { + handle.proxy(&proxy).unwrap(); + } + (_, &Some(ref proxy), _) => { + handle.proxy(&proxy).unwrap(); + } + _ => {} + } + + let body = serde_json::to_vec(&event).unwrap(); + let mut retry_after = None; + let mut headers = curl::easy::List::new(); + headers.append(&format!("X-Sentry-Auth: {}", dsn.to_auth(Some(&user_agent)))).unwrap(); + headers.append("Expect:").unwrap(); + headers.append("Content-Type: application/json").unwrap(); + handle.http_headers(headers).unwrap(); + handle.upload(true).unwrap(); + handle.in_filesize(body.len() as u64).unwrap(); + handle.read_function(move |buf| Ok((&body[..]).read(buf).unwrap_or(0))).unwrap(); + handle.verbose(true).unwrap(); + handle.debug_function(move |info, data| { + let prefix = match info { + curl::easy::InfoType::HeaderIn => "< ", + curl::easy::InfoType::HeaderOut => "> ", + curl::easy::InfoType::DataOut => "", + _ => return + }; + sentry_debug!("curl: {}{}", prefix, String::from_utf8_lossy(data).trim()); + }).unwrap(); + { - Ok(resp) => { - if resp.status() == 429 { - if let Some(retry_after) = resp - .headers() - .get(RETRY_AFTER) - .and_then(|x| x.to_str().ok()) - .and_then(parse_retry_after) - { - disabled = retry_after; + let mut handle = handle.transfer(); + let retry_after_setter = &mut retry_after; + handle.header_function(move |data| { + if let Ok(data) = std::str::from_utf8(data) { + let mut iter = data.split(':'); + if let Some(key) = iter.next().map(|x| x.to_lowercase()) { + if key == "retry-after" { + *retry_after_setter = iter.next().map(|x| x.trim().to_string()); + } } } + true + }).unwrap(); + handle.perform().ok(); + } + + match handle.response_code() { + Ok(429) => { + if let Some(retry_after) = retry_after + .as_ref() + .map(|x| x.as_str()) + .and_then(parse_retry_after) + { + disabled = retry_after; + } } - Err(err) => { - sentry_debug!("Failed to send event: {}", err); + Ok(200) | Ok(201) => {} + _ => { + sentry_debug!("Failed to send event"); } } @@ -187,80 +410,17 @@ fn spawn_http_sender( } } }) - .unwrap() // TODO: Panic, change API to return Result? -} - -impl HttpTransport { - /// Creates a new transport. - pub fn new(options: &ClientOptions) -> HttpTransport { - let dsn = options.dsn.clone().unwrap(); - let user_agent = options.user_agent.to_string(); - let http_proxy = options.http_proxy.as_ref().map(|x| x.to_string()); - let https_proxy = options.https_proxy.as_ref().map(|x| x.to_string()); - - let (sender, receiver) = sync_channel(30); - let shutdown_signal = Arc::new(Condvar::new()); - let shutdown_immediately = Arc::new(AtomicBool::new(false)); - #[allow(clippy::mutex_atomic)] - let queue_size = Arc::new(Mutex::new(0)); - let mut client = Client::builder(); - if let Some(url) = http_proxy { - client = client.proxy(Proxy::http(&url).unwrap()); - }; - if let Some(url) = https_proxy { - client = client.proxy(Proxy::https(&url).unwrap()); - }; - let _handle = Some(spawn_http_sender( - client.build().unwrap(), - receiver, - dsn.clone(), - shutdown_signal.clone(), - shutdown_immediately.clone(), - queue_size.clone(), - user_agent, - )); - HttpTransport { - dsn, - sender: Mutex::new(sender), - shutdown_signal, - shutdown_immediately, - queue_size, - _handle, - } } } -impl Transport for HttpTransport { - fn send_event(&self, event: Event<'static>) { - // we count up before we put the item on the queue and in case the - // queue is filled with too many items or we shut down, we decrement - // the count again as there is nobody that can pick it up. - *self.queue_size.lock().unwrap() += 1; - if self.sender.lock().unwrap().try_send(Some(event)).is_err() { - *self.queue_size.lock().unwrap() -= 1; - } - } +#[cfg(feature = "with_reqwest_transport")] +type DefaultTransport = ReqwestHttpTransport; - fn shutdown(&self, timeout: Duration) -> bool { - sentry_debug!("shutting down http transport"); - let guard = self.queue_size.lock().unwrap(); - if *guard == 0 { - true - } else { - if let Ok(sender) = self.sender.lock() { - sender.send(None).ok(); - } - self.shutdown_signal.wait_timeout(guard, timeout).is_ok() - } - } -} +#[cfg(all( + feature = "with_curl_transport", + not(feature = "with_reqwest_transport") +))] +type DefaultTransport = CurlHttpTransport; -impl Drop for HttpTransport { - fn drop(&mut self) { - sentry_debug!("dropping http transport"); - self.shutdown_immediately.store(true, Ordering::SeqCst); - if let Ok(sender) = self.sender.lock() { - sender.send(None).ok(); - } - } -} +/// The default http transport. +pub type HttpTransport = DefaultTransport; diff --git a/src/utils.rs b/src/utils.rs index 8c148a724..08a1ffd9f 100644 --- a/src/utils.rs +++ b/src/utils.rs @@ -1,9 +1,7 @@ //! Useful utilities for working with events. use std::thread; -use crate::protocol::{ - Context, DebugImage, DeviceContext, Map, OsContext, RuntimeContext, Stacktrace, Thread, -}; +use crate::protocol::{Context, DebugImage, Stacktrace, Thread}; #[cfg(all(feature = "with_device_info", target_os = "macos"))] mod model_support { @@ -230,7 +228,7 @@ pub fn os_context() -> Option { use uname::uname; if let Ok(info) = uname() { Some( - OsContext { + crate::protocol::OsContext { name: Some(info.sysname), kernel_version: Some(info.version), version: Some(info.release), @@ -246,7 +244,7 @@ pub fn os_context() -> Option { { use crate::constants::PLATFORM; Some( - OsContext { + crate::protocol::OsContext { name: Some(PLATFORM.into()), ..Default::default() } @@ -264,11 +262,11 @@ pub fn rust_context() -> Option { #[cfg(feature = "with_device_info")] { use crate::constants::{RUSTC_CHANNEL, RUSTC_VERSION}; - let ctx = RuntimeContext { + let ctx = crate::protocol::RuntimeContext { name: Some("rustc".into()), version: RUSTC_VERSION.map(|x| x.into()), other: { - let mut map = Map::default(); + let mut map = crate::protocol::Map::default(); if let Some(channel) = RUSTC_CHANNEL { map.insert("channel".to_string(), channel.into()); } @@ -292,7 +290,7 @@ pub fn device_context() -> Option { let family = device_family(); let arch = cpu_arch(); Some( - DeviceContext { + crate::protocol::DeviceContext { model, family, arch,