Skip to main content

mecha10_cli/services/
logs.rs

1//! Logs service for managing log file operations
2//!
3//! Provides business logic for collecting, reading, and filtering log files.
4
5use 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    /// Create a new logs service
43    pub fn new(logs_dir: PathBuf) -> Self {
44        Self { logs_dir }
45    }
46
47    /// Get the logs directory path
48    #[allow(dead_code)]
49    pub fn logs_dir(&self) -> &PathBuf {
50        &self.logs_dir
51    }
52
53    /// Collect log files matching filters
54    ///
55    /// Returns a map of log file stem names to their paths.
56    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    /// Read logs from multiple sources and apply filters
106    ///
107    /// Returns all matching log lines with source information.
108    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    /// Check if a line should be displayed based on filters
139    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    /// Open log files for following (watching)
157    ///
158    /// Returns a map of readers positioned at the end of each file.
159    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    /// Read new lines from a reader
177    ///
178    /// Returns lines that have been added since the last read.
179    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}