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::protocol;
9use tower_layer::Layer;
10use tower_service::Service;
11
12/// Tower Layer that logs Http Request Headers.
13///
14/// The Service created by this Layer can also optionally start a new
15/// performance monitoring transaction for each incoming request,
16/// continuing the trace based on incoming distributed tracing headers.
17///
18/// The created transaction will automatically use the request URI as its name.
19/// This is sometimes not desirable in case the request URI contains unique IDs
20/// or similar. In this case, users should manually override the transaction name
21/// in the request handler using the [`Scope::set_transaction`](sentry_core::Scope::set_transaction)
22/// method.
23#[derive(Clone, Default)]
24pub struct SentryHttpLayer {
25    start_transaction: bool,
26}
27
28impl SentryHttpLayer {
29    /// Creates a new Layer that only logs Request Headers.
30    pub fn new() -> Self {
31        Self::default()
32    }
33
34    /// Creates a new Layer which starts a new performance monitoring transaction
35    /// for each incoming request.
36    pub fn with_transaction() -> Self {
37        Self {
38            start_transaction: true,
39        }
40    }
41}
42
43/// Tower Service that logs Http Request Headers.
44///
45/// The Service can also optionally start a new performance monitoring transaction
46/// for each incoming request, continuing the trace based on incoming
47/// distributed tracing headers.
48#[derive(Clone)]
49pub struct SentryHttpService<S> {
50    service: S,
51    start_transaction: bool,
52}
53
54impl<S> Layer<S> for SentryHttpLayer {
55    type Service = SentryHttpService<S>;
56
57    fn layer(&self, service: S) -> Self::Service {
58        Self::Service {
59            service,
60            start_transaction: self.start_transaction,
61        }
62    }
63}
64
65/// The Future returned from [`SentryHttpService`].
66#[pin_project::pin_project(PinnedDrop)]
67pub struct SentryHttpFuture<F> {
68    on_first_poll: Option<(
69        sentry_core::protocol::Request,
70        Option<sentry_core::TransactionContext>,
71    )>,
72    transaction: Option<(
73        sentry_core::TransactionOrSpan,
74        Option<sentry_core::TransactionOrSpan>,
75    )>,
76    #[pin]
77    future: F,
78}
79
80impl<F, ResBody, Error> Future for SentryHttpFuture<F>
81where
82    F: Future<Output = Result<Response<ResBody>, Error>>,
83{
84    type Output = F::Output;
85
86    fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output> {
87        let slf = self.project();
88        if let Some((sentry_req, trx_ctx)) = slf.on_first_poll.take() {
89            sentry_core::configure_scope(|scope| {
90                if let Some(trx_ctx) = trx_ctx {
91                    let transaction: sentry_core::TransactionOrSpan =
92                        sentry_core::start_transaction(trx_ctx).into();
93                    transaction.set_request(sentry_req.clone());
94                    let parent_span = scope.get_span();
95                    scope.set_span(Some(transaction.clone()));
96                    *slf.transaction = Some((transaction, parent_span));
97                }
98
99                scope.add_event_processor(move |mut event| {
100                    if event.request.is_none() {
101                        event.request = Some(sentry_req.clone());
102                    }
103                    Some(event)
104                });
105            });
106        }
107        match slf.future.poll(cx) {
108            Poll::Ready(res) => {
109                if let Some((transaction, parent_span)) = slf.transaction.take() {
110                    if transaction.get_status().is_none() {
111                        let status = match &res {
112                            Ok(res) => map_status(res.status()),
113                            Err(_) => protocol::SpanStatus::UnknownError,
114                        };
115                        transaction.set_status(status);
116                    }
117                    transaction.finish();
118                    sentry_core::configure_scope(|scope| scope.set_span(parent_span));
119                }
120                Poll::Ready(res)
121            }
122            Poll::Pending => Poll::Pending,
123        }
124    }
125}
126
127#[pinned_drop]
128impl<F> PinnedDrop for SentryHttpFuture<F> {
129    fn drop(self: Pin<&mut Self>) {
130        let slf = self.project();
131
132        // If the future gets dropped without being polled to completion,
133        // still finish the transaction to make sure this is not lost.
134        if let Some((transaction, parent_span)) = slf.transaction.take() {
135            if transaction.get_status().is_none() {
136                transaction.set_status(protocol::SpanStatus::Aborted);
137            }
138            transaction.finish();
139            sentry_core::configure_scope(|scope| scope.set_span(parent_span));
140        }
141    }
142}
143
144impl<S, ReqBody, ResBody> Service<Request<ReqBody>> for SentryHttpService<S>
145where
146    S: Service<Request<ReqBody>, Response = Response<ResBody>>,
147{
148    type Response = S::Response;
149    type Error = S::Error;
150    type Future = SentryHttpFuture<S::Future>;
151
152    fn poll_ready(&mut self, cx: &mut Context<'_>) -> Poll<Result<(), Self::Error>> {
153        self.service.poll_ready(cx)
154    }
155
156    fn call(&mut self, request: Request<ReqBody>) -> Self::Future {
157        let sentry_req = sentry_core::protocol::Request {
158            method: Some(request.method().to_string()),
159            url: get_url_from_request(&request),
160            headers: request
161                .headers()
162                .into_iter()
163                .filter(|(_, value)| !value.is_sensitive())
164                .map(|(header, value)| {
165                    (
166                        header.to_string(),
167                        value.to_str().unwrap_or_default().into(),
168                    )
169                })
170                .collect(),
171            ..Default::default()
172        };
173        let trx_ctx = if self.start_transaction {
174            let headers = request.headers().into_iter().flat_map(|(header, value)| {
175                value.to_str().ok().map(|value| (header.as_str(), value))
176            });
177            let tx_name = format!("{} {}", request.method(), path_from_request(&request));
178            Some(sentry_core::TransactionContext::continue_from_headers(
179                &tx_name,
180                "http.server",
181                headers,
182            ))
183        } else {
184            None
185        };
186
187        SentryHttpFuture {
188            on_first_poll: Some((sentry_req, trx_ctx)),
189            transaction: None,
190            future: self.service.call(request),
191        }
192    }
193}
194
195fn path_from_request<B>(request: &Request<B>) -> &str {
196    #[cfg(feature = "axum-matched-path")]
197    if let Some(matched_path) = request.extensions().get::<axum::extract::MatchedPath>() {
198        return matched_path.as_str();
199    }
200
201    request.uri().path()
202}
203
204fn map_status(status: StatusCode) -> protocol::SpanStatus {
205    match status {
206        StatusCode::UNAUTHORIZED => protocol::SpanStatus::Unauthenticated,
207        StatusCode::FORBIDDEN => protocol::SpanStatus::PermissionDenied,
208        StatusCode::NOT_FOUND => protocol::SpanStatus::NotFound,
209        StatusCode::TOO_MANY_REQUESTS => protocol::SpanStatus::ResourceExhausted,
210        status if status.is_client_error() => protocol::SpanStatus::InvalidArgument,
211        StatusCode::NOT_IMPLEMENTED => protocol::SpanStatus::Unimplemented,
212        StatusCode::SERVICE_UNAVAILABLE => protocol::SpanStatus::Unavailable,
213        status if status.is_server_error() => protocol::SpanStatus::InternalError,
214        StatusCode::CONFLICT => protocol::SpanStatus::AlreadyExists,
215        status if status.is_success() => protocol::SpanStatus::Ok,
216        _ => protocol::SpanStatus::UnknownError,
217    }
218}
219
220fn get_url_from_request<B>(request: &Request<B>) -> Option<url::Url> {
221    let uri = request.uri().clone();
222    let mut uri_parts = uri.into_parts();
223    uri_parts.scheme.get_or_insert(uri::Scheme::HTTP);
224    if uri_parts.authority.is_none() {
225        let host = request.headers().get(header::HOST)?.as_bytes();
226        uri_parts.authority = Some(host.try_into().ok()?);
227    }
228    let uri = uri::Uri::from_parts(uri_parts).ok()?;
229    uri.to_string().parse().ok()
230}