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#[derive(Clone, Default)]
24pub struct SentryHttpLayer {
25 start_transaction: bool,
26}
27
28impl SentryHttpLayer {
29 pub fn new() -> Self {
31 Self::default()
32 }
33
34 pub fn with_transaction() -> Self {
37 Self {
38 start_transaction: true,
39 }
40 }
41}
42
43#[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#[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 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}