autopush_common/middleware/
sentry.rs

1use std::{cell::RefCell, marker::PhantomData, rc::Rc, sync::Arc};
2
3use actix_web::{
4    dev::{Service, ServiceRequest, ServiceResponse, Transform},
5    Error, HttpMessage,
6};
7use cadence::{CountedExt, StatsdClient};
8use futures::{future::LocalBoxFuture, FutureExt};
9use futures_util::future::{ok, Ready};
10use sentry::{protocol::Event, Hub};
11
12use crate::{errors::ReportableError, tags::Tags};
13
14#[derive(Clone)]
15pub struct SentryWrapper<E> {
16    metrics: Arc<StatsdClient>,
17    metric_label: String,
18    phantom: PhantomData<E>,
19    sentry_disabled: bool,
20}
21
22impl<E> SentryWrapper<E> {
23    pub fn new(metrics: Arc<StatsdClient>, metric_label: String, sentry_disabled: bool) -> Self {
24        Self {
25            metrics,
26            metric_label,
27            phantom: PhantomData,
28            sentry_disabled,
29        }
30    }
31}
32
33impl<S, B, E> Transform<S, ServiceRequest> for SentryWrapper<E>
34where
35    S: Service<ServiceRequest, Response = ServiceResponse<B>, Error = Error>,
36    S::Future: 'static,
37    E: ReportableError + actix_web::ResponseError + 'static,
38{
39    type Response = ServiceResponse<B>;
40    type Error = Error;
41    type Transform = SentryWrapperMiddleware<S, E>;
42    type InitError = ();
43    type Future = Ready<Result<Self::Transform, Self::InitError>>;
44
45    fn new_transform(&self, service: S) -> Self::Future {
46        ok(SentryWrapperMiddleware {
47            service: Rc::new(RefCell::new(service)),
48            metrics: self.metrics.clone(),
49            metric_label: self.metric_label.clone(),
50            phantom: PhantomData,
51            sentry_disabled: self.sentry_disabled,
52        })
53    }
54}
55
56#[derive(Debug)]
57pub struct SentryWrapperMiddleware<S, E> {
58    service: Rc<RefCell<S>>,
59    metrics: Arc<StatsdClient>,
60    metric_label: String,
61    phantom: PhantomData<E>,
62    sentry_disabled: bool,
63}
64
65impl<S, B, E> Service<ServiceRequest> for SentryWrapperMiddleware<S, E>
66where
67    S: Service<ServiceRequest, Response = ServiceResponse<B>, Error = Error>,
68    S::Future: 'static,
69    E: ReportableError + actix_web::ResponseError + 'static,
70{
71    type Response = ServiceResponse<B>;
72    type Error = Error;
73    type Future = LocalBoxFuture<'static, Result<Self::Response, Self::Error>>;
74
75    actix_web::dev::forward_ready!(service);
76
77    /// Set up the hub to add request data to events
78    fn call(&self, sreq: ServiceRequest) -> Self::Future {
79        let hub = Hub::new_from_top(Hub::main());
80        let _ = hub.push_scope();
81        let sentry_request = sentry_request_from_http(&sreq);
82        hub.configure_scope(|scope| {
83            scope.add_event_processor(Box::new(move |event| process_event(event, &sentry_request)))
84        });
85
86        // get the tag information
87        let mut tags = Tags::from_request_head(sreq.head());
88        let metrics = self.metrics.clone();
89        let metric_label = self.metric_label.clone();
90        let sentry_disabled = self.sentry_disabled;
91        if let Some(rtags) = sreq.request().extensions().get::<Tags>() {
92            trace!("Sentry: found tags in request: {:?}", &rtags.tags);
93            for (k, v) in rtags.tags.clone() {
94                tags.tags.insert(k, v);
95            }
96        };
97        sreq.extensions_mut().insert(tags.clone());
98
99        let fut = self.service.call(sreq);
100
101        async move {
102            let response: Self::Response = match fut.await {
103                Ok(response) => response,
104                Err(error) => {
105                    if let Some(reportable_err) = error.as_error::<E>() {
106                        // if it's not reportable, and we have access to the metrics, record it as a metric.
107                        if !reportable_err.is_sentry_event() {
108                            // The error (e.g. VapidErrorKind::InvalidKey(String)) might be too cardinal,
109                            // but we may need that information to debug a production issue. We can
110                            // add an info here, temporarily turn on info level debugging on a given server,
111                            // capture it, and then turn it off before we run out of money.
112                            maybe_emit_metrics(&metrics, &metric_label, reportable_err);
113                            debug!("Sentry: Not reporting error (service error): {:?}", error);
114                            return Err(error);
115                        }
116                    };
117                    debug!("Reporting error to Sentry (service error): {}", error);
118                    let event = event_from_actix_error::<E>(&error);
119                    if sentry_disabled {
120                        log_event(&event);
121                    } else {
122                        let event_id = hub.capture_event(event);
123                        trace!("event_id = {}", event_id);
124                    }
125                    return Err(error);
126                }
127            };
128            // Check for errors inside the response
129            if let Some(error) = response.response().error() {
130                if let Some(reportable_err) = error.as_error::<E>() {
131                    if !reportable_err.is_sentry_event() {
132                        maybe_emit_metrics(&metrics, &metric_label, reportable_err);
133                        debug!("Not reporting error (service error): {:?}", error);
134                        return Ok(response);
135                    }
136                }
137                debug!("Reporting error to Sentry (response error): {}", error);
138                let event = event_from_actix_error::<E>(error);
139                let event_id = hub.capture_event(event);
140                trace!("event_id = {}", event_id);
141            }
142            Ok(response)
143        }
144        .boxed_local()
145    }
146}
147
148/// Log the Sentry event to STDERR. (This is copied from syncstorage-rs)
149fn log_event(event: &Event) {
150    let exception = event.exception.last();
151    let error_type = exception.map_or("UnknownError", |e| e.ty.as_str());
152    let error_value = exception
153        .and_then(|e| e.value.as_deref())
154        .unwrap_or("No error message");
155    error!("{}", error_value;
156        "error_type" => error_type,
157        "tags" => ?event.tags,
158        "extra" => ?event.extra,
159        "url" => event.request.as_ref().and_then(|r| r.url.as_ref()).map(|u| u.to_string()).unwrap_or_default(),
160        "method" => event.request.as_ref().and_then(|r| r.method.as_deref()).unwrap_or_default(),
161        "stacktrace" => exception.and_then(|e| e.stacktrace.as_ref()).map(|s| format!("{:?}", s.frames)).unwrap_or_default(),
162    );
163}
164
165/// Emit metrics when a [ReportableError::metric_label] is returned
166fn maybe_emit_metrics<E>(metrics: &StatsdClient, label_prefix: &str, err: &E)
167where
168    E: ReportableError,
169{
170    let Some(label) = err.metric_label() else {
171        return;
172    };
173    debug!("Sending error to metrics: {:?}", err);
174    let label = format!("{label_prefix}.{label}");
175    let mut builder = metrics.incr_with_tags(&label);
176    let tags = err.tags();
177    for (key, val) in &tags {
178        builder = builder.with_tag(key, val);
179    }
180    builder.send();
181}
182
183/// Build a Sentry request struct from the HTTP request
184fn sentry_request_from_http(request: &ServiceRequest) -> sentry::protocol::Request {
185    sentry::protocol::Request {
186        url: format!(
187            "{}://{}{}",
188            request.connection_info().scheme(),
189            request.connection_info().host(),
190            request.uri()
191        )
192        .parse()
193        .ok(),
194        method: Some(request.method().to_string()),
195        headers: request
196            .headers()
197            .iter()
198            .map(|(k, v)| (k.to_string(), v.to_str().unwrap_or_default().to_string()))
199            .collect(),
200        ..Default::default()
201    }
202}
203
204/// Add request data to a Sentry event
205#[allow(clippy::unnecessary_wraps)]
206fn process_event(
207    mut event: Event<'static>,
208    request: &sentry::protocol::Request,
209) -> Option<Event<'static>> {
210    if event.request.is_none() {
211        event.request = Some(request.clone());
212    }
213
214    // TODO: Use ServiceRequest::match_pattern for the event transaction.
215    //       Coming in Actix v3.
216
217    Some(event)
218}
219
220/// Convert Actix errors into a Sentry event. ReportableError is handled
221/// explicitly so the event can include a backtrace and source error
222/// information.
223fn event_from_actix_error<E>(error: &actix_web::Error) -> sentry::protocol::Event<'static>
224where
225    E: ReportableError + actix_web::ResponseError + 'static,
226{
227    // Actix errors don't have support source/cause, so to get more information
228    // about the error we need to downcast.
229    if let Some(reportable_err) = error.as_error::<E>() {
230        // Use our error and associated backtrace for the event
231        crate::sentry::event_from_error(reportable_err)
232    } else {
233        // Fallback to the Actix error
234        sentry::event_from_error(error)
235    }
236}