#![cfg_attr(docsrs, feature(doc_cfg))]
use std::num::NonZeroUsize;
use std::panic;
use std::sync::atomic::AtomicBool;
use std::sync::Arc;
use std::{backtrace::Backtrace, sync::atomic::Ordering};
#[cfg(feature = "log")]
use log::Log;
#[cfg(any(feature = "log", feature = "tracing"))]
use ring_channel::RingSender;
use ring_channel::{ring_channel, RingReceiver};
use ureq::json;
mod error;
#[cfg(feature = "tracing")]
mod tracing_layer;
#[cfg(feature = "tracing")]
pub use tracing_layer::TracingLayer;
#[cfg(feature = "log")]
mod log_wrapper;
pub use error::Error;
#[derive(Clone, Debug)]
struct Config {
api_key: String,
backend_url: String,
#[cfg(any(feature = "log", feature = "tracing"))]
report_on_log_errors: bool,
environment: Option<String>,
version: Option<String>,
is_enabled: Arc<AtomicBool>,
}
pub struct Client {
config: Config,
#[cfg(any(feature = "log", feature = "tracing"))]
log_rx: RingReceiver<LogEvent>,
#[cfg(any(feature = "log", feature = "tracing"))]
log_tx: RingSender<LogEvent>,
}
impl Client {
pub fn set_enabled(&self, enabled: bool) {
self.config.is_enabled.store(enabled, Ordering::Relaxed);
}
#[cfg_attr(docsrs, doc(cfg(feature = "log")))]
#[cfg(feature = "log")]
pub fn set_logger(&self, logger: impl Log + 'static) -> Result<(), Error> {
let wrapper = log_wrapper::LogWrapper {
next: logger,
tx: self.log_tx.clone(),
rx: self.log_rx.clone(),
config: self.config.clone(),
};
log::set_boxed_logger(Box::new(wrapper))?;
Ok(())
}
#[cfg_attr(docsrs, doc(cfg(feature = "tracing")))]
#[cfg(feature = "tracing")]
pub fn tracing_layer(&self) -> TracingLayer {
TracingLayer {
config: self.config.clone(),
rx: self.log_rx.clone(),
tx: self.log_tx.clone(),
}
}
}
struct ReportLocation {
file: String,
line: u32,
col: Option<u32>,
}
struct LogEvent {
timestamp: u64,
level: u8,
message: String,
module: Option<String>,
file: Option<String>,
line: Option<u32>,
}
pub struct Builder {
config: Config,
}
impl Builder {
pub fn environment(mut self, name: impl Into<String>) -> Self {
self.config.environment = Some(name.into());
self
}
pub fn version(mut self, version: impl Into<String>) -> Self {
self.config.version = Some(version.into());
self
}
pub fn backend_url(mut self, url: impl AsRef<str>) -> Self {
self.config.backend_url = format!("{}/ingress", url.as_ref());
self
}
#[cfg_attr(docsrs, doc(cfg(feature = "tracing")))]
#[cfg_attr(docsrs, doc(cfg(feature = "log")))]
#[cfg(any(feature = "log", feature = "tracing"))]
pub fn send_report_on_log_errors(mut self, enabled: bool) -> Self {
self.config.report_on_log_errors = enabled;
self
}
pub fn build(self) -> Result<Client, Error> {
if self.config.api_key.is_empty() {
return Err(Error::EmptyApiKey);
}
let (_log_tx, log_rx) = ring_channel(NonZeroUsize::try_from(100).unwrap());
init_hook(self.config.clone(), log_rx.clone());
Ok(Client {
config: self.config,
#[cfg(any(feature = "log", feature = "tracing"))]
log_tx: _log_tx,
#[cfg(any(feature = "log", feature = "tracing"))]
log_rx,
})
}
}
pub fn builder(api_key: impl Into<String>) -> Builder {
let api_key = api_key.into().trim().to_string();
Builder {
config: Config {
api_key,
backend_url: "https://app.dontpanic.rs/ingress".into(),
#[cfg(any(feature = "log", feature = "tracing"))]
report_on_log_errors: true,
version: None,
environment: None,
is_enabled: Arc::new(AtomicBool::new(true)),
},
}
}
fn init_hook(config: Config, log_recv: RingReceiver<LogEvent>) {
let previous_panic_hook = panic::take_hook();
panic::set_hook(Box::new(move |info| {
if !config.is_enabled.load(Ordering::Relaxed) {
previous_panic_hook(info);
return;
}
let title;
if let Some(panic_msg) = info.payload().downcast_ref::<&str>() {
title = *panic_msg;
} else if let Some(panic_msg) = info.payload().downcast_ref::<String>() {
title = panic_msg;
} else {
previous_panic_hook(info);
return;
}
let title = title.to_string();
let location = info.location().map(|location| ReportLocation {
file: location.file().to_string(),
line: location.line(),
col: Some(location.column()),
});
send_report(&config, title, location, &log_recv);
previous_panic_hook(info);
}));
}
fn send_report(
config: &Config,
title: impl Into<String>,
loc: Option<ReportLocation>,
log_recv: &RingReceiver<LogEvent>,
) {
let mut log = vec![];
while let Ok(log_event) = log_recv.try_recv() {
log.push(json!({
"ts": log_event.timestamp,
"lvl": log_event.level,
"msg": log_event.message,
"mod": log_event.module,
"f": log_event.file,
"l": log_event.line,
}));
}
let handle = std::thread::current();
let backtrace = Backtrace::force_capture();
let location = loc.as_ref().map(|loc| {
ureq::json!({
"f": loc.file,
"l": loc.line,
"c": loc.col
})
});
let title = title.into();
let event = ureq::json!({
"title": &title,
"loc": location,
"ver": config.version,
"tid": format!("{:?}", handle.id()),
"tname": handle.name(),
"os": std::env::consts::OS,
"arch": std::env::consts::ARCH,
"trace": backtrace.to_string(),
"log": log
});
let res = ureq::post(&config.backend_url).send_json(ureq::json!({
"key": config.api_key,
"env": config.environment,
"data": event,
}));
if let Err(e) = res {
match e {
ureq::Error::Status(code, response) => eprintln!(
"Error sending report to {}. Code: {}, Response: {:?}",
config.backend_url,
code,
response.into_string()
),
ureq::Error::Transport(e) => eprintln!(
"Transport error sending report to {}. Error: {:?}",
config.backend_url, e
),
};
}
}