use serde::{Deserialize, Serialize};
use std::path::Path;
use std::{
collections::HashMap,
fs,
io::{self},
sync::{Arc, Mutex},
};
use ttlog::event::LogLevel;
use crate::utils::Utils;
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct LogsInfo {
pub total_size: u64,
pub total_size_formatted: String,
pub total_files: usize,
pub bin_files: BinFilesInfo,
pub log_files: LogFilesInfo,
pub directory_path: String,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct BinFilesInfo {
pub count: usize,
pub total_size: u64,
pub total_size_formatted: String,
pub min_size: Option<u64>,
pub min_size_formatted: Option<String>,
pub max_size: Option<u64>,
pub max_size_formatted: Option<String>,
pub avg_size: u64,
pub avg_size_formatted: String,
pub files: Vec<FileInfo>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct LogFilesInfo {
pub count: usize,
pub total_size: u64,
pub total_size_formatted: String,
pub total_lines: usize,
pub total_events: usize,
pub files: Vec<FileInfo>,
}
#[derive(Debug, Serialize, Deserialize, Clone)]
pub struct FileInfo {
pub name: String,
pub size: u64,
pub size_formatted: String,
pub timestamp: Option<String>,
pub process_id: Option<String>,
pub line_count: Option<usize>,
pub event_count: Option<usize>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct LogFileEvent {
pub file: String,
pub kv: serde_json::Value,
pub level: u8,
pub message: String,
pub position: (u32, u32),
pub target: String,
pub thread_id: u8,
pub timestamp: u64,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ResolvedLog {
pub level: LogLevel,
pub timestamp: String,
pub thread_id: u8,
pub message: String,
pub target: String,
pub kv: serde_json::Value,
pub file: String,
pub position: (u32, u32),
}
pub struct Logs {
cache: Arc<Mutex<HashMap<String, Vec<ResolvedLog>>>>,
info_cache: Arc<Mutex<HashMap<String, LogsInfo>>>,
}
static LOGS_INSTANCE: std::sync::OnceLock<Logs> = std::sync::OnceLock::new();
impl Logs {
pub fn instance() -> &'static Logs {
LOGS_INSTANCE.get_or_init(|| Logs {
cache: Arc::new(Mutex::new(HashMap::new())),
info_cache: Arc::new(Mutex::new(HashMap::new())),
})
}
pub fn clear_cache(&self) {
if let Ok(mut cache) = self.cache.lock() {
cache.clear();
}
if let Ok(mut info_cache) = self.info_cache.lock() {
info_cache.clear();
}
}
pub fn get_logs_info(&self, path: &str) -> Result<LogsInfo, io::Error> {
if let Ok(cache) = self.info_cache.lock() {
if let Some(cached_info) = cache.get(path) {
return Ok(cached_info.clone());
}
}
let info = Self::load_logs_info_from_dir(path)?;
if let Ok(mut cache) = self.info_cache.lock() {
cache.insert(path.to_string(), info.clone());
}
Ok(info)
}
pub fn get_logs_info_static(path: &str) -> Result<LogsInfo, io::Error> {
Self::instance().get_logs_info(path)
}
fn load_logs_info_from_dir(path: &str) -> Result<LogsInfo, io::Error> {
let path_obj = Path::new(path);
if !path_obj.exists() {
return Err(io::Error::new(
io::ErrorKind::NotFound,
format!("Path does not exist: {}", path_obj.display()),
));
}
let mut total_size = 0u64;
let mut total_files = 0usize;
let mut bin_files = Vec::new();
let mut log_files = Vec::new();
let mut total_lines = 0usize;
let mut total_events = 0usize;
read_directory(
path_obj,
&mut total_size,
&mut total_files,
&mut bin_files,
&mut log_files,
&mut total_lines,
&mut total_events,
)?;
let bin_total_size: u64 = bin_files.iter().map(|f| f.size).sum();
let bin_count = bin_files.len();
let bin_min_size = bin_files.iter().map(|f| f.size).min();
let bin_max_size = bin_files.iter().map(|f| f.size).max();
let bin_avg_size = if bin_count > 0 {
bin_total_size / bin_count as u64
} else {
0
};
let log_total_size: u64 = log_files.iter().map(|f| f.size).sum();
let log_count = log_files.len();
Ok(LogsInfo {
total_size,
total_size_formatted: Utils::format_size(total_size),
total_files,
directory_path: path.to_string(),
bin_files: BinFilesInfo {
count: bin_count,
total_size: bin_total_size,
total_size_formatted: Utils::format_size(bin_total_size),
min_size: bin_min_size,
min_size_formatted: bin_min_size.map(Utils::format_size),
max_size: bin_max_size,
max_size_formatted: bin_max_size.map(Utils::format_size),
avg_size: bin_avg_size,
avg_size_formatted: Utils::format_size(bin_avg_size),
files: bin_files,
},
log_files: LogFilesInfo {
count: log_count,
total_size: log_total_size,
total_size_formatted: Utils::format_size(log_total_size),
total_lines,
total_events,
files: log_files,
},
})
}
}
fn read_directory(
path: &Path,
total_size: &mut u64,
total_files: &mut usize,
bin_files: &mut Vec<FileInfo>,
log_files: &mut Vec<FileInfo>,
total_lines: &mut usize,
total_events: &mut usize,
) -> Result<(), io::Error> {
if path.is_dir() {
for entry in fs::read_dir(path)? {
let entry = entry?;
let entry_path = entry.path();
if entry_path.is_dir() {
read_directory(
&entry_path,
total_size,
total_files,
bin_files,
log_files,
total_lines,
total_events,
)?;
} else if entry_path.is_file() {
if let Ok(metadata) = fs::metadata(&entry_path) {
let size = metadata.len();
*total_size += size;
*total_files += 1;
let file_name = entry_path
.file_name()
.and_then(|n| n.to_str())
.unwrap_or("")
.to_string();
let (timestamp, process_id) = extract_metadata(&file_name);
let (line_count, event_count) = if file_name.ends_with(".log") {
match count_log_file_stats(&entry.path()) {
Ok((lines, events)) => {
*total_lines += lines;
*total_events += events;
(Some(lines), Some(events))
},
Err(_) => (None, None),
}
} else {
(None, None)
};
let file_info = FileInfo {
name: file_name.clone(),
size,
size_formatted: Utils::format_size(size),
timestamp,
process_id,
line_count,
event_count,
};
if file_name.ends_with(".bin") {
bin_files.push(file_info);
} else if file_name.ends_with(".log") {
log_files.push(file_info);
}
}
}
}
}
Ok(())
}
fn count_log_file_stats(path: &Path) -> Result<(usize, usize), io::Error> {
use std::io::{BufRead, BufReader};
let file = fs::File::open(path)?;
let reader = BufReader::with_capacity(64 * 1024, file);
let mut line_count = 0;
let mut event_count = 0;
for line in reader.lines() {
let line = line?;
line_count += 1;
if line.trim().is_empty() {
continue;
}
if serde_json::from_str::<LogFileEvent>(&line).is_ok() {
event_count += 1;
}
}
Ok((line_count, event_count))
}
fn extract_metadata(filename: &str) -> (Option<String>, Option<String>) {
let parts: Vec<&str> = filename.split('-').collect();
let process_id = if parts.len() >= 2 && parts[0] == "ttlog" {
Some(parts[1].to_string())
} else {
None
};
let timestamp = if parts.len() >= 3 {
let timestamp_str = parts[2];
if timestamp_str.len() == 14 {
let year = ×tamp_str[0..4];
let month = ×tamp_str[4..6];
let day = ×tamp_str[6..8];
let hour = ×tamp_str[8..10];
let minute = ×tamp_str[10..12];
let second = ×tamp_str[12..14];
Some(format!(
"{}-{}-{} {}:{}:{}",
year, month, day, hour, minute, second
))
} else {
None
}
} else {
None
};
(timestamp, process_id)
}