Skip to main content

saf_core/logging/
mod.rs

1//! Structured debug logging system for SAF.
2//!
3//! Provides the `saf_log!` macro for emitting structured debug output
4//! in a DSL format designed for AI coding agents:
5//!
6//! ```text
7//! [module::phase][tag] narrative | key=value key=value
8//! ```
9//!
10//! Controlled at runtime via the `SAF_LOG` environment variable.
11//! No output when `SAF_LOG` is unset. Zero recompilation needed.
12//!
13//! # Usage
14//!
15//! ```ignore
16//! use saf_core::saf_log;
17//!
18//! // Full form: narrative + key-value pairs (use ; to separate)
19//! saf_log!(pta::solve, worklist, "pts grew"; val=node_id, delta=&added);
20//!
21//! // Narrative only
22//! saf_log!(pta::solve, convergence, "fixpoint reached");
23//!
24//! // Keys only (no narrative)
25//! saf_log!(pta::solve, stats; iter=12, worklist=342);
26//! ```
27
28pub mod formatter;
29pub mod registry;
30pub mod value;
31
32#[cfg(feature = "logging-subscriber")]
33pub mod subscriber;
34
35pub use value::{PtsDelta, SafLogValue, SafPair, SafRatio};
36
37/// Emit a structured SAF debug log event.
38///
39/// The macro validates `module::phase` at compile time via the registry,
40/// serializes key-value pairs via [`SafLogValue`], and emits a tracing event
41/// at `TRACE` level with target `"saf_debug"`.
42///
43/// # Forms
44///
45/// ```ignore
46/// // Full: narrative + key-values (semicolon separates narrative from keys)
47/// saf_log!(module::phase, tag, "narrative"; key=expr, key2=expr2);
48///
49/// // Narrative only (no key-values)
50/// saf_log!(module::phase, tag, "narrative");
51///
52/// // Keys only (no narrative)
53/// saf_log!(module::phase, tag; key=expr, key2=expr2);
54/// ```
55///
56/// The output DSL uses `|` as separator, but the macro uses `;` because
57/// Rust macro rules do not allow `|` after `expr` fragments.
58#[macro_export]
59macro_rules! saf_log {
60    // Form 1: narrative + key-value pairs
61    ($module:ident :: $phase:ident, $tag:ident, $narrative:expr; $($key:ident = $val:expr),+ $(,)?) => {{
62        // Compile-time validation: reference the registry struct
63        #[allow(unused, clippy::no_effect)]
64        const _: () = {
65            fn _validate() {
66                let _ = core::mem::size_of::<$crate::__saf_log_registry::$module::$phase>();
67            }
68        };
69
70        // Only do work if tracing would deliver the event
71        if tracing::event_enabled!(target: "saf_debug", tracing::Level::TRACE) {
72            use $crate::logging::value::SafLogValue as _;
73            let mut _kv_buf = String::new();
74            $(
75                if !_kv_buf.is_empty() {
76                    _kv_buf.push(' ');
77                }
78                _kv_buf.push_str(concat!(stringify!($key), "="));
79                ($val).fmt_saf_log(&mut _kv_buf);
80            )+
81
82            tracing::event!(
83                target: "saf_debug",
84                tracing::Level::TRACE,
85                saf_module = stringify!($module),
86                saf_phase = stringify!($phase),
87                saf_tag = stringify!($tag),
88                saf_narrative = $narrative,
89                saf_kv = _kv_buf.as_str(),
90            );
91        }
92    }};
93
94    // Form 2: narrative only (no key-value pairs)
95    ($module:ident :: $phase:ident, $tag:ident, $narrative:expr) => {{
96        #[allow(unused, clippy::no_effect)]
97        const _: () = {
98            fn _validate() {
99                let _ = core::mem::size_of::<$crate::__saf_log_registry::$module::$phase>();
100            }
101        };
102
103        if tracing::event_enabled!(target: "saf_debug", tracing::Level::TRACE) {
104            tracing::event!(
105                target: "saf_debug",
106                tracing::Level::TRACE,
107                saf_module = stringify!($module),
108                saf_phase = stringify!($phase),
109                saf_tag = stringify!($tag),
110                saf_narrative = $narrative,
111                saf_kv = "",
112            );
113        }
114    }};
115
116    // Form 3: keys only (no narrative)
117    ($module:ident :: $phase:ident, $tag:ident; $($key:ident = $val:expr),+ $(,)?) => {{
118        #[allow(unused, clippy::no_effect)]
119        const _: () = {
120            fn _validate() {
121                let _ = core::mem::size_of::<$crate::__saf_log_registry::$module::$phase>();
122            }
123        };
124
125        if tracing::event_enabled!(target: "saf_debug", tracing::Level::TRACE) {
126            use $crate::logging::value::SafLogValue as _;
127            let mut _kv_buf = String::new();
128            $(
129                if !_kv_buf.is_empty() {
130                    _kv_buf.push(' ');
131                }
132                _kv_buf.push_str(concat!(stringify!($key), "="));
133                ($val).fmt_saf_log(&mut _kv_buf);
134            )+
135
136            tracing::event!(
137                target: "saf_debug",
138                tracing::Level::TRACE,
139                saf_module = stringify!($module),
140                saf_phase = stringify!($phase),
141                saf_tag = stringify!($tag),
142                saf_narrative = "",
143                saf_kv = _kv_buf.as_str(),
144            );
145        }
146    }};
147}
148
149#[cfg(test)]
150mod macro_tests {
151    #[test]
152    fn test_saf_log_form2() {
153        crate::saf_log!(pta::solve, worklist, "test message");
154    }
155
156    #[test]
157    fn test_saf_log_form1() {
158        crate::saf_log!(pta::solve, worklist, "test"; val=42_u32);
159    }
160
161    #[test]
162    fn test_saf_log_form3() {
163        crate::saf_log!(pta::solve, worklist; val=42_u32);
164    }
165}