roboticus_cli/cli/admin/misc/
local_logs.rs1fn 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