log_args_runtime/
lib.rs

1//! log-args-runtime
2//!
3//! Runtime support for the `log_args` procedural macros. This crate provides:
4//! - Context storage and helpers to push/pop context across sync and async boundaries
5//! - Logging macros (`info!`, `warn!`, `error!`, `debug!`, `trace!`) that automatically
6//!   merge inherited context into your events
7//! - `log_with_context!` which enriches an underlying `tracing` macro
8//!
9//! Feature flags
10//! - `with_context` (off by default): When enabled, the runtime includes a `context` field
11//!   (debug-formatted map) in each log when there is context available. Configure your
12//!   `tracing-subscriber` JSON formatter with `.flatten_event(true)` to surface the fields
13//!   at the top level in JSON output.
14//!
15//! Quick start
16//! ```no_run
17//! use tracing::{info, Level};
18//!
19//! fn init() {
20//!     tracing_subscriber::fmt().json().flatten_event(true).with_max_level(Level::DEBUG).init();
21//! }
22//!
23//! fn main() {
24//!     init();
25//!     log_args_runtime::info!("hello");
26//! }
27//! ```
28//!
29use std::cell::RefCell;
30use std::collections::HashMap;
31use std::sync::{Arc, Mutex};
32
33// Feature gate evaluated in this crate, not at the macro callsite.
34// Downstream crates won't see unexpected cfg values.
35pub const WITH_CONTEXT_ENABLED: bool = cfg!(feature = "with_context");
36
37// Global context store for cross-boundary persistence
38static GLOBAL_CONTEXT: std::sync::LazyLock<Arc<Mutex<HashMap<String, String>>>> =
39    std::sync::LazyLock::new(|| Arc::new(Mutex::new(HashMap::new())));
40
41/// Set global context that persists across all boundaries
42pub fn set_global_context(key: &str, value: &str) {
43    if let Ok(mut global) = GLOBAL_CONTEXT.lock() {
44        global.insert(key.to_string(), value.to_string());
45    }
46}
47
48/// Get global context for cross-boundary persistence
49pub fn get_global_context() -> Option<HashMap<String, String>> {
50    if let Ok(global) = GLOBAL_CONTEXT.lock() {
51        if !global.is_empty() {
52            return Some(global.clone());
53        }
54    }
55    None
56}
57
58// Thread-local storage for context stacks
59thread_local! {
60    static CONTEXT_STACK: RefCell<Vec<HashMap<String, String>>> = RefCell::new(Vec::new());
61    static ASYNC_CONTEXT_STACK: RefCell<Vec<HashMap<String, String>>> = RefCell::new(Vec::new());
62}
63
64/// Guard for synchronous context that automatically pops on drop
65#[doc(hidden)]
66pub struct ContextGuard;
67
68impl Drop for ContextGuard {
69    fn drop(&mut self) {
70        CONTEXT_STACK.with(|stack| {
71            stack.borrow_mut().pop();
72        });
73    }
74}
75
76// Function to get a context value from the current span context
77pub fn get_context_value(key: &str) -> Option<String> {
78    // First, try async context stack
79    if let Ok(stack) = ASYNC_CONTEXT_STACK.try_with(|stack| stack.borrow().clone()) {
80        for context_map in stack.iter().rev() {
81            if let Some(value) = context_map.get(key) {
82                return Some(value.clone());
83            }
84        }
85    }
86
87    // Then try sync context stack
88    let result = CONTEXT_STACK.with(|stack| {
89        let stack = stack.borrow();
90        for context_map in stack.iter().rev() {
91            if let Some(value) = context_map.get(key) {
92                return Some(value.clone());
93            }
94        }
95        None
96    });
97
98    if result.is_some() {
99        return result;
100    }
101
102    // Finally, try global context store for cross-boundary persistence
103    if let Ok(global) = GLOBAL_CONTEXT.lock() {
104        if let Some(value) = global.get(key) {
105            return Some(value.clone());
106        }
107    }
108
109    None
110}
111
112/// Get current synchronous context
113#[doc(hidden)]
114pub fn get_context() -> HashMap<String, String> {
115    CONTEXT_STACK.with(|stack| {
116        stack
117            .borrow()
118            .iter()
119            .fold(HashMap::new(), |mut acc, context| {
120                acc.extend(context.clone());
121                acc
122            })
123    })
124}
125
126#[doc(hidden)]
127pub fn get_async_context() -> HashMap<String, String> {
128    ASYNC_CONTEXT_STACK
129        .try_with(|stack| {
130            stack
131                .borrow()
132                .iter()
133                .fold(HashMap::new(), |mut acc, context| {
134                    acc.extend(context.clone());
135                    acc
136                })
137        })
138        .unwrap_or_default()
139}
140
141#[doc(hidden)]
142pub fn get_current_async_stack() -> Vec<HashMap<String, String>> {
143    ASYNC_CONTEXT_STACK
144        .try_with(|stack| stack.borrow().clone())
145        .unwrap_or_else(|_| vec![HashMap::new()])
146}
147
148/// Push context for synchronous functions with span
149#[doc(hidden)]
150pub fn push_context(context: HashMap<String, String>) -> ContextGuard {
151    CONTEXT_STACK.with(|stack| {
152        stack.borrow_mut().push(context);
153    });
154    ContextGuard
155}
156
157/// Push context for asynchronous functions with span
158#[doc(hidden)]
159pub fn push_async_context(context: HashMap<String, String>) -> AsyncContextGuard {
160    ASYNC_CONTEXT_STACK.with(|stack| {
161        stack.borrow_mut().push(context);
162    });
163    AsyncContextGuard
164}
165
166/// Guard for async context that automatically pops on drop
167pub struct AsyncContextGuard;
168
169impl Drop for AsyncContextGuard {
170    fn drop(&mut self) {
171        ASYNC_CONTEXT_STACK.with(|stack| {
172            stack.borrow_mut().pop();
173        });
174    }
175}
176
177// Helper macro to dynamically add context fields to log statements
178// This macro is now completely dynamic with no hardcoded field names
179#[macro_export]
180macro_rules! add_context_fields {
181    ($log_macro:path, $ctx:expr, $($args:tt)*) => {
182        // Completely dynamic approach - no hardcoded field names
183        // Create field tokens for all context fields dynamically
184        let mut field_tokens = Vec::new();
185
186        // Add all context fields dynamically without hardcoding any field names
187        for (key, value) in $ctx.iter() {
188            // Create a field token for any field name
189            let field_token = if key.contains('.') {
190                // Handle dotted field names (like "user.id")
191                format!("\"{key}\" = %{value}", key = key, value = value)
192            } else {
193                // Handle regular field names
194                format!("{key} = %{value}", key = key, value = value)
195            };
196            field_tokens.push(field_token);
197        }
198
199        // Note: This approach still has Rust macro limitations
200        // The field tokens can't be directly injected into the macro call
201        // This is kept for potential future use or alternative implementations
202    };
203}
204
205#[macro_export]
206macro_rules! log_with_context {
207    ($log_macro:path, $context:expr, $($args:tt)*) => {
208        {
209            let ctx = $context;
210            // Avoid cfg in macro body; use a const from this crate instead.
211            if !$crate::WITH_CONTEXT_ENABLED {
212                $log_macro!($($args)*);
213            } else {
214                // Pass the context map as a debug-formatted field.
215                // The tracing-subscriber can be configured to flatten this.
216                $log_macro!(context = ?ctx, $($args)*);
217            }
218        }
219    };
220}
221
222/// Global context-aware logging macros that inherit parent context
223/// These can be used in any function to automatically include context from parent functions with span
224#[macro_export]
225macro_rules! info {
226    ($($t:tt)*) => {
227        $crate::log_with_context!(::tracing::info, $crate::get_context(), $($t)*)
228    };
229}
230
231#[macro_export]
232macro_rules! warn {
233    ($($t:tt)*) => {
234        $crate::log_with_context!(::tracing::warn, $crate::get_context(), $($t)*)
235    };
236}
237
238#[macro_export]
239macro_rules! error {
240    ($($t:tt)*) => {
241        $crate::log_with_context!(::tracing::error, $crate::get_context(), $($t)*)
242    };
243}
244
245#[macro_export]
246macro_rules! debug {
247    ($($t:tt)*) => {
248        $crate::log_with_context!(::tracing::debug, $crate::get_context(), $($t)*)
249    };
250}
251
252#[macro_export]
253macro_rules! trace {
254    ($($t:tt)*) => {
255        $crate::log_with_context!(::tracing::trace, $crate::get_context(), $($t)*)
256    };
257}
258
259/// Automatically capture and preserve current context for function execution
260/// This ensures context is maintained across function boundaries without user intervention
261pub fn auto_capture_context() -> ContextGuard {
262    let current_context = get_context();
263
264    // Push to both async and sync stacks to ensure maximum compatibility
265    let _async_guard = push_async_context(current_context.clone());
266    let _sync_guard = push_context(current_context);
267
268    // Return the existing ContextGuard (empty struct)
269    ContextGuard
270}
271
272/// Capture current context and store it globally for cross-boundary persistence
273/// This function is automatically called by the macro to ensure context is preserved
274pub fn capture_context() -> ContextGuard {
275    // Merge async and sync contexts so we don't lose fields pushed to the async stack
276    let mut current_context = get_async_context();
277    let sync_ctx = get_context();
278    current_context.extend(sync_ctx);
279
280    // Store each context field globally for cross-boundary access
281    for (key, value) in &current_context {
282        set_global_context(key, value);
283    }
284
285    // Also push to context stacks for immediate access after capture
286    let _async_guard = push_async_context(current_context.clone());
287    let _sync_guard = push_context(current_context);
288
289    // Return the existing ContextGuard (empty struct)
290    ContextGuard
291}
292
293/// Get inherited context as a formatted string for automatic span propagation
294/// This function retrieves all context fields from the current span context
295/// and formats them as a string for logging
296pub fn get_inherited_context_string() -> String {
297    let mut context_parts = Vec::new();
298
299    // First, try to get context from tracing span (most reliable for cross-boundary propagation)
300    let current_span = tracing::Span::current();
301    if !current_span.is_none() {
302        // Try to extract fields from the current span
303        // This works across async boundaries when spans are properly propagated
304        // Note: Direct span field extraction is complex, so we rely on other methods
305    }
306
307    // Try async context stack (most likely to have the context)
308    if let Ok(stack) = ASYNC_CONTEXT_STACK.try_with(|stack| stack.borrow().clone()) {
309        // Search through all contexts in the stack, not just the most recent
310        for context_map in stack.iter().rev() {
311            for (key, value) in context_map {
312                // Skip function name to avoid duplication
313                if key != "function"
314                    && !context_parts
315                        .iter()
316                        .any(|p: &String| p.starts_with(&format!("{key}=")))
317                {
318                    context_parts.push(format!("{key}={value}"));
319                }
320            }
321        }
322    }
323
324    // Also try sync context stack and merge results
325    CONTEXT_STACK.with(|stack| {
326        let stack = stack.borrow();
327        for context_map in stack.iter().rev() {
328            for (key, value) in context_map {
329                // Skip function name and avoid duplicates
330                if key != "function"
331                    && !context_parts
332                        .iter()
333                        .any(|p: &String| p.starts_with(&format!("{key}=")))
334                {
335                    context_parts.push(format!("{key}={value}"));
336                }
337            }
338        }
339    });
340
341    // If still no context, try global context store (for cross-boundary persistence)
342    if context_parts.is_empty() {
343        if let Some(global_context) = get_global_context() {
344            for (key, value) in global_context {
345                if key != "function" {
346                    context_parts.push(format!("{key}={value}"));
347                }
348            }
349        }
350    }
351
352    if context_parts.is_empty() {
353        "".to_string()
354    } else {
355        context_parts.join(",")
356    }
357}
358
359/// Get inherited context fields as individual key-value pairs
360/// This function returns a HashMap of inherited context fields for dynamic field injection
361pub fn get_inherited_fields_map() -> std::collections::HashMap<String, String> {
362    let mut context_map = std::collections::HashMap::new();
363
364    // Try async context stack first
365    if let Ok(stack) = ASYNC_CONTEXT_STACK.try_with(|stack| stack.borrow().clone()) {
366        for stack_context in stack.iter().rev() {
367            for (key, value) in stack_context {
368                // Skip function name to avoid duplication
369                if key != "function" {
370                    context_map.insert(key.clone(), value.clone());
371                }
372            }
373            if !context_map.is_empty() {
374                return context_map; // Use the most recent context
375            }
376        }
377    }
378
379    // If no async context, try sync context stack
380    if context_map.is_empty() {
381        CONTEXT_STACK.with(|stack| {
382            let stack = stack.borrow();
383            for stack_context in stack.iter().rev() {
384                for (key, value) in stack_context {
385                    // Skip function name to avoid duplication
386                    if key != "function" {
387                        context_map.insert(key.clone(), value.clone());
388                    }
389                }
390                if !context_map.is_empty() {
391                    return; // Use the most recent context
392                }
393            }
394        });
395    }
396
397    context_map
398}