Skip to main content

vibe_ready/log/
logger_macro.rs

1use crate::log::log_level::LogLevel;
2use crate::log::logger::VibeLogger;
3use chrono::Local;
4use indexmap::IndexMap;
5use lazy_static::lazy_static;
6use serde_json::Value;
7use tokio::sync::RwLock;
8
9/// Prefix used by vibe-ready structured log tags.
10pub static LOG_PREFIX: &str = "V";
11
12/// Trait implemented by global log listener callbacks.
13pub trait LogListenerTrait: Fn(String, LogLevel, String, String) + Send + Sync {}
14/// Boxed global log listener used by the log macro pipeline.
15pub type DBLogListener = Box<dyn LogListenerTrait>;
16impl<T> LogListenerTrait for T where T: Fn(String, LogLevel, String, String) + Send + Sync {}
17
18/// Builds JSON log content from field names and values.
19///
20/// # Returns
21///
22/// A JSON object string when serialization succeeds, or a debug/error string
23/// when serialization fails.
24pub fn create_log_content(first_field: Option<&str>, values: Option<Vec<Value>>) -> String {
25    if first_field.is_none() && values.is_none() {
26        return "{}".to_string();
27    }
28
29    let first_field = first_field.unwrap_or_default();
30    let values = values.unwrap_or_default();
31
32    let keys: Vec<&str> = first_field.split('|').collect();
33
34    let mut map = IndexMap::new();
35
36    for (i, key) in keys.iter().enumerate() {
37        if i < values.len() {
38            map.insert(*key, values[i].clone());
39        }
40    }
41
42    match serde_json::to_string(&map) {
43        Ok(json_str) => json_str,
44        Err(_) => format!("{:?}", map),
45    }
46}
47
48/// Receives a structured log event from exported log macros.
49///
50/// # Returns
51///
52/// This function returns `()` after dispatching to the global listener and stdout.
53pub fn on_log(
54    location: String,
55    level: LogLevel,
56    ext: Option<String>,
57    tag: &str,
58    first_field: Option<&str>,
59    values: Option<Vec<Value>>,
60    suffix: &str,
61) {
62    let content = create_log_content(first_field, values);
63    let tag_data = match ext {
64        Some(ext) => format!("{}-{}-{}-{}", LOG_PREFIX, ext, tag, suffix),
65        None => format!("{}-{}-{}", LOG_PREFIX, tag, suffix),
66    };
67
68    match GLOBAL_DB_LOG_LISTENER.try_read() {
69        Ok(listener) => {
70            if let Some(on_log) = &*listener {
71                on_log(location.clone(), level, tag_data.clone(), content.clone());
72            }
73        }
74        Err(error) => {
75            println!(
76                "Error occurred while locking GLOBAL_LOG_LISTENER: {:?}",
77                error
78            );
79        }
80    }
81
82    let time = Local::now().format("%H:%M:%S%.3f");
83    println!("{} {} {:?} {}", time, tag_data.clone(), level, content);
84}
85
86#[doc(hidden)]
87#[macro_export]
88macro_rules! location {
89    () => {{
90        let location = std::panic::Location::caller();
91        let location_str = format!(
92            "{}:{}:{}",
93            location.file(),
94            location.line(),
95            location.column()
96        );
97        location_str
98    }};
99}
100
101#[doc(hidden)]
102#[macro_export]
103macro_rules! internal_log_db {
104    ($level:expr, $suffix:expr, $tag:expr, $first_field:expr, $( $value:expr ),*) => {
105        let values = vec![$( $crate::__serde_json::json!($value) ),*];
106        let location = std::panic::Location::caller();
107        let location = format!(
108            "{}:{}:{}",
109            location.file(),
110            location.line(),
111            location.column()
112        );
113        $crate::__vibe_internal_log_on_log(location.clone(), $level, None, $tag, Some($first_field), Some(values), $suffix)
114    };
115
116    ($level:expr, $suffix:expr, $tag:expr, $( $value:expr ),*) => {
117        let values = vec![$( $crate::__serde_json::json!($value) ),*];
118        let location = std::panic::Location::caller();
119        let location = format!(
120            "{}:{}:{}",
121            location.file(),
122            location.line(),
123            location.column()
124        );
125        $crate::__vibe_internal_log_on_log(location.clone(), $level, None, $tag, None, None, $suffix)
126    };
127
128    ($level:expr, $suffix:expr, $tag:expr) => {
129        let location = std::panic::Location::caller();
130        let location = format!(
131            "{}:{}:{}",
132            location.file(),
133            location.line(),
134            location.column()
135        );
136        $crate::__vibe_internal_log_on_log(location.clone(), $level, None, $tag, None, None, $suffix)
137    };
138}
139
140#[doc(hidden)]
141#[macro_export]
142macro_rules! internal_log_db_ext {
143    ($ext:expr, $level:expr, $suffix:expr, $tag:expr, $first_field:expr, $( $value:expr ),*) => {
144        let values = vec![$( $crate::__serde_json::json!($value) ),*];
145        let location = std::panic::Location::caller();
146        let location = format!(
147            "{}:{}:{}",
148            location.file(),
149            location.line(),
150            location.column()
151        );
152        $crate::__vibe_internal_log_on_log(location.clone(), $level, $ext, $tag, Some($first_field), Some(values), $suffix)
153    };
154
155    ($ext:expr, $level:expr, $suffix:expr, $tag:expr, $( $value:expr ),*) => {
156        let values = vec![$( $crate::__serde_json::json!($value) ),*];
157        let location = std::panic::Location::caller();
158        let location = format!(
159            "{}:{}:{}",
160            location.file(),
161            location.line(),
162            location.column()
163        );
164        $crate::__vibe_internal_log_on_log(location.clone(), $level, $ext, $tag, None, None, $suffix)
165    };
166
167    ($ext:expr, $level:expr, $suffix:expr, $tag:expr) => {
168        let location = std::panic::Location::caller();
169        let location = format!(
170            "{}:{}:{}",
171            location.file(),
172            location.line(),
173            location.column()
174        );
175        $crate::__vibe_internal_log_on_log(location.clone(), $level, $ext, $tag, None, None, $suffix)
176    };
177}
178
179/// Emits an info-level structured log entry.
180///
181/// # Examples
182///
183/// ```
184/// vibe_ready::log_i!("startup", "status", "ready");
185/// ```
186#[macro_export]
187macro_rules! log_i {
188    ($tag:expr, $first_field:expr, $( $value:expr ),*) => {
189        $crate::internal_log_db!($crate::VibeLogLevel::Info, "I", $tag, $first_field, $( $value ),*)
190    };
191
192    ($tag:expr, $( $value:expr ),*) => {
193        $crate::internal_log_db!($crate::VibeLogLevel::Info, "I", $tag, $( $value ),*)
194    };
195
196    ($tag:expr) => {
197        $crate::internal_log_db!($crate::VibeLogLevel::Info, "I", $tag)
198    };
199}
200
201/// Emits an info-level structured log entry with an extension segment in the tag.
202///
203/// # Examples
204///
205/// ```
206/// vibe_ready::log_i_!(Some("host".to_string()), "startup", "status", "ready");
207/// ```
208#[macro_export]
209macro_rules! log_i_ {
210    ($ext:expr, $tag:expr, $first_field:expr, $( $value:expr ),*) => {
211        $crate::internal_log_db_ext!($ext, $crate::VibeLogLevel::Info, "I", $tag, $first_field, $( $value ),*)
212    };
213
214    ($ext:expr, $tag:expr, $( $value:expr ),*) => {
215        $crate::internal_log_db_ext!($ext, $crate::VibeLogLevel::Info, "I", $tag, $( $value ),*)
216    };
217
218    ($ext:expr, $tag:expr) => {
219        $crate::internal_log_db_ext!($ext, $crate::VibeLogLevel::Info, "I", $tag)
220    };
221}
222
223/// Emits an info-level trace-style structured log entry.
224#[macro_export]
225macro_rules! log_t {
226    ($tag:expr, $first_field:expr, $( $value:expr ),*) => {
227        $crate::internal_log_db!($crate::VibeLogLevel::Info, "T", $tag, $first_field, $( $value ),*)
228    };
229
230    ($tag:expr, $( $value:expr ),*) => {
231        $crate::internal_log_db!($crate::VibeLogLevel::Info, "T", $tag, $( $value ),*)
232    };
233
234    ($tag:expr) => {
235        $crate::internal_log_db!($crate::VibeLogLevel::Info, "T", $tag)
236    };
237}
238
239/// Emits an info-level trace-style structured log entry with an extension segment.
240#[macro_export]
241macro_rules! log_t_ {
242    ($ext:expr, $tag:expr, $first_field:expr, $( $value:expr ),*) => {
243        $crate::internal_log_db_ext!($ext, $crate::VibeLogLevel::Info, "T", $tag, $first_field, $( $value ),*)
244    };
245
246    ($ext:expr, $tag:expr, $( $value:expr ),*) => {
247        $crate::internal_log_db_ext!($ext, $crate::VibeLogLevel::Info, "T", $tag, $( $value ),*)
248    };
249
250    ($ext:expr, $tag:expr) => {
251        $crate::internal_log_db_ext!($ext, $crate::VibeLogLevel::Info, "T", $tag)
252    };
253}
254
255/// Emits a debug-level read-style structured log entry.
256#[macro_export]
257macro_rules! log_r {
258    ($tag:expr, $first_field:expr, $( $value:expr ),*) => {
259        $crate::internal_log_db!($crate::VibeLogLevel::Debug, "R", $tag, $first_field, $( $value ),*)
260    };
261
262    ($tag:expr, $( $value:expr ),*) => {
263        $crate::internal_log_db!($crate::VibeLogLevel::Debug, "R", $tag, $( $value ),*)
264    };
265
266    ($tag:expr) => {
267        $crate::internal_log_db!($crate::VibeLogLevel::Debug, "R", $tag)
268    };
269}
270
271/// Emits a debug-level read-style structured log entry with an extension segment.
272#[macro_export]
273macro_rules! log_r_ {
274    ($ext:expr, $tag:expr, $first_field:expr, $( $value:expr ),*) => {
275        $crate::internal_log_db_ext!($ext, $crate::VibeLogLevel::Debug, "R", $tag, $first_field, $( $value ),*)
276    };
277
278    ($ext:expr, $tag:expr, $( $value:expr ),*) => {
279        $crate::internal_log_db_ext!($ext, $crate::VibeLogLevel::Debug, "R", $tag, $( $value ),*)
280    };
281
282    ($ext:expr, $tag:expr) => {
283        $crate::internal_log_db_ext!($ext, $crate::VibeLogLevel::Debug, "R", $tag)
284    };
285}
286
287/// Emits a debug-level state-style structured log entry.
288#[macro_export]
289macro_rules! log_s {
290    ($tag:expr, $first_field:expr, $( $value:expr ),*) => {
291        $crate::internal_log_db!($crate::VibeLogLevel::Debug, "S", $tag, $first_field, $( $value ),*)
292    };
293
294    ($tag:expr, $( $value:expr ),*) => {
295        $crate::internal_log_db!($crate::VibeLogLevel::Debug, "S", $tag, $( $value ),*)
296    };
297
298    ($tag:expr) => {
299        $crate::internal_log_db!($crate::VibeLogLevel::Debug, "S", $tag)
300    };
301}
302
303/// Emits a debug-level state-style structured log entry with an extension segment.
304#[macro_export]
305macro_rules! log_s_ {
306    ($ext:expr, $tag:expr, $first_field:expr, $( $value:expr ),*) => {
307        $crate::internal_log_db_ext!($ext, $crate::VibeLogLevel::Debug, "S", $tag, $first_field, $( $value ),*)
308    };
309
310    ($ext:expr, $tag:expr, $( $value:expr ),*) => {
311        $crate::internal_log_db_ext!($ext, $crate::VibeLogLevel::Debug, "S", $tag, $( $value ),*)
312    };
313
314    ($ext:expr, $tag:expr) => {
315        $crate::internal_log_db_ext!($ext, $crate::VibeLogLevel::Debug, "S", $tag)
316    };
317}
318
319/// Emits an error-level structured log entry.
320///
321/// # Examples
322///
323/// ```
324/// vibe_ready::log_e!("startup", "reason", "failed");
325/// ```
326#[macro_export]
327macro_rules! log_e {
328    ($tag:expr, $first_field:expr, $( $value:expr ),*) => {
329        $crate::internal_log_db!($crate::VibeLogLevel::Error, "E", $tag, $first_field, $( $value ),*)
330    };
331
332    ($tag:expr, $( $value:expr ),*) => {
333        $crate::internal_log_db!($crate::VibeLogLevel::Error, "E", $tag, $( $value ),*)
334    };
335
336    ($tag:expr) => {
337        $crate::internal_log_db!($crate::VibeLogLevel::Error, "E", $tag)
338    };
339}
340
341/// Emits an error-level structured log entry with an extension segment.
342#[macro_export]
343macro_rules! log_e_ {
344    ($ext:expr, $tag:expr, $first_field:expr, $( $value:expr ),*) => {
345        $crate::internal_log_db_ext!($ext, $crate::VibeLogLevel::Error, "E", $tag, $first_field, $( $value ),*)
346    };
347
348    ($ext:expr, $tag:expr, $( $value:expr ),*) => {
349        $crate::internal_log_db_ext!($ext, $crate::VibeLogLevel::Error, "E", $tag, $( $value ),*)
350    };
351
352    ($ext:expr, $tag:expr) => {
353        $crate::internal_log_db_ext!($ext, $crate::VibeLogLevel::Error, "E", $tag)
354    };
355}
356
357/// Converts an array-like value to a JSON string.
358///
359/// # Examples
360///
361/// ```
362/// let values = vec![1, 2, 3];
363/// let json = vibe_ready::array_to_json_string!(values);
364/// assert_eq!(json, "[1,2,3]");
365/// ```
366#[macro_export]
367macro_rules! array_to_json_string {
368    ($vec:expr) => {{
369        let cloned_vec = $vec.clone();
370
371        match $crate::__serde_json::to_string(&cloned_vec) {
372            Ok(json) => json,
373            Err(e) => format!("JSON serialization failed: {}", e),
374        }
375    }};
376}
377
378/// Converts displayable objects to a JSON-array-like string using `Display`.
379#[macro_export]
380macro_rules! obj_array_to_json_string {
381    ($vec:expr) => {{
382        format!(
383            "[{}]",
384            $vec.iter()
385                .map(|x| x.to_string())
386                .collect::<Vec<_>>()
387                .join(", ")
388        )
389    }};
390}
391
392/// Converts a string-keyed map of basic serializable values to JSON.
393#[macro_export]
394macro_rules! basic_type_map_to_json_string {
395    ($map:expr) => {{
396        use std::collections::HashMap;
397
398        let cloned_map: HashMap<String, _> = $map.clone();
399
400        (|| -> String {
401            match $crate::__serde_json::to_string(&cloned_map) {
402                Ok(json) => json,
403                Err(e) => format!("JSON serialization failed: {}", e),
404            }
405        })()
406    }};
407}
408
409/// Implements `Display` for serializable types by rendering JSON.
410#[macro_export]
411macro_rules! impl_display_json {
412    ($($struct:ty),*) => {
413        $(
414            impl std::fmt::Display for $struct {
415                fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
416                    match $crate::__serde_json::to_string(self) {
417                        Ok(json) => write!(f, "{}", json),
418                        Err(e) => write!(f, "Serialization error: {}", e),
419                    }
420                }
421            }
422        )*
423    };
424}
425
426/// Logs an error with a backtrace and returns the same error expression.
427///
428/// # Examples
429///
430/// ```
431/// use vibe_ready::{err, VibeEngineError, VibeErrorCode};
432///
433/// let error = err!(VibeEngineError::from_error_code(VibeErrorCode::RuntimeError));
434/// assert_eq!(error.code(), VibeErrorCode::RuntimeError.code());
435/// ```
436#[macro_export]
437macro_rules! err {
438    ($err:expr) => {{
439        log::error!(
440            "Error: {:?}, {:?}",
441            $err,
442            std::backtrace::Backtrace::capture()
443        );
444        $err
445    }};
446}
447
448lazy_static! {
449    static ref GLOBAL_DB_LOG_LISTENER: RwLock<Option<DBLogListener>> = RwLock::new(None);
450}
451
452impl VibeLogger {
453    /// Registers or clears the global listener used by exported log macros.
454    ///
455    /// # Returns
456    ///
457    /// This method returns `()` after updating the listener when the lock is available.
458    pub fn register_log_listener<T: LogListenerTrait + 'static>(listener: Option<T>) {
459        if let Ok(mut guard) = GLOBAL_DB_LOG_LISTENER.try_write() {
460            match listener {
461                None => *guard = None,
462                Some(listener) => *guard = Some(Box::new(listener)),
463            }
464        }
465    }
466
467    /// Clears the global listener used by exported log macros.
468    ///
469    /// # Returns
470    ///
471    /// This method returns `()` after clearing the listener when the lock is available.
472    pub fn clear_global_log_listener() {
473        if let Ok(mut guard) = GLOBAL_DB_LOG_LISTENER.try_write() {
474            *guard = None;
475        }
476    }
477}
478
479#[cfg(test)]
480mod strict_tests {
481    use super::*;
482    include!(concat!(
483        env!("CARGO_MANIFEST_DIR"),
484        "/test/unit/log/logger_macro_tests.rs"
485    ));
486}