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