use std::collections::HashMap;
use std::panic::{catch_unwind, AssertUnwindSafe};
use std::sync::{Arc, RwLock};
use crate::configuration::Configuration;
use crate::delivery_queue::DeliveryQueue;
use crate::event_builder::{capture_backtrace, Event, EventBuilder, Frame};
use crate::pii_scrubber::Value;
pub struct Reporter {
configuration: Arc<RwLock<Configuration>>,
delivery_queue: DeliveryQueue,
}
impl Reporter {
pub fn new(configuration: Arc<RwLock<Configuration>>, delivery_queue: DeliveryQueue) -> Self {
Reporter {
configuration,
delivery_queue,
}
}
pub fn report(
&self,
exception_class: &str,
message: &str,
backtrace: Vec<Frame>,
context: HashMap<String, Value>,
) {
let _ = catch_unwind(AssertUnwindSafe(|| {
let config = self.configuration.read().unwrap();
if !config.is_enabled() {
return;
}
let event: Event =
EventBuilder::new(&config).build(exception_class, message, backtrace, context);
drop(config);
self.delivery_queue.push(event);
}));
}
pub fn report_with_captured_backtrace(
&self,
exception_class: &str,
message: &str,
context: HashMap<String, Value>,
) {
let backtrace = { capture_backtrace(&self.configuration.read().unwrap()) };
self.report(exception_class, message, backtrace, context);
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::client::Client;
use std::net::TcpListener;
use std::sync::atomic::{AtomicI32, Ordering};
use std::time::Duration;
fn serving_reporter(status_ok: bool, environment: &str) -> (Reporter, Arc<AtomicI32>) {
use std::io::{Read, Write};
let listener = TcpListener::bind("127.0.0.1:0").unwrap();
let addr = listener.local_addr().unwrap();
let received = Arc::new(AtomicI32::new(0));
let received_clone = Arc::clone(&received);
std::thread::spawn(move || {
for stream in listener.incoming().flatten() {
let mut stream = stream;
let mut buf = [0u8; 8192];
let mut total = Vec::new();
loop {
let n = stream.read(&mut buf).unwrap_or(0);
if n == 0 {
break;
}
total.extend_from_slice(&buf[..n]);
if total.windows(4).any(|w| w == b"\r\n\r\n") {
break;
}
}
received_clone.fetch_add(1, Ordering::SeqCst);
let status_line = if status_ok {
"HTTP/1.1 200 OK"
} else {
"HTTP/1.1 500 Internal Server Error"
};
let _ = stream.write_all(
format!("{status_line}\r\nContent-Length: 2\r\nConnection: close\r\n\r\n{{}}")
.as_bytes(),
);
}
});
let mut config = Configuration::new();
config.dsn = Some(format!("http://key@{addr}/events"));
config.environment = environment.to_string();
config.timeout = Duration::from_secs(2);
let configuration = Arc::new(RwLock::new(config));
let client = Arc::new(Client::new(Arc::clone(&configuration)));
let delivery_queue = DeliveryQueue::new(10, client);
(Reporter::new(configuration, delivery_queue), received)
}
#[test]
fn report_skips_when_disabled() {
let (reporter, received) = serving_reporter(true, "development");
reporter.report_with_captured_backtrace("Error", "boom", HashMap::new());
std::thread::sleep(Duration::from_millis(100));
assert_eq!(received.load(Ordering::SeqCst), 0);
}
#[test]
fn report_delivers_when_enabled() {
let (reporter, received) = serving_reporter(true, "production");
reporter.report_with_captured_backtrace("Error", "boom", HashMap::new());
let deadline = std::time::Instant::now() + Duration::from_secs(2);
while received.load(Ordering::SeqCst) < 1 && std::time::Instant::now() < deadline {
std::thread::sleep(Duration::from_millis(5));
}
assert_eq!(received.load(Ordering::SeqCst), 1);
}
#[test]
fn report_never_panics_even_when_delivery_fails() {
let (reporter, _received) = serving_reporter(false, "production");
reporter.report_with_captured_backtrace("Error", "boom", HashMap::new());
}
}