use crate::commands::LogSource;
use anyhow::Result;
use std::collections::HashMap;
use std::io::{BufRead, BufReader, Seek};
use std::path::PathBuf;
pub struct LogsService {
logs_dir: PathBuf,
}
#[derive(Debug, Clone)]
pub struct LogFileFilters {
pub node: Option<String>,
pub source: LogSource,
}
#[derive(Debug, Clone)]
pub struct LogContentFilters {
pub pattern: Option<String>,
pub level: Option<String>,
pub lines: Option<usize>,
}
#[derive(Debug, Clone)]
#[allow(dead_code)]
pub struct LogFile {
pub name: String,
pub path: PathBuf,
}
#[derive(Debug, Clone)]
pub struct LogLine {
pub source_name: String,
pub content: String,
}
impl LogsService {
pub fn new(logs_dir: PathBuf) -> Self {
Self { logs_dir }
}
#[allow(dead_code)]
pub fn logs_dir(&self) -> &PathBuf {
&self.logs_dir
}
pub fn collect_log_files(&self, filters: &LogFileFilters) -> Result<HashMap<String, PathBuf>> {
let mut sources = HashMap::new();
if !self.logs_dir.exists() {
std::fs::create_dir_all(&self.logs_dir)?;
return Ok(sources);
}
let entries = std::fs::read_dir(&self.logs_dir)?;
for entry in entries.flatten() {
let path = entry.path();
if !path.is_file() {
continue;
}
if path.extension().and_then(|e| e.to_str()) != Some("log") {
continue;
}
let file_stem = path
.file_stem()
.and_then(|s| s.to_str())
.unwrap_or_default()
.to_string();
if let Some(node_name) = &filters.node {
if !file_stem.contains(node_name) {
continue;
}
}
let should_include = match filters.source {
LogSource::All => true,
LogSource::Nodes => !file_stem.contains("framework") && !file_stem.contains("service"),
LogSource::Framework => file_stem.contains("framework") || file_stem.contains("cli"),
LogSource::Services => {
file_stem.contains("service") || file_stem.contains("redis") || file_stem.contains("postgres")
}
};
if should_include {
sources.insert(file_stem, path);
}
}
Ok(sources)
}
pub async fn read_logs(
&self,
log_sources: &HashMap<String, PathBuf>,
filters: &LogContentFilters,
) -> Result<Vec<LogLine>> {
let mut all_lines = Vec::new();
for (name, path) in log_sources {
if let Ok(content) = tokio::fs::read_to_string(path).await {
for line in content.lines() {
if !self.should_display_line(line, filters) {
continue;
}
all_lines.push(LogLine {
source_name: name.clone(),
content: line.to_string(),
});
}
}
}
if let Some(n) = filters.lines {
let start = if all_lines.len() > n { all_lines.len() - n } else { 0 };
all_lines = all_lines[start..].to_vec();
}
Ok(all_lines)
}
pub fn should_display_line(&self, line: &str, filters: &LogContentFilters) -> bool {
if let Some(pattern) = &filters.pattern {
if !line.to_lowercase().contains(&pattern.to_lowercase()) {
return false;
}
}
if let Some(level) = &filters.level {
let level_upper = level.to_uppercase();
if !line.contains(&level_upper) {
return false;
}
}
true
}
pub fn open_for_follow(
&self,
log_sources: &HashMap<String, PathBuf>,
) -> Result<HashMap<String, BufReader<std::fs::File>>> {
let mut readers = HashMap::new();
for (name, path) in log_sources {
if let Ok(file) = std::fs::File::open(path) {
let mut reader = BufReader::new(file);
let _ = reader.seek(std::io::SeekFrom::End(0));
readers.insert(name.clone(), reader);
}
}
Ok(readers)
}
pub fn read_new_lines(
&self,
reader: &mut BufReader<std::fs::File>,
filters: &LogContentFilters,
) -> Result<Vec<String>> {
let mut lines = Vec::new();
let mut line = String::new();
loop {
line.clear();
match reader.read_line(&mut line) {
Ok(0) => {
break;
}
Ok(_) => {
if self.should_display_line(&line, filters) {
lines.push(line.clone());
}
}
Err(_) => {
break;
}
}
}
Ok(lines)
}
}