Skip to main content

sentry_tower/
http.rs

1use std::convert::TryInto;
2use std::future::Future;
3use std::pin::Pin;
4use std::task::{Context, Poll};
5
6use http::{header, uri, Request, Response, StatusCode};
7use pin_project::pinned_drop;
8use sentry_core::utils::{is_sensitive_header, scrub_pii_from_url};
9use sentry_core::{protocol, Hub};
10use tower_layer::Layer;
11use tower_service::Service;
12
13/// Tower Layer that captures Http Request information.
14///
15/// The Service created by this Layer can optionally start a new
16/// performance monitoring transaction for each incoming request,
17/// continuing the trace based on incoming distributed tracing headers.
18///
19/// The created transaction will automatically use the request URI as its name.
20/// This is sometimes not desirable in case the request URI contains unique IDs
21/// or similar. In this case, users should manually override the transaction name
22/// in the request handler using the [`Scope::set_transaction`](sentry_core::Scope::set_transaction)
23/// method.
24///
25/// By default, the service will filter out potentially sensitive headers from the captured
26/// requests. By enabling `with_pii`, you can opt in to capturing all headers instead.
27#[derive(Clone, Default)]
28pub struct SentryHttpLayer {
29    start_transaction: bool,
30    with_pii: bool,
31}
32
33impl SentryHttpLayer {
34    /// Creates a new Layer that only captures request information.
35    /// If a client is bound to the main Hub (i.e. the SDK has already been initialized), set `with_pii` based on the `send_default_pii` client option.
36    pub fn new() -> Self {
37        let mut slf = Self::default();
38        Hub::main()
39            .client()
40            .inspect(|client| slf.with_pii = client.options().send_default_pii);
41        slf
42    }
43
44    /// Creates a new Layer which starts a new performance monitoring transaction
45    /// for each incoming request.
46    #[deprecated(since = "0.38.0", note = "please use `enable_transaction` instead")]
47    pub fn with_transaction() -> Self {
48        Self {
49            start_transaction: true,
50            with_pii: false,
51        }
52    }
53
54    /// Enable starting a new performance monitoring transaction for each incoming request.
55    #[must_use]
56    pub fn enable_transaction(mut self) -> Self {
57        self.start_transaction = true;
58        self
59    }
60
61    /// Include PII in captured requests. Potentially sensitive headers are not filtered out.
62    #[must_use]
63    pub fn enable_pii(mut self) -> Self {
64        self.with_pii = true;
65        self
66    }
67}
68
69/// Tower Service that captures Http Request information.
70///
71/// The Service can optionally start a new performance monitoring transaction
72/// for each incoming request, continuing the trace based on incoming
73/// distributed tracing headers.
74///
75/// If `with_pii` is disabled, sensitive headers will be filtered out.
76#[derive(Clone)]
77pub struct SentryHttpService<S> {
78    service: S,
79    start_transaction: bool,
80    with_pii: bool,
81}
82
83impl<S> Layer<S> for SentryHttpLayer {
84    type Service = SentryHttpService<S>;
85
86    fn layer(&self, service: S) -> Self::Service {
87        Self::Service {
88            service,
89            start_transaction: self.start_transaction,
90            with_pii: self.with_pii,
91        }
92    }
93}
94
95/// The Future returned from [`SentryHttpService`].
96#[pin_project::pin_project(PinnedDrop)]
97pub struct SentryHttpFuture<F> {
98    on_first_poll: Option<(
99        sentry_core::protocol::Request,
100        Option<sentry_core::TransactionContext>,
101    )>,
102    transaction: Option<(
103        sentry_core::TransactionOrSpan,
104        Option<sentry_core::TransactionOrSpan>,
105    )>,
106    #[pin]
107    future: F,
108}
109
110impl<F, ResBody, Error> Future for SentryHttpFuture<F>
111where
112    F: Future<Output = Result<Response<ResBody>, Error>>,
113{
114    type Output = F::Output;
115
116    fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output> {
117        let slf = self.project();
118        if let Some((sentry_req, trx_ctx)) = slf.on_first_poll.take() {
119            sentry_core::configure_scope(|scope| {
120                if let Some(trx_ctx) = trx_ctx {
121                    let transaction = sentry_core::start_transaction(trx_ctx);
122                    transaction.set_origin("auto.http.tower");
123                    let transaction: sentry_core::TransactionOrSpan = transaction.into();
124                    transaction.set_request(sentry_req.clone());
125                    let parent_span = scope.get_span();
126                    scope.set_span(Some(transaction.clone()));
127                    *slf.transaction = Some((transaction, parent_span));
128                }
129
130                scope.add_event_processor(move |mut event| {
131                    if event.request.is_none() {
132                        event.request = Some(sentry_req.clone());
133                    }
134                    Some(event)
135                });
136            });
137        }
138        match slf.future.poll(cx) {
139            Poll::Ready(res) => {
140                if let Some((transaction, parent_span)) = slf.transaction.take() {
141                    if transaction.get_status().is_none() {
142                        let status = match &res {
143                            Ok(res) => map_status(res.status()),
144                            Err(_) => protocol::SpanStatus::UnknownError,
145                        };
146                        transaction.set_status(status);
147                    }
148                    transaction.finish();
149                    sentry_core::configure_scope(|scope| scope.set_span(parent_span));
150                }
151                Poll::Ready(res)
152            }
153            Poll::Pending => Poll::Pending,
154        }
155    }
156}
157
158#[pinned_drop]
159impl<F> PinnedDrop for SentryHttpFuture<F> {
160    fn drop(self: Pin<&mut Self>) {
161        let slf = self.project();
162
163        // If the future gets dropped without being polled to completion,
164        // still finish the transaction to make sure this is not lost.
165        if let Some((transaction, parent_span)) = slf.transaction.take() {
166            if transaction.get_status().is_none() {
167                transaction.set_status(protocol::SpanStatus::Aborted);
168            }
169            transaction.finish();
170            sentry_core::configure_scope(|scope| scope.set_span(parent_span));
171        }
172    }
173}
174
175impl<S, ReqBody, ResBody> Service<Request<ReqBody>> for SentryHttpService<S>
176where
177    S: Service<Request<ReqBody>, Response = Response<ResBody>>,
178{
179    type Response = S::Response;
180    type Error = S::Error;
181    type Future = SentryHttpFuture<S::Future>;
182
183    fn poll_ready(&mut self, cx: &mut Context<'_>) -> Poll<Result<(), Self::Error>> {
184        self.service.poll_ready(cx)
185    }
186
187    fn call(&mut self, request: Request<ReqBody>) -> Self::Future {
188        let sentry_req = sentry_core::protocol::Request {
189            method: Some(request.method().to_string()),
190            url: get_url_from_request(&request).map(scrub_pii_from_url),
191            headers: request
192                .headers()
193                .into_iter()
194                .filter(|(_, value)| !value.is_sensitive())
195                .filter(|(header, _)| self.with_pii || !is_sensitive_header(header.as_str()))
196                .map(|(header, value)| {
197                    (
198                        header.to_string(),
199                        value.to_str().unwrap_or_default().into(),
200                    )
201                })
202                .collect(),
203            ..Default::default()
204        };
205        let trx_ctx = if self.start_transaction {
206            let headers = request.headers().into_iter().flat_map(|(header, value)| {
207                value.to_str().ok().map(|value| (header.as_str(), value))
208            });
209            let tx_name = format!("{} {}", request.method(), path_from_request(&request));
210            Some(sentry_core::TransactionContext::continue_from_headers(
211                &tx_name,
212                "http.server",
213                headers,
214            ))
215        } else {
216            None
217        };
218
219        SentryHttpFuture {
220            on_first_poll: Some((sentry_req, trx_ctx)),
221            transaction: None,
222            future: self.service.call(request),
223        }
224    }
225}
226
227fn path_from_request<B>(request: &Request<B>) -> &str {
228    #[cfg(feature = "axum-matched-path")]
229    if let Some(matched_path) = request.extensions().get::<axum::extract::MatchedPath>() {
230        return matched_path.as_str();
231    }
232
233    request.uri().path()
234}
235
236fn map_status(status: StatusCode) -> protocol::SpanStatus {
237    match status {
238        StatusCode::UNAUTHORIZED => protocol::SpanStatus::Unauthenticated,
239        StatusCode::FORBIDDEN => protocol::SpanStatus::PermissionDenied,
240        StatusCode::NOT_FOUND => protocol::SpanStatus::NotFound,
241        StatusCode::TOO_MANY_REQUESTS => protocol::SpanStatus::ResourceExhausted,
242        status if status.is_client_error() => protocol::SpanStatus::InvalidArgument,
243        StatusCode::NOT_IMPLEMENTED => protocol::SpanStatus::Unimplemented,
244        StatusCode::SERVICE_UNAVAILABLE => protocol::SpanStatus::Unavailable,
245        status if status.is_server_error() => protocol::SpanStatus::InternalError,
246        StatusCode::CONFLICT => protocol::SpanStatus::AlreadyExists,
247        status if status.is_success() => protocol::SpanStatus::Ok,
248        _ => protocol::SpanStatus::UnknownError,
249    }
250}
251
252fn get_url_from_request<B>(request: &Request<B>) -> Option<url::Url> {
253    let uri = request.uri().clone();
254    let mut uri_parts = uri.into_parts();
255    uri_parts.scheme.get_or_insert(uri::Scheme::HTTP);
256    if uri_parts.authority.is_none() {
257        let host = request.headers().get(header::HOST)?.as_bytes();
258        uri_parts.authority = Some(host.try_into().ok()?);
259    }
260    let uri = uri::Uri::from_parts(uri_parts).ok()?;
261    uri.to_string().parse().ok()
262}
263
264#[cfg(test)]
265mod tests {
266    use sentry_core::protocol::{Context, EnvelopeItem, TraceContext};
267    use sentry_core::{ClientOptions, Envelope};
268
269    use super::*;
270
271    fn trace_context_from_single_transaction(envelopes: &[Envelope]) -> TraceContext {
272        let [envelope] = envelopes else {
273            panic!("Expected exactly one envelope");
274        };
275
276        let mut items = envelope.items();
277        let Some(EnvelopeItem::Transaction(transaction)) = items.next() else {
278            panic!("Expected a transaction envelope item");
279        };
280        assert!(items.next().is_none(), "expected only one envelope item");
281
282        match transaction.contexts.get("trace") {
283            Some(Context::Trace(trace)) => *trace.clone(),
284            unexpected => panic!("expected trace context, got {unexpected:#?}"),
285        }
286    }
287
288    fn run_request_with_org_ids(incoming_org_id: &str, client_org_id: &str) -> Vec<Envelope> {
289        sentry::test::with_captured_envelopes_options(
290            || {
291                tokio::runtime::Runtime::new().unwrap().block_on(async {
292                    let mut service =
293                        SentryHttpLayer::new()
294                            .enable_transaction()
295                            .layer(tower::service_fn(|_request| async {
296                                Ok::<_, std::convert::Infallible>(Response::new(()))
297                            }));
298                    let request = Request::builder()
299                        .uri("http://example.com/test")
300                        .header(
301                            "sentry-trace",
302                            "09e04486820349518ac7b5d2adbf6ba5-9cf635fa5b870b3a-1",
303                        )
304                        .header("baggage", format!("sentry-org_id={incoming_org_id}"))
305                        .body(())
306                        .unwrap();
307
308                    service.call(request).await.unwrap();
309                });
310            },
311            ClientOptions::new()
312                .org_id(client_org_id.parse().unwrap())
313                .strict_trace_continuation(true)
314                .traces_sample_rate(1.0),
315        )
316    }
317
318    #[test]
319    fn transaction_continues_matching_org_id() {
320        let envelopes = run_request_with_org_ids("42", "42");
321        let trace = trace_context_from_single_transaction(&envelopes);
322
323        assert_eq!(
324            trace.trace_id.to_string(),
325            "09e04486820349518ac7b5d2adbf6ba5"
326        );
327        assert_eq!(
328            trace.parent_span_id.map(|span_id| span_id.to_string()),
329            Some("9cf635fa5b870b3a".to_owned())
330        );
331    }
332
333    #[test]
334    fn transaction_rejects_mismatched_org_id() {
335        let envelopes = run_request_with_org_ids("43", "42");
336        let trace = trace_context_from_single_transaction(&envelopes);
337
338        assert_ne!(
339            trace.trace_id.to_string(),
340            "09e04486820349518ac7b5d2adbf6ba5"
341        );
342        assert_eq!(trace.parent_span_id, None);
343    }
344}