use std::{cell::RefCell, marker::PhantomData, rc::Rc, sync::Arc};
use actix_web::{
dev::{Service, ServiceRequest, ServiceResponse, Transform},
Error, HttpMessage,
};
use cadence::{CountedExt, StatsdClient};
use futures::{future::LocalBoxFuture, FutureExt};
use futures_util::future::{ok, Ready};
use sentry::{protocol::Event, Hub};
use crate::{errors::ReportableError, tags::Tags};
#[derive(Clone)]
pub struct SentryWrapper<E> {
metrics: Arc<StatsdClient>,
metric_label: String,
phantom: PhantomData<E>,
}
impl<E> SentryWrapper<E> {
pub fn new(metrics: Arc<StatsdClient>, metric_label: String) -> Self {
Self {
metrics,
metric_label,
phantom: PhantomData,
}
}
}
impl<S, B, E> Transform<S, ServiceRequest> for SentryWrapper<E>
where
S: Service<ServiceRequest, Response = ServiceResponse<B>, Error = Error>,
S::Future: 'static,
E: ReportableError + actix_web::ResponseError + 'static,
{
type Response = ServiceResponse<B>;
type Error = Error;
type Transform = SentryWrapperMiddleware<S, E>;
type InitError = ();
type Future = Ready<Result<Self::Transform, Self::InitError>>;
fn new_transform(&self, service: S) -> Self::Future {
ok(SentryWrapperMiddleware {
service: Rc::new(RefCell::new(service)),
metrics: self.metrics.clone(),
metric_label: self.metric_label.clone(),
phantom: PhantomData,
})
}
}
#[derive(Debug)]
pub struct SentryWrapperMiddleware<S, E> {
service: Rc<RefCell<S>>,
metrics: Arc<StatsdClient>,
metric_label: String,
phantom: PhantomData<E>,
}
impl<S, B, E> Service<ServiceRequest> for SentryWrapperMiddleware<S, E>
where
S: Service<ServiceRequest, Response = ServiceResponse<B>, Error = Error>,
S::Future: 'static,
E: ReportableError + actix_web::ResponseError + 'static,
{
type Response = ServiceResponse<B>;
type Error = Error;
type Future = LocalBoxFuture<'static, Result<Self::Response, Self::Error>>;
actix_web::dev::forward_ready!(service);
fn call(&self, sreq: ServiceRequest) -> Self::Future {
let hub = Hub::new_from_top(Hub::main());
let _ = hub.push_scope();
let sentry_request = sentry_request_from_http(&sreq);
hub.configure_scope(|scope| {
scope.add_event_processor(Box::new(move |event| process_event(event, &sentry_request)))
});
let mut tags = Tags::from_request_head(sreq.head());
let metrics = self.metrics.clone();
let metric_label = self.metric_label.clone();
if let Some(rtags) = sreq.request().extensions().get::<Tags>() {
trace!("Sentry: found tags in request: {:?}", &rtags.tags);
for (k, v) in rtags.tags.clone() {
tags.tags.insert(k, v);
}
};
sreq.extensions_mut().insert(tags.clone());
let fut = self.service.call(sreq);
async move {
let response: Self::Response = match fut.await {
Ok(response) => response,
Err(error) => {
if let Some(reportable_err) = error.as_error::<E>() {
if !reportable_err.is_sentry_event() {
if let Some(label) = reportable_err.metric_label() {
info!("Sentry: Sending error to metrics: {:?}", reportable_err);
let _ = metrics.incr(&format!("{}.{}", metric_label, label));
}
debug!("Sentry: Not reporting error (service error): {:?}", error);
return Err(error);
}
};
debug!("Reporting error to Sentry (service error): {}", error);
let event = event_from_actix_error::<E>(&error);
let event_id = hub.capture_event(event);
trace!("event_id = {}", event_id);
return Err(error);
}
};
if let Some(error) = response.response().error() {
if let Some(reportable_err) = error.as_error::<E>() {
if !reportable_err.is_sentry_event() {
if let Some(label) = reportable_err.metric_label() {
info!("Sentry: Sending error to metrics: {:?}", reportable_err);
let _ = metrics.incr(&format!("{}.{}", metric_label, label));
}
debug!("Not reporting error (service error): {:?}", error);
return Ok(response);
}
}
debug!("Reporting error to Sentry (response error): {}", error);
let event = event_from_actix_error::<E>(error);
let event_id = hub.capture_event(event);
trace!("event_id = {}", event_id);
}
Ok(response)
}
.boxed_local()
}
}
fn sentry_request_from_http(request: &ServiceRequest) -> sentry::protocol::Request {
sentry::protocol::Request {
url: format!(
"{}://{}{}",
request.connection_info().scheme(),
request.connection_info().host(),
request.uri()
)
.parse()
.ok(),
method: Some(request.method().to_string()),
headers: request
.headers()
.iter()
.map(|(k, v)| (k.to_string(), v.to_str().unwrap_or_default().to_string()))
.collect(),
..Default::default()
}
}
#[allow(clippy::unnecessary_wraps)]
fn process_event(
mut event: Event<'static>,
request: &sentry::protocol::Request,
) -> Option<Event<'static>> {
if event.request.is_none() {
event.request = Some(request.clone());
}
Some(event)
}
fn event_from_actix_error<E>(error: &actix_web::Error) -> sentry::protocol::Event<'static>
where
E: ReportableError + actix_web::ResponseError + 'static,
{
if let Some(reportable_err) = error.as_error::<E>() {
crate::sentry::event_from_error(reportable_err)
} else {
sentry::event_from_error(error)
}
}