fluidattacks-core 0.19.0

Fluid Attacks Core Library
Documentation
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}");
}