1use crate::module_exports::{ModuleContext, ModuleExports, ModuleFunction, ModuleParam};
6use shape_value::ValueWord;
7use std::sync::atomic::{AtomicU8, Ordering};
8
9const LEVEL_DEBUG: u8 = 0;
11const LEVEL_INFO: u8 = 1;
12const LEVEL_WARN: u8 = 2;
13const LEVEL_ERROR: u8 = 3;
14
15static MIN_LEVEL: AtomicU8 = AtomicU8::new(LEVEL_DEBUG);
17
18fn format_fields(args: &[ValueWord]) -> String {
20 let fields_arg = args.get(1);
21 match fields_arg {
22 Some(f) if !f.is_none() && !f.is_unit() => {
23 let json = f.to_json_value();
24 if let serde_json::Value::Object(map) = json {
25 if map.is_empty() {
26 return String::new();
27 }
28 let pairs: Vec<String> = map.iter().map(|(k, v)| format!("{}={}", k, v)).collect();
29 format!(" {}", pairs.join(" "))
30 } else {
31 format!(" fields={}", json)
32 }
33 }
34 _ => String::new(),
35 }
36}
37
38fn parse_level(name: &str) -> Result<u8, String> {
39 match name.to_lowercase().as_str() {
40 "debug" => Ok(LEVEL_DEBUG),
41 "info" => Ok(LEVEL_INFO),
42 "warn" | "warning" => Ok(LEVEL_WARN),
43 "error" => Ok(LEVEL_ERROR),
44 _ => Err(format!(
45 "log.set_level() unknown level '{}'. Use: debug, info, warn, error",
46 name
47 )),
48 }
49}
50
51pub fn create_log_module() -> ModuleExports {
53 let mut module = ModuleExports::new("log");
54 module.description = "Structured logging utilities".to_string();
55
56 let msg_param = ModuleParam {
57 name: "message".to_string(),
58 type_name: "string".to_string(),
59 required: true,
60 description: "Log message".to_string(),
61 ..Default::default()
62 };
63
64 let fields_param = ModuleParam {
65 name: "fields".to_string(),
66 type_name: "object".to_string(),
67 required: false,
68 description: "Optional structured fields to attach to the log entry".to_string(),
69 ..Default::default()
70 };
71
72 module.add_function_with_schema(
74 "debug",
75 |args: &[ValueWord], _ctx: &ModuleContext| {
76 if MIN_LEVEL.load(Ordering::Relaxed) > LEVEL_DEBUG {
77 return Ok(ValueWord::unit());
78 }
79 let msg = args
80 .first()
81 .and_then(|a| a.as_str())
82 .ok_or_else(|| "log.debug() requires a message string".to_string())?;
83 let fields = format_fields(args);
84 tracing::debug!("[shape] {}{}", msg, fields);
85 Ok(ValueWord::unit())
86 },
87 ModuleFunction {
88 description: "Log a debug-level message".to_string(),
89 params: vec![msg_param.clone(), fields_param.clone()],
90 return_type: Some("unit".to_string()),
91 },
92 );
93
94 module.add_function_with_schema(
96 "info",
97 |args: &[ValueWord], _ctx: &ModuleContext| {
98 if MIN_LEVEL.load(Ordering::Relaxed) > LEVEL_INFO {
99 return Ok(ValueWord::unit());
100 }
101 let msg = args
102 .first()
103 .and_then(|a| a.as_str())
104 .ok_or_else(|| "log.info() requires a message string".to_string())?;
105 let fields = format_fields(args);
106 tracing::info!("[shape] {}{}", msg, fields);
107 Ok(ValueWord::unit())
108 },
109 ModuleFunction {
110 description: "Log an info-level message".to_string(),
111 params: vec![msg_param.clone(), fields_param.clone()],
112 return_type: Some("unit".to_string()),
113 },
114 );
115
116 module.add_function_with_schema(
118 "warn",
119 |args: &[ValueWord], _ctx: &ModuleContext| {
120 if MIN_LEVEL.load(Ordering::Relaxed) > LEVEL_WARN {
121 return Ok(ValueWord::unit());
122 }
123 let msg = args
124 .first()
125 .and_then(|a| a.as_str())
126 .ok_or_else(|| "log.warn() requires a message string".to_string())?;
127 let fields = format_fields(args);
128 tracing::warn!("[shape] {}{}", msg, fields);
129 Ok(ValueWord::unit())
130 },
131 ModuleFunction {
132 description: "Log a warning-level message".to_string(),
133 params: vec![msg_param.clone(), fields_param.clone()],
134 return_type: Some("unit".to_string()),
135 },
136 );
137
138 module.add_function_with_schema(
140 "error",
141 |args: &[ValueWord], _ctx: &ModuleContext| {
142 if MIN_LEVEL.load(Ordering::Relaxed) > LEVEL_ERROR {
143 return Ok(ValueWord::unit());
144 }
145 let msg = args
146 .first()
147 .and_then(|a| a.as_str())
148 .ok_or_else(|| "log.error() requires a message string".to_string())?;
149 let fields = format_fields(args);
150 tracing::error!("[shape] {}{}", msg, fields);
151 Ok(ValueWord::unit())
152 },
153 ModuleFunction {
154 description: "Log an error-level message".to_string(),
155 params: vec![msg_param, fields_param],
156 return_type: Some("unit".to_string()),
157 },
158 );
159
160 module.add_function_with_schema(
162 "set_level",
163 |args: &[ValueWord], _ctx: &ModuleContext| {
164 let level_str = args
165 .first()
166 .and_then(|a| a.as_str())
167 .ok_or_else(|| "log.set_level() requires a level string argument".to_string())?;
168
169 let level = parse_level(level_str)?;
170 MIN_LEVEL.store(level, Ordering::Relaxed);
171 Ok(ValueWord::unit())
172 },
173 ModuleFunction {
174 description: "Set the minimum log level (debug, info, warn, error)".to_string(),
175 params: vec![ModuleParam {
176 name: "level".to_string(),
177 type_name: "string".to_string(),
178 required: true,
179 description: "Minimum log level: debug, info, warn, error".to_string(),
180 allowed_values: Some(vec![
181 "debug".to_string(),
182 "info".to_string(),
183 "warn".to_string(),
184 "error".to_string(),
185 ]),
186 ..Default::default()
187 }],
188 return_type: Some("unit".to_string()),
189 },
190 );
191
192 module
193}
194
195#[cfg(test)]
196mod tests {
197 use super::*;
198
199 fn s(val: &str) -> ValueWord {
200 ValueWord::from_string(std::sync::Arc::new(val.to_string()))
201 }
202
203 fn test_ctx() -> crate::module_exports::ModuleContext<'static> {
204 let registry = Box::leak(Box::new(crate::type_schema::TypeSchemaRegistry::new()));
205 crate::module_exports::ModuleContext {
206 schemas: registry,
207 invoke_callable: None,
208 raw_invoker: None,
209 function_hashes: None,
210 vm_state: None,
211 granted_permissions: None,
212 scope_constraints: None,
213 set_pending_resume: None,
214 set_pending_frame_resume: None,
215 }
216 }
217
218 #[test]
219 fn test_log_module_creation() {
220 let module = create_log_module();
221 assert_eq!(module.name, "log");
222 assert!(module.has_export("debug"));
223 assert!(module.has_export("info"));
224 assert!(module.has_export("warn"));
225 assert!(module.has_export("error"));
226 assert!(module.has_export("set_level"));
227 }
228
229 #[test]
230 fn test_log_debug() {
231 let module = create_log_module();
232 let ctx = test_ctx();
233 MIN_LEVEL.store(LEVEL_DEBUG, Ordering::Relaxed);
235 let f = module.get_export("debug").unwrap();
236 let result = f(&[s("test message")], &ctx);
237 assert!(result.is_ok());
238 assert!(result.unwrap().is_unit());
239 }
240
241 #[test]
242 fn test_log_info() {
243 let module = create_log_module();
244 let ctx = test_ctx();
245 let f = module.get_export("info").unwrap();
246 let result = f(&[s("info message")], &ctx);
247 assert!(result.is_ok());
248 }
249
250 #[test]
251 fn test_log_warn() {
252 let module = create_log_module();
253 let ctx = test_ctx();
254 let f = module.get_export("warn").unwrap();
255 let result = f(&[s("warning message")], &ctx);
256 assert!(result.is_ok());
257 }
258
259 #[test]
260 fn test_log_error() {
261 let module = create_log_module();
262 let ctx = test_ctx();
263 let f = module.get_export("error").unwrap();
264 let result = f(&[s("error message")], &ctx);
265 assert!(result.is_ok());
266 }
267
268 #[test]
269 fn test_log_requires_string() {
270 let module = create_log_module();
271 let ctx = test_ctx();
272 let f = module.get_export("info").unwrap();
273 assert!(f(&[ValueWord::from_f64(42.0)], &ctx).is_err());
274 assert!(f(&[], &ctx).is_err());
275 }
276
277 #[test]
278 fn test_set_level_valid() {
279 let module = create_log_module();
280 let ctx = test_ctx();
281 let f = module.get_export("set_level").unwrap();
282 assert!(f(&[s("info")], &ctx).is_ok());
283 assert_eq!(MIN_LEVEL.load(Ordering::Relaxed), LEVEL_INFO);
284 assert!(f(&[s("debug")], &ctx).is_ok());
286 assert_eq!(MIN_LEVEL.load(Ordering::Relaxed), LEVEL_DEBUG);
287 }
288
289 #[test]
290 fn test_set_level_invalid() {
291 let module = create_log_module();
292 let ctx = test_ctx();
293 let f = module.get_export("set_level").unwrap();
294 assert!(f(&[s("critical")], &ctx).is_err());
295 }
296
297 #[test]
298 fn test_set_level_case_insensitive() {
299 let module = create_log_module();
300 let ctx = test_ctx();
301 let f = module.get_export("set_level").unwrap();
302 assert!(f(&[s("WARN")], &ctx).is_ok());
303 assert_eq!(MIN_LEVEL.load(Ordering::Relaxed), LEVEL_WARN);
304 assert!(f(&[s("Warning")], &ctx).is_ok());
305 assert_eq!(MIN_LEVEL.load(Ordering::Relaxed), LEVEL_WARN);
306 let _ = f(&[s("debug")], &ctx);
308 }
309
310 #[test]
311 fn test_log_level_filtering() {
312 let module = create_log_module();
313 let ctx = test_ctx();
314 let set_level = module.get_export("set_level").unwrap();
315 let debug_fn = module.get_export("debug").unwrap();
316 let error_fn = module.get_export("error").unwrap();
317
318 set_level(&[s("error")], &ctx).unwrap();
320 let result = debug_fn(&[s("should be dropped")], &ctx);
321 assert!(result.is_ok());
322 assert!(result.unwrap().is_unit());
323
324 let result = error_fn(&[s("error still works")], &ctx);
326 assert!(result.is_ok());
327
328 let _ = set_level(&[s("debug")], &ctx);
330 }
331
332 #[test]
333 fn test_log_schemas() {
334 let module = create_log_module();
335
336 let info_schema = module.get_schema("info").unwrap();
337 assert_eq!(info_schema.params.len(), 2);
338 assert!(info_schema.params[0].required);
339 assert!(!info_schema.params[1].required);
340 assert_eq!(info_schema.return_type.as_deref(), Some("unit"));
341
342 let level_schema = module.get_schema("set_level").unwrap();
343 assert_eq!(level_schema.params.len(), 1);
344 assert!(level_schema.params[0].allowed_values.is_some());
345 }
346}