#![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::*;
#[cfg(feature = "sentry")]
use hyper_rustls::ConfigBuilderExt;
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 {
}
pub struct CrashOptions {
pub format: SendFormat,
pub sender: Arc<dyn Fn (SendFormat, Vec<u8>) + Sync + Send>,
get_breadcrumbs: fn () -> Vec<Breadcrumb>,
release: String,
dist: String,
environment: String,
command: String,
path: PathBuf,
report_username: bool,
}
impl Default for CrashOptions {
fn default() -> Self {
let mut release : String = String::new();
if let (Some(revision),Some(pipeline_id)) = (build_revision(), build_pipeline_id()) {
release = format!("{}-{}", revision, pipeline_id);
}
#[allow(unused_mut)]
let mut sender : Arc<dyn Fn (SendFormat, Vec<u8>) + Sync + Send> = Arc::new(|_format, data: Vec<u8>| {
std::io::stdout().write_all(&data).unwrap();
});
#[allow(unused_mut)]
let mut format = SendFormat::PlainText;
#[cfg(feature = "sentry")]
if let Some(dsn) = buildinfy::build_sentry_dsn() {
if let Some(current) = dsn.strip_prefix("https://") {
if let Some((key, current)) = current.split_once('@') {
if let Some((host, project)) = current.split_once('/') {
format = SendFormat::JsonSentry;
sender = Arc::new(move |_, body: Vec<u8>| {
let url = format!("https://{host}/api/{project}/store/");
let mut client_req = hyper::Request::builder()
.method("POST")
.uri(url);
let headers = client_req.headers_mut().unwrap();
headers.insert(hyper::header::HeaderName::from_lowercase(b"x-sentry-auth").unwrap(), hyper::header::HeaderValue::from_str(&format!("Sentry sentry_version=7, sentry_client=indigo, sentry_key={key}")).unwrap());
let client_req = client_req
.body(hyper::Body::from(body.to_vec()))
.expect("request builder");
let client = {
let tls = tokio_rustls::rustls::ClientConfig::builder()
.with_safe_defaults()
.with_native_roots()
.with_no_client_auth();
let https = hyper_rustls::HttpsConnectorBuilder::new()
.with_tls_config(tls)
.https_or_http()
.enable_http1()
.build();
hyper::Client::builder()
.http1_preserve_header_case(true)
.http1_title_case_headers(true)
.build::<_, hyper::Body>(https)
};
let rt = tokio::runtime::Builder::new_multi_thread()
.enable_all()
.worker_threads(1)
.build()
.expect("build runtime");
rt.block_on(async move {
let resp = client.request(client_req).await;
eprintln!("sent crashy error report to sentry: {}", if resp.is_ok() { "ok" } else { "failed" });
if resp.is_err() {
eprintln!("{:?}", resp);
}
});
});
}
}
}
}
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,
}
}
}
pub struct LatestLogs {
pub latest: Vec<(String,f64,log::Level)>,
}
struct Logger {
latest: Arc<Mutex<LatestLogs>>,
}
impl log::Log for Logger {
fn enabled(&self, metadata: &log::Metadata) -> bool {
metadata.level() <= log::Level::Info
}
fn log(&self, record: &log::Record) {
if self.enabled(record.metadata()) {
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.target(), record.args(), file, lineno).unwrap();
}
}
if line.is_empty() {
write!(&mut line, "{}: {}", record.target(), record.args()).unwrap();
}
eprintln!("{}", line);
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() -> (Self, Arc<Mutex<LatestLogs>>) {
let latest = Arc::new(Mutex::new(LatestLogs{
latest: Vec::with_capacity(LOGGER_DEFAULT_LATEST_CAPACITY),
}));
(Self {
latest: latest.clone(),
}, latest)
}
}
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<'a> Display for Quoted<'a> {
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 {
counter: Arc<Mutex<u32>>,
}
impl Drop for CrashHandler {
fn drop(&mut self) {
while *self.counter.lock().unwrap() > 0 {
std::thread::sleep(std::time::Duration::from_millis(100));
}
}
}
pub fn setup_crashy() -> CrashHandler {
setup_crashy_with_options(Box::default())
}
pub fn setup_crashy_with_options(options: Box<CrashOptions>) -> CrashHandler {
let (logger, latest) = Logger::new();
log::set_boxed_logger(Box::new(logger))
.map(|()| log::set_max_level(log::LevelFilter::Info)).unwrap();
let counter = Arc::new(Mutex::new(0));
let _retval = latest.clone();
let counter_copy = counter.clone();
std::panic::set_hook(Box::new(move |pi: &core::panic::PanicInfo| {
let mut started = false;
let mut frames = Vec::new();
backtrace::trace(|frame| {
let mut display = true;
backtrace::resolve_frame(frame, |symbol| {
let mut n = String::from("-");
let mut f = String::from("");
let mut l = 0;
if let Some(name) = symbol.name() {
if name.as_bytes() == b"rust_begin_unwind" {
started = true;
display = false;
}
if name.as_bytes().starts_with(b"_ZN4core9panicking9panic_fmt17") {
display = false;
}
n = format!("{}", name);
}
if let Some(filename) = symbol.filename() {
f = filename.display().to_string();
}
if let Some(line_no) = symbol.lineno() {
l = line_no;
}
if started && display {
frames.push((n, f, l));
}
});
true });
let mut msg = String::new();
if let Some(s) = pi.payload().downcast_ref::<&str>() {
write!(&mut msg, "{}", s).unwrap();
} else if let Some(s) = pi.payload().downcast_ref::<String>() {
write!(&mut msg, "{}", s).unwrap();
} else {
write!(&mut msg, "{}", pi).unwrap();
}
let mut out = String::new();
if options.format == SendFormat::PlainText {
writeln!(&mut out, "{TERM_BRIGHT_RED}=========={TERM_RESET} CRASH {TERM_BRIGHT_RED}=========={TERM_RESET} [{}]", Utc::now()).unwrap();
if let Some(s) = pi.payload().downcast_ref::<&str>() {
write!(&mut out, "panic {s:?} occurred").unwrap();
} else if let Some(s) = pi.payload().downcast_ref::<String>() {
write!(&mut out, "panic {s:?} occurred").unwrap();
} else {
write!(&mut out, "panic occurred").unwrap();
}
if let Some(location) = pi.location() {
writeln!(&mut out, " in file '{}' at line {}",
location.file(),
location.line(),
).unwrap();
} else {
writeln!(&mut out).unwrap();
}
for (name, filename, line_no) in frames {
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();
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();
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);
writeln!(&mut out, "{TERM_BRIGHT_RED} {TERM_BOLD}{TERM_BRIGHT_WHITE}{} / {} / {} / {}{TERM_RESET}", 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(), CStr::from_ptr(&version.machine as *const std::os::raw::c_char).to_string_lossy()).unwrap();
}
let locker = latest.lock().unwrap();
for (message, timestamp, level) in &locker.latest {
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 = NaiveDateTime::from_timestamp_opt(*timestamp as i64, ((*timestamp % 1.0) * 1_000_000_000.0) as u32);
if let Some(t) = t {
let t = Utc.from_utc_datetime(&t);
let ms = ((*timestamp % 1.0) * 1_000.0) as u32;
writeln!(&mut 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!(&mut out, "{TERM_BRIGHT_BLUE}<!>{TERM_RESET} xx-xx-xx xx:xx:xx.xxx {}{} {}{TERM_RESET}", color, level, message).unwrap();
}
}
} 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": "panic""#).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 frames.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();
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();
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 = latest.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 = options.format;
let sender = options.sender.clone();
*counter_copy.lock().unwrap() += 1;
let counter_copy = counter_copy.clone();
std::thread::spawn(move || {
(sender)(format, out.into_bytes());
*counter_copy.lock().unwrap() -= 1;
});
}));
CrashHandler{counter}
}
#[cfg(test)]
mod crashy {
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());
}
fn bar() {
info!("bar()");
error!("bar error");
let _t = trace("bar");
foo();
}
#[test] #[ignore]
fn it_works() {
let crash_options = CrashOptions{ format: SendFormat::JsonSentry, ..CrashOptions::default() };
setup_crashy_with_options(Box::new(crash_options));
bar();
bar();
let result = 2 + 2;
panic!("oh oh {}", result);
}
}