autopush_common/middleware/
sentry.rs1use 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 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 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 !reportable_err.is_sentry_event() {
108 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 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
148fn 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
165fn 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
183fn 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#[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 Some(event)
218}
219
220fn event_from_actix_error<E>(error: &actix_web::Error) -> sentry::protocol::Event<'static>
224where
225 E: ReportableError + actix_web::ResponseError + 'static,
226{
227 if let Some(reportable_err) = error.as_error::<E>() {
230 crate::sentry::event_from_error(reportable_err)
232 } else {
233 sentry::event_from_error(error)
235 }
236}