mecha10_cli/services/
logs.rs1use crate::commands::LogSource;
6use anyhow::Result;
7use std::collections::HashMap;
8use std::io::{BufRead, BufReader, Seek};
9use std::path::PathBuf;
10
11pub struct LogsService {
12 logs_dir: PathBuf,
13}
14
15#[derive(Debug, Clone)]
16pub struct LogFileFilters {
17 pub node: Option<String>,
18 pub source: LogSource,
19}
20
21#[derive(Debug, Clone)]
22pub struct LogContentFilters {
23 pub pattern: Option<String>,
24 pub level: Option<String>,
25 pub lines: Option<usize>,
26}
27
28#[derive(Debug, Clone)]
29#[allow(dead_code)]
30pub struct LogFile {
31 pub name: String,
32 pub path: PathBuf,
33}
34
35#[derive(Debug, Clone)]
36pub struct LogLine {
37 pub source_name: String,
38 pub content: String,
39}
40
41impl LogsService {
42 pub fn new(logs_dir: PathBuf) -> Self {
44 Self { logs_dir }
45 }
46
47 #[allow(dead_code)]
49 pub fn logs_dir(&self) -> &PathBuf {
50 &self.logs_dir
51 }
52
53 pub fn collect_log_files(&self, filters: &LogFileFilters) -> Result<HashMap<String, PathBuf>> {
57 let mut sources = HashMap::new();
58
59 if !self.logs_dir.exists() {
60 std::fs::create_dir_all(&self.logs_dir)?;
61 return Ok(sources);
62 }
63
64 let entries = std::fs::read_dir(&self.logs_dir)?;
65
66 for entry in entries.flatten() {
67 let path = entry.path();
68 if !path.is_file() {
69 continue;
70 }
71
72 if path.extension().and_then(|e| e.to_str()) != Some("log") {
73 continue;
74 }
75
76 let file_stem = path
77 .file_stem()
78 .and_then(|s| s.to_str())
79 .unwrap_or_default()
80 .to_string();
81
82 if let Some(node_name) = &filters.node {
83 if !file_stem.contains(node_name) {
84 continue;
85 }
86 }
87
88 let should_include = match filters.source {
89 LogSource::All => true,
90 LogSource::Nodes => !file_stem.contains("framework") && !file_stem.contains("service"),
91 LogSource::Framework => file_stem.contains("framework") || file_stem.contains("cli"),
92 LogSource::Services => {
93 file_stem.contains("service") || file_stem.contains("redis") || file_stem.contains("postgres")
94 }
95 };
96
97 if should_include {
98 sources.insert(file_stem, path);
99 }
100 }
101
102 Ok(sources)
103 }
104
105 pub async fn read_logs(
109 &self,
110 log_sources: &HashMap<String, PathBuf>,
111 filters: &LogContentFilters,
112 ) -> Result<Vec<LogLine>> {
113 let mut all_lines = Vec::new();
114
115 for (name, path) in log_sources {
116 if let Ok(content) = tokio::fs::read_to_string(path).await {
117 for line in content.lines() {
118 if !self.should_display_line(line, filters) {
119 continue;
120 }
121
122 all_lines.push(LogLine {
123 source_name: name.clone(),
124 content: line.to_string(),
125 });
126 }
127 }
128 }
129
130 if let Some(n) = filters.lines {
131 let start = if all_lines.len() > n { all_lines.len() - n } else { 0 };
132 all_lines = all_lines[start..].to_vec();
133 }
134
135 Ok(all_lines)
136 }
137
138 pub fn should_display_line(&self, line: &str, filters: &LogContentFilters) -> bool {
140 if let Some(pattern) = &filters.pattern {
141 if !line.to_lowercase().contains(&pattern.to_lowercase()) {
142 return false;
143 }
144 }
145
146 if let Some(level) = &filters.level {
147 let level_upper = level.to_uppercase();
148 if !line.contains(&level_upper) {
149 return false;
150 }
151 }
152
153 true
154 }
155
156 pub fn open_for_follow(
160 &self,
161 log_sources: &HashMap<String, PathBuf>,
162 ) -> Result<HashMap<String, BufReader<std::fs::File>>> {
163 let mut readers = HashMap::new();
164
165 for (name, path) in log_sources {
166 if let Ok(file) = std::fs::File::open(path) {
167 let mut reader = BufReader::new(file);
168 let _ = reader.seek(std::io::SeekFrom::End(0));
169 readers.insert(name.clone(), reader);
170 }
171 }
172
173 Ok(readers)
174 }
175
176 pub fn read_new_lines(
180 &self,
181 reader: &mut BufReader<std::fs::File>,
182 filters: &LogContentFilters,
183 ) -> Result<Vec<String>> {
184 let mut lines = Vec::new();
185 let mut line = String::new();
186
187 loop {
188 line.clear();
189 match reader.read_line(&mut line) {
190 Ok(0) => {
191 break;
192 }
193 Ok(_) => {
194 if self.should_display_line(&line, filters) {
195 lines.push(line.clone());
196 }
197 }
198 Err(_) => {
199 break;
200 }
201 }
202 }
203
204 Ok(lines)
205 }
206}