use std::sync::Arc;
use std::task::Context;
use std::task::Poll;
use actix_service::Service;
use actix_service::Transform;
use actix_web::dev::ServiceRequest;
use actix_web::dev::ServiceResponse;
use actix_web::Error;
use actix_web::HttpRequest;
use futures::future::ok;
use futures::future::Ready;
use sentry::internals::ScopeGuard;
use sentry::Hub;
pub trait ActixWebHubExt {
fn from_request(req: &HttpRequest) -> Arc<Hub>;
fn run_from_request<F: FnOnce() -> R, R>(req: &HttpRequest, f: F) -> R;
}
impl ActixWebHubExt for Hub {
fn from_request(req: &HttpRequest) -> Arc<Hub> {
let exts = req.extensions();
let context = exts.get::<SentryExtension>().unwrap();
Arc::clone(&context.hub)
}
fn run_from_request<F: FnOnce() -> R, R>(req: &HttpRequest, f: F) -> R {
let hub = Hub::from_request(req);
Hub::run(hub, f)
}
}
pub struct SentryMiddleware {
current_hub: bool,
report_code: u16,
}
impl SentryMiddleware {
pub fn new(report_code: u16) -> SentryMiddleware {
SentryMiddleware {
current_hub: false,
report_code,
}
}
pub fn with_current_hub(report_code: u16) -> SentryMiddleware {
let mut middleware = SentryMiddleware::new(report_code);
middleware.current_hub = true;
middleware
}
}
impl<S, B> Transform<S> for SentryMiddleware
where
S: Service<Request = ServiceRequest, Response = ServiceResponse<B>, Error = Error>,
S::Future: 'static,
B: 'static,
{
type Request = ServiceRequest;
type Response = ServiceResponse<B>;
type Error = Error;
type InitError = ();
type Transform = MiddlewareService<S>;
type Future = Ready<Result<Self::Transform, Self::InitError>>;
fn new_transform(&self, service: S) -> Self::Future {
ok(MiddlewareService {
current_hub: self.current_hub,
report_code: self.report_code,
service,
})
}
}
pub struct MiddlewareService<S> {
current_hub: bool,
report_code: u16,
service: S,
}
impl<S, B> Service for MiddlewareService<S>
where
S: Service<Request = ServiceRequest, Response = ServiceResponse<B>, Error = Error>,
S::Future: 'static,
B: 'static,
{
type Request = ServiceRequest;
type Response = ServiceResponse<B>;
type Error = Error;
type Future = crate::BoxedFuture<Self::Response, Self::Error>;
fn poll_ready(&mut self, ctx: &mut Context) -> Poll<Result<(), Self::Error>> {
self.service.poll_ready(ctx)
}
fn call(&mut self, mut req: ServiceRequest) -> Self::Future {
let hub = if self.current_hub {
Hub::current()
} else {
Hub::main()
};
let hub = Arc::new(Hub::new_from_top(hub));
let scope = hub.push_scope();
let request = sentry_request_context(&req);
hub.configure_scope(move |scope| {
scope.add_event_processor(Box::new(move |mut event| {
event.request = Some(request.clone());
Some(event)
}));
});
let report_code = self.report_code;
req.head_mut()
.extensions_mut()
.insert(SentryExtension { hub, scope });
let response = self.service.call(req);
Box::pin(async move {
let response = response.await?;
let sentry: Option<SentryExtension> = response.request().extensions_mut().remove();
if let Some(sentry) = sentry {
let code = response.response().status().as_u16();
if code >= report_code {
let level = sentry_level_for_code(code);
let message = response
.response()
.error()
.map(ToString::to_string)
.unwrap_or_else(|| format!("HTTP {}", response.response().status()));
sentry.hub.capture_event(sentry::protocol::Event {
level,
message: Some(message),
..Default::default()
});
}
drop(sentry.scope);
drop(sentry.hub);
}
Ok(response)
})
}
}
struct SentryExtension {
hub: Arc<Hub>,
scope: ScopeGuard,
}
fn sentry_level_for_code(code: u16) -> sentry::Level {
match code {
code if code < 400 => sentry::Level::Info,
code if code < 500 => sentry::Level::Warning,
_ => sentry::Level::Error,
}
}
fn sentry_request_context(req: &ServiceRequest) -> sentry::protocol::Request {
let url = req.match_info().get_ref().uri().to_string().parse().ok();
let headers = req
.headers()
.iter()
.map(|(key, value)| {
let key = key.to_string();
let value = value.to_str().unwrap_or("<bytes>").to_string();
(key, value)
})
.collect();
sentry::protocol::Request {
headers,
method: Some(req.method().to_string()),
query_string: Some(req.query_string().to_string()),
url,
..Default::default()
}
}
#[cfg(test)]
mod tests {
use actix_web::test::call_service;
use actix_web::test::init_service;
use actix_web::test::TestRequest;
use actix_web::web;
use actix_web::App;
use actix_web::Error;
use actix_web::HttpResponse;
use failure::err_msg;
use futures::executor::block_on;
use sentry::capture_message;
use sentry::test::with_captured_events;
use sentry::Hub;
use sentry::Level;
use super::ActixWebHubExt;
use super::SentryMiddleware;
async fn respond_500() -> Result<HttpResponse, Error> {
Err(Error::from(err_msg("test")))
}
#[actix_rt::test]
async fn capture_event() {
let mut app = init_service(
App::new()
.wrap(SentryMiddleware::with_current_hub(500))
.service(web::resource("/test").to(|req| {
Hub::run_from_request(&req, || {
capture_message("test", Level::Error);
});
HttpResponse::Ok()
})),
)
.await;
let request = TestRequest::with_uri("https://server:1234/test").to_request();
let events = with_captured_events(|| {
block_on(call_service(&mut app, request));
});
assert_eq!(events.len(), 1);
let event = events.into_iter().next().unwrap();
let request = event.request.unwrap();
assert_eq!(request.method.unwrap(), "GET");
assert_eq!(request.query_string.unwrap(), "");
assert_eq!(request.url.unwrap().to_string(), "https://server:1234/test");
}
#[actix_rt::test]
async fn capture_event_on_eror() {
let mut app = init_service(
App::new()
.wrap(SentryMiddleware::with_current_hub(500))
.service(web::resource("/test").to(respond_500)),
)
.await;
let request = TestRequest::with_uri("https://server:1234/test").to_request();
let events = with_captured_events(|| {
block_on(call_service(&mut app, request));
});
assert_eq!(events.len(), 1);
let event = events.into_iter().next().unwrap();
assert_eq!(event.message.unwrap(), "test");
}
#[actix_rt::test]
async fn capture_event_on_400() {
let mut app = init_service(
App::new()
.wrap(SentryMiddleware::with_current_hub(400))
.service(web::resource("/test").to(|| HttpResponse::BadRequest())),
)
.await;
let request = TestRequest::with_uri("https://server:1234/test").to_request();
let events = with_captured_events(|| {
block_on(call_service(&mut app, request));
});
assert_eq!(events.len(), 1);
let event = events.into_iter().next().unwrap();
assert_eq!(event.message.unwrap(), "HTTP 400 Bad Request");
}
#[actix_rt::test]
async fn capture_event_on_500() {
let mut app = init_service(
App::new()
.wrap(SentryMiddleware::with_current_hub(500))
.service(web::resource("/test").to(|| HttpResponse::InternalServerError())),
)
.await;
let request = TestRequest::with_uri("https://server:1234/test").to_request();
let events = with_captured_events(|| {
block_on(call_service(&mut app, request));
});
assert_eq!(events.len(), 1);
let event = events.into_iter().next().unwrap();
assert_eq!(event.message.unwrap(), "HTTP 500 Internal Server Error");
}
#[actix_rt::test]
async fn main_hub_misses_test_events() {
let mut app = init_service(App::new().wrap(SentryMiddleware::new(500)).service(
web::resource("/test").to(|req| {
Hub::run_from_request(&req, || {
capture_message("test", Level::Error);
});
HttpResponse::Ok()
}),
))
.await;
let request = TestRequest::with_uri("https://server:1234/test").to_request();
let events = with_captured_events(|| {
block_on(call_service(&mut app, request));
});
assert_eq!(events.len(), 0);
}
#[actix_rt::test]
async fn skip_event_on_400() {
let mut app = init_service(
App::new()
.wrap(SentryMiddleware::with_current_hub(401))
.service(web::resource("/test").to(|| HttpResponse::BadRequest())),
)
.await;
let request = TestRequest::with_uri("https://server:1234/test").to_request();
let events = with_captured_events(|| {
block_on(call_service(&mut app, request));
});
assert_eq!(events.len(), 0);
}
}