diff --git a/sentry-tower/Cargo.toml b/sentry-tower/Cargo.toml index 35c5d831d..733e0a6a0 100644 --- a/sentry-tower/Cargo.toml +++ b/sentry-tower/Cargo.toml @@ -11,9 +11,14 @@ Sentry integration for tower-based crates. """ edition = "2018" +[features] +http = ["http_", "pin-project"] + [dependencies] tower-layer = "0.3" tower-service = "0.3" +http_ = { package = "http", version = "0.2.6", optional = true } +pin-project = { version = "1.0.10", optional = true } sentry-core = { version = "0.23.0", path = "../sentry-core", default-features = false, features = ["client"] } [dev-dependencies] diff --git a/sentry-tower/src/http.rs b/sentry-tower/src/http.rs new file mode 100644 index 000000000..1db10150c --- /dev/null +++ b/sentry-tower/src/http.rs @@ -0,0 +1,149 @@ +use std::future::Future; +use std::pin::Pin; +use std::task::{Context, Poll}; + +use http_::Request; +use tower_layer::Layer; +use tower_service::Service; + +/// Tower Layer that logs Http Request Headers. +/// +/// The Service created by this Layer can also optionally start a new +/// performance monitoring transaction for each incoming request, +/// continuing the trace based on incoming distributed tracing headers. +#[derive(Clone, Default)] +pub struct SentryHttpLayer { + start_transaction: bool, +} + +impl SentryHttpLayer { + /// Creates a new Layer that only logs Request Headers. + pub fn new() -> Self { + Self::default() + } + + /// Creates a new Layer which starts a new performance monitoring transaction + /// for each incoming request. + pub fn with_transaction() -> Self { + Self { + start_transaction: true, + } + } +} + +/// Tower Service that logs Http Request Headers. +/// +/// The Service can also optionally start a new performance monitoring transaction +/// for each incoming request, continuing the trace based on incoming +/// distributed tracing headers. +#[derive(Clone)] +pub struct SentryHttpService { + service: S, + start_transaction: bool, +} + +impl Layer for SentryHttpLayer { + type Service = SentryHttpService; + + fn layer(&self, service: S) -> Self::Service { + Self::Service { + service, + start_transaction: self.start_transaction, + } + } +} + +/// The Future returned from [`SentryHttpService`]. +#[pin_project::pin_project] +pub struct SentryHttpFuture { + transaction: Option<( + sentry_core::TransactionOrSpan, + Option, + )>, + #[pin] + future: F, +} + +impl Future for SentryHttpFuture +where + F: Future, +{ + type Output = F::Output; + + fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll { + let slf = self.project(); + match slf.future.poll(cx) { + Poll::Ready(res) => { + if let Some((transaction, parent_span)) = slf.transaction.take() { + transaction.finish(); + sentry_core::configure_scope(|scope| scope.set_span(parent_span)); + } + Poll::Ready(res) + } + Poll::Pending => Poll::Pending, + } + } +} + +impl Service> for SentryHttpService +where + S: Service>, +{ + type Response = S::Response; + type Error = S::Error; + type Future = SentryHttpFuture; + + fn poll_ready(&mut self, cx: &mut Context<'_>) -> Poll> { + self.service.poll_ready(cx) + } + + fn call(&mut self, request: Request) -> Self::Future { + let transaction = sentry_core::configure_scope(|scope| { + let sentry_req = sentry_core::protocol::Request { + method: Some(request.method().to_string()), + url: request.uri().to_string().parse().ok(), + headers: request + .headers() + .into_iter() + .map(|(header, value)| { + ( + header.to_string(), + value.to_str().unwrap_or_default().into(), + ) + }) + .collect(), + ..Default::default() + }; + scope.add_event_processor(move |mut event| { + if event.request.is_none() { + event.request = Some(sentry_req.clone()); + } + Some(event) + }); + + if self.start_transaction { + let headers = request.headers().into_iter().flat_map(|(header, value)| { + value.to_str().ok().map(|value| (header.as_str(), value)) + }); + let tx_name = format!("{} {}", request.method(), request.uri().path()); + let tx_ctx = sentry_core::TransactionContext::continue_from_headers( + &tx_name, + "http.server", + headers, + ); + let transaction: sentry_core::TransactionOrSpan = + sentry_core::start_transaction(tx_ctx).into(); + let parent_span = scope.get_span(); + scope.set_span(Some(transaction.clone())); + Some((transaction, parent_span)) + } else { + None + } + }); + + SentryHttpFuture { + transaction, + future: self.service.call(request), + } + } +} diff --git a/sentry-tower/src/lib.rs b/sentry-tower/src/lib.rs index eb51dfa2d..3dba2023b 100644 --- a/sentry-tower/src/lib.rs +++ b/sentry-tower/src/lib.rs @@ -102,13 +102,19 @@ #![doc(html_logo_url = "https://sentry-brand.storage.googleapis.com/sentry-glyph-black.png")] #![warn(missing_docs)] -use sentry_core::{Hub, SentryFuture, SentryFutureExt}; use std::marker::PhantomData; use std::sync::Arc; use std::task::{Context, Poll}; + +use sentry_core::{Hub, SentryFuture, SentryFutureExt}; use tower_layer::Layer; use tower_service::Service; +#[cfg(feature = "http")] +mod http; +#[cfg(feature = "http")] +pub use http::*; + /// Provides a hub for each request pub trait HubProvider where @@ -140,7 +146,7 @@ pub struct NewFromTopProvider; impl HubProvider, Request> for NewFromTopProvider { fn hub(&self, _request: &Request) -> Arc { - // The Clippy lint here is a falste positive, the suggestion to write + // The Clippy lint here is a false positive, the suggestion to write // `Hub::with(Hub::new_from_top)` does not compiles: // 143 | Hub::with(Hub::new_from_top).into() // | ^^^^^^^^^ implementation of `std::ops::FnOnce` is not general enough diff --git a/sentry-types/src/protocol/v7.rs b/sentry-types/src/protocol/v7.rs index 32d51f35a..f64602096 100644 --- a/sentry-types/src/protocol/v7.rs +++ b/sentry-types/src/protocol/v7.rs @@ -46,7 +46,7 @@ pub mod debugid { /// An arbitrary (JSON) value. pub use self::value::Value; -/// The internally useed map type. +/// The internally used map type. pub use self::map::Map; /// A wrapper type for collections with attached meta data.