robinpath_modules/modules/
log_mod.rs1use robinpath::{RobinPath, Value};
2use std::sync::{Arc, Mutex};
3
4struct LogConfig {
5 level: u8,
6 file_path: Option<String>,
7 format: String,
8 indent: usize,
9 timers: std::collections::HashMap<String, u64>,
10}
11
12impl LogConfig {
13 fn new() -> Self {
14 Self {
15 level: 0, file_path: None,
17 format: "text".to_string(),
18 indent: 0,
19 timers: std::collections::HashMap::new(),
20 }
21 }
22}
23
24fn now_ms() -> u64 {
25 std::time::SystemTime::now()
26 .duration_since(std::time::UNIX_EPOCH)
27 .unwrap_or_default()
28 .as_millis() as u64
29}
30
31fn level_num(s: &str) -> u8 {
32 match s.to_lowercase().as_str() {
33 "debug" => 0, "info" => 1, "warn" | "warning" => 2, "error" => 3, "fatal" => 4, _ => 0,
34 }
35}
36
37fn level_name(n: u8) -> &'static str {
38 match n { 0 => "DEBUG", 1 => "INFO", 2 => "WARN", 3 => "ERROR", 4 => "FATAL", _ => "DEBUG" }
39}
40
41fn format_log_line(level: u8, messages: &[Value], config: &LogConfig) -> String {
42 let parts: Vec<String> = messages.iter().map(|v| v.to_display_string()).collect();
43 let msg = parts.join(" ");
44 let indent = " ".repeat(config.indent * 2);
45 if config.format == "json" {
46 let now = now_ms();
47 format!("{{\"level\":\"{}\",\"message\":\"{}\",\"timestamp\":{}}}", level_name(level), msg.replace('"', "\\\""), now)
48 } else {
49 let now = chrono_like_now();
50 format!("{} [{}] {}{}", now, level_name(level), indent, msg)
51 }
52}
53
54fn chrono_like_now() -> String {
55 let ts = now_ms() / 1000;
56 let days = (ts as i64).div_euclid(86400);
57 let time_of_day = (ts as i64).rem_euclid(86400);
58 let hour = (time_of_day / 3600) as u32;
59 let min = ((time_of_day % 3600) / 60) as u32;
60 let sec = (time_of_day % 60) as u32;
61 let z = days + 719468;
62 let era = z.div_euclid(146097);
63 let doe = z.rem_euclid(146097);
64 let yoe = (doe - doe / 1460 + doe / 36524 - doe / 146096) / 365;
65 let y = yoe + era * 400;
66 let doy = doe - (365 * yoe + yoe / 4 - yoe / 100);
67 let mp = (5 * doy + 2) / 153;
68 let d = (doy - (153 * mp + 2) / 5 + 1) as u32;
69 let m = if mp < 10 { mp + 3 } else { mp - 9 } as u32;
70 let y = if m <= 2 { y + 1 } else { y };
71 format!("{:04}-{:02}-{:02}T{:02}:{:02}:{:02}Z", y, m, d, hour, min, sec)
72}
73
74fn do_log(level: u8, messages: &[Value], state: &Mutex<LogConfig>) -> Result<Value, String> {
75 let config = state.lock().unwrap();
76 if level < config.level {
77 return Ok(Value::Null);
78 }
79 let line = format_log_line(level, messages, &config);
80 if let Some(ref path) = config.file_path {
82 use std::io::Write;
83 if let Ok(mut f) = std::fs::OpenOptions::new().create(true).append(true).open(path) {
84 let _ = writeln!(f, "{}", line);
85 }
86 }
87 Ok(Value::String(line))
88}
89
90pub fn register(rp: &mut RobinPath) {
91 let state = Arc::new(Mutex::new(LogConfig::new()));
92
93 let s = state.clone();
94 rp.register_builtin("log.debug", move |args, _| do_log(0, args, &s));
95 let s = state.clone();
96 rp.register_builtin("log.info", move |args, _| do_log(1, args, &s));
97 let s = state.clone();
98 rp.register_builtin("log.warn", move |args, _| do_log(2, args, &s));
99 let s = state.clone();
100 rp.register_builtin("log.error", move |args, _| do_log(3, args, &s));
101 let s = state.clone();
102 rp.register_builtin("log.fatal", move |args, _| do_log(4, args, &s));
103
104 let s = state.clone();
105 rp.register_builtin("log.setLevel", move |args, _| {
106 let level = args.first().map(|v| v.to_display_string()).unwrap_or_else(|| "debug".to_string());
107 s.lock().unwrap().level = level_num(&level);
108 Ok(Value::Bool(true))
109 });
110
111 let s = state.clone();
112 rp.register_builtin("log.getLevel", move |_args, _| {
113 let config = s.lock().unwrap();
114 Ok(Value::String(level_name(config.level).to_lowercase()))
115 });
116
117 let s = state.clone();
118 rp.register_builtin("log.setFile", move |args, _| {
119 let path = args.first().map(|v| v.to_display_string());
120 s.lock().unwrap().file_path = path;
121 Ok(Value::Bool(true))
122 });
123
124 let s = state.clone();
125 rp.register_builtin("log.setFormat", move |args, _| {
126 let fmt = args.first().map(|v| v.to_display_string()).unwrap_or_else(|| "text".to_string());
127 s.lock().unwrap().format = fmt;
128 Ok(Value::Bool(true))
129 });
130
131 let s = state.clone();
132 rp.register_builtin("log.clear", move |_args, _| {
133 let mut config = s.lock().unwrap();
134 config.level = 0;
135 config.file_path = None;
136 config.format = "text".to_string();
137 config.indent = 0;
138 config.timers.clear();
139 Ok(Value::Bool(true))
140 });
141
142 let s = state.clone();
143 rp.register_builtin("log.group", move |args, _| {
144 let label = args.first().map(|v| v.to_display_string()).unwrap_or_default();
145 let mut config = s.lock().unwrap();
146 let indent = " ".repeat(config.indent * 2);
147 config.indent += 1;
148 Ok(Value::String(format!("{}--- {} ---", indent, label)))
149 });
150
151 let s = state.clone();
152 rp.register_builtin("log.groupEnd", move |_args, _| {
153 let mut config = s.lock().unwrap();
154 if config.indent > 0 { config.indent -= 1; }
155 Ok(Value::Bool(true))
156 });
157
158 let s = state.clone();
159 rp.register_builtin("log.time", move |args, _| {
160 let label = args.first().map(|v| v.to_display_string()).unwrap_or_else(|| "default".to_string());
161 s.lock().unwrap().timers.insert(label, now_ms());
162 Ok(Value::Bool(true))
163 });
164
165 let s = state.clone();
166 rp.register_builtin("log.timeEnd", move |args, _| {
167 let label = args.first().map(|v| v.to_display_string()).unwrap_or_else(|| "default".to_string());
168 let mut config = s.lock().unwrap();
169 if let Some(start) = config.timers.remove(&label) {
170 let elapsed = now_ms() - start;
171 Ok(Value::String(format!("{}: {}ms", label, elapsed)))
172 } else {
173 Err(format!("Timer \"{}\" not found", label))
174 }
175 });
176
177 rp.register_builtin("log.table", |args, _| {
178 let data = args.first().cloned().unwrap_or(Value::Null);
179 if let Value::Array(arr) = &data {
180 if arr.is_empty() {
181 return Ok(Value::String("(empty)".to_string()));
182 }
183 let columns: Vec<String> = if let Some(Value::Object(first)) = arr.first() {
185 first.keys().cloned().collect()
186 } else {
187 vec!["value".to_string()]
188 };
189 let mut rows: Vec<Vec<String>> = vec![columns.clone()];
190 for item in arr {
191 if let Value::Object(obj) = item {
192 rows.push(columns.iter().map(|c| obj.get(c).map(|v| v.to_display_string()).unwrap_or_default()).collect());
193 } else {
194 rows.push(vec![item.to_display_string()]);
195 }
196 }
197 let widths: Vec<usize> = (0..columns.len()).map(|i| {
199 rows.iter().map(|r| r.get(i).map(|s| s.len()).unwrap_or(0)).max().unwrap_or(0)
200 }).collect();
201 let mut lines = Vec::new();
202 for (idx, row) in rows.iter().enumerate() {
203 let cells: Vec<String> = row.iter().enumerate()
204 .map(|(i, s)| format!("{:width$}", s, width = widths.get(i).copied().unwrap_or(0)))
205 .collect();
206 lines.push(format!("| {} |", cells.join(" | ")));
207 if idx == 0 {
208 let sep: Vec<String> = widths.iter().map(|&w| "-".repeat(w)).collect();
209 lines.push(format!("|-{}-|", sep.join("-|-")));
210 }
211 }
212 Ok(Value::String(lines.join("\n")))
213 } else {
214 Ok(Value::String(data.to_display_string()))
215 }
216 });
217}