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 crate::error::ItraceError;
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
use std::path::PathBuf;

#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Config {
    #[serde(default)]
    pub general: GeneralConfig,
    #[serde(default)]
    pub logging: LoggingConfig,
    #[serde(default, rename = "level")]
    pub level_styles: HashMap<String, LevelStyle>,
    #[serde(default)]
    pub datetime: DateTimeConfig,
    #[serde(default)]
    pub columns: ColumnsConfig,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct GeneralConfig {
    #[serde(default = "default_true")]
    pub colors: bool,
    pub output: Option<PathBuf>,
}

impl Default for GeneralConfig {
    fn default() -> Self {
        Self {
            colors: true,
            output: None,
        }
    }
}

fn default_true() -> bool {
    true
}

#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct LoggingConfig {
    #[serde(default = "default_level")]
    pub default: String,
    #[serde(default)]
    pub targets: HashMap<String, String>,
}

impl Default for LoggingConfig {
    fn default() -> Self {
        Self {
            default: default_level(),
            targets: HashMap::new(),
        }
    }
}

fn default_level() -> String {
    "info".to_string()
}

#[derive(Debug, Clone, Default, Serialize, Deserialize)]
pub struct LevelStyle {
    pub bg: Option<Color>,
    pub fg: Option<Color>,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(untagged)]
pub enum Color {
    Ansi { ansi: u8 },
    Rgb { rgb: [u8; 3] },
}

#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct DateTimeConfig {
    #[serde(default = "default_datetime_format")]
    pub format: String,
}

impl Default for DateTimeConfig {
    fn default() -> Self {
        Self {
            format: default_datetime_format(),
        }
    }
}

fn default_datetime_format() -> String {
    "%H:%M:%S%.6f".to_string()
}

/// Le colonne custom stanno tutte sotto [columns] nel TOML:
///
/// [columns]
/// order = ["app", "zone", "node"]
///
/// [columns.default]
/// width = 10
/// align = "left"
///
/// [columns.app]
/// width = 10
/// align = "center"
/// bg.rgb = [195, 215, 235]
/// fg.rgb = [30, 60, 100]
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ColumnsConfig {
    #[serde(default)]
    pub order: Vec<String>,
    /// Fallback per colonne senza definizione esplicita
    #[serde(default)]
    pub default: ColumnDef,
    /// Colonne custom: nome → definizione
    #[serde(default, flatten)]
    pub definitions: HashMap<String, ColumnDef>,
}

impl Default for ColumnsConfig {
    fn default() -> Self {
        Self {
            order: Vec::new(),
            default: ColumnDef::default(),
            definitions: HashMap::new(),
        }
    }
}

#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ColumnDef {
    #[serde(default = "default_width")]
    pub width: usize,
    #[serde(default)]
    pub align: Alignment,
    pub bg: Option<Color>,
    pub fg: Option<Color>,
}

impl Default for ColumnDef {
    fn default() -> Self {
        Self {
            width: default_width(),
            align: Alignment::default(),
            bg: None,
            fg: None,
        }
    }
}

fn default_width() -> usize {
    10
}

#[derive(Debug, Clone, Default, Serialize, Deserialize)]
#[serde(rename_all = "lowercase")]
pub enum Alignment {
    #[default]
    Left,
    Right,
    Center,
}

impl Config {
    pub fn load(tracer_name: &str) -> Result<Self, ItraceError> {
        let path = Self::resolve_path(tracer_name)?;
        if !path.exists() {
            return Err(ItraceError::TracerNotFound { path });
        }
        let content = std::fs::read_to_string(&path).map_err(|e| ItraceError::Io {
            path: path.clone(),
            source: e,
        })?;
        let config: Config = toml::from_str(&content).map_err(|e| ItraceError::Toml {
            path: path.clone(),
            source: e,
        })?;
        Ok(config)
    }

    pub fn resolve_path(tracer_name: &str) -> Result<PathBuf, ItraceError> {
        let base = dirs::config_dir().ok_or(ItraceError::NoConfigDir)?;
        Ok(base.join("itrace").join(format!("{tracer_name}.toml")))
    }

    pub fn to_env_filter_string(&self) -> String {
        let mut parts: Vec<String> = self
            .logging
            .targets
            .iter()
            .map(|(target, level)| format!("{target}={level}"))
            .collect();
        parts.insert(0, self.logging.default.clone());
        parts.join(",")
    }
}