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}
20
21impl<E> SentryWrapper<E> {
22 pub fn new(metrics: Arc<StatsdClient>, metric_label: String) -> Self {
23 Self {
24 metrics,
25 metric_label,
26 phantom: PhantomData,
27 }
28 }
29}
30
31impl<S, B, E> Transform<S, ServiceRequest> for SentryWrapper<E>
32where
33 S: Service<ServiceRequest, Response = ServiceResponse<B>, Error = Error>,
34 S::Future: 'static,
35 E: ReportableError + actix_web::ResponseError + 'static,
36{
37 type Response = ServiceResponse<B>;
38 type Error = Error;
39 type Transform = SentryWrapperMiddleware<S, E>;
40 type InitError = ();
41 type Future = Ready<Result<Self::Transform, Self::InitError>>;
42
43 fn new_transform(&self, service: S) -> Self::Future {
44 ok(SentryWrapperMiddleware {
45 service: Rc::new(RefCell::new(service)),
46 metrics: self.metrics.clone(),
47 metric_label: self.metric_label.clone(),
48 phantom: PhantomData,
49 })
50 }
51}
52
53#[derive(Debug)]
54pub struct SentryWrapperMiddleware<S, E> {
55 service: Rc<RefCell<S>>,
56 metrics: Arc<StatsdClient>,
57 metric_label: String,
58 phantom: PhantomData<E>,
59}
60
61impl<S, B, E> Service<ServiceRequest> for SentryWrapperMiddleware<S, E>
62where
63 S: Service<ServiceRequest, Response = ServiceResponse<B>, Error = Error>,
64 S::Future: 'static,
65 E: ReportableError + actix_web::ResponseError + 'static,
66{
67 type Response = ServiceResponse<B>;
68 type Error = Error;
69 type Future = LocalBoxFuture<'static, Result<Self::Response, Self::Error>>;
70
71 actix_web::dev::forward_ready!(service);
72
73 fn call(&self, sreq: ServiceRequest) -> Self::Future {
74 let hub = Hub::new_from_top(Hub::main());
76 let _ = hub.push_scope();
77 let sentry_request = sentry_request_from_http(&sreq);
78 hub.configure_scope(|scope| {
79 scope.add_event_processor(Box::new(move |event| process_event(event, &sentry_request)))
80 });
81
82 let mut tags = Tags::from_request_head(sreq.head());
84 let metrics = self.metrics.clone();
85 let metric_label = self.metric_label.clone();
86 if let Some(rtags) = sreq.request().extensions().get::<Tags>() {
87 trace!("Sentry: found tags in request: {:?}", &rtags.tags);
88 for (k, v) in rtags.tags.clone() {
89 tags.tags.insert(k, v);
90 }
91 };
92 sreq.extensions_mut().insert(tags.clone());
93
94 let fut = self.service.call(sreq);
95
96 async move {
97 let response: Self::Response = match fut.await {
98 Ok(response) => response,
99 Err(error) => {
100 if let Some(reportable_err) = error.as_error::<E>() {
101 if !reportable_err.is_sentry_event() {
103 maybe_emit_metrics(&metrics, &metric_label, reportable_err);
108 debug!("Sentry: Not reporting error (service error): {:?}", error);
109 return Err(error);
110 }
111 };
112 debug!("Reporting error to Sentry (service error): {}", error);
113 let event = event_from_actix_error::<E>(&error);
114 let event_id = hub.capture_event(event);
115 trace!("event_id = {}", event_id);
116 return Err(error);
117 }
118 };
119 if let Some(error) = response.response().error() {
121 if let Some(reportable_err) = error.as_error::<E>() {
122 if !reportable_err.is_sentry_event() {
123 maybe_emit_metrics(&metrics, &metric_label, reportable_err);
124 debug!("Not reporting error (service error): {:?}", error);
125 return Ok(response);
126 }
127 }
128 debug!("Reporting error to Sentry (response error): {}", error);
129 let event = event_from_actix_error::<E>(error);
130 let event_id = hub.capture_event(event);
131 trace!("event_id = {}", event_id);
132 }
133 Ok(response)
134 }
135 .boxed_local()
136 }
137}
138
139fn maybe_emit_metrics<E>(metrics: &StatsdClient, label_prefix: &str, err: &E)
141where
142 E: ReportableError,
143{
144 let Some(label) = err.metric_label() else {
145 return;
146 };
147 debug!("Sending error to metrics: {:?}", err);
148 let label = format!("{label_prefix}.{label}");
149 let mut builder = metrics.incr_with_tags(&label);
150 let tags = err.tags();
151 for (key, val) in &tags {
152 builder = builder.with_tag(key, val);
153 }
154 builder.send();
155}
156
157fn sentry_request_from_http(request: &ServiceRequest) -> sentry::protocol::Request {
159 sentry::protocol::Request {
160 url: format!(
161 "{}://{}{}",
162 request.connection_info().scheme(),
163 request.connection_info().host(),
164 request.uri()
165 )
166 .parse()
167 .ok(),
168 method: Some(request.method().to_string()),
169 headers: request
170 .headers()
171 .iter()
172 .map(|(k, v)| (k.to_string(), v.to_str().unwrap_or_default().to_string()))
173 .collect(),
174 ..Default::default()
175 }
176}
177
178#[allow(clippy::unnecessary_wraps)]
180fn process_event(
181 mut event: Event<'static>,
182 request: &sentry::protocol::Request,
183) -> Option<Event<'static>> {
184 if event.request.is_none() {
185 event.request = Some(request.clone());
186 }
187
188 Some(event)
192}
193
194fn event_from_actix_error<E>(error: &actix_web::Error) -> sentry::protocol::Event<'static>
198where
199 E: ReportableError + actix_web::ResponseError + 'static,
200{
201 if let Some(reportable_err) = error.as_error::<E>() {
204 crate::sentry::event_from_error(reportable_err)
206 } else {
207 sentry::event_from_error(error)
209 }
210}