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#[derive(Clone, Default)]
28pub struct SentryHttpLayer {
29 start_transaction: bool,
30 with_pii: bool,
31}
32
33impl SentryHttpLayer {
34 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 #[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 #[must_use]
56 pub fn enable_transaction(mut self) -> Self {
57 self.start_transaction = true;
58 self
59 }
60
61 #[must_use]
63 pub fn enable_pii(mut self) -> Self {
64 self.with_pii = true;
65 self
66 }
67}
68
69#[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#[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 match &res {
142 Ok(res) => {
143 if !transaction
144 .get_trace_context()
145 .data
146 .contains_key("http.response.status_code")
147 {
148 transaction.set_data(
149 "http.response.status_code",
150 res.status().as_u16().into(),
151 );
152 }
153 if transaction.get_status().is_none() {
154 transaction.set_status(map_status(res.status()));
155 }
156 }
157 Err(_) => {
158 if transaction.get_status().is_none() {
159 transaction.set_status(protocol::SpanStatus::UnknownError);
160 }
161 }
162 }
163 transaction.finish();
164 sentry_core::configure_scope(|scope| scope.set_span(parent_span));
165 }
166 Poll::Ready(res)
167 }
168 Poll::Pending => Poll::Pending,
169 }
170 }
171}
172
173#[pinned_drop]
174impl<F> PinnedDrop for SentryHttpFuture<F> {
175 fn drop(self: Pin<&mut Self>) {
176 let slf = self.project();
177
178 if let Some((transaction, parent_span)) = slf.transaction.take() {
181 if transaction.get_status().is_none() {
182 transaction.set_status(protocol::SpanStatus::Aborted);
183 }
184 transaction.finish();
185 sentry_core::configure_scope(|scope| scope.set_span(parent_span));
186 }
187 }
188}
189
190impl<S, ReqBody, ResBody> Service<Request<ReqBody>> for SentryHttpService<S>
191where
192 S: Service<Request<ReqBody>, Response = Response<ResBody>>,
193{
194 type Response = S::Response;
195 type Error = S::Error;
196 type Future = SentryHttpFuture<S::Future>;
197
198 fn poll_ready(&mut self, cx: &mut Context<'_>) -> Poll<Result<(), Self::Error>> {
199 self.service.poll_ready(cx)
200 }
201
202 fn call(&mut self, request: Request<ReqBody>) -> Self::Future {
203 let sentry_req = sentry_core::protocol::Request {
204 method: Some(request.method().to_string()),
205 url: get_url_from_request(&request).map(scrub_pii_from_url),
206 headers: request
207 .headers()
208 .into_iter()
209 .filter(|(_, value)| !value.is_sensitive())
210 .filter(|(header, _)| self.with_pii || !is_sensitive_header(header.as_str()))
211 .map(|(header, value)| {
212 (
213 header.to_string(),
214 value.to_str().unwrap_or_default().into(),
215 )
216 })
217 .collect(),
218 ..Default::default()
219 };
220 let trx_ctx = if self.start_transaction {
221 let headers = request.headers().into_iter().flat_map(|(header, value)| {
222 value.to_str().ok().map(|value| (header.as_str(), value))
223 });
224 let tx_name = format!("{} {}", request.method(), path_from_request(&request));
225 Some(sentry_core::TransactionContext::continue_from_headers(
226 &tx_name,
227 "http.server",
228 headers,
229 ))
230 } else {
231 None
232 };
233
234 SentryHttpFuture {
235 on_first_poll: Some((sentry_req, trx_ctx)),
236 transaction: None,
237 future: self.service.call(request),
238 }
239 }
240}
241
242fn path_from_request<B>(request: &Request<B>) -> &str {
243 #[cfg(feature = "axum-matched-path")]
244 if let Some(matched_path) = request.extensions().get::<axum::extract::MatchedPath>() {
245 return matched_path.as_str();
246 }
247
248 request.uri().path()
249}
250
251fn map_status(status: StatusCode) -> protocol::SpanStatus {
252 match status {
253 StatusCode::UNAUTHORIZED => protocol::SpanStatus::Unauthenticated,
254 StatusCode::FORBIDDEN => protocol::SpanStatus::PermissionDenied,
255 StatusCode::NOT_FOUND => protocol::SpanStatus::NotFound,
256 StatusCode::TOO_MANY_REQUESTS => protocol::SpanStatus::ResourceExhausted,
257 status if status.is_client_error() => protocol::SpanStatus::InvalidArgument,
258 StatusCode::NOT_IMPLEMENTED => protocol::SpanStatus::Unimplemented,
259 StatusCode::SERVICE_UNAVAILABLE => protocol::SpanStatus::Unavailable,
260 status if status.is_server_error() => protocol::SpanStatus::InternalError,
261 StatusCode::CONFLICT => protocol::SpanStatus::AlreadyExists,
262 status if status.is_success() => protocol::SpanStatus::Ok,
263 _ => protocol::SpanStatus::UnknownError,
264 }
265}
266
267fn get_url_from_request<B>(request: &Request<B>) -> Option<url::Url> {
268 let uri = request.uri().clone();
269 let mut uri_parts = uri.into_parts();
270 uri_parts.scheme.get_or_insert(uri::Scheme::HTTP);
271 if uri_parts.authority.is_none() {
272 let host = request.headers().get(header::HOST)?.as_bytes();
273 uri_parts.authority = Some(host.try_into().ok()?);
274 }
275 let uri = uri::Uri::from_parts(uri_parts).ok()?;
276 uri.to_string().parse().ok()
277}
278
279#[cfg(test)]
280mod tests {
281 use sentry_core::protocol::{Context, EnvelopeItem, TraceContext};
282 use sentry_core::{ClientOptions, Envelope};
283
284 use super::*;
285
286 fn trace_context_from_single_transaction(envelopes: &[Envelope]) -> TraceContext {
287 let [envelope] = envelopes else {
288 panic!("Expected exactly one envelope");
289 };
290
291 let mut items = envelope.items();
292 let Some(EnvelopeItem::Transaction(transaction)) = items.next() else {
293 panic!("Expected a transaction envelope item");
294 };
295 assert!(items.next().is_none(), "expected only one envelope item");
296
297 match transaction.contexts.get("trace") {
298 Some(Context::Trace(trace)) => *trace.clone(),
299 unexpected => panic!("expected trace context, got {unexpected:#?}"),
300 }
301 }
302
303 fn run_request_with_org_ids(incoming_org_id: &str, client_org_id: &str) -> Vec<Envelope> {
304 sentry::test::with_captured_envelopes_options(
305 || {
306 tokio::runtime::Runtime::new().unwrap().block_on(async {
307 let mut service =
308 SentryHttpLayer::new()
309 .enable_transaction()
310 .layer(tower::service_fn(|_request| async {
311 Ok::<_, std::convert::Infallible>(Response::new(()))
312 }));
313 let request = Request::builder()
314 .uri("http://example.com/test")
315 .header(
316 "sentry-trace",
317 "09e04486820349518ac7b5d2adbf6ba5-9cf635fa5b870b3a-1",
318 )
319 .header("baggage", format!("sentry-org_id={incoming_org_id}"))
320 .body(())
321 .unwrap();
322
323 service.call(request).await.unwrap();
324 });
325 },
326 ClientOptions::new()
327 .org_id(client_org_id.parse().unwrap())
328 .strict_trace_continuation(true)
329 .traces_sample_rate(1.0),
330 )
331 }
332
333 #[test]
334 fn transaction_continues_matching_org_id() {
335 let envelopes = run_request_with_org_ids("42", "42");
336 let trace = trace_context_from_single_transaction(&envelopes);
337
338 assert_eq!(
339 trace.trace_id.to_string(),
340 "09e04486820349518ac7b5d2adbf6ba5"
341 );
342 assert_eq!(
343 trace.parent_span_id.map(|span_id| span_id.to_string()),
344 Some("9cf635fa5b870b3a".to_owned())
345 );
346 }
347
348 #[test]
349 fn transaction_rejects_mismatched_org_id() {
350 let envelopes = run_request_with_org_ids("43", "42");
351 let trace = trace_context_from_single_transaction(&envelopes);
352
353 assert_ne!(
354 trace.trace_id.to_string(),
355 "09e04486820349518ac7b5d2adbf6ba5"
356 );
357 assert_eq!(trace.parent_span_id, None);
358 }
359}