1use conduit::{Host, RequestExt, Scheme, StatusCode};
2use conduit_middleware::{AfterResult, BeforeResult, Middleware};
3use sentry_core::protocol::{ClientSdkPackage, Event, Request, SessionStatus, SpanStatus};
4use sentry_core::{Hub, ScopeGuard, TransactionOrSpan};
5use std::borrow::Cow;
6
7pub struct SentryMiddleware {
8 start_transactions: bool,
9 track_sessions: bool,
10 with_pii: bool,
11}
12
13impl Default for SentryMiddleware {
14 fn default() -> Self {
15 let (with_pii, track_sessions) = Hub::with_active(|hub| {
18 let client = hub.client();
19
20 let with_pii = client
21 .as_ref()
22 .map_or(false, |client| client.options().send_default_pii);
23
24 let track_sessions = client.as_ref().map_or(false, |client| {
25 let options = client.options();
26 options.auto_session_tracking
27 && options.session_mode == sentry_core::SessionMode::Request
28 });
29
30 (with_pii, track_sessions)
31 });
32
33 SentryMiddleware {
34 start_transactions: false,
35 track_sessions,
36 with_pii,
37 }
38 }
39}
40
41impl SentryMiddleware {
42 pub fn new() -> SentryMiddleware {
43 Default::default()
44 }
45
46 pub fn with_transactions() -> SentryMiddleware {
47 SentryMiddleware {
48 start_transactions: true,
49 ..SentryMiddleware::default()
50 }
51 }
52}
53
54impl Middleware for SentryMiddleware {
55 fn before(&self, req: &mut dyn RequestExt) -> BeforeResult {
56 let scope = Hub::with_active(|hub| hub.push_scope());
59
60 if self.track_sessions {
62 sentry_core::start_session();
63 }
64
65 let sentry_req = sentry_request_from_http(req, self.with_pii);
67
68 if self.start_transactions {
69 let name = req.path();
71
72 let headers = req.headers().iter().flat_map(|(header, value)| {
74 value.to_str().ok().map(|value| (header.as_str(), value))
75 });
76
77 let ctx = sentry_core::TransactionContext::continue_from_headers(
79 name,
80 "http.server",
81 headers,
82 );
83
84 let transaction = sentry_core::start_transaction(ctx);
86
87 transaction.set_request(sentry_req.clone());
89
90 sentry_core::configure_scope(|scope| scope.set_span(Some(transaction.into())));
92 }
93
94 sentry_core::configure_scope(|scope| {
96 scope.add_event_processor(Box::new(move |event| {
97 Some(process_event(event, &sentry_req))
98 }));
99 });
100
101 req.mut_extensions().insert(scope);
103
104 Ok(())
105 }
106
107 fn after(&self, req: &mut dyn RequestExt, result: AfterResult) -> AfterResult {
108 if let Some(scope) = req.mut_extensions().remove::<ScopeGuard>() {
109 #[cfg(feature = "router")]
110 {
111 sentry_core::configure_scope(|scope| {
112 use conduit_router::RoutePattern;
116
117 let transaction = req
118 .extensions()
119 .get::<RoutePattern>()
120 .map(|pattern| pattern.pattern());
121
122 scope.set_transaction(transaction);
123 });
124 }
125
126 if let Some(TransactionOrSpan::Transaction(transaction)) =
128 sentry_core::configure_scope(|scope| scope.get_span())
129 {
130 if transaction.get_status().is_none() {
133 let status = result
134 .as_ref()
135 .map(|res| map_status(res.status()))
136 .unwrap_or(SpanStatus::UnknownError);
137 transaction.set_status(status);
138 }
139
140 transaction.finish();
142 }
143
144 if let Err(error) = &result {
146 sentry_core::capture_error(error.as_ref());
147 }
148
149 if self.track_sessions {
151 let status = match &result {
152 Ok(_) => SessionStatus::Exited,
153 Err(_) => SessionStatus::Abnormal,
154 };
155 sentry_core::end_session_with_status(status);
156 }
157
158 drop(scope);
160 }
161
162 result
163 }
164}
165
166fn sentry_request_from_http(request: &dyn RequestExt, with_pii: bool) -> Request {
168 let method = Some(request.method().to_string());
169
170 let scheme = match request.scheme() {
171 Scheme::Http => "http",
172 Scheme::Https => "https",
173 };
174
175 let host = match request.host() {
176 Host::Name(name) => Cow::from(name),
177 Host::Socket(addr) => Cow::from(addr.to_string()),
178 };
179
180 let path = request.path();
181
182 let mut url = format!("{}://{}{}", scheme, host, path);
183
184 if let Some(query_string) = request.query_string() {
185 url += "?";
186 url += query_string;
187 }
188
189 let headers = request
190 .headers()
191 .iter()
192 .filter(|(_name, value)| !value.is_sensitive())
193 .map(|(k, v)| (k.to_string(), v.to_str().unwrap_or_default().to_string()))
194 .collect();
195
196 let mut sentry_req = Request {
197 url: url.parse().ok(),
198 method,
199 headers,
200 ..Default::default()
201 };
202
203 if with_pii {
205 let remote_addr = request.remote_addr().to_string();
206 sentry_req.env.insert("REMOTE_ADDR".into(), remote_addr);
207 };
208
209 sentry_req
210}
211
212fn map_status(status: StatusCode) -> SpanStatus {
214 match status {
215 StatusCode::UNAUTHORIZED => SpanStatus::Unauthenticated,
216 StatusCode::FORBIDDEN => SpanStatus::PermissionDenied,
217 StatusCode::NOT_FOUND => SpanStatus::NotFound,
218 StatusCode::TOO_MANY_REQUESTS => SpanStatus::ResourceExhausted,
219 StatusCode::CONFLICT => SpanStatus::AlreadyExists,
220 StatusCode::NOT_IMPLEMENTED => SpanStatus::Unimplemented,
221 StatusCode::SERVICE_UNAVAILABLE => SpanStatus::Unavailable,
222 status if status.is_informational() => SpanStatus::Ok,
223 status if status.is_success() => SpanStatus::Ok,
224 status if status.is_redirection() => SpanStatus::Ok,
225 status if status.is_client_error() => SpanStatus::InvalidArgument,
226 status if status.is_server_error() => SpanStatus::InternalError,
227 _ => SpanStatus::UnknownError,
228 }
229}
230
231fn process_event(mut event: Event<'static>, request: &Request) -> Event<'static> {
233 if event.request.is_none() {
235 event.request = Some(request.clone());
236 }
237
238 if let Some(sdk) = event.sdk.take() {
240 let mut sdk = sdk.into_owned();
241 sdk.packages.push(ClientSdkPackage {
242 name: "sentry-conduit".into(),
243 version: env!("CARGO_PKG_VERSION").into(),
244 });
245 event.sdk = Some(Cow::Owned(sdk));
246 }
247
248 event
249}