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 {
13 logs_dir: PathBuf,
14}
15
16#[derive(Debug, Clone)]
18pub struct LogFileFilters {
19 pub node: Option<String>,
20 pub source: LogSource,
21}
22
23#[derive(Debug, Clone)]
25pub struct LogContentFilters {
26 pub pattern: Option<String>,
27 pub level: Option<String>,
28 pub lines: Option<usize>,
29}
30
31#[derive(Debug, Clone)]
33#[allow(dead_code)]
34pub struct LogFile {
35 pub name: String,
36 pub path: PathBuf,
37}
38
39#[derive(Debug, Clone)]
41pub struct LogLine {
42 pub source_name: String,
43 pub content: String,
44}
45
46impl LogsService {
47 pub fn new(logs_dir: PathBuf) -> Self {
49 Self { logs_dir }
50 }
51
52 #[allow(dead_code)]
54 pub fn logs_dir(&self) -> &PathBuf {
55 &self.logs_dir
56 }
57
58 pub fn collect_log_files(&self, filters: &LogFileFilters) -> Result<HashMap<String, PathBuf>> {
62 let mut sources = HashMap::new();
63
64 if !self.logs_dir.exists() {
66 std::fs::create_dir_all(&self.logs_dir)?;
67 return Ok(sources);
68 }
69
70 let entries = std::fs::read_dir(&self.logs_dir)?;
72
73 for entry in entries.flatten() {
74 let path = entry.path();
75 if !path.is_file() {
76 continue;
77 }
78
79 if path.extension().and_then(|e| e.to_str()) != Some("log") {
81 continue;
82 }
83
84 let file_stem = path
86 .file_stem()
87 .and_then(|s| s.to_str())
88 .unwrap_or_default()
89 .to_string();
90
91 if let Some(node_name) = &filters.node {
93 if !file_stem.contains(node_name) {
94 continue;
95 }
96 }
97
98 let should_include = match filters.source {
100 LogSource::All => true,
101 LogSource::Nodes => !file_stem.contains("framework") && !file_stem.contains("service"),
102 LogSource::Framework => file_stem.contains("framework") || file_stem.contains("cli"),
103 LogSource::Services => {
104 file_stem.contains("service") || file_stem.contains("redis") || file_stem.contains("postgres")
105 }
106 };
107
108 if should_include {
109 sources.insert(file_stem, path);
110 }
111 }
112
113 Ok(sources)
114 }
115
116 pub async fn read_logs(
120 &self,
121 log_sources: &HashMap<String, PathBuf>,
122 filters: &LogContentFilters,
123 ) -> Result<Vec<LogLine>> {
124 let mut all_lines = Vec::new();
125
126 for (name, path) in log_sources {
128 if let Ok(content) = tokio::fs::read_to_string(path).await {
129 for line in content.lines() {
130 if !self.should_display_line(line, filters) {
132 continue;
133 }
134
135 all_lines.push(LogLine {
136 source_name: name.clone(),
137 content: line.to_string(),
138 });
139 }
140 }
141 }
142
143 if let Some(n) = filters.lines {
145 let start = if all_lines.len() > n { all_lines.len() - n } else { 0 };
146 all_lines = all_lines[start..].to_vec();
147 }
148
149 Ok(all_lines)
150 }
151
152 pub fn should_display_line(&self, line: &str, filters: &LogContentFilters) -> bool {
154 if let Some(pattern) = &filters.pattern {
156 if !line.to_lowercase().contains(&pattern.to_lowercase()) {
157 return false;
158 }
159 }
160
161 if let Some(level) = &filters.level {
163 let level_upper = level.to_uppercase();
164 if !line.contains(&level_upper) {
165 return false;
166 }
167 }
168
169 true
170 }
171
172 pub fn open_for_follow(
176 &self,
177 log_sources: &HashMap<String, PathBuf>,
178 ) -> Result<HashMap<String, BufReader<std::fs::File>>> {
179 let mut readers = HashMap::new();
180
181 for (name, path) in log_sources {
182 if let Ok(file) = std::fs::File::open(path) {
183 let mut reader = BufReader::new(file);
184 let _ = reader.seek(std::io::SeekFrom::End(0));
186 readers.insert(name.clone(), reader);
187 }
188 }
189
190 Ok(readers)
191 }
192
193 pub fn read_new_lines(
197 &self,
198 reader: &mut BufReader<std::fs::File>,
199 filters: &LogContentFilters,
200 ) -> Result<Vec<String>> {
201 let mut lines = Vec::new();
202 let mut line = String::new();
203
204 loop {
205 line.clear();
206 match reader.read_line(&mut line) {
207 Ok(0) => {
208 break;
210 }
211 Ok(_) => {
212 if self.should_display_line(&line, filters) {
214 lines.push(line.clone());
215 }
216 }
217 Err(_) => {
218 break;
219 }
220 }
221 }
222
223 Ok(lines)
224 }
225}