itrace 0.1.1

Structured, columnar tracing for Rust applications
Documentation
// Copyright (c) 2026 Claudio Carraro <wiclac@pm.me>
// SPDX-License-Identifier: BSD-3-Clause

use std::collections::{HashMap, HashSet};
use std::sync::Arc;

use arc_swap::ArcSwap;
use tracing::{Event, Level, Subscriber};
use tracing_subscriber::fmt::{FmtContext, FormatEvent, FormatFields};
use tracing_subscriber::registry::LookupSpan;

use crate::color;
use crate::config::{Alignment, Color, Config};

pub struct ItraceFormatter {
    config: Arc<ArcSwap<Config>>,
}

impl ItraceFormatter {
    pub fn new(config: Arc<ArcSwap<Config>>) -> Self {
        Self { config }
    }
}

impl<S, N> FormatEvent<S, N> for ItraceFormatter
where
    S: Subscriber + for<'a> LookupSpan<'a>,
    N: for<'a> FormatFields<'a> + 'static,
{
    fn format_event(
        &self,
        _ctx: &FmtContext<'_, S, N>,
        mut writer: tracing_subscriber::fmt::format::Writer<'_>,
        event: &Event<'_>,
    ) -> std::fmt::Result {
        // Snapshot atomico della config — zero lock in lettura
        let config = self.config.load();

        // 1. Visita tutti i fields dell'evento
        let mut visitor = FieldVisitor::default();
        event.record(&mut visitor);

        // 2. Estrai il messaggio (field speciale di tracing)
        let message = visitor.fields.remove("message").unwrap_or_default();

        // 3. Level badge
        write_level_badge(&mut writer, event.metadata().level(), &config)?;

        // 4. DateTime (spazio neutro prima)
        write!(writer, " ")?;
        let dt = crate::datetime::format_now(&config.datetime.format);
        write_column(
            &mut writer,
            &dt,
            dt.len() + 2,
            &Alignment::Left,
            None,
            None,
            config.general.colors,
        )?;

        // 5. Colonne custom in ordine dichiarato nel TOML
        // Uno spazio neutro separa visivamente ogni colonna colorata
        let mut consumed: HashSet<&str> = HashSet::new();
        for col_name in &config.columns.order {
            let def = config
                .columns
                .definitions
                .get(col_name)
                .unwrap_or(&config.columns.default);

            let value = visitor
                .fields
                .get(col_name)
                .map(String::as_str)
                .unwrap_or("");

            if !value.is_empty() {
                consumed.insert(col_name.as_str());
            }

            write!(writer, " ")?;
            write_column(
                &mut writer,
                value,
                def.width,
                &def.align,
                def.bg.as_ref(),
                def.fg.as_ref(),
                config.general.colors,
            )?;
        }

        // 6. Field non consumati → appesi al messaggio finale
        let extras: Vec<String> = visitor
            .fields
            .iter()
            .filter(|(k, _)| !consumed.contains(k.as_str()))
            .map(|(k, v)| format!("{k}={v}"))
            .collect();

        let full_message = if extras.is_empty() {
            message
        } else {
            format!("{message} [{}]", extras.join(", "))
        };

        write!(writer, " {full_message}")?;
        writeln!(writer)
    }
}

// ---------------------------------------------------------------------------
// FieldVisitor
// ---------------------------------------------------------------------------

#[derive(Default)]
struct FieldVisitor {
    fields: HashMap<String, String>,
}

impl tracing::field::Visit for FieldVisitor {
    fn record_str(&mut self, field: &tracing::field::Field, value: &str) {
        self.fields
            .insert(field.name().to_string(), value.to_string());
    }

    fn record_i64(&mut self, field: &tracing::field::Field, value: i64) {
        self.fields
            .insert(field.name().to_string(), value.to_string());
    }

    fn record_u64(&mut self, field: &tracing::field::Field, value: u64) {
        self.fields
            .insert(field.name().to_string(), value.to_string());
    }

    fn record_bool(&mut self, field: &tracing::field::Field, value: bool) {
        self.fields
            .insert(field.name().to_string(), value.to_string());
    }

    fn record_debug(&mut self, field: &tracing::field::Field, value: &dyn std::fmt::Debug) {
        self.fields
            .insert(field.name().to_string(), format!("{value:?}"));
    }
}

// ---------------------------------------------------------------------------
// Rendering helpers
// ---------------------------------------------------------------------------

fn write_level_badge(
    writer: &mut tracing_subscriber::fmt::format::Writer<'_>,
    level: &Level,
    config: &Config,
) -> std::fmt::Result {
    let badge = match *level {
        Level::TRACE => "T",
        Level::DEBUG => "D",
        Level::INFO => "I",
        Level::WARN => "W",
        Level::ERROR => "E",
    };

    let level_key = match *level {
        Level::TRACE => "trace",
        Level::DEBUG => "debug",
        Level::INFO => "info",
        Level::WARN => "warn",
        Level::ERROR => "error",
    };

    let style = config.level_styles.get(level_key);

    if config.general.colors {
        if let Some(s) = style {
            if let Some(bg) = &s.bg {
                write!(writer, "{}", color::color_bg(bg))?;
            }
            if let Some(fg) = &s.fg {
                write!(writer, "{}", color::color_fg(fg))?;
            }
        }
    }

    write!(writer, " {badge} ")?;

    if config.general.colors {
        write!(writer, "{}", color::RESET)?;
    }

    Ok(())
}

fn write_column(
    writer: &mut tracing_subscriber::fmt::format::Writer<'_>,
    value: &str,
    width: usize,
    align: &Alignment,
    bg: Option<&Color>,
    fg: Option<&Color>,
    colors: bool,
) -> std::fmt::Result {
    if colors {
        if let Some(bg) = bg {
            write!(writer, "{}", color::color_bg(bg))?;
        }
        if let Some(fg) = fg {
            write!(writer, "{}", color::color_fg(fg))?;
        }
    }

    let padded = match align {
        Alignment::Left => format!(" {value:<width$} "),
        Alignment::Right => format!(" {value:>width$} "),
        Alignment::Center => format!(" {value:^width$} "),
    };
    write!(writer, "{padded}")?;

    if colors {
        write!(writer, "{}", color::RESET)?;
    }

    Ok(())
}