use crate::config::Config;
use anyhow::{Context, Result};
use clap::Args as ClapArgs;
use std::path::PathBuf;
#[derive(Debug, ClapArgs)]
pub struct Args {
jail: String,
#[arg(long)]
service: Option<String>,
#[arg(short, long)]
follow: bool,
#[arg(short, long, default_value = "50")]
lines: usize,
#[arg(long)]
timestamps: bool,
}
fn get_log_paths(jail: &str, service: Option<&str>) -> Vec<PathBuf> {
let mut paths = Vec::new();
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 {
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)));
}
}
paths.push(PathBuf::from(format!("/var/log/{}/{}.log", jail, svc)));
} else {
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
}
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())
}
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());
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() {
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");
let client = reqwest::Client::builder()
.timeout(std::time::Duration::from_secs(3))
.build()?;
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(());
}
for (path, lines) in &found_logs {
println!("=== {} ===", path.display());
for line in lines {
if args.timestamps {
println!("{}", line);
} else {
println!("{}", line);
}
}
println!();
}
if args.follow {
if let Some((path, _)) = found_logs.first() {
println!("Following {} (Ctrl+C to stop)...", path.display());
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(_) => {
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());
}
}