#![cfg_attr(docsrs, feature(doc_cfg))]
#![forbid(unsafe_code)]
mod backtrace;
mod client;
mod config;
mod event;
mod hub;
mod level;
pub mod panic;
mod scope;
mod source;
mod transport;
mod util;
#[cfg(any(feature = "log", feature = "tracing", feature = "anyhow"))]
pub mod integrations;
use std::sync::{Arc, RwLock};
pub use client::Client;
pub use config::{BeforeSend, Config, ConfigBuilder};
pub use event::{Breadcrumb, Cause, Event, Frame, User};
pub use hub::{configure_scope, with_scope};
pub use level::Level;
pub use scope::Scope;
pub use transport::{SendOutcome, Transport};
#[cfg(feature = "anyhow")]
#[cfg_attr(docsrs, doc(cfg(feature = "anyhow")))]
pub use integrations::anyhow::capture_anyhow;
pub const VERSION: &str = env!("CARGO_PKG_VERSION");
static CLIENT: RwLock<Option<Arc<Client>>> = RwLock::new(None);
#[must_use = "the returned guard flushes and closes the client when dropped; bind it to a name"]
pub fn init(config: Config) -> ClientGuard {
let _ = close(None);
let install_hook = config.panic_hook;
let client = Client::new(config);
if let Ok(mut guard) = CLIENT.write() {
*guard = Some(client);
}
if install_hook {
panic::register();
}
ClientGuard { _private: () }
}
pub(crate) fn with_client<R>(f: impl FnOnce(&Client) -> R) -> Option<R> {
let client = {
let guard = CLIENT.read().ok()?;
guard.as_ref().cloned()?
};
Some(f(&client))
}
pub(crate) fn finalize_and_capture(client: &Client, mut event: Event) -> Option<String> {
let cfg = client.config();
event.environment = cfg.environment.clone();
if event.release.is_none() {
event.release = cfg.release.clone();
}
let scope = hub::current_scope_snapshot();
scope.apply_to(&mut event, cfg.max_breadcrumbs);
client.capture(event)
}
fn attach_backtrace(event: &mut Event, frames: Vec<Frame>) {
if frames.is_empty() {
return;
}
event.backtrace = Some(backtrace::frames_to_string(&frames));
if let Ok(v) = serde_json::to_value(&frames) {
event.set_metadata("exception_frames", v);
}
}
pub fn capture_message(message: impl AsRef<str>, level: Level) -> Option<String> {
with_client(|client| {
let cfg = client.config();
if !cfg.enabled() || level < cfg.min_level {
return None;
}
let mut event = Event::new(level, message.as_ref());
if cfg.attach_stacktrace {
let frames = backtrace::current_frames(cfg);
attach_backtrace(&mut event, frames);
}
finalize_and_capture(client, event)
})
.flatten()
}
pub fn capture_error<E: std::error::Error + 'static>(error: &E) -> Option<String> {
capture_error_inner(error, std::any::type_name::<E>(), Level::Error)
}
pub fn capture_error_dyn(error: &(dyn std::error::Error + 'static)) -> Option<String> {
capture_error_inner(error, "", Level::Error)
}
pub fn capture_error_at_level<E: std::error::Error + 'static>(
error: &E,
level: Level,
) -> Option<String> {
capture_error_inner(error, std::any::type_name::<E>(), level)
}
fn capture_error_inner(
error: &(dyn std::error::Error + 'static),
class: &str,
level: Level,
) -> Option<String> {
with_client(|client| {
let cfg = client.config();
if !cfg.enabled() || level < cfg.min_level {
return None;
}
let mut event = Event::new(level, error.to_string());
if !class.is_empty() {
event.set_metadata("exception_class", class);
}
let frames = backtrace::current_frames(cfg);
attach_backtrace(&mut event, frames);
let causes = collect_causes(error);
if !causes.is_empty() {
if let Ok(v) = serde_json::to_value(&causes) {
event.set_metadata("exception_causes", v);
}
}
finalize_and_capture(client, event)
})
.flatten()
}
pub(crate) fn collect_causes(error: &(dyn std::error::Error + 'static)) -> Vec<Cause> {
const MAX_DEPTH: usize = 5;
let mut causes = Vec::new();
let mut current = error.source();
while let Some(err) = current {
if causes.len() >= MAX_DEPTH {
break;
}
causes.push(Cause {
class: None,
message: err.to_string(),
});
current = err.source();
}
causes
}
pub fn capture_event(event: Event) -> Option<String> {
with_client(|client| {
let cfg = client.config();
if !cfg.enabled() || event.level < cfg.min_level {
return None;
}
finalize_and_capture(client, event)
})
.flatten()
}
pub fn add_breadcrumb(crumb: Breadcrumb) {
configure_scope(|scope| {
scope.add_breadcrumb(crumb);
});
}
pub fn register_panic_hook() {
panic::register();
}
pub fn flush(timeout: Option<std::time::Duration>) -> bool {
with_client(|client| client.flush(timeout)).unwrap_or(false)
}
pub fn close(timeout: Option<std::time::Duration>) -> bool {
let client = {
match CLIENT.write() {
Ok(mut guard) => guard.take(),
Err(poisoned) => poisoned.into_inner().take(),
}
};
match client {
Some(c) => c.close(timeout),
None => true,
}
}
#[must_use = "dropping this guard closes the client; keep it bound for the process lifetime"]
pub struct ClientGuard {
_private: (),
}
impl Drop for ClientGuard {
fn drop(&mut self) {
let _ = close(None);
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::sync::Mutex as StdMutex;
use std::sync::atomic::{AtomicUsize, Ordering};
use std::time::Duration;
struct CaptureTransport {
events: Arc<StdMutex<Vec<serde_json::Value>>>,
}
impl Transport for CaptureTransport {
fn send(&self, body: &[u8]) -> SendOutcome {
let arr: Vec<serde_json::Value> = serde_json::from_slice(body).unwrap();
self.events.lock().unwrap().extend(arr);
SendOutcome::Success
}
}
fn init_capturing() -> Arc<StdMutex<Vec<serde_json::Value>>> {
let events = Arc::new(StdMutex::new(Vec::new()));
let transport = Arc::new(CaptureTransport {
events: events.clone(),
});
let cfg = Config::builder()
.api_key("elp_test")
.environment("test")
.min_level(Level::Debug)
.flush_interval(Duration::from_millis(10))
.panic_hook(false)
.transport(transport)
.build();
std::mem::forget(init(cfg)); events
}
#[derive(Debug)]
struct MyError {
msg: String,
source: Option<Box<MyError>>,
}
impl std::fmt::Display for MyError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.write_str(&self.msg)
}
}
impl std::error::Error for MyError {
fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
self.source
.as_ref()
.map(|b| b.as_ref() as &(dyn std::error::Error + 'static))
}
}
static TEST_LOCK: StdMutex<()> = StdMutex::new(());
#[test]
fn capture_message_respects_min_level_and_delivers() {
let _g = TEST_LOCK.lock().unwrap();
hub::reset_for_test();
let events = {
let events = Arc::new(StdMutex::new(Vec::new()));
let transport = Arc::new(CaptureTransport {
events: events.clone(),
});
let cfg = Config::builder()
.api_key("elp_test")
.min_level(Level::Warning)
.flush_interval(Duration::from_millis(10))
.panic_hook(false)
.transport(transport)
.build();
std::mem::forget(init(cfg));
events
};
assert!(capture_message("debug noise", Level::Debug).is_none());
assert!(capture_message("real problem", Level::Error).is_some());
flush(Some(Duration::from_secs(2)));
let captured = events.lock().unwrap();
assert_eq!(captured.len(), 1);
assert_eq!(captured[0]["message"], "real problem");
assert_eq!(captured[0]["environment"], "production");
close(None);
}
#[test]
fn capture_error_records_class_and_causes() {
let _g = TEST_LOCK.lock().unwrap();
hub::reset_for_test();
let events = init_capturing();
let err = MyError {
msg: "outer failed".into(),
source: Some(Box::new(MyError {
msg: "inner cause".into(),
source: None,
})),
};
capture_error(&err);
flush(Some(Duration::from_secs(2)));
let captured = events.lock().unwrap();
assert_eq!(captured.len(), 1);
let e = &captured[0];
assert_eq!(e["message"], "outer failed");
assert_eq!(e["level"], "error");
assert!(
e["metadata"]["exception_class"]
.as_str()
.unwrap()
.contains("MyError")
);
let causes = e["metadata"]["exception_causes"].as_array().unwrap();
assert_eq!(causes.len(), 1);
assert_eq!(causes[0]["message"], "inner cause");
drop(captured);
close(None);
}
#[test]
fn before_send_can_drop_and_mutate() {
let _g = TEST_LOCK.lock().unwrap();
hub::reset_for_test();
let events = Arc::new(StdMutex::new(Vec::new()));
let transport = Arc::new(CaptureTransport {
events: events.clone(),
});
let dropped = Arc::new(AtomicUsize::new(0));
let dropped2 = dropped.clone();
let cfg = Config::builder()
.api_key("elp_test")
.min_level(Level::Debug)
.flush_interval(Duration::from_millis(10))
.panic_hook(false)
.transport(transport)
.before_send(move |mut e| {
if e.message.contains("secret") {
dropped2.fetch_add(1, Ordering::SeqCst);
return None;
}
e.message = e.message.replace("world", "REDACTED");
Some(e)
})
.build();
std::mem::forget(init(cfg));
capture_message("hello secret", Level::Error);
capture_message("hello world", Level::Error);
flush(Some(Duration::from_secs(2)));
assert_eq!(dropped.load(Ordering::SeqCst), 1);
let captured = events.lock().unwrap();
assert_eq!(captured.len(), 1);
assert_eq!(captured[0]["message"], "hello REDACTED");
drop(captured);
close(None);
}
#[test]
fn scope_user_and_tags_attach_to_events() {
let _g = TEST_LOCK.lock().unwrap();
hub::reset_for_test();
let events = init_capturing();
configure_scope(|s| {
s.set_user(User::new().id("u-42").email("a@b.c"));
s.set_tag("region", "us-east");
});
add_breadcrumb(Breadcrumb::new("nav", "opened checkout"));
capture_message("explode", Level::Error);
flush(Some(Duration::from_secs(2)));
let captured = events.lock().unwrap();
let e = &captured[0];
assert_eq!(e["user"]["id"], "u-42");
assert_eq!(e["tags"]["region"], "us-east");
assert_eq!(e["breadcrumbs"][0]["message"], "opened checkout");
drop(captured);
close(None);
}
#[test]
fn disabled_without_key_is_noop() {
let _g = TEST_LOCK.lock().unwrap();
hub::reset_for_test();
close(None); assert!(capture_message("nothing", Level::Error).is_none());
}
}