Skip to main content

roboticus_cli/cli/admin/misc/
local_logs.rs

1fn try_read_log_file(lines: usize, _level: &str) {
2    let (DIM, BOLD, ACCENT, GREEN, YELLOW, RED, CYAN, RESET, MONO) = colors();
3    let (OK, ACTION, WARN, DETAIL, ERR) = icons();
4    let log_dir = roboticus_core::home_dir().join(".roboticus").join("logs");
5
6    if !log_dir.exists() {
7        println!("  No log directory found at {}", log_dir.display());
8        return;
9    }
10
11    let mut entries: Vec<_> = match std::fs::read_dir(&log_dir) {
12        Ok(rd) => rd.filter_map(|e| e.ok()).collect(),
13        Err(e) => {
14            println!("  Error reading log directory: {e}");
15            return;
16        }
17    };
18
19    entries.sort_by_key(|e| std::cmp::Reverse(e.metadata().ok().and_then(|m| m.modified().ok())));
20
21    if let Some(latest) = entries.first() {
22        let path = latest.path();
23        println!("  {DIM}Reading: {}{RESET}\n", path.display());
24        match std::fs::read_to_string(&path) {
25            Ok(content) => {
26                let all_lines: Vec<&str> = content.lines().collect();
27                let start = if all_lines.len() > lines {
28                    all_lines.len() - lines
29                } else {
30                    0
31                };
32                for line in &all_lines[start..] {
33                    println!("{line}");
34                }
35            }
36            Err(e) => println!("  Error reading log file: {e}"),
37        }
38    } else {
39        println!("  No log files found.");
40    }
41}
42
43pub async fn cmd_logs(
44    base_url: &str,
45    lines: usize,
46    follow: bool,
47    level: &str,
48    json: bool,
49) -> Result<(), Box<dyn std::error::Error>> {
50    let (DIM, BOLD, ACCENT, GREEN, YELLOW, RED, CYAN, RESET, MONO) = colors();
51    let (OK, ACTION, WARN, DETAIL, ERR) = icons();
52    if follow {
53        println!("  {BOLD}Tailing logs{RESET} (level >= {level}, Ctrl+C to stop)\n");
54
55        let client = super::http_client()?;
56        let resp = client
57            .get(format!("{base_url}/api/logs"))
58            .query(&[
59                ("follow", "true"),
60                ("level", level),
61                ("lines", &lines.to_string()),
62            ])
63            .send()
64            .await?;
65
66        if !resp.status().is_success() {
67            eprintln!("  Server returned {}", resp.status());
68            eprintln!("  Log tailing requires a running server.");
69
70            try_read_log_file(lines, level);
71            return Ok(());
72        }
73
74        let mut stream = resp.bytes_stream();
75        use futures_util::StreamExt;
76        while let Some(chunk) = stream.next().await {
77            match chunk {
78                Ok(bytes) => {
79                    let text = String::from_utf8_lossy(&bytes);
80                    for line in text.lines() {
81                        if let Some(data) = line.strip_prefix("data: ") {
82                            println!("{data}");
83                        }
84                    }
85                }
86                Err(e) => {
87                    eprintln!("  Stream error: {e}");
88                    break;
89                }
90            }
91        }
92    } else {
93        let resp = super::http_client()?
94            .get(format!("{base_url}/api/logs?lines={lines}&level={level}"))
95            .send()
96            .await;
97
98        match resp {
99            Ok(r) if r.status().is_success() => {
100                let body: serde_json::Value = r.json().await.unwrap_or_default();
101                if json {
102                    println!("{}", serde_json::to_string_pretty(&body)?);
103                    return Ok(());
104                }
105                if let Some(entries) = body.get("entries").and_then(|v| v.as_array()) {
106                    if entries.is_empty() {
107                        println!("  No log entries found.");
108                    }
109                    for entry in entries {
110                        let ts = entry
111                            .get("timestamp")
112                            .and_then(|v| v.as_str())
113                            .unwrap_or("");
114                        let lvl = entry
115                            .get("level")
116                            .and_then(|v| v.as_str())
117                            .unwrap_or("info");
118                        let msg = entry.get("message").and_then(|v| v.as_str()).unwrap_or("");
119                        let target = entry.get("target").and_then(|v| v.as_str()).unwrap_or("");
120                        let color = match lvl {
121                            "ERROR" | "error" => RED,
122                            "WARN" | "warn" => YELLOW,
123                            "INFO" | "info" => GREEN,
124                            "DEBUG" | "debug" => CYAN,
125                            _ => DIM,
126                        };
127                        println!("{color}{ts} [{lvl:>5}] {target}: {msg}{RESET}");
128                    }
129                } else {
130                    println!("  No log entries returned.");
131                }
132            }
133            Ok(r) => {
134                eprintln!("  Server returned {}", r.status());
135                try_read_log_file(lines, level);
136            }
137            Err(_) => {
138                eprintln!("  Server not reachable. Reading log files directly...\n");
139                try_read_log_file(lines, level);
140            }
141        }
142    }
143    Ok(())
144}
145