baobab/core/logger/src/system_logger.rs
2025-08-07 09:59:16 +02:00

170 lines
5.8 KiB
Rust

//! 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<P: AsRef<Path>>(
logs_root: P,
components: &[String],
) -> Result<Vec<WorkerGuard>> {
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::<Vec<_>>();
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<P: AsRef<Path>>(
logs_root: P,
component: &str,
) -> Result<(Box<dyn Layer<tracing_subscriber::Registry> + 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());
}
}