bzzz-core 0.1.0

Bzzz core library - Declarative orchestration engine for AI Agents
Documentation
//! Structured Logging Module
//!
//! Provides JSON-formatted structured logging support.
//!
//! # Example
//!
//! ```
//! use bzzz_core::{StructuredLogger, LogEntry, LogLevel};
//!
//! // Create a logger with Info level
//! let logger = StructuredLogger::info_level();
//!
//! // Log with structured fields
//! logger.info("Starting execution");
//!
//! // Create a log entry with context
//! let entry = LogEntry::new(LogLevel::Info, "Worker completed")
//!     .with_target("parallel_executor")
//!     .with_field("worker_id", "w1")
//!     .with_field("duration_ms", 150);
//!
//! println!("{}", entry.to_json());
//! ```

use std::fmt;
use std::sync::OnceLock;
use std::time::SystemTime;

use serde::{Serialize, Serializer};

use crate::RunId;

/// Global logger instance
static GLOBAL_LOGGER: OnceLock<StructuredLogger> = OnceLock::new();

/// Log level
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum LogLevel {
    Trace,
    Debug,
    Info,
    Warn,
    Error,
}

impl fmt::Display for LogLevel {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        match self {
            LogLevel::Trace => write!(f, "TRACE"),
            LogLevel::Debug => write!(f, "DEBUG"),
            LogLevel::Info => write!(f, "INFO"),
            LogLevel::Warn => write!(f, "WARN"),
            LogLevel::Error => write!(f, "ERROR"),
        }
    }
}

impl Serialize for LogLevel {
    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
    where
        S: Serializer,
    {
        serializer.serialize_str(&self.to_string())
    }
}

/// Structured log entry
#[derive(Debug, Clone, Serialize)]
pub struct LogEntry {
    /// Timestamp in ISO 8601 format
    pub timestamp: String,
    /// Log level
    pub level: LogLevel,
    /// Log message
    pub message: String,
    /// Target/module
    pub target: String,
    /// Additional fields
    #[serde(flatten)]
    pub fields: serde_json::Map<String, serde_json::Value>,
}

impl LogEntry {
    /// Create a new log entry
    pub fn new(level: LogLevel, message: impl Into<String>) -> Self {
        LogEntry {
            timestamp: Self::current_timestamp(),
            level,
            message: message.into(),
            target: "bzzz".to_string(),
            fields: serde_json::Map::new(),
        }
    }

    /// Set the target/module
    pub fn with_target(mut self, target: impl Into<String>) -> Self {
        self.target = target.into();
        self
    }

    /// Add a run_id field
    pub fn with_run_id(mut self, run_id: &RunId) -> Self {
        self.fields.insert(
            "run_id".into(),
            serde_json::Value::String(run_id.as_str().to_string()),
        );
        self
    }

    /// Add a step_id field
    pub fn with_step_id(mut self, step_id: &str) -> Self {
        self.fields.insert(
            "step_id".into(),
            serde_json::Value::String(step_id.to_string()),
        );
        self
    }

    /// Add a worker field
    pub fn with_worker(mut self, worker: &str) -> Self {
        self.fields.insert(
            "worker".into(),
            serde_json::Value::String(worker.to_string()),
        );
        self
    }

    /// Add a field
    pub fn with_field(mut self, key: impl Into<String>, value: impl Serialize) -> Self {
        if let Ok(json_value) = serde_json::to_value(value) {
            self.fields.insert(key.into(), json_value);
        }
        self
    }

    /// Add multiple fields
    pub fn with_fields(
        mut self,
        fields: impl IntoIterator<Item = (String, serde_json::Value)>,
    ) -> Self {
        for (key, value) in fields {
            self.fields.insert(key, value);
        }
        self
    }

    /// Get current timestamp in ISO 8601 format
    fn current_timestamp() -> String {
        let now = SystemTime::now()
            .duration_since(SystemTime::UNIX_EPOCH)
            .unwrap_or_default();

        let secs = now.as_secs();
        let nanos = now.subsec_nanos();

        // Simple ISO 8601 format
        chrono_builder(secs, nanos)
    }

    /// Convert to JSON string
    pub fn to_json(&self) -> String {
        serde_json::to_string(self).unwrap_or_else(|_| {
            format!(
                r#"{{"timestamp":"{}","level":"{}","message":"{}"}}"#,
                self.timestamp, self.level, self.message
            )
        })
    }
}

/// Build ISO 8601 datetime string
fn chrono_builder(secs: u64, nanos: u32) -> String {
    // Calculate date components
    const SECS_PER_DAY: u64 = 86400;
    const SECS_PER_HOUR: u64 = 3600;
    const SECS_PER_MIN: u64 = 60;

    let days_since_epoch = secs / SECS_PER_DAY;
    let secs_remaining = secs % SECS_PER_DAY;

    let hour = secs_remaining / SECS_PER_HOUR;
    let mins_remaining = secs_remaining % SECS_PER_HOUR;
    let minute = mins_remaining / SECS_PER_MIN;
    let second = mins_remaining % SECS_PER_MIN;

    // Calculate year/month/day (simplified)
    let (year, month, day) = days_to_ymd(days_since_epoch as i64);

    format!(
        "{:04}-{:02}-{:02}T{:02}:{:02}:{:02}.{:06}Z",
        year,
        month,
        day,
        hour,
        minute,
        second,
        nanos / 1000
    )
}

/// Convert days since epoch to year/month/day
fn days_to_ymd(days: i64) -> (i32, u32, u32) {
    // Simplified calculation
    let year = 1970 + (days / 365) as i32;
    let day_of_year = (days % 365) as u32;

    // Simplified month calculation
    let (month, day) = if day_of_year < 31 {
        (1, day_of_year + 1)
    } else if day_of_year < 59 {
        (2, day_of_year - 30)
    } else if day_of_year < 90 {
        (3, day_of_year - 58)
    } else if day_of_year < 120 {
        (4, day_of_year - 89)
    } else if day_of_year < 151 {
        (5, day_of_year - 119)
    } else if day_of_year < 181 {
        (6, day_of_year - 150)
    } else if day_of_year < 212 {
        (7, day_of_year - 180)
    } else if day_of_year < 243 {
        (8, day_of_year - 211)
    } else if day_of_year < 273 {
        (9, day_of_year - 242)
    } else if day_of_year < 304 {
        (10, day_of_year - 272)
    } else if day_of_year < 334 {
        (11, day_of_year - 303)
    } else {
        (12, day_of_year - 333)
    };

    (year, month, day)
}

/// Structured logger
pub struct StructuredLogger {
    level: LogLevel,
}

impl StructuredLogger {
    /// Create a new structured logger
    pub fn new(level: LogLevel) -> Self {
        StructuredLogger { level }
    }

    /// Create with Info level
    pub fn info_level() -> Self {
        Self::new(LogLevel::Info)
    }

    /// Log a message
    pub fn log(&self, level: LogLevel, message: impl Into<String>) {
        if level as u8 >= self.level as u8 {
            let entry = LogEntry::new(level, message);
            println!("{}", entry.to_json());
        }
    }

    /// Log with fields
    pub fn log_with_fields(
        &self,
        level: LogLevel,
        message: impl Into<String>,
        fields: impl IntoIterator<Item = (String, serde_json::Value)>,
    ) {
        if level as u8 >= self.level as u8 {
            let entry = LogEntry::new(level, message).with_fields(fields);
            println!("{}", entry.to_json());
        }
    }

    /// Log trace
    pub fn trace(&self, message: impl Into<String>) {
        self.log(LogLevel::Trace, message);
    }

    /// Log debug
    pub fn debug(&self, message: impl Into<String>) {
        self.log(LogLevel::Debug, message);
    }

    /// Log info
    pub fn info(&self, message: impl Into<String>) {
        self.log(LogLevel::Info, message);
    }

    /// Log warn
    pub fn warn(&self, message: impl Into<String>) {
        self.log(LogLevel::Warn, message);
    }

    /// Log error
    pub fn error(&self, message: impl Into<String>) {
        self.log(LogLevel::Error, message);
    }
}

impl Default for StructuredLogger {
    fn default() -> Self {
        Self::info_level()
    }
}

/// Get the global logger instance
pub fn global_logger() -> &'static StructuredLogger {
    GLOBAL_LOGGER.get_or_init(StructuredLogger::info_level)
}

/// Initialize the global logger with a specific level
pub fn init_global_logger(level: LogLevel) {
    let _ = GLOBAL_LOGGER.get_or_init(|| StructuredLogger::new(level));
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn test_log_level_display() {
        assert_eq!(LogLevel::Info.to_string(), "INFO");
        assert_eq!(LogLevel::Error.to_string(), "ERROR");
    }

    #[test]
    fn test_log_entry_creation() {
        let entry = LogEntry::new(LogLevel::Info, "test message");
        assert_eq!(entry.message, "test message");
        assert_eq!(entry.level, LogLevel::Info);
    }

    #[test]
    fn test_log_entry_with_field() {
        let entry = LogEntry::new(LogLevel::Info, "test").with_field("key", "value");

        assert!(entry.fields.contains_key("key"));
    }

    #[test]
    fn test_log_entry_with_run_id() {
        let run_id = RunId::new();
        let entry = LogEntry::new(LogLevel::Info, "test").with_run_id(&run_id);

        assert!(entry.fields.contains_key("run_id"));
        assert_eq!(
            entry.fields.get("run_id").unwrap().as_str().unwrap(),
            run_id.as_str()
        );
    }

    #[test]
    fn test_log_entry_with_worker() {
        let entry = LogEntry::new(LogLevel::Info, "test").with_worker("w1");

        assert!(entry.fields.contains_key("worker"));
        assert_eq!(entry.fields.get("worker").unwrap().as_str().unwrap(), "w1");
    }

    #[test]
    fn test_global_logger() {
        // Global logger should be accessible
        let logger = global_logger();
        logger.info("test message");
    }

    #[test]
    fn test_log_entry_to_json() {
        let entry = LogEntry::new(LogLevel::Info, "test message");
        let json = entry.to_json();

        assert!(json.contains("\"level\":\"INFO\""));
        assert!(json.contains("\"message\":\"test message\""));
    }

    #[tokio::test]
    async fn test_structured_logger() {
        let logger = StructuredLogger::info_level();

        // These should not panic
        logger.info("info message");
        logger.warn("warning message");
        logger.error("error message");
    }
}