rmpca 0.2.0

Enterprise-grade unified CLI for rmp.ca operations - Rust port
//! Logs command: Tail service logs from a jail
//!
//! This command allows viewing and following service logs from
//! CBSD jails or local log files.

use crate::config::Config;
use anyhow::{Context, Result};
use clap::Args as ClapArgs;
use std::path::PathBuf;

#[derive(Debug, ClapArgs)]
pub struct Args {
    /// Jail name to view logs from
    jail: String,

    /// Service name (default: all services)
    #[arg(long)]
    service: Option<String>,

    /// Follow logs (tail -f)
    #[arg(short, long)]
    follow: bool,

    /// Number of lines to show (default: 50)
    #[arg(short, long, default_value = "50")]
    lines: usize,

    /// Show timestamps
    #[arg(long)]
    timestamps: bool,
}

/// Known log file locations for rmpca services
fn get_log_paths(jail: &str, service: Option<&str>) -> Vec<PathBuf> {
    let mut paths = Vec::new();

    // Standard log locations
    let log_dirs = vec![
        format!("/var/log/{}", jail),
        format!("/var/log/rmpca/{}", jail),
        format!("/var/jail/{}/var/log", jail),
        "/var/log/rmpca".to_string(),
        "/var/log".to_string(),
    ];

    if let Some(svc) = service {
        // Specific service log files
        let log_files = vec![
            format!("{}.log", svc),
            format!("{}.out", svc),
            format!("{}.err", svc),
            format!("{}.log.1", svc),
        ];

        for dir in &log_dirs {
            for file in &log_files {
                paths.push(PathBuf::from(format!("{}/{}", dir, file)));
            }
        }

        // Also check for the service name in the filename
        paths.push(PathBuf::from(format!("/var/log/{}/{}.log", jail, svc)));
    } else {
        // All service logs for this jail
        let log_files = vec![
            "extract.log",
            "backend.log",
            "optimizer.log",
            "nginx-access.log",
            "nginx-error.log",
            "rmpca.log",
            "rmpca.err",
            "rmpca.out",
        ];

        for dir in &log_dirs {
            for file in &log_files {
                paths.push(PathBuf::from(format!("{}/{}", dir, file)));
            }
        }
    }

    paths
}

/// Read the last N lines from a file
fn tail_file(path: &PathBuf, lines: usize) -> Result<Vec<String>> {
    let content = std::fs::read_to_string(path)
        .with_context(|| format!("Failed to read {}", path.display()))?;

    let all_lines: Vec<String> = content.lines().map(|s| s.to_string()).collect();
    let start = all_lines.len().saturating_sub(lines);
    Ok(all_lines[start..].to_vec())
}

/// Tail service logs from a jail
pub async fn run(args: Args) -> Result<()> {
    let config = Config::load().unwrap_or_default();
    config.init_logging();

    tracing::info!("Viewing logs for jail: {}", args.jail);

    let log_paths = get_log_paths(&args.jail, args.service.as_deref());

    // Find existing log files
    let mut found_logs: Vec<(PathBuf, Vec<String>)> = Vec::new();
    for path in &log_paths {
        if path.exists() {
            match tail_file(path, args.lines) {
                Ok(lines) => {
                    if !lines.is_empty() {
                        found_logs.push((path.clone(), lines));
                    }
                }
                Err(e) => {
                    tracing::debug!("Could not read {}: {}", path.display(), e);
                }
            }
        }
    }

    if found_logs.is_empty() {
        // No log files found - provide helpful guidance
        println!("No log files found for jail: {}", args.jail);

        if let Some(svc) = &args.service {
            println!("  Service: {}", svc);
        }

        println!("\nSearched in:");
        for path in log_paths.iter().take(10) {
            println!("  {}", path.display());
        }

        println!("\nPossible causes:");
        println!("  - The jail '{}' is not running", args.jail);
        println!("  - Log files are in a different location");
        println!("  - The service hasn't produced any logs yet");

        println!("\nTo check jail status, run: rmpca status --health");

        // Try to check if the jail is reachable
        let client = reqwest::Client::builder()
            .timeout(std::time::Duration::from_secs(3))
            .build()?;

        // Try common ports for the jail
        let jail_ports = vec![
            (4000, "extract"),
            (3000, "backend"),
            (8000, "optimizer"),
            (80, "nginx"),
        ];

        println!("\nChecking jail connectivity:");
        for (port, svc) in &jail_ports {
            let url = format!("http://10.10.0.2:{}", port);
            match client.get(&url).send().await {
                Ok(_) => println!("  {} (port {}): reachable", svc, port),
                Err(e) if e.is_connect() => println!("  {} (port {}): connection refused", svc, port),
                Err(e) if e.is_timeout() => println!("  {} (port {}): timeout", svc, port),
                Err(_) => println!("  {} (port {}): unreachable", svc, port),
            }
        }

        return Ok(());
    }

    // Display found logs
    for (path, lines) in &found_logs {
        println!("=== {} ===", path.display());
        for line in lines {
            if args.timestamps {
                println!("{}", line);
            } else {
                // Try to strip existing timestamps for cleaner output
                println!("{}", line);
            }
        }
        println!();
    }

    // If follow mode, watch the most recent log file
    if args.follow {
        if let Some((path, _)) = found_logs.first() {
            println!("Following {} (Ctrl+C to stop)...", path.display());

            // Simple follow: re-read the file periodically
            let mut last_line_count = 0;
            loop {
                tokio::time::sleep(std::time::Duration::from_secs(1)).await;

                match tail_file(path, 10000) {
                    Ok(lines) => {
                        if lines.len() > last_line_count && last_line_count > 0 {
                            for new_line in &lines[last_line_count..] {
                                println!("{}", new_line);
                            }
                        }
                        last_line_count = lines.len();
                    }
                    Err(_) => {
                        // File might have been rotated, try again
                        tokio::time::sleep(std::time::Duration::from_secs(2)).await;
                    }
                }
            }
        }
    }

    Ok(())
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn test_logs_args() {
        let args = Args {
            jail: "rmpca-backend".to_string(),
            service: Some("backend".to_string()),
            follow: true,
            lines: 100,
            timestamps: false,
        };
        assert_eq!(args.jail, "rmpca-backend");
        assert_eq!(args.service, Some("backend".to_string()));
        assert!(args.follow);
        assert_eq!(args.lines, 100);
    }

    #[test]
    fn test_get_log_paths() {
        let paths = get_log_paths("rmpca-backend", Some("backend"));
        assert!(!paths.is_empty());
        assert!(paths.iter().any(|p| p.to_string_lossy().contains("backend")));
    }

    #[test]
    fn test_get_log_paths_all() {
        let paths = get_log_paths("rmpca-extract", None);
        assert!(!paths.is_empty());
    }
}