mod formatter;
mod sources;
use std::sync::OnceLock;
use tracing_subscriber::{layer::SubscriberExt, util::SubscriberInitExt, EnvFilter};
static IN_BATCH: OnceLock<bool> = OnceLock::new();
static STATIC_FIELDS: OnceLock<Vec<(&'static str, String)>> = OnceLock::new();
fn in_batch() -> bool {
*IN_BATCH.get_or_init(sources::is_batch)
}
pub fn init_logging(product_id: &str) {
let batch = sources::is_batch();
let _ = IN_BATCH.set(batch);
let fields = sources::source_fields(product_id);
let _ = STATIC_FIELDS.set(fields.clone());
let filter = EnvFilter::try_from_default_env().unwrap_or_else(|_| EnvFilter::new("info"));
if batch {
tracing_subscriber::registry()
.with(filter)
.with(
tracing_subscriber::fmt::layer()
.with_writer(std::io::stderr)
.with_ansi(false)
.event_format(formatter::BatchJsonFormatter::new(fields)),
)
.init();
} else {
tracing_subscriber::registry()
.with(filter)
.with(
tracing_subscriber::fmt::layer()
.with_writer(std::io::stderr)
.with_ansi(false)
.with_target(false),
)
.init();
}
}
pub fn init_panic_hook() {
let batch = in_batch();
let prev = std::panic::take_hook();
std::panic::set_hook(Box::new(move |info| {
if batch {
emit_panic_json(info);
} else {
prev(info);
}
}));
}
fn emit_panic_json(info: &std::panic::PanicHookInfo<'_>) {
use std::io::Write as _;
let msg = info
.payload()
.downcast_ref::<&str>()
.copied()
.or_else(|| info.payload().downcast_ref::<String>().map(String::as_str))
.unwrap_or("unknown panic");
let location = info.location().map_or_else(
|| "unknown".to_owned(),
|loc| format!("{}:{}", loc.file(), loc.line()),
);
let millis = std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.map_or(0, |d| u64::try_from(d.as_millis()).unwrap_or(u64::MAX));
let mut map = serde_json::Map::new();
map.insert("timestamp".to_owned(), millis.into());
map.insert("level".to_owned(), "CRITICAL".into());
map.insert("message".to_owned(), "Unhandled panic".into());
map.insert("error.kind".to_owned(), "panic".into());
map.insert("error.message".to_owned(), msg.into());
map.insert("error.stack".to_owned(), location.into());
if let Some(fields) = STATIC_FIELDS.get() {
for (key, value) in fields {
map.insert((*key).to_owned(), value.clone().into());
}
}
let json = serde_json::to_string(&serde_json::Value::Object(map))
.unwrap_or_else(|_| r#"{"level":"CRITICAL","message":"Unhandled panic"}"#.to_owned());
let _ = writeln!(std::io::stderr(), "{json}");
}