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