#![allow(dead_code)]
use std::ffi::CStr;
use std::sync::{Arc, Mutex};
use std::{io::Write, path::PathBuf};
use std::fmt::{Write as WriteFmt, Display};
use rand::Rng;
use chrono::prelude::*;
use buildinfy::*;
pub use log;
mod termcolors;
use crate::termcolors::*;
#[cfg(feature = "trace")]
pub use tracing_opentelemetry::OpenTelemetrySpanExt;
#[cfg(feature = "trace")]
pub use opentelemetry::trace::TraceContextExt;
const LOGGER_DEFAULT_LATEST_CAPACITY: usize = 128;
#[derive(PartialEq, Eq, Copy, Clone)]
pub enum SendFormat {
None,
PlainText,
JsonSentry,
}
pub struct Breadcrumb {
}
type Completer = Box<dyn Fn() + Send + Sync>;
pub struct CrashyOptions {
pub format: SendFormat,
pub sender: Arc<dyn Fn (SendFormat, Vec<u8>, usize, Completer) + Sync + Send>,
get_breadcrumbs: fn () -> Vec<Breadcrumb>,
release: String,
dist: String,
environment: String,
command: String,
path: PathBuf,
pub report_username: bool,
pub level: log::LevelFilter,
pub systemd_log: bool,
}
#[cfg(feature = "sentry")]
mod sentry {
pub use httpclienty::*;
}
#[cfg(feature = "sentry")]
use sentry::*;
impl Default for CrashyOptions {
fn default() -> Self {
let mut release : String = String::new();
if let Some(version) = build_pipeline_id().or(build_revision()) {
release = version.to_string();
}
#[allow(unused_mut)]
let mut sender : Arc<dyn Fn (SendFormat, Vec<u8>, usize, Completer) + Sync + Send> = Arc::new(|_format, data: Vec<u8>, header_len: usize, completer| {
std::io::stderr().write_all(&data[header_len..]).unwrap();
completer();
});
#[allow(unused_mut)]
let mut format = SendFormat::PlainText;
let systemd_log = std::env::var("JOURNAL_STREAM").is_ok();
Self {
format,
sender,
get_breadcrumbs: || {
Vec::new()
},
release,
dist: build_pipeline_id_per_project().unwrap_or_default().to_string(),
environment: build_reference().unwrap_or_default().to_string(),
command: std::env::args().reduce(|x,y| format!("{} {}", x, y)).unwrap_or_default(),
path: std::env::current_dir().unwrap_or_default(),
report_username: false,
level: log::LevelFilter::Info,
systemd_log,
}
}
}
impl CrashyOptions {
#[cfg(feature = "async")]
pub fn default_async(handle: &tokio::runtime::Handle) -> Self {
#[allow(unused_mut)]
let mut retval = Self::default();
#[cfg(any(feature = "nacl", feature = "sentry"))]
if let Some(dsn) = buildinfy::build_sentry_dsn() {
retval = retval.with_crash_report_url_async(dsn, handle);
}
retval
}
pub fn default_sync() -> Self {
#[allow(unused_mut)]
let mut retval = Self::default();
#[cfg(any(feature = "nacl", feature = "sentry"))]
if let Some(dsn) = buildinfy::build_sentry_dsn() {
retval = retval.with_crash_report_url_sync(dsn);
}
retval
}
#[cfg(any(feature = "nacl", feature = "sentry"))]
fn with_crash_report_url<H: Fn (std::pin::Pin<Box<dyn std::future::Future<Output=()>+Sync+Send+'static>>) + Sync + Send + 'static>(mut self, sentry_dsn: &str, h: H) -> Self {
if let Some(current) = sentry_dsn.strip_prefix("http://") {
if let Some((key, current)) = current.split_once('@') {
if let Some((host, project)) = current.split_once('/') {
let url = format!("http://{host}/api/{project}/store/");
let key = key.to_string();
self.format = SendFormat::JsonSentry;
self.sender = Arc::new(move |_format: SendFormat, body: Vec<u8>, _header_len: usize, completer| {
let mut client_req = httpclienty::Request::builder()
.method("POST")
.uri(&url);
let headers = client_req.headers_mut().unwrap();
headers.insert(httpclienty::header::HeaderName::from_lowercase(b"x-sentry-auth").unwrap(), httpclienty::header::HeaderValue::from_str(&format!("Sentry sentry_version=7, sentry_client=indigo, sentry_key={key}")).unwrap());
let client_req = client_req
.body(Full::new(body.into()))
.expect("request builder");
h(Box::pin(async move {
let state = get_http_client_state();
let resp = perform_http_request(client_req, state, None).await;
match resp {
Ok(resp) => {
log::error!("sent crashy error report to sentry: {} ({})", if resp.status() == 200 { "ok" } else { "failed" }, resp.status());
},
Err(err) => {
log::error!("error sending crashy error report to sentry: {:?}", err);
},
}
completer();
}));
});
}
}
return self;
}
if let Some(current) = sentry_dsn.strip_prefix("https://") {
if let Some((key, current)) = current.split_once('@') {
if let Some((host, project)) = current.split_once('/') {
let url = format!("https://{host}/api/{project}/store/");
let key = key.to_string();
self.format = SendFormat::JsonSentry;
self.sender = Arc::new(move |_format: SendFormat, body: Vec<u8>, _header_len: usize, completer| {
let mut client_req = httpclienty::Request::builder()
.method("POST")
.uri(&url);
let headers = client_req.headers_mut().unwrap();
headers.insert(httpclienty::header::HeaderName::from_lowercase(b"x-sentry-auth").unwrap(), httpclienty::header::HeaderValue::from_str(&format!("Sentry sentry_version=7, sentry_client=indigo, sentry_key={key}")).unwrap());
let client_req = client_req
.body(Full::new(body.into()))
.expect("request builder");
h(Box::pin(async move {
let state = get_http_client_state();
let resp = perform_http_request(client_req, state, None).await;
match resp {
Ok(resp) => {
log::error!("sent crashy error report to sentry: {} ({})", if resp.status() == 200 { "ok" } else { "failed" }, resp.status());
},
Err(err) => {
log::error!("error sending crashy error report to sentry: {:?}", err);
},
}
completer();
}));
});
}
}
return self;
}
#[cfg(feature = "nacl")]
if let Some(current) = dsn.strip_prefix("nacl://") {
if let Some((key, current)) = current.split_once('@') {
if let Some((host, project)) = current.split_once('/') {
self.format = SendFormat::JsonSentry;
self.sender = Arc::new(move |_format: SendFormat, body: Vec<u8>, _header_len: usize, completer| {
let host = host.to_string();
let port = 443;
h(Box::pin(async move {
if let Ok(Ok(stream)) = tokio::time::timeout(std::time::Duration::from_secs(2), tokio::net::TcpStream::connect((host.as_str(),port))).await {
stream.set_nodelay(true).unwrap();
use networky::*;
pub const PUBLIC_KEY : &str = "554c3d0282c39f3a777248b0dacc0052a92615e77e30ac4b37862397f9e48e08";
let data = from_hex_slice(PUBLIC_KEY.as_bytes())
.map_err(|_| "public key was not hex").unwrap()
.try_into()
.map_err(|_| "public key was incorrect size").unwrap();
let pk = vec![PublicSignKey{data}];
match networky::progress(connection(stream, "crashy", 0, &host, &pk, body), std::time::Duration::from_secs(60)).await {
Ok(_) => {},
Err(_) => {},
}
}
completer();
}));
});
}
}
return self;
}
self
}
#[cfg(any(feature = "nacl", feature = "sentry"))]
pub fn with_crash_report_url_sync(self, sentry_dsn: &str) -> Self {
self.with_crash_report_url(sentry_dsn, |f| {
std::thread::spawn(|| {
let rt = tokio::runtime::Builder::new_current_thread()
.enable_all()
.build()
.expect("build runtime");
rt.block_on(f);
});
})
}
#[cfg(any(feature = "nacl", feature = "sentry"))]
pub fn with_crash_report_url_async(self, sentry_dsn: &str, handle: &tokio::runtime::Handle) -> Self {
let handle = handle.clone();
self.with_crash_report_url(sentry_dsn, move |f| {
let crash_waiter_active = CRASH_STATE.get_or_init(Mutex::default).lock().unwrap().waiter;
if crash_waiter_active {
handle.spawn(f);
} else {
std::thread::spawn(|| {
let rt = tokio::runtime::Builder::new_current_thread()
.enable_all()
.build()
.expect("build runtime");
rt.block_on(f);
});
}
})
}
}
#[cfg(feature = "nacl")]
pub fn from_hex_slice(s: &[u8]) -> Result<Vec<u8>,networky::EncodingError> {
use networky::*;
(0..s.len())
.step_by(2)
.map(|i| -> Result<u8,EncodingError> {
Ok((from_hex_u8(s[i])? << 4) | from_hex_u8(s[i+1])?)
})
.collect::<Result<Vec<u8>,EncodingError>>()
}
#[cfg(feature = "nacl")]
async fn connection(mut stream: tokio::net::TcpStream, description: &str, version: u32, host: &String, public_sign_key_of_server: &[networky::nacl::PublicSignKey], mut body: Vec<u8>) -> Result<bool,String> {
use networky::nacl::*;
use tokio::io::AsyncWriteExt as _;
use tokio::io::AsyncReadExt as _;
let (pk, sk) = crypto_box_keypair(&mut rand::rngs::OsRng);
let mut output = RawString::with_capacity(4 + 1 + host.len() + pk.data.len());
output.write_u64(version as u64, 4).unwrap_infallible(); output.write_var_bytes(host.as_bytes()).unwrap_infallible();
output.write_var_bytes(description.as_bytes()).unwrap_infallible();
output.write_bytes(&pk.data).unwrap_infallible();
let output = output.build();
stream.write_all(&output).await.map_err(|_| "WriteError".to_string())?;
let mut signature = Signature { data: [0u8; networky::nacl::bindings::crypto_sign_ed25519_BYTES as usize] };
stream.read_exact(&mut signature.data).await.map_err(|_| "ReadError".to_string())?;
let mut public_key_of_session = PublicBoxKey{data: [0u8; 32]};
stream.read_exact(&mut public_key_of_session.data).await.map_err(|_| "ReadError".to_string())?;
if !public_sign_key_of_server.iter().any(|key| networky::nacl::crypto_sign_verify(&signature, &public_key_of_session.data, key).is_ok()) {
return Err("CertificateError".to_string());
}
let mut send_nonce = nonce_for_client();
let send_cache = crypto_box_prepare(&sk, &public_key_of_session);
send(&mut body, &mut send_nonce, &send_cache, true, &mut stream).await.map_err(|_| "WriteError".to_string())?;
let mut buffer = Vec::new();
stream.read_to_end(&mut buffer).await.map_err(|_| "WriteError".to_string())?;
Ok(true)
}
#[cfg(feature = "nacl")]
pub async fn send<Out: tokio::io::AsyncWrite + std::marker::Unpin>(data: &mut [u8], send_nonce: &mut networky::Nonce, send_cache: &networky::CryptoBoxCache, server: bool, stream: &mut Out) -> Result<(),std::io::Error> {
use networky::nacl::*;
use tokio::io::AsyncWriteExt as _;
let tag = crypto_box_in_place(data, send_nonce, send_cache);
let mut header = [0u8; 9 + bindings::crypto_box_MACBYTES as usize];
let mut header_writer = RawWriter::with(&mut header);
header_writer.write_var_u64((tag.data.len() + data.len()) as u64).unwrap();
header_writer.write_bytes(&tag.data).unwrap();
let header = header_writer.build();
stream.write_all(header).await?;
stream.write_all(data).await?;
if server {
decrease_nonce(&mut send_nonce.data);
} else {
increase_nonce(&mut send_nonce.data);
}
Ok(())
}
#[derive(Default)]
struct LatestLogs {
latest: Vec<(String,f64,log::Level)>,
}
struct Logger {
latest: Arc<Mutex<LatestLogs>>,
systemd_log: std::sync::atomic::AtomicBool,
}
impl log::Log for Logger {
fn enabled(&self, metadata: &log::Metadata) -> bool {
metadata.level() <= log::max_level()
}
fn log(&self, record: &log::Record) {
if !self.enabled(record.metadata()) {
return;
}
let mut line = String::new();
let time = std::time::SystemTime::now().duration_since(std::time::SystemTime::UNIX_EPOCH).unwrap().as_secs_f64();
if let Some(file) = record.file() {
if let Some(lineno) = record.line() {
write!(&mut line, "{}: {} [{}:{}]", record.module_path().unwrap_or_else(|| record.target()), record.args(), file, lineno).unwrap();
}
}
if line.is_empty() {
write!(&mut line, "{}: {}", record.module_path().unwrap_or_else(|| record.target()), record.args()).unwrap();
}
if self.systemd_log.load(std::sync::atomic::Ordering::Relaxed) {
let output = format!("{}{}\n", systemd_logging_prefix(record.level()), line);
let _ = std::io::stderr().write(output.as_bytes());
} else {
let mut output = String::new();
print_log_to_console(&line, time, record.level(), &mut output);
let _ = std::io::stderr().write(output.as_bytes());
}
let mut locker = self.latest.lock().unwrap();
if locker.latest.len() == locker.latest.capacity() {
locker.latest.remove(0);
}
locker.latest.push((line,time,record.level()));
}
fn flush(&self) {}
}
impl Logger {
fn new(systemd_log: bool) -> (Self, Arc<Mutex<LatestLogs>>) {
let latest = Arc::new(Mutex::new(LatestLogs{
latest: Vec::with_capacity(LOGGER_DEFAULT_LATEST_CAPACITY),
}));
(Self {
systemd_log: std::sync::atomic::AtomicBool::new(systemd_log),
latest: latest.clone(),
}, latest)
}
fn set_systemd_log(&self, enable: bool) {
self.systemd_log.store(enable, std::sync::atomic::Ordering::Relaxed);
}
}
fn json_escaped_write(f: &mut core::fmt::Formatter<'_>, src: &str) -> Result<(),core::fmt::Error> {
let mut utf16_buf = [0u16; 2];
for c in src.chars() {
match c {
'\x08' => write!(f, "\\b")?,
'\x0c' => write!(f, "\\f")?,
'\n' => write!(f, "\\n")?,
'\r' => write!(f, "\\r")?,
'\t' => write!(f, "\\t")?,
'"' => write!(f, "\\\"")?,
'\\' => write!(f, "\\\\")?,
' ' => write!(f, " ")?,
c if (c as u32) < 0x20 => {
let encoded = c.encode_utf16(&mut utf16_buf);
for utf16 in encoded {
write!(f, "\\u{:04X}", utf16)?;
}
},
c => write!(f, "{}", c)?,
}
}
Ok(())
}
struct Quoted<'a>(&'a str);
impl Display for Quoted<'_> {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, r#"""#)?;
json_escaped_write(f, self.0)?;
write!(f, r#"""#)?;
Ok(())
}
}
pub struct CrashHandler {
}
impl Drop for CrashHandler {
fn drop(&mut self) {
CRASH_STATE.get_or_init(Mutex::default).lock().unwrap().waiter = false;
while CRASH_STATE.get_or_init(Mutex::default).lock().unwrap().counter > 0 {
std::thread::sleep(std::time::Duration::from_millis(100));
}
}
}
#[must_use = "store result in a variable (not `_`) to wait for all crash reports to be send"]
pub fn setup_crashy() -> CrashHandler {
setup_crashy_with_options(CrashyOptions::default())
}
#[must_use = "store result in a variable (not `_`) to wait for all crash reports to be send"]
#[cfg(feature = "async")]
pub fn setup_crashy_async(handle: &tokio::runtime::Handle) -> CrashHandler {
setup_crashy_with_options(CrashyOptions::default_async(handle))
}
#[must_use = "store result in a variable (not `_`) to wait for all crash reports to be send"]
pub fn setup_crashy_sync() -> CrashHandler {
setup_crashy_with_options(CrashyOptions::default_sync())
}
#[derive(Default)]
struct CrashState {
options: CrashyOptions,
counter: usize,
logger: Arc<Mutex<LatestLogs>>,
waiter: bool,
}
static CRASH_STATE : std::sync::OnceLock<Mutex<CrashState>> = std::sync::OnceLock::new();
thread_local! {
static PANIC_INFO: std::cell::Cell<Option<(String,String,String,Vec<(String,String,u32)>)>> = const { std::cell::Cell::new(None) };
}
#[must_use = "store result in a variable to wait for all crash reports to be send"]
pub fn setup_crashy_with_options(options: CrashyOptions) -> CrashHandler {
let (logger, latest) = Logger::new(options.systemd_log);
log::set_boxed_logger(Box::new(logger)).expect("crashy: could not set logger");
log::set_max_level(options.level);
{
let mut crash_state = CRASH_STATE.get_or_init(Mutex::default).lock().unwrap();
crash_state.options = options;
crash_state.logger = latest;
}
unsafe {
libc::signal(libc::SIGABRT, signal_handler as libc::sighandler_t);
libc::signal(libc::SIGSEGV, signal_handler as libc::sighandler_t);
#[cfg(target_family = "unix")]
libc::signal(libc::SIGBUS, signal_handler as libc::sighandler_t);
libc::signal(libc::SIGILL, signal_handler as libc::sighandler_t);
libc::signal(libc::SIGFPE, signal_handler as libc::sighandler_t);
}
std::panic::set_hook(Box::new(|pi| {
let mut msg = String::new();
if let Some(s) = pi.payload().downcast_ref::<&str>() {
let _ = write!(&mut msg, "{s}");
} else if let Some(s) = pi.payload().downcast_ref::<String>() {
let _ = write!(&mut msg, "{s}");
}
let mut error = "".to_string();
if let Some(location) = pi.location() {
write!(&mut msg, " [{}:{}]",
location.file(),
location.line(),
).unwrap();
error = format!("{}-{}-{}", location.file(), location.line(), location.column());
}
let frames = stack();
handle_crash("panic", &error, &msg, &frames);
PANIC_INFO.with(move |b| b.set(Some(("panic".to_string(), error.to_string(), msg.to_string(), frames))));
}));
CrashHandler{}
}
pub fn take_info_of_last_panic() -> (String, String, String, Vec<(String,String,u32)>) {
PANIC_INFO.with(|b| b.take()).unwrap()
}
#[cfg(windows)]
fn strsignal(signal: std::ffi::c_int) -> &'static CStr {
let name = match signal {
libc::SIGABRT => "SIGABRT",
libc::SIGSEGV => "SIGSEGV",
libc::SIGILL => "SIGILL",
libc::SIGFPE => "SIGFPE",
_ => "unknown",
};
unsafe { CStr::from_bytes_with_nul_unchecked(name.as_bytes()) }
}
#[cfg(not(windows))]
fn strsignal(signal: std::ffi::c_int) -> &'static CStr {
unsafe { CStr::from_ptr(libc::strsignal(signal)) }
}
pub unsafe extern "C" fn signal_handler(signal: std::ffi::c_int) {
let name = strsignal(signal);
let name = name.to_str().unwrap_or("unknown");
let error = format!("{name} ({signal})");
print_header("signal", &error, "");
let frames = stack();
_handle_crash("", false, &error, "signal", &frames, true);
libc::_exit(-1);
}
pub fn stack() -> Vec<(String,String,u32)> {
let mut frames = Vec::new();
let mut display = false;
let full = std::env::var("RUST_BACKTRACE").as_ref().map(String::as_str) == Ok("full");
backtrace::trace(|frame| {
backtrace::resolve_frame(frame, |symbol| {
let mut n = String::from("-");
let mut f = String::from("");
let mut l = 0;
let mut skip = false;
if let Some(name) = symbol.name() {
n = format!("{}", name);
if n.ends_with("rust_begin_unwind") || n.starts_with("crashy::signal_handler") || n.contains("::__rust_end_short_backtrace") {
skip = true;
display = true;
}
if n.starts_with("std::rt::") || n.starts_with("tokio::") || n.starts_with("<tokio::") || n.starts_with("std::thread::local::") || n.starts_with("<core::panic::unwind_safe::") || n.starts_with("<core::pin::Pin<P> as core::future::") || n.starts_with("std::panic::") || n == "__rust_try" || n.starts_with("core::ops::function::FnOnce") || n.starts_with("core::panicking::panic_fmt::") || n.starts_with("core::result::unwrap_failed::") || n.starts_with("core::result::Result<T,E>::expect") {
skip = true;
}
if n.contains("::__rust_begin_short_backtrace") {
skip = true;
display = false;
}
}
if let Some(filename) = symbol.filename() {
f = filename.display().to_string();
}
if let Some(line_no) = symbol.lineno() {
l = line_no;
}
if (!skip || full) && display {
frames.push((n, f, l));
}
});
!matches!(frames.last().map(|(s,_,_)| s.as_str()), Some("main"))
});
frames
}
pub fn handle_error(type_of_error: &str, error: &str, msg: &str, stack: &[(String,String,u32)]) {
_handle_crash(msg, true, error, type_of_error, stack, false)
}
pub fn handle_crash(type_of_error: &str, error: &str, msg: &str, stack: &[(String,String,u32)]) {
_handle_crash(msg, false, error, type_of_error, stack, false)
}
fn _handle_crash(msg: &str, handled: bool, error: &str, type_of_error: &str, stack: &[(String,String,u32)], header_printed: bool) {
let mut header_len = 0;
let (format, sender, out) = {
let mut crash_state = CRASH_STATE.get_or_init(Mutex::default).lock().unwrap();
let options = &crash_state.options;
let mut out = String::new();
if options.format == SendFormat::PlainText {
write_header(&mut out, type_of_error, error, msg);
if header_printed {
header_len = out.len();
}
for (name, filename, line_no) in stack {
writeln!(&mut out, "{TERM_BRIGHT_YELLOW}~~> {TERM_BOLD}{TERM_BRIGHT_WHITE}{}{TERM_RESET}", name).unwrap();
writeln!(&mut out, " {TERM_BRIGHT_BLACK}[{}:{}]{TERM_RESET}", filename, line_no).unwrap();
}
#[cfg(feature = "trace")]
{
let span = tracing::span::Span::current().context();
let span = span.span();
let span = span.span_context();
let trace_id = span.trace_id();
let span_id = span.span_id();
if trace_id != opentelemetry::trace::TraceId::INVALID {
writeln!(&mut out, "{TERM_BRIGHT_RED} {TERM_BOLD}{TERM_BRIGHT_WHITE}{:x} / {:x}{TERM_RESET}", trace_id, span_id).unwrap();
}
}
writeln!(&mut out, "{TERM_BRIGHT_RED} {TERM_BOLD}{TERM_BRIGHT_WHITE}{}{TERM_RESET}", options.command).unwrap();
#[cfg(unix)]
unsafe {
let mut version = std::mem::zeroed::<libc::utsname>();
libc::uname(&mut version);
let machine_model = get_machine_model();
writeln!(&mut out, "{TERM_BRIGHT_RED} {TERM_BOLD}{TERM_BRIGHT_WHITE}{} / {} / {} / {} / {}{TERM_RESET}", CStr::from_ptr(&version.machine as *const std::os::raw::c_char).to_string_lossy(), machine_model.as_deref().unwrap_or("-"), CStr::from_ptr(&version.sysname as *const std::os::raw::c_char).to_string_lossy(), CStr::from_ptr(&version.release as *const std::os::raw::c_char).to_string_lossy(), CStr::from_ptr(&version.nodename as *const std::os::raw::c_char).to_string_lossy()).unwrap();
}
let locker = crash_state.logger.lock().unwrap();
for (message, timestamp, level) in &locker.latest {
print_log_to_console(message, *timestamp, *level, &mut out);
}
} else if options.format == SendFormat::JsonSentry {
let id : u128 = rand::rngs::OsRng.gen::<u128>();
let time = std::time::SystemTime::now().duration_since(std::time::SystemTime::UNIX_EPOCH).unwrap().as_secs();
write!(&mut out, r"{{").unwrap();
write!(&mut out, r#""event_id": "{:032x}""#, id).unwrap();
write!(&mut out, r#","timestamp": {}"#, time).unwrap();
write!(&mut out, r#","platform": "rust""#).unwrap();
write!(&mut out, r#","logger": "indigo_crashy""#).unwrap();
if !options.release.is_empty() {
write!(&mut out, r#","release": {}"#, Quoted(&options.release)).unwrap();
}
if !options.dist.is_empty() {
write!(&mut out, r#","dist": {}"#, Quoted(&options.dist)).unwrap();
}
if !options.environment.is_empty() {
write!(&mut out, r#","environment": {}"#, Quoted(&options.environment)).unwrap();
}
write!(&mut out, r#","level": "fatal""#).unwrap();
write!(&mut out, r#","exception": {{"values":[{{"#).unwrap();
write!(&mut out, r#""type": {}"#, Quoted(error)).unwrap();
write!(&mut out, r#","mechanism": {{ "type": {}, "handled": {} }}"#, Quoted(type_of_error), handled).unwrap();
write!(&mut out, r#","value": {}"#, Quoted(msg)).unwrap();
write!(&mut out, r#","stacktrace": {{"frames":["#).unwrap();
let mut sep = "";
for (name, filename, line_no) in stack.iter().rev() {
write!(&mut out, r#"{}{{"function": {}, "filename": {}, "lineno": {}}}"#, sep, Quoted(name), Quoted(filename), line_no).unwrap();
sep = ",";
}
write!(&mut out, r#"]}}"#).unwrap();
write!(&mut out, r#"}}]}}"#).unwrap();
#[cfg(unix)]
{
let mut version;
unsafe {
version = std::mem::zeroed::<libc::utsname>();
libc::uname(&mut version);
}
write!(&mut out, r#","contexts": {{"#).unwrap();
{
write!(&mut out, r#""os": {{"#).unwrap();
unsafe {
write!(&mut out, r#""name": {}"#, Quoted(&CStr::from_ptr(&version.sysname as *const std::os::raw::c_char).to_string_lossy())).unwrap();
write!(&mut out, r#","version": {}"#, Quoted(&CStr::from_ptr(&version.release as *const std::os::raw::c_char).to_string_lossy())).unwrap();
}
write!(&mut out, r#"}}"#).unwrap();
write!(&mut out, r#","device": {{"#).unwrap();
unsafe {
write!(&mut out, r#""name": {}"#, Quoted(&CStr::from_ptr(&version.nodename as *const std::os::raw::c_char).to_string_lossy())).unwrap();
let machine_model = get_machine_model();
write!(&mut out, r#","model": {}"#, Quoted(machine_model.as_deref().unwrap_or("-"))).unwrap();
write!(&mut out, r#","arch": {}"#, Quoted(&CStr::from_ptr(&version.machine as *const std::os::raw::c_char).to_string_lossy())).unwrap();
}
write!(&mut out, r#"}}"#).unwrap();
#[cfg(feature = "trace")]
{
let span = tracing::span::Span::current();
let span = span.context();
let span = span.span();
let span = span.span_context();
let trace_id = span.trace_id();
let span_id = span.span_id();
if trace_id != opentelemetry::trace::TraceId::INVALID {
write!(&mut out, r#","trace": {{"trace_id":"{}","span_id":"{}"}}"#, trace_id, span_id).unwrap();
}
}
}
write!(&mut out, r#"}}"#).unwrap();
unsafe {
write!(&mut out, r#","server_name": {}"#, Quoted(&CStr::from_ptr(&version.nodename as *const std::os::raw::c_char).to_string_lossy())).unwrap();
}
}
write!(&mut out, r#","breadcrumbs": {{"values":["#).unwrap();
let mut sep = "";
let locker = crash_state.logger.lock().unwrap();
for (message, timestamp, level) in &locker.latest {
let level = match level {
log::Level::Error => "error",
log::Level::Warn => "warning",
log::Level::Info => "info",
log::Level::Debug => "debug",
log::Level::Trace => "verbose",
};
write!(&mut out, r#"{}{{"message": {}, "timestamp": {}, "level": {}}}"#, sep, Quoted(message), timestamp, Quoted(level)).unwrap();
sep = ",";
}
write!(&mut out, r#"]}}"#).unwrap();
write!(&mut out, r#"}}"#).unwrap();
}
let format = crash_state.options.format;
let sender = crash_state.options.sender.clone();
crash_state.counter += 1;
(format, sender, out)
};
let completer = Box::new(|| {
let mut crash_state = CRASH_STATE.get_or_init(Mutex::default).lock().unwrap();
crash_state.counter -= 1;
});
(sender)(format, out.into_bytes(), header_len, completer);
}
fn write_header(out: &mut String, type_of_error: &str, error: &str, msg: &str) {
writeln!(out, "{TERM_BRIGHT_RED}=========={TERM_RESET} CRASH {TERM_BRIGHT_RED}=========={TERM_RESET} [{}]", Utc::now()).unwrap();
if !msg.is_empty() {
writeln!(out, "{TERM_BRIGHT_GREEN} {TERM_RESET}{}", msg).unwrap();
}
writeln!(out, "{TERM_BRIGHT_CYAN} {TERM_RESET}{}: {}", type_of_error, error).unwrap();
}
fn print_header(type_of_error: &str, error: &str, msg: &str) {
eprintln!("{TERM_BRIGHT_RED}=========={TERM_RESET} CRASH {TERM_BRIGHT_RED}=========={TERM_RESET} [{}]", Utc::now());
if !msg.is_empty() {
eprintln!("{TERM_BRIGHT_GREEN} {TERM_RESET}{}", msg);
}
eprintln!("{TERM_BRIGHT_CYAN} {TERM_RESET}{}: {}", type_of_error, error);
}
fn systemd_logging_prefix(level: log::Level) -> &'static str {
match level {
log::Level::Error => "<3> ", log::Level::Warn => "<4> ", log::Level::Info => "<5> ", log::Level::Debug => "<6> ", log::Level::Trace => "<7> ", }
}
fn print_log_to_console(message: &str, timestamp: f64, level: log::Level, out: &mut String) {
let color = match level {
log::Level::Error => TERM_DIM_RED,
log::Level::Warn => TERM_DIM_YELLOW,
log::Level::Info => TERM_DIM_GREEN,
log::Level::Debug => TERM_DIM_CYAN,
log::Level::Trace => TERM_DIM_MAGENTA,
};
let level = match level {
log::Level::Error => "[ERROR]",
log::Level::Warn => " [WARN]",
log::Level::Info => " [INFO]",
log::Level::Debug => "[DEBUG]",
log::Level::Trace => " [VERB]",
};
let t = DateTime::from_timestamp(timestamp as i64, ((timestamp % 1.0) * 1_000_000_000.0) as u32);
if let Some(t) = t {
let ms = ((timestamp % 1.0) * 1_000.0) as u32;
writeln!(out, "{TERM_BRIGHT_BLUE}<!>{TERM_RESET} {:04}-{:02}-{:02} {:02}:{:02}:{:02}.{:03} {}{} {}{TERM_RESET}", t.year(), t.month(), t.day(), t.hour(), t.minute(), t.second(), ms, color, level, message).unwrap();
} else {
writeln!(out, "{TERM_BRIGHT_BLUE}<!>{TERM_RESET} xx-xx-xx xx:xx:xx.xxx {}{} {}{TERM_RESET}", color, level, message).unwrap();
}
}
fn get_machine_model() -> Option<String> {
#[allow(unused_assignments)]
#[allow(unused_mut)]
let mut retval = None;
#[cfg(target_os = "linux")]
{
let contents = std::fs::read_to_string("/proc/cpuinfo");
retval = contents.ok().as_ref()
.and_then(|x| x.split_once("\nmodel name\t: ")).map(|(_,x)| x)
.and_then(|x| x.split_once('\n')).map(|(x,_)| x)
.map(|x| x.to_string())
}
#[cfg(any(target_os = "macos", target_os = "freebsd"))]
{
let mut buffer : [libc::c_char; 256] = [0; 256];
let mut size = core::mem::size_of_val(&buffer);
unsafe {
if libc::sysctlbyname(b"hw.model\0" as * const u8 as * const libc::c_char,
&mut buffer as * mut i8 as * mut libc::c_void, &mut size,
core::ptr::null_mut(), 0) == 0 {
retval = Some(CStr::from_ptr(buffer.as_ptr()).to_string_lossy()).map(|x| x.to_string());
}
}
}
retval
}
#[cfg(test)]
mod crashy_test {
use rand::{rngs::OsRng, RngCore};
use crate::*;
use log::*;
use std::panic::Location;
static mut SPAN_ID : Option<u64> = None;
static mut TRACE_ID : u64 = 0;
#[derive(Debug)]
struct Span {
trace_id: u64,
span_id: u64,
parent_id: Option<u64>,
description: &'static str,
start: std::time::Instant,
}
impl Drop for Span {
fn drop(&mut self) {
let now = std::time::Instant::now();
eprintln!("trace: {:?} -> {:?}", self, now - self.start);
unsafe {
if let Some(current_span_id) = SPAN_ID {
if current_span_id == self.span_id {
SPAN_ID = self.parent_id;
}
}
}
}
}
fn trace(description: &'static str) -> Span {
let trace_id;
let span_id;
let mut parent_id = None;
unsafe {
if SPAN_ID.is_some() {
parent_id = SPAN_ID;
} else {
TRACE_ID = OsRng.next_u64();
}
trace_id = TRACE_ID;
span_id = OsRng.next_u64();
SPAN_ID = Some(span_id);
}
Span {
trace_id,
span_id,
parent_id,
description,
start: std::time::Instant::now(),
}
}
#[track_caller]
fn get_caller_location() -> &'static Location<'static> {
Location::caller()
}
fn foo() {
warn!("foo()");
let _t = trace("foo");
println!("{:?}", get_caller_location());
panic!("foo panic bar");
}
fn bar() {
info!("bar()");
error!("bar error");
let _t = trace("bar");
foo();
}
#[test] #[ignore]
fn unix_signal() {
let _crash_handler = setup_crashy();
info!("just before abort");
unsafe { libc::abort(); }
}
#[test] #[ignore]
fn it_works() {
let crash_options = CrashyOptions::default_sync();
let _crash_handler = setup_crashy_with_options(crash_options);
bar();
bar();
let result = 2 + 2;
panic!("oh oh {}", result);
}
#[test]
fn machine_model() {
let model = get_machine_model();
eprintln!("model: {:?}", model);
}
}