sentry_conduit/
lib.rs

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        // Read `send_default_pii` and `auto_session_tracking` options from
16        // Sentry configuration by default
17        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        // Push a `Scope` to the stack so that all further `configure_scope()`
57        // calls are scoped to this specific request.
58        let scope = Hub::with_active(|hub| hub.push_scope());
59
60        // Start a `Session`, if session tracking is enabled
61        if self.track_sessions {
62            sentry_core::start_session();
63        }
64
65        // Extract HTTP request information from `req`
66        let sentry_req = sentry_request_from_http(req, self.with_pii);
67
68        if self.start_transactions {
69            // Request path will be taken as the default transaction name
70            let name = req.path();
71
72            // Create a HTTP headers iterator that Sentry can ingest
73            let headers = req.headers().iter().flat_map(|(header, value)| {
74                value.to_str().ok().map(|value| (header.as_str(), value))
75            });
76
77            // Create a new transaction context
78            let ctx = sentry_core::TransactionContext::continue_from_headers(
79                name,
80                "http.server",
81                headers,
82            );
83
84            // ... and turn it into a `Transaction`
85            let transaction = sentry_core::start_transaction(ctx);
86
87            // Attach HTTP request data to the transaction
88            transaction.set_request(sentry_req.clone());
89
90            // Configure Sentry to use the `Transaction` for this request
91            sentry_core::configure_scope(|scope| scope.set_span(Some(transaction.into())));
92        }
93
94        // .Configure Sentry to use the HTTP request data for issue reports
95        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        // Save the `ScopeGuard` in the request to ensure that it's not dropped yet
102        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                    // unfortunately, `RoutePattern` is only available in the `after` handler
113                    // so we can't add the `transaction` field to any captures that happen
114                    // before this is called.
115                    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 a `Transaction` was started for this request ...
127            if let Some(TransactionOrSpan::Transaction(transaction)) =
128                sentry_core::configure_scope(|scope| scope.get_span())
129            {
130                // Set the HTTP response status code on the transaction
131                // if it has not been set yet.
132                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                // Finish the `Transaction` (aka. send it to Sentry)
141                transaction.finish();
142            }
143
144            // Capture `Err` results as errors
145            if let Err(error) = &result {
146                sentry_core::capture_error(error.as_ref());
147            }
148
149            // End the `Session`, if session tracking is enabled
150            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            // Explicitly drop the `Scope` (technically unnecessary)
159            drop(scope);
160        }
161
162        result
163    }
164}
165
166/// Build a Sentry request struct from the HTTP request
167fn 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 PII is enabled, include the remote address
204    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
212/// Turn `http::StatusCode` into a type that Sentry understands.
213fn 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
231/// Add request data to a Sentry event
232fn process_event(mut event: Event<'static>, request: &Request) -> Event<'static> {
233    // Request
234    if event.request.is_none() {
235        event.request = Some(request.clone());
236    }
237
238    // SDK
239    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}