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        // Create logs directory if it doesn't exist
65        if !self.logs_dir.exists() {
66            std::fs::create_dir_all(&self.logs_dir)?;
67            return Ok(sources);
68        }
69
70        // Get all log files
71        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            // Check file extension
80            if path.extension().and_then(|e| e.to_str()) != Some("log") {
81                continue;
82            }
83
84            // Get filename without extension
85            let file_stem = path
86                .file_stem()
87                .and_then(|s| s.to_str())
88                .unwrap_or_default()
89                .to_string();
90
91            // Apply node filter if specified
92            if let Some(node_name) = &filters.node {
93                if !file_stem.contains(node_name) {
94                    continue;
95                }
96            }
97
98            // Apply source filter
99            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    /// Read logs from multiple sources and apply filters
117    ///
118    /// Returns all matching log lines with source information.
119    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        // Read all log files
127        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                    // Apply filters
131                    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        // Apply line limit (tail behavior)
144        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    /// Check if a line should be displayed based on filters
153    pub fn should_display_line(&self, line: &str, filters: &LogContentFilters) -> bool {
154        // Apply text filter
155        if let Some(pattern) = &filters.pattern {
156            if !line.to_lowercase().contains(&pattern.to_lowercase()) {
157                return false;
158            }
159        }
160
161        // Apply level filter
162        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    /// Open log files for following (watching)
173    ///
174    /// Returns a map of readers positioned at the end of each file.
175    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                // Seek to end
185                let _ = reader.seek(std::io::SeekFrom::End(0));
186                readers.insert(name.clone(), reader);
187            }
188        }
189
190        Ok(readers)
191    }
192
193    /// Read new lines from a reader
194    ///
195    /// Returns lines that have been added since the last read.
196    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                    // No new data
209                    break;
210                }
211                Ok(_) => {
212                    // Apply filters
213                    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}