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
11/// Service for managing log file operations
12pub struct LogsService {
13    logs_dir: PathBuf,
14}
15
16/// Filters for log file collection
17#[derive(Debug, Clone)]
18pub struct LogFileFilters {
19    pub node: Option<String>,
20    pub source: LogSource,
21}
22
23/// Filters for log content
24#[derive(Debug, Clone)]
25pub struct LogContentFilters {
26    pub pattern: Option<String>,
27    pub level: Option<String>,
28    pub lines: Option<usize>,
29}
30
31/// A log file with metadata
32#[derive(Debug, Clone)]
33#[allow(dead_code)]
34pub struct LogFile {
35    pub name: String,
36    pub path: PathBuf,
37}
38
39/// A log line with source information
40#[derive(Debug, Clone)]
41pub struct LogLine {
42    pub source_name: String,
43    pub content: String,
44}
45
46impl LogsService {
47    /// Create a new logs service
48    pub fn new(logs_dir: PathBuf) -> Self {
49        Self { logs_dir }
50    }
51
52    /// Get the logs directory path
53    #[allow(dead_code)]
54    pub fn logs_dir(&self) -> &PathBuf {
55        &self.logs_dir
56    }
57
58    /// Collect log files matching filters
59    ///
60    /// Returns a map of log file stem names to their paths.
61    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    /// Read logs from multiple sources and apply filters
111    ///
112    /// Returns all matching log lines with source information.
113    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    /// Check if a line should be displayed based on filters
144    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    /// Open log files for following (watching)
162    ///
163    /// Returns a map of readers positioned at the end of each file.
164    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    /// Read new lines from a reader
182    ///
183    /// Returns lines that have been added since the last read.
184    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}