mecha10-cli 0.1.47

Mecha10 CLI tool
Documentation
//! Logs service for managing log file operations
//!
//! Provides business logic for collecting, reading, and filtering log files.

use crate::commands::LogSource;
use anyhow::Result;
use std::collections::HashMap;
use std::io::{BufRead, BufReader, Seek};
use std::path::PathBuf;

/// Service for managing log file operations
pub struct LogsService {
    logs_dir: PathBuf,
}

/// Filters for log file collection
#[derive(Debug, Clone)]
pub struct LogFileFilters {
    pub node: Option<String>,
    pub source: LogSource,
}

/// Filters for log content
#[derive(Debug, Clone)]
pub struct LogContentFilters {
    pub pattern: Option<String>,
    pub level: Option<String>,
    pub lines: Option<usize>,
}

/// A log file with metadata
#[derive(Debug, Clone)]
#[allow(dead_code)]
pub struct LogFile {
    pub name: String,
    pub path: PathBuf,
}

/// A log line with source information
#[derive(Debug, Clone)]
pub struct LogLine {
    pub source_name: String,
    pub content: String,
}

impl LogsService {
    /// Create a new logs service
    pub fn new(logs_dir: PathBuf) -> Self {
        Self { logs_dir }
    }

    /// Get the logs directory path
    #[allow(dead_code)]
    pub fn logs_dir(&self) -> &PathBuf {
        &self.logs_dir
    }

    /// Collect log files matching filters
    ///
    /// Returns a map of log file stem names to their paths.
    pub fn collect_log_files(&self, filters: &LogFileFilters) -> Result<HashMap<String, PathBuf>> {
        let mut sources = HashMap::new();

        // Create logs directory if it doesn't exist
        if !self.logs_dir.exists() {
            std::fs::create_dir_all(&self.logs_dir)?;
            return Ok(sources);
        }

        // Get all log files
        let entries = std::fs::read_dir(&self.logs_dir)?;

        for entry in entries.flatten() {
            let path = entry.path();
            if !path.is_file() {
                continue;
            }

            // Check file extension
            if path.extension().and_then(|e| e.to_str()) != Some("log") {
                continue;
            }

            // Get filename without extension
            let file_stem = path
                .file_stem()
                .and_then(|s| s.to_str())
                .unwrap_or_default()
                .to_string();

            // Apply node filter if specified
            if let Some(node_name) = &filters.node {
                if !file_stem.contains(node_name) {
                    continue;
                }
            }

            // Apply source filter
            let should_include = match filters.source {
                LogSource::All => true,
                LogSource::Nodes => !file_stem.contains("framework") && !file_stem.contains("service"),
                LogSource::Framework => file_stem.contains("framework") || file_stem.contains("cli"),
                LogSource::Services => {
                    file_stem.contains("service") || file_stem.contains("redis") || file_stem.contains("postgres")
                }
            };

            if should_include {
                sources.insert(file_stem, path);
            }
        }

        Ok(sources)
    }

    /// Read logs from multiple sources and apply filters
    ///
    /// Returns all matching log lines with source information.
    pub async fn read_logs(
        &self,
        log_sources: &HashMap<String, PathBuf>,
        filters: &LogContentFilters,
    ) -> Result<Vec<LogLine>> {
        let mut all_lines = Vec::new();

        // Read all log files
        for (name, path) in log_sources {
            if let Ok(content) = tokio::fs::read_to_string(path).await {
                for line in content.lines() {
                    // Apply filters
                    if !self.should_display_line(line, filters) {
                        continue;
                    }

                    all_lines.push(LogLine {
                        source_name: name.clone(),
                        content: line.to_string(),
                    });
                }
            }
        }

        // Apply line limit (tail behavior)
        if let Some(n) = filters.lines {
            let start = if all_lines.len() > n { all_lines.len() - n } else { 0 };
            all_lines = all_lines[start..].to_vec();
        }

        Ok(all_lines)
    }

    /// Check if a line should be displayed based on filters
    pub fn should_display_line(&self, line: &str, filters: &LogContentFilters) -> bool {
        // Apply text filter
        if let Some(pattern) = &filters.pattern {
            if !line.to_lowercase().contains(&pattern.to_lowercase()) {
                return false;
            }
        }

        // Apply level filter
        if let Some(level) = &filters.level {
            let level_upper = level.to_uppercase();
            if !line.contains(&level_upper) {
                return false;
            }
        }

        true
    }

    /// Open log files for following (watching)
    ///
    /// Returns a map of readers positioned at the end of each file.
    pub fn open_for_follow(
        &self,
        log_sources: &HashMap<String, PathBuf>,
    ) -> Result<HashMap<String, BufReader<std::fs::File>>> {
        let mut readers = HashMap::new();

        for (name, path) in log_sources {
            if let Ok(file) = std::fs::File::open(path) {
                let mut reader = BufReader::new(file);
                // Seek to end
                let _ = reader.seek(std::io::SeekFrom::End(0));
                readers.insert(name.clone(), reader);
            }
        }

        Ok(readers)
    }

    /// Read new lines from a reader
    ///
    /// Returns lines that have been added since the last read.
    pub fn read_new_lines(
        &self,
        reader: &mut BufReader<std::fs::File>,
        filters: &LogContentFilters,
    ) -> Result<Vec<String>> {
        let mut lines = Vec::new();
        let mut line = String::new();

        loop {
            line.clear();
            match reader.read_line(&mut line) {
                Ok(0) => {
                    // No new data
                    break;
                }
                Ok(_) => {
                    // Apply filters
                    if self.should_display_line(&line, filters) {
                        lines.push(line.clone());
                    }
                }
                Err(_) => {
                    break;
                }
            }
        }

        Ok(lines)
    }
}