reifydb_core/interface/logging/
macros.rs

1// Copyright (c) reifydb.com 2025
2// This file is licensed under the AGPL-3.0-or-later, see license.md file
3
4//! Logging macros for convenient usage
5
6/// Main logging macro with support for structured fields
7#[macro_export]
8macro_rules! log {
9    // Simple message (no formatting)
10    ($level:expr, $msg:expr) => {{
11        // Check if it's a format string by trying to format it
12        // This allows both literal strings and format strings with inline variables
13        let message = format!($msg);
14        let record = $crate::interface::logging::Record::new(
15            $level,
16            module_path!(),
17            message,
18        )
19        .with_location(file!(), line!());
20        $crate::interface::logging::log(record);
21    }};
22
23    // Format string with explicit arguments
24    ($level:expr, $fmt:expr, $($arg:tt)*) => {{
25        let message = format!($fmt, $($arg)*);
26        let record = $crate::interface::logging::Record::new(
27            $level,
28            module_path!(),
29            message,
30        )
31        .with_location(file!(), line!());
32        $crate::interface::logging::log(record);
33    }};
34}
35
36/// Trace level logging
37#[macro_export]
38macro_rules! log_trace {
39    // Simple message
40    ($msg:expr) => {
41        $crate::log!($crate::interface::logging::LogLevel::Trace, $msg)
42    };
43
44    // Message with fields: log_trace!("message", { key: value, ... })
45    ($msg:expr, { $($key:expr => $value:expr),+ $(,)? }) => {{
46        let mut record = $crate::interface::logging::Record::new(
47            $crate::interface::logging::LogLevel::Trace,
48            module_path!(),
49            $msg,
50        )
51        .with_location(file!(), line!());
52        $(
53            record = record.with_field($key, $value);
54        )+
55        $crate::interface::logging::log(record);
56    }};
57
58    // Format string with arguments
59    ($fmt:expr, $($arg:tt)*) => {
60        $crate::log!($crate::interface::logging::LogLevel::Trace, $fmt, $($arg)*)
61    };
62}
63
64/// Debug level logging
65#[macro_export]
66macro_rules! log_debug {
67    // Simple message
68    ($msg:expr) => {
69        $crate::log!($crate::interface::logging::LogLevel::Debug, $msg)
70    };
71
72    // Message with fields: log_debug!("message", { key: value, ... })
73    ($msg:expr, { $($key:expr => $value:expr),+ $(,)? }) => {{
74        let mut record = $crate::interface::logging::Record::new(
75            $crate::interface::logging::LogLevel::Debug,
76            module_path!(),
77            $msg,
78        )
79        .with_location(file!(), line!());
80        $(
81            record = record.with_field($key, $value);
82        )+
83        $crate::interface::logging::log(record);
84    }};
85
86    // Format string with arguments
87    ($fmt:expr, $($arg:tt)*) => {
88        $crate::log!($crate::interface::logging::LogLevel::Debug, $fmt, $($arg)*)
89    };
90}
91
92/// Info level logging
93#[macro_export]
94macro_rules! log_info {
95    // Simple message
96    ($msg:expr) => {
97        $crate::log!($crate::interface::logging::LogLevel::Info, $msg)
98    };
99
100    // Message with fields: log_info!("message", { key: value, ... })
101    ($msg:expr, { $($key:expr => $value:expr),+ $(,)? }) => {{
102        let mut record = $crate::interface::logging::Record::new(
103            $crate::interface::logging::LogLevel::Info,
104            module_path!(),
105            $msg,
106        )
107        .with_location(file!(), line!());
108        $(
109            record = record.with_field($key, $value);
110        )+
111        $crate::interface::logging::log(record);
112    }};
113
114    // Format string with arguments
115    ($fmt:expr, $($arg:tt)*) => {
116        $crate::log!($crate::interface::logging::LogLevel::Info, $fmt, $($arg)*)
117    };
118}
119
120/// Warning level logging
121#[macro_export]
122macro_rules! log_warn {
123    // Simple message
124    ($msg:expr) => {
125        $crate::log!($crate::interface::logging::LogLevel::Warn, $msg)
126    };
127
128    // Message with fields: log_warn!("message", { key: value, ... })
129    ($msg:expr, { $($key:expr => $value:expr),+ $(,)? }) => {{
130        let mut record = $crate::interface::logging::Record::new(
131            $crate::interface::logging::LogLevel::Warn,
132            module_path!(),
133            $msg,
134        )
135        .with_location(file!(), line!());
136        $(
137            record = record.with_field($key, $value);
138        )+
139        $crate::interface::logging::log(record);
140    }};
141
142    // Format string with arguments
143    ($fmt:expr, $($arg:tt)*) => {
144        $crate::log!($crate::interface::logging::LogLevel::Warn, $fmt, $($arg)*)
145    };
146}
147
148/// Error level logging
149#[macro_export]
150macro_rules! log_error {
151    // Simple message
152    ($msg:expr) => {
153        $crate::log!($crate::interface::logging::LogLevel::Error, $msg)
154    };
155
156    // Message with fields: log_error!("message", { key: value, ... })
157    ($msg:expr, { $($key:expr => $value:expr),+ $(,)? }) => {{
158        let mut record = $crate::interface::logging::Record::new(
159            $crate::interface::logging::LogLevel::Error,
160            module_path!(),
161            $msg,
162        )
163        .with_location(file!(), line!());
164        $(
165            record = record.with_field($key, $value);
166        )+
167        $crate::interface::logging::log(record);
168    }};
169
170    // Format string with arguments
171    ($fmt:expr, $($arg:tt)*) => {
172        $crate::log!($crate::interface::logging::LogLevel::Error, $fmt, $($arg)*)
173    };
174}
175
176/// Critical level logging with guaranteed synchronous delivery
177#[macro_export]
178macro_rules! log_critical {
179    // Simple message
180    ($msg:expr) => {
181        $crate::log!($crate::interface::logging::LogLevel::Critical, $msg)
182    };
183
184    // Message with fields: log_critical!("message", { key: value, ... })
185    ($msg:expr, { $($key:expr => $value:expr),+ $(,)? }) => {{
186        let mut record = $crate::interface::logging::Record::new(
187            $crate::interface::logging::LogLevel::Critical,
188            module_path!(),
189            $msg,
190        )
191        .with_location(file!(), line!());
192        $(
193            record = record.with_field($key, $value);
194        )+
195        $crate::interface::logging::log(record);
196    }};
197
198    // Format string with arguments
199    ($fmt:expr, $($arg:tt)*) => {
200        $crate::log!($crate::interface::logging::LogLevel::Critical, $fmt, $($arg)*)
201    };
202}
203
204#[cfg(test)]
205mod tests {
206	use crossbeam_channel::unbounded;
207
208	use super::super::{mock::with_mock_logger, *};
209	use crate::{log, log_critical, log_debug, log_error, log_info, log_trace, log_warn};
210
211	#[derive(Debug)]
212	#[allow(dead_code)]
213	struct TestStruct {
214		value: i32,
215		name: String,
216	}
217
218	#[test]
219	fn test_literal_string() {
220		let (sender, receiver) = unbounded();
221
222		with_mock_logger(sender, || {
223			log_debug!("simple message");
224		});
225
226		let record = receiver.try_recv().unwrap();
227		assert_eq!(record.message, "simple message");
228		assert_eq!(record.level, LogLevel::Debug);
229	}
230
231	#[test]
232	fn test_inline_variable_syntax() {
233		let (sender, receiver) = unbounded();
234
235		with_mock_logger(sender, || {
236			let change = TestStruct {
237				value: 42,
238				name: "test".to_string(),
239			};
240
241			log_debug!("{change:?}");
242		});
243
244		let record = receiver.try_recv().unwrap();
245		assert!(record.message.contains("TestStruct"));
246		assert!(record.message.contains("value: 42"));
247		assert!(record.message.contains("name: \"test\""));
248	}
249
250	#[test]
251	fn test_traditional_format_syntax() {
252		let (sender, receiver) = unbounded();
253
254		with_mock_logger(sender, || {
255			let change = TestStruct {
256				value: 99,
257				name: "traditional".to_string(),
258			};
259
260			log_debug!("{:?}", change);
261		});
262
263		let record = receiver.try_recv().unwrap();
264		assert!(record.message.contains("TestStruct"));
265		assert!(record.message.contains("value: 99"));
266		assert!(record.message.contains("name: \"traditional\""));
267	}
268
269	#[test]
270	fn test_traditional_with_display() {
271		let (sender, receiver) = unbounded();
272
273		with_mock_logger(sender, || {
274			let x = 123;
275			log_info!("Value: {}", x);
276		});
277
278		let record = receiver.try_recv().unwrap();
279		assert_eq!(record.message, "Value: 123");
280		assert_eq!(record.level, LogLevel::Info);
281	}
282
283	#[test]
284	fn test_multiple_inline_variables() {
285		let (sender, receiver) = unbounded();
286
287		with_mock_logger(sender, || {
288			let name = "Alice";
289			let age = 30;
290			let city = "New York";
291
292			log_info!("{name} is {age} years old and lives in {city}");
293		});
294
295		let record = receiver.try_recv().unwrap();
296		assert_eq!(record.message, "Alice is 30 years old and lives in New York");
297	}
298
299	#[test]
300	fn test_mixed_formatting() {
301		let (sender, receiver) = unbounded();
302
303		with_mock_logger(sender, || {
304			let count = 5;
305			let items = vec!["apple", "banana", "orange"];
306
307			log_warn!("Found {count} items: {items:?}");
308		});
309
310		let record = receiver.try_recv().unwrap();
311		assert!(record.message.contains("Found 5 items"));
312		assert!(record.message.contains("[\"apple\", \"banana\", \"orange\"]"));
313		assert_eq!(record.level, LogLevel::Warn);
314	}
315
316	#[test]
317	fn test_all_log_levels_with_inline_syntax() {
318		let (sender, receiver) = unbounded();
319
320		with_mock_logger(sender, || {
321			let value = 42;
322
323			log_trace!("Trace: {value}");
324			log_debug!("Debug: {value}");
325			log_info!("Info: {value}");
326			log_warn!("Warn: {value}");
327			log_error!("Error: {value}");
328			log_critical!("Critical: {value}");
329		});
330
331		// Collect all logs
332		let mut logs = Vec::new();
333		while let Ok(record) = receiver.try_recv() {
334			logs.push(record);
335		}
336
337		assert_eq!(logs.len(), 6);
338
339		assert_eq!(logs[0].message, "Trace: 42");
340		assert_eq!(logs[0].level, LogLevel::Trace);
341
342		assert_eq!(logs[1].message, "Debug: 42");
343		assert_eq!(logs[1].level, LogLevel::Debug);
344
345		assert_eq!(logs[2].message, "Info: 42");
346		assert_eq!(logs[2].level, LogLevel::Info);
347
348		assert_eq!(logs[3].message, "Warn: 42");
349		assert_eq!(logs[3].level, LogLevel::Warn);
350
351		assert_eq!(logs[4].message, "Error: 42");
352		assert_eq!(logs[4].level, LogLevel::Error);
353
354		assert_eq!(logs[5].message, "Critical: 42");
355		assert_eq!(logs[5].level, LogLevel::Critical);
356	}
357
358	#[test]
359	fn test_comptokenize_inline_expressions() {
360		let (sender, receiver) = unbounded();
361
362		with_mock_logger(sender, || {
363			let numbers = vec![1, 2, 3, 4, 5];
364
365			log_info!("Sum: {}", numbers.iter().sum::<i32>());
366			log_info!("Sum inline: {sum}", sum = numbers.iter().sum::<i32>());
367		});
368
369		let log1 = receiver.try_recv().unwrap();
370		let log2 = receiver.try_recv().unwrap();
371
372		assert_eq!(log1.message, "Sum: 15");
373		assert_eq!(log2.message, "Sum inline: 15");
374	}
375
376	#[test]
377	fn test_escaped_braces() {
378		let (sender, receiver) = unbounded();
379
380		with_mock_logger(sender, || {
381			let value = 10;
382			log_debug!("The value {{in braces}} is {value}");
383		});
384
385		let record = receiver.try_recv().unwrap();
386		assert_eq!(record.message, "The value {in braces} is 10");
387	}
388
389	#[test]
390	fn test_empty_format_string() {
391		let (sender, receiver) = unbounded();
392
393		with_mock_logger(sender, || {
394			log_info!("");
395		});
396
397		let record = receiver.try_recv().unwrap();
398		assert_eq!(record.message, "");
399	}
400
401	#[test]
402	fn test_format_specifiers() {
403		let (sender, receiver) = unbounded();
404
405		with_mock_logger(sender, || {
406			let pi = 3.1456789;
407			let hex = 255;
408
409			// Traditional syntax with format specifiers
410			log_debug!("{:.2}", pi);
411			log_debug!("{:x}", hex);
412
413			// Inline syntax with format specifiers
414			log_debug!("{pi:.2}");
415			log_debug!("{hex:x}");
416		});
417
418		let log1 = receiver.try_recv().unwrap();
419		let log2 = receiver.try_recv().unwrap();
420		let log3 = receiver.try_recv().unwrap();
421		let log4 = receiver.try_recv().unwrap();
422
423		assert_eq!(log1.message, "3.15");
424		assert_eq!(log2.message, "ff");
425		assert_eq!(log3.message, "3.15");
426		assert_eq!(log4.message, "ff");
427	}
428
429	#[test]
430	fn test_multiline_strings() {
431		let (sender, receiver) = unbounded();
432
433		with_mock_logger(sender, || {
434			let error = "Connection failed";
435			log_error!("Error occurred:\n{error}\nPlease retry");
436		});
437
438		let record = receiver.try_recv().unwrap();
439		assert_eq!(record.message, "Error occurred:\nConnection failed\nPlease retry");
440	}
441
442	#[test]
443	fn test_raw_log_macro() {
444		let (sender, receiver) = unbounded();
445
446		with_mock_logger(sender, || {
447			let custom_value = "custom";
448
449			// Test the raw log! macro directly
450			log!(LogLevel::Info, "Raw log message");
451			log!(LogLevel::Warn, "Raw with value: {}", 123);
452			log!(LogLevel::Error, "Raw inline: {custom_value}");
453		});
454
455		let log1 = receiver.try_recv().unwrap();
456		let log2 = receiver.try_recv().unwrap();
457		let log3 = receiver.try_recv().unwrap();
458
459		assert_eq!(log1.message, "Raw log message");
460		assert_eq!(log1.level, LogLevel::Info);
461
462		assert_eq!(log2.message, "Raw with value: 123");
463		assert_eq!(log2.level, LogLevel::Warn);
464
465		assert_eq!(log3.message, "Raw inline: custom");
466		assert_eq!(log3.level, LogLevel::Error);
467	}
468
469	#[test]
470	fn test_logging_macros_compile() {
471		// These should compile without errors
472		// Note: they won't actually log anything since no logger is
473		// initialized
474
475		// Basic logging macros
476		log_trace!("This is a trace message");
477		log_debug!("This is a debug message");
478		log_info!("This is an info message");
479		log_warn!("This is a warning message");
480		log_error!("This is an error message");
481		log_critical!("This is a critical message");
482
483		// With format arguments
484		let value = 42;
485		log_info!("Value is: {}", value);
486		log_debug!("Debug value: {} and string: {}", value, "test");
487
488		// With structured fields using ReifyDB Values
489		// Both keys and values are converted via IntoValue trait
490		log_info!("User logged in", {
491		    "user_id" => 123i32,
492		    "username" => "alice",
493		    "active" => true
494		});
495
496		log_debug!("Processing request", {
497		    "request_id" => "abc-123",
498		    "method" => "GET",
499		    "path" => "/api/users",
500		    "status_code" => 200u16
501		});
502
503		log_error!("Database connection failed", {
504		    "error_code" => 500i32,
505		    "retry_count" => 3u8,
506		    "message" => "Connection timeout"
507		});
508
509		// Test with various ReifyDB value types
510		log_warn!("Performance warning", {
511		    "duration_ms" => 1500i64,
512		    "threshold_ms" => 1000i64,
513		    "exceeded_by" => 500i64
514		});
515
516		// Test with numeric keys (they become Values too)
517		log_debug!("Numeric keys test", {
518		    1 => "first",
519		    2 => "second",
520		    3 => "third"
521		});
522
523		// Test with boolean values
524		log_info!("Feature flags", {
525		    "feature_a" => true,
526		    "feature_b" => false,
527		    "feature_c" => true
528		});
529
530		// Test with optional values
531		let optional_value: Option<i32> = Some(42);
532		let none_value: Option<&str> = None;
533		log_debug!("Optional values", {
534		    "some_value" => optional_value,
535		    "none_value" => none_value
536		});
537	}
538
539	#[test]
540	fn test_logging_with_different_types() {
541		// Test that various Rust types can be used as both keys and
542		// values
543
544		// Integer types
545		log_info!("Integer types", {
546		    "i8" => 127i8,
547		    "i16" => 32767i16,
548		    "i32" => 2147483647i32,
549		    "i64" => 9223372036854775807i64,
550		    "u8" => 255u8,
551		    "u16" => 65535u16,
552		    "u32" => 4294967295u32,
553		    "u64" => 18446744073709551615u64
554		});
555
556		// String types
557		let owned_string = String::from("owned");
558		log_debug!("String types", {
559		    "literal" => "string literal",
560		    "owned" => owned_string.clone(),
561		    "reference" => owned_string.as_str()
562		});
563
564		// Mixed types as keys
565		log_trace!("Mixed key types", {
566		    "string_key" => "value1",
567		    123 => "numeric_key",
568		    true => "boolean_key"
569		});
570	}
571
572	#[test]
573	fn test_format_with_fields() {
574		let user = "alice";
575		let attempts = 3;
576
577		// Format message with fields - this should work but requires
578		// careful macro design For now, we can use simple message
579		// with fields
580		log_error!("Login failed", {
581		    "user" => user,
582		    "attempts" => attempts,
583		    "max_attempts" => 5
584		});
585	}
586}