mecha10-cli 0.1.47

Mecha10 CLI tool
Documentation
//! Logs command handler
//!
//! Orchestrates viewing and filtering logs from multiple sources.

use crate::commands::{LogSource, LogsArgs};
use crate::context::CliContext;
use crate::paths;
use crate::services::{LogContentFilters, LogFileFilters, LogsService};
use anyhow::Result;
use std::io::Write;
use std::sync::atomic::{AtomicBool, Ordering};
use std::sync::Arc;
use std::time::Duration;

/// Handle logs command
///
/// Views and filters logs from multiple sources (nodes, framework, services).
///
/// # Arguments
///
/// * `ctx` - CLI execution context
/// * `args` - Logs command arguments
pub async fn handle_logs(ctx: &mut CliContext, args: &LogsArgs) -> Result<()> {
    println!();
    println!("📋 Viewing Logs");
    println!("━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━");
    println!();

    // Determine if we should follow (default is true unless --no-follow is set)
    let should_follow = args.follow && !args.no_follow;

    // Check project initialization
    if !ctx.is_project_initialized() {
        return Err(anyhow::anyhow!(
            "Not in a Mecha10 project directory.\n\
             Run 'mecha10 init' to create a new project."
        ));
    }

    // Get logs service
    let project = ctx.project()?;
    let logs_dir = project.root().join(paths::project::LOGS_DIR);
    let logs_service = LogsService::new(logs_dir);

    // Determine log source
    let source = args.source.unwrap_or(LogSource::All);

    // Show active filters
    display_filters(args, source);

    // Collect log sources
    let file_filters = LogFileFilters {
        node: args.node.clone(),
        source,
    };
    let log_sources = logs_service.collect_log_files(&file_filters)?;

    if log_sources.is_empty() {
        println!("No log files found.");
        println!();
        println!("Tip: Run 'mecha10 dev' to start nodes and generate logs");
        println!();
        return Ok(());
    }

    // Create content filters
    let content_filters = LogContentFilters {
        pattern: args.filter.clone(),
        level: args.level.clone(),
        lines: args.lines,
    };

    // Display logs
    if should_follow {
        // Follow mode (watch) - default behavior
        follow_logs(&logs_service, &log_sources, &content_filters, args.timestamps).await?;
    } else {
        // Static display
        display_logs(&logs_service, &log_sources, &content_filters, args.timestamps).await?;
    }

    Ok(())
}

/// Display active filters
fn display_filters(args: &LogsArgs, source: LogSource) {
    let mut filters = Vec::new();

    if let Some(node) = &args.node {
        filters.push(format!("Node: {}", node));
    }
    filters.push(format!("Source: {:?}", source));
    if let Some(pattern) = &args.filter {
        filters.push(format!("Pattern: {}", pattern));
    }
    if let Some(level) = &args.level {
        filters.push(format!("Level: {}", level));
    }
    if let Some(n) = args.lines {
        filters.push(format!("Lines: {}", n));
    }

    if !filters.is_empty() {
        println!("Filters: {}", filters.join(" | "));
        println!();
    }
}

/// Display logs from multiple sources
async fn display_logs(
    logs_service: &LogsService,
    log_sources: &std::collections::HashMap<String, std::path::PathBuf>,
    filters: &LogContentFilters,
    _show_timestamps: bool,
) -> Result<()> {
    let lines = logs_service.read_logs(log_sources, filters).await?;

    // Display lines
    let line_count = lines.len();
    for log_line in lines {
        if log_sources.len() > 1 {
            println!("[{}] {}", log_line.source_name, log_line.content);
        } else {
            println!("{}", log_line.content);
        }
    }

    println!();
    println!("Showing {} lines from {} source(s)", line_count, log_sources.len());
    println!();

    Ok(())
}

/// Follow logs from multiple sources (watch mode)
async fn follow_logs(
    logs_service: &LogsService,
    log_sources: &std::collections::HashMap<String, std::path::PathBuf>,
    filters: &LogContentFilters,
    _show_timestamps: bool,
) -> Result<()> {
    // Display initial content
    display_logs(logs_service, log_sources, filters, _show_timestamps).await?;

    println!("Following logs (Ctrl+C to stop)...");
    println!();

    // Flush stdout to ensure message is displayed
    std::io::stdout().flush()?;

    // Set up signal handler
    let running = Arc::new(AtomicBool::new(true));
    let r = running.clone();

    ctrlc::set_handler(move || {
        r.store(false, Ordering::SeqCst);
    })?;

    // Open all log files for reading
    let mut readers = logs_service.open_for_follow(log_sources)?;

    // Follow loop
    while running.load(Ordering::SeqCst) {
        let mut had_activity = false;

        for (name, reader) in &mut readers {
            let new_lines = logs_service.read_new_lines(reader, filters)?;

            if !new_lines.is_empty() {
                had_activity = true;

                for line in new_lines {
                    // Display with source prefix if multiple sources
                    if log_sources.len() > 1 {
                        print!("[{}] {}", name, line);
                    } else {
                        print!("{}", line);
                    }

                    // Flush stdout immediately for real-time display
                    let _ = std::io::stdout().flush();
                }
            }
        }

        if !had_activity {
            // No new data from any source, sleep briefly
            std::thread::sleep(Duration::from_millis(100));
        }
    }

    println!();
    println!("Stopped following logs");
    println!();

    Ok(())
}