fluidattacks-core 0.19.0

Fluid Attacks Core Library
Documentation
use serde_json::{Map, Value};
use std::fmt;
use tracing::field::{Field, Visit};
use tracing::{Event, Subscriber};
use tracing_subscriber::{
    fmt::{format::Writer, FmtContext, FormatEvent, FormatFields},
    registry::LookupSpan,
};

pub struct BatchJsonFormatter {
    static_fields: Vec<(&'static str, String)>,
}

impl BatchJsonFormatter {
    pub const fn new(static_fields: Vec<(&'static str, String)>) -> Self {
        Self { static_fields }
    }
}

fn timestamp_ms() -> u64 {
    std::time::SystemTime::now()
        .duration_since(std::time::UNIX_EPOCH)
        .map_or(0, |d| u64::try_from(d.as_millis()).unwrap_or(u64::MAX))
}

struct JsonVisitor<'a>(&'a mut Map<String, Value>);

impl Visit for JsonVisitor<'_> {
    fn record_f64(&mut self, field: &Field, value: f64) {
        let json_value = serde_json::Number::from_f64(value)
            .map_or_else(|| Value::String(value.to_string()), Value::Number);
        self.0.insert(field.name().to_owned(), json_value);
    }

    fn record_i64(&mut self, field: &Field, value: i64) {
        self.0
            .insert(field.name().to_owned(), Value::Number(value.into()));
    }

    fn record_u64(&mut self, field: &Field, value: u64) {
        self.0
            .insert(field.name().to_owned(), Value::Number(value.into()));
    }

    fn record_bool(&mut self, field: &Field, value: bool) {
        self.0.insert(field.name().to_owned(), Value::Bool(value));
    }

    fn record_str(&mut self, field: &Field, value: &str) {
        self.0
            .insert(field.name().to_owned(), Value::String(value.to_owned()));
    }

    fn record_error(&mut self, field: &Field, value: &(dyn std::error::Error + 'static)) {
        self.0
            .insert(field.name().to_owned(), Value::String(value.to_string()));
    }

    fn record_debug(&mut self, field: &Field, value: &dyn fmt::Debug) {
        self.0
            .insert(field.name().to_owned(), Value::String(format!("{value:?}")));
    }
}

impl<S, N> FormatEvent<S, N> for BatchJsonFormatter
where
    S: Subscriber + for<'a> LookupSpan<'a>,
    N: for<'a> FormatFields<'a> + 'static,
{
    fn format_event(
        &self,
        _ctx: &FmtContext<'_, S, N>,
        mut writer: Writer<'_>,
        event: &Event<'_>,
    ) -> fmt::Result {
        let mut map = Map::new();

        map.insert("timestamp".to_owned(), Value::Number(timestamp_ms().into()));
        map.insert(
            "level".to_owned(),
            Value::String(event.metadata().level().to_string()),
        );
        map.insert(
            "name".to_owned(),
            Value::String(event.metadata().target().to_owned()),
        );

        if let Some(file) = event.metadata().file() {
            if let Some(line) = event.metadata().line() {
                map.insert(
                    "file_location".to_owned(),
                    Value::String(format!("{file}:{line}")),
                );
            }
        }

        for (key, value) in &self.static_fields {
            map.insert((*key).to_owned(), Value::String(value.clone()));
        }

        event.record(&mut JsonVisitor(&mut map));

        let json = serde_json::to_string(&Value::Object(map)).unwrap_or_else(|_| {
            r#"{"level":"ERROR","message":"log serialization failed"}"#.to_owned()
        });

        writeln!(writer, "{json}")
    }
}