use anyhow::{Context, Result};
use chrono::{DateTime, Utc};
use serde::{Deserialize, Serialize};
use std::collections::{BTreeMap, HashMap};
use std::fs::{File, OpenOptions};
use std::sync::{Arc, Mutex};
use tracing::{debug, error, info, warn};
use std::time::Duration;
use std::path::Path;
use std::io::Write;
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub enum LogLevel {
Trace,
Debug,
Info,
Warn,
Error,
Fatal,
}
impl LogLevel {
pub fn as_str(&self) -> &'static str {
match self {
LogLevel::Trace => "TRACE",
LogLevel::Debug => "DEBUG",
LogLevel::Info => "INFO",
LogLevel::Warn => "WARN",
LogLevel::Error => "ERROR",
LogLevel::Fatal => "FATAL",
}
}
pub fn from_str(s: &str) -> Option<Self> {
match s {
"TRACE" => Some(LogLevel::Trace),
"DEBUG" => Some(LogLevel::Debug),
"INFO" => Some(LogLevel::Info),
"WARN" => Some(LogLevel::Warn),
"ERROR" => Some(LogLevel::Error),
"FATAL" => Some(LogLevel::Fatal),
_ => None,
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct LogEntry {
pub id: String,
pub timestamp: DateTime<Utc>,
pub level: LogLevel,
pub service: String,
pub operation: Option<String>,
pub message: String,
pub fields: HashMap<String, serde_json::Value>>,
pub file: Option<String>,
pub line: Option<u32>,
pub column: Option<u32>,
pub thread_id: Option<String>,
pub trace_id: Option<String>,
pub span_id: Option<String>,
}
impl LogEntry {
pub fn new(
level: LogLevel,
service: String,
message: String,
) -> Self {
Self {
id: Uuid::new_v4().to_string(),
timestamp: Utc::now(),
level,
service,
operation: None,
message,
fields: HashMap::new(),
file: None,
line: None,
column: None,
thread_id: None,
trace_id: None,
span_id: None,
}
}
pub fn field<T: Serialize>(&mut self, key: &str, value: T) -> Result<&mut Self> {
let json_value: _ = serde_json::to_value(value)
.context("Failed to serialize field value")?;
self.fields.insert(key.to_string(), json_value);
Ok(self)
}
pub fn operation(&mut self, operation: &str) -> &mut Self {
self.operation = Some(operation.to_string());
self
}
pub fn location(&mut self, file: &str, line: u32, column: u32) -> &mut Self {
self.file = Some(file.to_string());
self.line = Some(line);
self.column = Some(column);
self
}
pub fn thread_id(&mut self, thread_id: &str) -> &mut Self {
self.thread_id = Some(thread_id.to_string());
self
}
pub fn trace_context(&mut self, trace_id: &str, span_id: &str) -> &mut Self {
self.trace_id = Some(trace_id.to_string());
self.span_id = Some(span_id.to_string());
self
}
pub fn to_json(&self) -> Result<String> {
serde_json::to_string(self)
.context("Failed to serialize log entry to JSON")
}
pub fn to_pretty_json(&self) -> Result<String> {
serde_json::to_string_pretty(self)
.context("Failed to serialize log entry to pretty JSON")
}
}
#[derive(Debug, Clone)]
pub struct LogAggregatorConfig {
pub service_name: String,
pub log_dir: String,
pub max_file_size: u64,
pub max_files: usize,
pub rotation_interval: chrono::Duration,
pub json_format: bool,
pub enable_file: bool,
pub enable_console: bool,
pub min_level: LogLevel,
pub elk_enabled: bool,
pub elasticsearch_endpoint: Option<String>,
pub logstash_endpoint: Option<String>,
}
#[derive(Debug)]
pub struct LogAggregator {
config: LogAggregatorConfig,
current_file: Arc<Mutex<Option<File>>>,
log_queue: Arc<Mutex<Vec<LogEntry>>>,
log_sender: mpsc::UnboundedSender<LogEntry>,
}
impl LogAggregator {
pub fn new(config: LogAggregatorConfig) -> Result<Self> {
if config.enable_file && !Path::new(&config.log_dir).exists() {
std::fs::create_dir_all(&config.log_dir)
.context("Failed to create log directory")?;
}
let (log_sender, mut log_receiver) = mpsc::unbounded_channel::<LogEntry>();
let log_dir: _ = config.log_dir.clone();
let current_file: _ = Arc::new(Mutex::new(None)),;
let current_file_clone: _ = current_file.clone();
tokio::spawn(async move {
while let Some(log_entry) = log_receiver.recv().await {
if let Err(e) = process_log_entry(&log_entry, &log_dir, ¤t_file_clone).await {
error!("Failed to process log entry: {}", e);
}
}
});
info!("Log aggregator initialized for service: {}", config.service_name);
Ok(Self {
config,
current_file,
log_queue: Arc::new(Mutex::new(Vec::new()))
log_sender,
})
}
pub fn log(&self, level: LogLevel, message: &str) -> LogEntry {
LogEntry::new(level, self.config.service_name.clone(), message.to_string())
}
pub fn trace(&self, message: &str) -> LogEntry {
self.log(LogLevel::Trace, message)
}
pub fn debug(&self, message: &str) -> LogEntry {
self.log(LogLevel::Debug, message)
}
pub fn info(&self, message: &str) -> LogEntry {
self.log(LogLevel::Info, message)
}
pub fn warn(&self, message: &str) -> LogEntry {
self.log(LogLevel::Warn, message)
}
pub fn error(&self, message: &str) -> LogEntry {
self.log(LogLevel::Error, message)
}
pub fn fatal(&self, message: &str) -> LogEntry {
self.log(LogLevel::Fatal, message)
}
pub async fn write(&self, mut log_entry: LogEntry) -> Result<()> {
let location: _ = std::panic::Location::caller();
log_entry.location(
location.file(),
location.line(),
location.column(),
);
log_entry.thread_id(&format!("{:?}", std::thread::current().id());
self.log_sender.send(log_entry)
.context("Failed to send log entry to queue")?;
Ok(())
}
pub async fn flush(&self) -> Result<()> {
let mut queue = self.log_queue.lock().unwrap();
queue.clear();
Ok(())
}
pub fn get_stats(&self) -> HashMap<String, usize> {
let queue: _ = self.log_queue.lock().unwrap();
let mut stats = HashMap::new();
stats.insert("buffered_logs".to_string(), queue.len());
for entry in queue.iter() {
let level_key: _ = format!("logs_{}", entry.level.as_str().to_lowercase());
*stats.entry(level_key).or_insert(0) += 1;
}
stats
}
}
async fn process_log_entry(
log_entry: &LogEntry,
log_dir: &str,
current_file: &Arc<Mutex<Option<File>>>,
) -> Result<()> {
let formatted: _ = if log_entry.fields.is_empty() {
format!("[{}] {}: {}",
log_entry.timestamp.format("%Y-%m-%d %H:%M:%S%.3f"),
log_entry.level.as_str(),
log_entry.message)
} else {
let json: _ = log_entry.to_json()?;
format!("[{}] {}: {} | {}",
log_entry.timestamp.format("%Y-%m-%d %H:%M:%S%.3f"),
log_entry.level.as_str(),
log_entry.message,
json)
};
if let Ok(mut file) = current_file.lock() {
if let Some(ref mut f) = *file {
writeln!(f, "{}", formatted)
.context("Failed to write to log file")?;
f.flush()
.context("Failed to flush log file")?;
}
}
info!("{}", formatted); Ok(())
}
pub fn init_logger(config: LogAggregatorConfig) -> Result<Arc<LogAggregator>> {
let aggregator: _ = Arc::new(Mutex::new(LogAggregator::new(config)),?);
tracing_subscriber::fmt()
.with_max_level(tracing::Level::DEBUG)
.init();
info!("Global logger initialized");
Ok(aggregator))
}
#[cfg(test)]
mod tests {
#[test]
fn test_log_entry_creation() {
let entry: _ = LogEntry::new(
LogLevel::Info,
"test-service".to_string(),
"Test message".to_string(),
);
assert_eq!(entry.level, LogLevel::Info);
assert_eq!(entry.service, "test-service");
assert_eq!(entry.message, "Test message");
assert!(!entry.id.is_empty());
}
#[test]
fn test_log_entry_fields() {
let mut entry = LogEntry::new(
LogLevel::Debug,
"test-service".to_string(),
"Test message".to_string(),
);
entry
.field("user_id", 12345)
.unwrap()
.field("action", "login")
.unwrap();
assert_eq!(entry.fields.get("user_id"), Some(&serde_json::Value::from(12345));
assert_eq!(entry.fields.get("action"), Some(&serde_json::Value::from("login"));
}
#[test]
fn test_log_level_conversion() {
assert_eq!(LogLevel::Info.as_str(), "INFO");
assert_eq!(LogLevel::from_str("INFO"), Some(LogLevel::Info));
assert_eq!(LogLevel::from_str("invalid"), None);
}
#[test]
fn test_log_entry_json_serialization() {
let mut entry = LogEntry::new(
LogLevel::Info,
"test-service".to_string(),
"Test message".to_string(),
);
entry
.field("key", "value")
.unwrap()
.operation("test_operation");
let json: _ = entry.to_json();
assert!(json.is_ok());
}
#[test]
fn test_log_aggregator_creation() {
let config: _ = LogAggregatorConfig {
service_name: "test-service".to_string(),
log_dir: "/tmp/amberjs-logs".to_string(),
max_file_size: 10 * 1024 * 1024, max_files: 5,
rotation_interval: chrono::Duration::hours(1),
json_format: true,
enable_file: true,
enable_console: true,
min_level: LogLevel::Info,
elk_enabled: false,
elasticsearch_endpoint: None,
logstash_endpoint: None,
};
let aggregator: _ = LogAggregator::new(config);
assert!(aggregator.is_ok());
}
}