//! System Logger Implementation //! //! This module implements the system-wide logging functionality that captures //! all non-job-specific logs from every component with target-based filtering. use crate::{LoggerError, Result, custom_formatter::HeroFormatter}; use std::path::{Path, PathBuf}; use tracing_subscriber::{ filter::{EnvFilter, LevelFilter}, fmt, layer::SubscriberExt, util::SubscriberInitExt, Layer, }; use tracing_appender::{non_blocking::WorkerGuard, rolling}; /// Initialize the system logger with component-based filtering /// /// This creates multiple file appenders, each filtered by a specific tracing target: /// - `tracing::info!(target: "supervisor", ...)` -> `logs/supervisor/` /// - `tracing::info!(target: "osis_actor", ...)` -> `logs/actor/osis/` /// - etc. pub fn init_system_logger>( logs_root: P, components: &[String], ) -> Result> { let logs_root = logs_root.as_ref(); // Ensure log directories exist crate::utils::ensure_log_directories(logs_root, components)?; let mut guards = Vec::new(); let mut layers = Vec::new(); // Create a layer for each component for component in components { let (layer, guard) = create_component_layer(logs_root, component)?; layers.push(layer); guards.push(guard); } // Create the registry with all layers let registry = tracing_subscriber::registry(); // Add all component layers to the registry let collected_layers = layers.into_iter().collect::>(); let registry = registry.with(collected_layers); // Add console output for development let console_layer = fmt::layer() .with_target(true) .with_thread_ids(true) .with_file(true) .with_line_number(true) .with_filter(EnvFilter::from_default_env().add_directive(LevelFilter::INFO.into())); // Set as global default registry.with(console_layer).init(); tracing::info!(target: "hero_logger", "System logger initialized with {} components", components.len()); Ok(guards) } /// Create a filtered layer for a specific component fn create_component_layer>( logs_root: P, component: &str, ) -> Result<(Box + Send + Sync>, WorkerGuard)> { let logs_root = logs_root.as_ref(); // Determine the log directory based on component type let log_dir = if component == "supervisor" { logs_root.join("supervisor") } else { // Extract actor type from component name (e.g., "osis_actor" -> "osis") let actor_type = component.strip_suffix("_actor").unwrap_or(component); logs_root.join("actor").join(actor_type) }; // Create hourly rolling file appender let file_appender = rolling::hourly(&log_dir, "log"); let (non_blocking, guard) = tracing_appender::non_blocking(file_appender); // Create a formatted layer with custom Hero formatter and target filtering let layer = fmt::layer() .with_writer(non_blocking) .event_format(HeroFormatter::new()) .with_ansi(false) // No ANSI colors in log files .with_filter( EnvFilter::new(format!("{}=trace", component)) .add_directive(LevelFilter::INFO.into()) ); Ok((layer.boxed(), guard)) } #[cfg(test)] mod tests { use super::*; use tempfile::TempDir; use tracing::{info, warn}; use std::time::Duration; use tokio::time::sleep; #[tokio::test] async fn test_system_logger_initialization() { let temp_dir = TempDir::new().unwrap(); let logs_root = temp_dir.path(); let components = vec![ "supervisor".to_string(), "osis_actor".to_string(), "sal_actor".to_string(), ]; let _guards = init_system_logger(logs_root, &components).unwrap(); // Test logging to different targets info!(target: "supervisor", "Supervisor started"); info!(target: "osis_actor", "OSIS actor ready"); info!(target: "sal_actor", "SAL actor ready"); // Give some time for async writing sleep(Duration::from_millis(100)).await; // Verify directories were created assert!(logs_root.join("supervisor").exists()); assert!(logs_root.join("actor/osis").exists()); assert!(logs_root.join("actor/sal").exists()); } #[tokio::test] async fn test_component_layer_creation() { let temp_dir = TempDir::new().unwrap(); let logs_root = temp_dir.path(); // Create supervisor layer let (supervisor_layer, _guard1) = create_component_layer(logs_root, "supervisor").unwrap(); assert!(logs_root.join("supervisor").exists()); // Create actor layer let (actor_layer, _guard2) = create_component_layer(logs_root, "osis_actor").unwrap(); assert!(logs_root.join("actor/osis").exists()); } #[tokio::test] async fn test_multiple_components() { let temp_dir = TempDir::new().unwrap(); let logs_root = temp_dir.path(); let components = vec![ "supervisor".to_string(), "osis_actor".to_string(), "sal_actor".to_string(), "v_actor".to_string(), "python_actor".to_string(), ]; let guards = init_system_logger(logs_root, &components).unwrap(); assert_eq!(guards.len(), components.len()); // Test that all directories were created assert!(logs_root.join("supervisor").exists()); assert!(logs_root.join("actor/osis").exists()); assert!(logs_root.join("actor/sal").exists()); assert!(logs_root.join("actor/v").exists()); assert!(logs_root.join("actor/python").exists()); } }