use crate::runtime::values::Value;
use std::collections::HashMap;
use std::env;
use std::fs::{File, OpenOptions};
use std::io::{self, Write};
use std::path::{Path, PathBuf};
use std::sync::Mutex;
use std::time::{SystemTime, UNIX_EPOCH};
#[derive(Debug, Clone)]
pub struct LogEntry {
pub timestamp: i64,
pub level: LogLevel,
pub message: String,
pub data: HashMap<String, Value>,
pub source: String,
}
#[derive(Debug, Clone, PartialEq)]
pub enum LogLevel {
Info,
Warning,
Error,
Audit,
Debug,
}
lazy_static::lazy_static! {
static ref LOG_STORAGE: Mutex<Vec<LogEntry>> = Mutex::new(Vec::new());
static ref LOG_FILE_HANDLE: Mutex<Option<File>> = Mutex::new(None);
}
pub fn initialize_file_logging() -> Result<(), String> {
if !should_log_to_file() {
return Ok(());
}
let log_file_path = get_log_file_path();
if let Some(parent) = log_file_path.parent() {
std::fs::create_dir_all(parent)
.map_err(|e| format!("Failed to create log directory: {}", e))?;
}
{
let mut handle = LOG_FILE_HANDLE.lock().unwrap();
if handle.is_some() {
*handle = None; }
}
rotate_logs_if_needed()?;
let file = OpenOptions::new()
.create(true)
.append(true)
.open(&log_file_path)
.map_err(|e| format!("Failed to open log file {}: {}", log_file_path.display(), e))?;
*LOG_FILE_HANDLE.lock().unwrap() = Some(file);
Ok(())
}
fn get_log_file_path() -> PathBuf {
if let Ok(log_file) = env::var("LOG_FILE") {
PathBuf::from(log_file)
} else {
let log_dir = get_log_directory();
log_dir.join("audit.log")
}
}
fn get_log_directory() -> PathBuf {
if let Ok(log_dir) = env::var("LOG_DIR") {
PathBuf::from(log_dir)
} else {
PathBuf::from("./logs")
}
}
fn should_log_to_file() -> bool {
match env::var("LOG_SINK").as_deref() {
Ok("file") | Ok("both") => true,
_ => false,
}
}
fn max_log_file_size() -> u64 {
env::var("LOG_ROTATE_SIZE")
.ok()
.and_then(|s| s.trim().parse().ok())
.unwrap_or(10 * 1024 * 1024) }
fn log_retention_days() -> u64 {
env::var("LOG_RETENTION_DAYS")
.ok()
.and_then(|s| s.trim().parse().ok())
.unwrap_or(30) }
fn rotate_logs_if_needed() -> Result<(), String> {
let log_file_path = get_log_file_path();
if !log_file_path.exists() {
cleanup_old_logs()?;
return Ok(());
}
let metadata = std::fs::metadata(&log_file_path)
.map_err(|e| format!("Failed to get log file metadata: {}", e))?;
if metadata.len() >= max_log_file_size() {
rotate_log_file(&log_file_path)?;
}
cleanup_old_logs()?;
Ok(())
}
fn rotate_log_file(log_file_path: &Path) -> Result<(), String> {
let timestamp = SystemTime::now()
.duration_since(UNIX_EPOCH)
.unwrap_or_default()
.as_secs();
let rotated_name = format!("{}.{}", log_file_path.display(), timestamp);
std::fs::rename(log_file_path, &rotated_name)
.map_err(|e| format!("Failed to rotate log file: {}", e))?;
Ok(())
}
fn cleanup_old_logs() -> Result<(), String> {
let log_dir = get_log_directory();
let retention_days = log_retention_days();
let cutoff_time = SystemTime::now()
.duration_since(UNIX_EPOCH)
.unwrap_or_default()
.as_secs()
- (retention_days * 24 * 60 * 60);
let entries =
std::fs::read_dir(&log_dir).map_err(|e| format!("Failed to read log directory: {}", e))?;
for entry in entries {
if let Ok(entry) = entry {
let path = entry.path();
if path.is_file() && path.extension().and_then(|s| s.to_str()) == Some("log") {
if let Ok(metadata) = path.metadata() {
if let Ok(modified) = metadata.modified() {
if let Ok(modified_secs) = modified.duration_since(UNIX_EPOCH) {
if modified_secs.as_secs() < cutoff_time {
let _ = std::fs::remove_file(&path);
}
}
}
}
}
}
}
Ok(())
}
fn write_to_file(entry: &LogEntry) -> Result<(), io::Error> {
if !should_log_to_file() {
return Ok(());
}
{
let handle = LOG_FILE_HANDLE.lock().unwrap();
if handle.is_none() {
let _ = initialize_file_logging();
}
}
let mut handle_guard = LOG_FILE_HANDLE.lock().unwrap();
if let Some(ref mut file) = *handle_guard {
let json_entry = format!(
r#"{{"timestamp":{},"level":"{:?}","message":"{}","source":"{}","data":{}}}\n"#,
entry.timestamp,
entry.level,
entry.message.replace('"', "\\\""),
entry.source.replace('"', "\\\""),
format_data_as_json(&entry.data)
);
file.write_all(json_entry.as_bytes())?;
file.flush()?;
}
Ok(())
}
fn format_data_as_json(data: &HashMap<String, Value>) -> String {
let mut parts = Vec::new();
for (key, value) in data {
let value_str = match value {
Value::String(s) => format!("\"{}\"", s.replace('"', "\\\"")),
Value::Int(i) => i.to_string(),
Value::Float(f) => f.to_string(),
Value::Bool(b) => b.to_string(),
Value::Null => "null".to_string(),
_ => format!("\"{:?}\"", value),
};
parts.push(format!("\"{}\":{}", key.replace('"', "\\\""), value_str));
}
format!("{{{}}}", parts.join(","))
}
pub fn info(message: &str, data: HashMap<String, Value>, source: Option<&str>) {
let source_str = source.unwrap_or("system").to_string();
log_message(LogLevel::Info, message, data, source_str);
}
pub fn warning(message: &str, data: HashMap<String, Value>, source: Option<&str>) {
let source_str = source.unwrap_or("system").to_string();
log_message(LogLevel::Warning, message, data, source_str);
}
pub fn error(message: &str, data: HashMap<String, Value>, source: Option<&str>) {
let source_str = source.unwrap_or("system").to_string();
log_message(LogLevel::Error, message, data, source_str);
}
pub fn audit(event: &str, data: HashMap<String, Value>, source: Option<&str>) {
let source_str = source.unwrap_or("audit").to_string();
log_message(LogLevel::Audit, event, data, source_str);
}
pub fn debug(message: &str, data: HashMap<String, Value>, source: Option<&str>) {
let source_str = source.unwrap_or("debug").to_string();
log_message(LogLevel::Debug, message, data, source_str);
}
fn log_level_order(level: &LogLevel) -> u8 {
match level {
LogLevel::Debug => 0,
LogLevel::Info | LogLevel::Audit => 1,
LogLevel::Warning => 2,
LogLevel::Error => 3,
}
}
fn max_entries() -> usize {
env::var("LOG_MAX_ENTRIES")
.ok()
.and_then(|s| s.trim().parse().ok())
.unwrap_or(1000)
}
fn sink_console() -> bool {
match env::var("LOG_SINK").as_deref() {
Ok("none") | Ok("file") => false, Ok("both") => true, _ => true, }
}
fn min_level_order() -> u8 {
match env::var("LOG_LEVEL").as_deref() {
Ok("debug") => 0,
Ok("info") | Ok("audit") => 1,
Ok("warning") | Ok("warn") => 2,
Ok("error") => 3,
_ => 0,
}
}
fn log_message(level: LogLevel, message: &str, data: HashMap<String, Value>, source: String) {
let timestamp = SystemTime::now()
.duration_since(UNIX_EPOCH)
.unwrap_or_default()
.as_secs() as i64;
let entry = LogEntry {
timestamp,
level: level.clone(),
message: message.to_string(),
data: data.clone(),
source: source.clone(),
};
if let Ok(mut storage) = LOG_STORAGE.lock() {
storage.push(entry.clone());
let cap = max_entries();
while storage.len() > cap {
storage.remove(0);
}
}
if level == LogLevel::Audit || should_log_to_file() {
let _ = write_to_file(&entry);
}
if sink_console() && log_level_order(&level) >= min_level_order() {
let level_str = match level {
LogLevel::Info => "INFO",
LogLevel::Warning => "WARN",
LogLevel::Error => "ERROR",
LogLevel::Audit => "AUDIT",
LogLevel::Debug => "DEBUG",
};
println!("[{}] {}: {} - {:?}", level_str, source, message, data);
}
}
pub fn get_entries() -> Vec<LogEntry> {
if let Ok(storage) = LOG_STORAGE.lock() {
storage.clone()
} else {
Vec::new()
}
}
pub fn get_entries_by_level(level: LogLevel) -> Vec<LogEntry> {
get_entries()
.into_iter()
.filter(|entry| entry.level == level)
.collect()
}
pub fn get_entries_by_source(source: &str) -> Vec<LogEntry> {
get_entries()
.into_iter()
.filter(|entry| entry.source == source)
.collect()
}
pub fn clear() {
if let Ok(mut storage) = LOG_STORAGE.lock() {
storage.clear();
}
}
pub fn get_stats() -> HashMap<String, Value> {
let entries = get_entries();
let mut stats = HashMap::new();
stats.insert(
"total_entries".to_string(),
Value::Int(entries.len() as i64),
);
let mut level_counts = HashMap::new();
for entry in &entries {
let level_str = match entry.level {
LogLevel::Info => "info",
LogLevel::Warning => "warning",
LogLevel::Error => "error",
LogLevel::Audit => "audit",
LogLevel::Debug => "debug",
};
let count = level_counts.entry(level_str.to_string()).or_insert(0);
*count += 1;
}
for (level, count) in level_counts {
stats.insert(format!("count_{}", level), Value::Int(count));
}
let mut source_counts = HashMap::new();
for entry in &entries {
let count = source_counts.entry(entry.source.clone()).or_insert(0);
*count += 1;
}
for (source, count) in source_counts {
stats.insert(format!("source_{}", source), Value::Int(count));
}
stats
}