sentry_tower/
lib.rs

1//! Adds support for automatic hub binding for each request received by the Tower server (or client,
2//! though usefulness is limited in this case).
3//!
4//! This allows breadcrumbs collected during the request handling to land in a specific hub, and
5//! avoid having them mixed across requests should a new hub be bound at each request.
6//!
7//! # Examples
8//!
9//! ```rust
10//! # use tower::ServiceBuilder;
11//! # use std::time::Duration;
12//! # type Request = String;
13//! use sentry_tower::NewSentryLayer;
14//!
15//! // Compose a Tower service where each request gets its own Sentry hub
16//! let service = ServiceBuilder::new()
17//!     .layer(NewSentryLayer::<Request>::new_from_top())
18//!     .timeout(Duration::from_secs(30))
19//!     .service(tower::service_fn(|req: Request| format!("hello {}", req)));
20//! ```
21//!
22//! More customization can be achieved through the `new` function, such as passing a [`Hub`]
23//! directly.
24//!
25//! ```rust
26//! # use tower::ServiceBuilder;
27//! # use std::{sync::Arc, time::Duration};
28//! # type Request = String;
29//! use sentry::Hub;
30//! use sentry_tower::SentryLayer;
31//!
32//! // Create a hub dedicated to web requests
33//! let hub = Arc::new(Hub::with(|hub| Hub::new_from_top(hub)));
34//!
35//! // Compose a Tower service
36//! let service = ServiceBuilder::new()
37//!     .layer(SentryLayer::<_, _, Request>::new(hub))
38//!     .timeout(Duration::from_secs(30))
39//!     .service(tower::service_fn(|req: Request| format!("hello {}", req)));
40//! ```
41//!
42//! The layer can also accept a closure to return a hub depending on the incoming request.
43//!
44//! ```rust
45//! # use tower::ServiceBuilder;
46//! # use std::{sync::Arc, time::Duration};
47//! # type Request = String;
48//! use sentry::Hub;
49//! use sentry_tower::SentryLayer;
50//!
51//! // Compose a Tower service
52//! let hello = Arc::new(Hub::with(|hub| Hub::new_from_top(hub)));
53//! let other = Arc::new(Hub::with(|hub| Hub::new_from_top(hub)));
54//!
55//! let service = ServiceBuilder::new()
56//!     .layer(SentryLayer::new(|req: &Request| match req.as_str() {
57//!         "hello" => hello.clone(),
58//!         _ => other.clone(),
59//!     }))
60//!     .timeout(Duration::from_secs(30))
61//!     .service(tower::service_fn(|req: Request| format!("{} world", req)));
62//! ```
63//!
64//! When using Tonic, the layer can be used directly by the Tonic stack:
65//!
66//! ```rust,no_run
67//! # use anyhow::{anyhow, Result};
68//! # use sentry_anyhow::capture_anyhow;
69//! # use tonic::{Request, Response, Status, transport::Server};
70//! # mod hello_world {
71//! #     include!("helloworld.rs");
72//! # }
73//! use hello_world::{greeter_server::*, *};
74//! use sentry_tower::NewSentryLayer;
75//!
76//! struct GreeterService;
77//!
78//! #[tonic::async_trait]
79//! impl Greeter for GreeterService {
80//!     async fn say_hello(
81//!         &self,
82//!         req: Request<HelloRequest>,
83//!     ) -> Result<Response<HelloReply>, Status> {
84//!         let HelloRequest { name } = req.into_inner();
85//!         if name == "world" {
86//!             capture_anyhow(&anyhow!("Trying to greet a planet"));
87//!             return Err(Status::invalid_argument("Cannot greet a planet"));
88//!         }
89//!         Ok(Response::new(HelloReply {
90//!             message: format!("Hello {}", name),
91//!         }))
92//!     }
93//! }
94//!
95//! # #[tokio::main]
96//! # async fn main() -> Result<()> {
97//! Server::builder()
98//!     .layer(NewSentryLayer::new_from_top())
99//!     .add_service(GreeterServer::new(GreeterService))
100//!     .serve("127.0.0.1:50051".parse().unwrap())
101//!     .await?;
102//! #     Ok(())
103//! # }
104//! ```
105//!
106//! ## Usage with `tower-http`
107//!
108//! The `http` feature of the `sentry-tower` crate offers another layer which will attach
109//! request details onto captured events, and optionally start a new performance monitoring
110//! transaction based on the incoming HTTP headers.  When using the tower integration via
111//! `sentry::integrations::tower`, this feature can also be enabled using the `tower-http`
112//! feature of the `sentry` crate instead of the `tower` feature.
113//!
114//! The created transaction will automatically use the request URI as its name.
115//! This is sometimes not desirable in case the request URI contains unique IDs
116//! or similar. In this case, users should manually override the transaction name
117//! in the request handler using the [`Scope::set_transaction`](sentry_core::Scope::set_transaction)
118//! method.
119//!
120//! When combining both layers, take care of the ordering of both. For example
121//! with [`tower::ServiceBuilder`], always define the `Hub` layer before the `Http`
122//! one, like so:
123//!
124//! ```rust
125//! # #[cfg(feature = "http")] {
126//! # type Request = http::Request<String>;
127//! let layer = tower::ServiceBuilder::new()
128//!     .layer(sentry_tower::NewSentryLayer::<Request>::new_from_top())
129//!     .layer(sentry_tower::SentryHttpLayer::new().enable_transaction());
130//! # }
131//! ```
132//!
133//! When using `axum`, either use [`tower::ServiceBuilder`] as shown above, or make sure you
134//! reorder the layers, like so:
135//!
136//! ```ignore
137//! let app = Router::new()
138//!     .route("/", get(handler))
139//!     .layer(sentry_tower::SentryHttpLayer::new().enable_transaction())
140//!     .layer(sentry_tower::NewSentryLayer::<Request>::new_from_top())
141//! ```
142//!
143//! This is because `axum` applies middleware in the opposite order as [`tower::ServiceBuilder`].
144//! Applying the layers in the wrong order can result in memory leaks.
145//!
146//! [`tower::ServiceBuilder`]: https://docs.rs/tower/latest/tower/struct.ServiceBuilder.html
147
148#![doc(html_favicon_url = "https://sentry-brand.storage.googleapis.com/favicon.ico")]
149#![doc(html_logo_url = "https://sentry-brand.storage.googleapis.com/sentry-glyph-black.png")]
150#![warn(missing_docs)]
151
152use std::marker::PhantomData;
153use std::sync::Arc;
154use std::task::{Context, Poll};
155
156use sentry_core::{Hub, SentryFuture, SentryFutureExt};
157use tower_layer::Layer;
158use tower_service::Service;
159
160#[cfg(feature = "http")]
161mod http;
162#[cfg(feature = "http")]
163pub use crate::http::*;
164
165/// Provides a hub for each request
166pub trait HubProvider<H, Request>
167where
168    H: Into<Arc<Hub>>,
169{
170    /// Returns a hub to be bound to the request
171    fn hub(&self, request: &Request) -> H;
172}
173
174impl<H, F, Request> HubProvider<H, Request> for F
175where
176    F: Fn(&Request) -> H,
177    H: Into<Arc<Hub>>,
178{
179    fn hub(&self, request: &Request) -> H {
180        (self)(request)
181    }
182}
183
184impl<Request> HubProvider<Arc<Hub>, Request> for Arc<Hub> {
185    fn hub(&self, _request: &Request) -> Arc<Hub> {
186        self.clone()
187    }
188}
189
190/// Provides a new hub made from the currently active hub for each request
191#[derive(Clone, Copy)]
192pub struct NewFromTopProvider;
193
194impl<Request> HubProvider<Arc<Hub>, Request> for NewFromTopProvider {
195    fn hub(&self, _request: &Request) -> Arc<Hub> {
196        // The Clippy lint here is a false positive, the suggestion to write
197        // `Hub::with(Hub::new_from_top)` does not compiles:
198        //     143 |         Hub::with(Hub::new_from_top).into()
199        //         |         ^^^^^^^^^ implementation of `std::ops::FnOnce` is not general enough
200        #[allow(clippy::redundant_closure)]
201        Hub::with(|hub| Hub::new_from_top(hub)).into()
202    }
203}
204
205/// Tower layer that binds a specific Sentry hub for each request made.
206pub struct SentryLayer<P, H, Request>
207where
208    P: HubProvider<H, Request>,
209    H: Into<Arc<Hub>>,
210{
211    provider: P,
212    _hub: PhantomData<(H, fn() -> Request)>,
213}
214
215impl<S, P, H, Request> Layer<S> for SentryLayer<P, H, Request>
216where
217    P: HubProvider<H, Request> + Clone,
218    H: Into<Arc<Hub>>,
219{
220    type Service = SentryService<S, P, H, Request>;
221
222    fn layer(&self, service: S) -> Self::Service {
223        SentryService {
224            service,
225            provider: self.provider.clone(),
226            _hub: PhantomData,
227        }
228    }
229}
230
231impl<P, H, Request> Clone for SentryLayer<P, H, Request>
232where
233    P: HubProvider<H, Request> + Clone,
234    H: Into<Arc<Hub>>,
235{
236    fn clone(&self) -> Self {
237        Self {
238            provider: self.provider.clone(),
239            _hub: PhantomData,
240        }
241    }
242}
243
244impl<P, H, Request> SentryLayer<P, H, Request>
245where
246    P: HubProvider<H, Request> + Clone,
247    H: Into<Arc<Hub>>,
248{
249    /// Build a new layer with the given Layer provider
250    pub fn new(provider: P) -> Self {
251        Self {
252            provider,
253            _hub: PhantomData,
254        }
255    }
256}
257
258/// Tower service that binds a specific Sentry hub for each request made.
259pub struct SentryService<S, P, H, Request>
260where
261    P: HubProvider<H, Request>,
262    H: Into<Arc<Hub>>,
263{
264    service: S,
265    provider: P,
266    _hub: PhantomData<(H, fn() -> Request)>,
267}
268
269impl<S, Request, P, H> Service<Request> for SentryService<S, P, H, Request>
270where
271    S: Service<Request>,
272    P: HubProvider<H, Request>,
273    H: Into<Arc<Hub>>,
274{
275    type Response = S::Response;
276    type Error = S::Error;
277    type Future = SentryFuture<S::Future>;
278
279    fn poll_ready(&mut self, cx: &mut Context<'_>) -> Poll<Result<(), Self::Error>> {
280        self.service.poll_ready(cx)
281    }
282
283    fn call(&mut self, request: Request) -> Self::Future {
284        let hub = self.provider.hub(&request).into();
285        let fut = Hub::run(hub.clone(), || self.service.call(request));
286        fut.bind_hub(hub)
287    }
288}
289
290impl<S, P, H, Request> Clone for SentryService<S, P, H, Request>
291where
292    S: Clone,
293    P: HubProvider<H, Request> + Clone,
294    H: Into<Arc<Hub>>,
295{
296    fn clone(&self) -> Self {
297        Self {
298            service: self.service.clone(),
299            provider: self.provider.clone(),
300            _hub: PhantomData,
301        }
302    }
303}
304
305impl<S, P, H, Request> SentryService<S, P, H, Request>
306where
307    P: HubProvider<H, Request> + Clone,
308    H: Into<Arc<Hub>>,
309{
310    /// Wrap a Tower service with a Tower layer that binds a Sentry hub for each request made.
311    pub fn new(provider: P, service: S) -> Self {
312        SentryLayer::<P, H, Request>::new(provider).layer(service)
313    }
314}
315
316/// Tower layer that binds a new Sentry hub for each request made
317pub type NewSentryLayer<Request> = SentryLayer<NewFromTopProvider, Arc<Hub>, Request>;
318
319impl<Request> NewSentryLayer<Request> {
320    /// Create a new Sentry layer that binds a new Sentry hub for each request made
321    pub fn new_from_top() -> Self {
322        Self {
323            provider: NewFromTopProvider,
324            _hub: PhantomData,
325        }
326    }
327}
328
329/// Tower service that binds a new Sentry hub for each request made.
330pub type NewSentryService<S, Request> = SentryService<S, NewFromTopProvider, Arc<Hub>, Request>;
331
332impl<S, Request> NewSentryService<S, Request> {
333    /// Wrap a Tower service with a Tower layer that binds a Sentry hub for each request made.
334    pub fn new_from_top(service: S) -> Self {
335        Self {
336            provider: NewFromTopProvider,
337            service,
338            _hub: PhantomData,
339        }
340    }
341}
342
343#[cfg(test)]
344mod tests {
345    use super::*;
346    use std::rc::Rc;
347
348    fn assert_sync<T: Sync>() {}
349
350    #[test]
351    fn test_layer_is_sync_when_request_isnt() {
352        assert_sync::<NewSentryLayer<Rc<()>>>(); // Rc<()> is not Sync
353    }
354
355    #[test]
356    fn test_service_is_sync_when_request_isnt() {
357        assert_sync::<NewSentryService<(), Rc<()>>>(); // Rc<()> is not Sync
358    }
359}