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() {
65 std::fs::create_dir_all(&self.logs_dir)?;
66 return Ok(sources);
67 }
68
69 let entries = std::fs::read_dir(&self.logs_dir)?;
70
71 for entry in entries.flatten() {
72 let path = entry.path();
73 if !path.is_file() {
74 continue;
75 }
76
77 if path.extension().and_then(|e| e.to_str()) != Some("log") {
78 continue;
79 }
80
81 let file_stem = path
82 .file_stem()
83 .and_then(|s| s.to_str())
84 .unwrap_or_default()
85 .to_string();
86
87 if let Some(node_name) = &filters.node {
88 if !file_stem.contains(node_name) {
89 continue;
90 }
91 }
92
93 let should_include = match filters.source {
94 LogSource::All => true,
95 LogSource::Nodes => !file_stem.contains("framework") && !file_stem.contains("service"),
96 LogSource::Framework => file_stem.contains("framework") || file_stem.contains("cli"),
97 LogSource::Services => {
98 file_stem.contains("service") || file_stem.contains("redis") || file_stem.contains("postgres")
99 }
100 };
101
102 if should_include {
103 sources.insert(file_stem, path);
104 }
105 }
106
107 Ok(sources)
108 }
109
110 pub async fn read_logs(
114 &self,
115 log_sources: &HashMap<String, PathBuf>,
116 filters: &LogContentFilters,
117 ) -> Result<Vec<LogLine>> {
118 let mut all_lines = Vec::new();
119
120 for (name, path) in log_sources {
121 if let Ok(content) = tokio::fs::read_to_string(path).await {
122 for line in content.lines() {
123 if !self.should_display_line(line, filters) {
124 continue;
125 }
126
127 all_lines.push(LogLine {
128 source_name: name.clone(),
129 content: line.to_string(),
130 });
131 }
132 }
133 }
134
135 if let Some(n) = filters.lines {
136 let start = if all_lines.len() > n { all_lines.len() - n } else { 0 };
137 all_lines = all_lines[start..].to_vec();
138 }
139
140 Ok(all_lines)
141 }
142
143 pub fn should_display_line(&self, line: &str, filters: &LogContentFilters) -> bool {
145 if let Some(pattern) = &filters.pattern {
146 if !line.to_lowercase().contains(&pattern.to_lowercase()) {
147 return false;
148 }
149 }
150
151 if let Some(level) = &filters.level {
152 let level_upper = level.to_uppercase();
153 if !line.contains(&level_upper) {
154 return false;
155 }
156 }
157
158 true
159 }
160
161 pub fn open_for_follow(
165 &self,
166 log_sources: &HashMap<String, PathBuf>,
167 ) -> Result<HashMap<String, BufReader<std::fs::File>>> {
168 let mut readers = HashMap::new();
169
170 for (name, path) in log_sources {
171 if let Ok(file) = std::fs::File::open(path) {
172 let mut reader = BufReader::new(file);
173 let _ = reader.seek(std::io::SeekFrom::End(0));
174 readers.insert(name.clone(), reader);
175 }
176 }
177
178 Ok(readers)
179 }
180
181 pub fn read_new_lines(
185 &self,
186 reader: &mut BufReader<std::fs::File>,
187 filters: &LogContentFilters,
188 ) -> Result<Vec<String>> {
189 let mut lines = Vec::new();
190 let mut line = String::new();
191
192 loop {
193 line.clear();
194 match reader.read_line(&mut line) {
195 Ok(0) => {
196 break;
197 }
198 Ok(_) => {
199 if self.should_display_line(&line, filters) {
200 lines.push(line.clone());
201 }
202 }
203 Err(_) => {
204 break;
205 }
206 }
207 }
208
209 Ok(lines)
210 }
211}