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 once_cell::sync::Lazy;
30use std::cell::RefCell;
31use std::collections::HashMap;
32use std::sync::{Arc, Mutex};
33
34// Feature gate evaluated in this crate, not at the macro callsite.
35// Downstream crates won't see unexpected cfg values.
36pub const WITH_CONTEXT_ENABLED: bool = cfg!(feature = "with_context");
37
38// Global context store for cross-boundary persistence
39static GLOBAL_CONTEXT: Lazy<Arc<Mutex<HashMap<String, String>>>> =
40    Lazy::new(|| Arc::new(Mutex::new(HashMap::new())));
41
42/// Set global context that persists across all boundaries
43pub fn set_global_context(key: &str, value: &str) {
44    if let Ok(mut global) = GLOBAL_CONTEXT.lock() {
45        global.insert(key.to_string(), value.to_string());
46    }
47}
48
49/// Get global context for cross-boundary persistence
50pub fn get_global_context() -> Option<HashMap<String, String>> {
51    if let Ok(global) = GLOBAL_CONTEXT.lock() {
52        if !global.is_empty() {
53            return Some(global.clone());
54        }
55    }
56    None
57}
58
59// Thread-local storage for context stacks
60thread_local! {
61    static CONTEXT_STACK: RefCell<Vec<HashMap<String, String>>> = const { RefCell::new(Vec::new()) };
62    static ASYNC_CONTEXT_STACK: RefCell<Vec<HashMap<String, String>>> = const { RefCell::new(Vec::new()) };
63}
64
65/// Guard for synchronous context that automatically pops on drop
66#[doc(hidden)]
67pub struct ContextGuard;
68
69impl Drop for ContextGuard {
70    fn drop(&mut self) {
71        CONTEXT_STACK.with(|stack| {
72            stack.borrow_mut().pop();
73        });
74    }
75}
76
77// Function to get a context value from the current span context
78pub fn get_context_value(key: &str) -> Option<String> {
79    // First, try async context stack
80    if let Ok(stack) = ASYNC_CONTEXT_STACK.try_with(|stack| stack.borrow().clone()) {
81        for context_map in stack.iter().rev() {
82            if let Some(value) = context_map.get(key) {
83                return Some(value.clone());
84            }
85        }
86    }
87
88    // Then try sync context stack
89    let result = CONTEXT_STACK.with(|stack| {
90        let stack = stack.borrow();
91        for context_map in stack.iter().rev() {
92            if let Some(value) = context_map.get(key) {
93                return Some(value.clone());
94            }
95        }
96        None
97    });
98
99    if result.is_some() {
100        return result;
101    }
102
103    // Finally, try global context store for cross-boundary persistence
104    if let Ok(global) = GLOBAL_CONTEXT.lock() {
105        if let Some(value) = global.get(key) {
106            return Some(value.clone());
107        }
108    }
109
110    None
111}
112
113/// Get current synchronous context
114#[doc(hidden)]
115pub fn get_context() -> HashMap<String, String> {
116    CONTEXT_STACK.with(|stack| {
117        stack
118            .borrow()
119            .iter()
120            .fold(HashMap::new(), |mut acc, context| {
121                acc.extend(context.clone());
122                acc
123            })
124    })
125}
126
127#[doc(hidden)]
128pub fn get_async_context() -> HashMap<String, String> {
129    ASYNC_CONTEXT_STACK
130        .try_with(|stack| {
131            stack
132                .borrow()
133                .iter()
134                .fold(HashMap::new(), |mut acc, context| {
135                    acc.extend(context.clone());
136                    acc
137                })
138        })
139        .unwrap_or_default()
140}
141
142#[doc(hidden)]
143pub fn get_current_async_stack() -> Vec<HashMap<String, String>> {
144    ASYNC_CONTEXT_STACK
145        .try_with(|stack| stack.borrow().clone())
146        .unwrap_or_else(|_| vec![HashMap::new()])
147}
148
149/// Push context for synchronous functions with span
150#[doc(hidden)]
151pub fn push_context(context: HashMap<String, String>) -> ContextGuard {
152    CONTEXT_STACK.with(|stack| {
153        stack.borrow_mut().push(context);
154    });
155    ContextGuard
156}
157
158/// Push context for asynchronous functions with span
159#[doc(hidden)]
160pub fn push_async_context(context: HashMap<String, String>) -> AsyncContextGuard {
161    ASYNC_CONTEXT_STACK.with(|stack| {
162        stack.borrow_mut().push(context);
163    });
164    AsyncContextGuard
165}
166
167/// Guard for async context that automatically pops on drop
168pub struct AsyncContextGuard;
169
170impl Drop for AsyncContextGuard {
171    fn drop(&mut self) {
172        ASYNC_CONTEXT_STACK.with(|stack| {
173            stack.borrow_mut().pop();
174        });
175    }
176}
177
178#[macro_export]
179macro_rules! log_with_context {
180    ($log_macro:path, $context:expr, $($args:tt)*) => {
181        {
182            let ctx = $context;
183            // Avoid cfg in macro body; use a const from this crate instead.
184            if !$crate::WITH_CONTEXT_ENABLED {
185                $log_macro!($($args)*);
186            } else {
187                // Pass the context map as a debug-formatted field.
188                // The tracing-subscriber can be configured to flatten this.
189                $log_macro!(context = ?ctx, $($args)*);
190            }
191        }
192    };
193}
194
195/// Global context-aware logging macros that inherit parent context
196/// These can be used in any function to automatically include context from parent functions with span
197#[macro_export]
198macro_rules! info {
199    ($($t:tt)*) => {
200        $crate::log_with_context!(::tracing::info, $crate::get_context(), $($t)*)
201    };
202}
203
204#[macro_export]
205macro_rules! warn {
206    ($($t:tt)*) => {
207        $crate::log_with_context!(::tracing::warn, $crate::get_context(), $($t)*)
208    };
209}
210
211#[macro_export]
212macro_rules! error {
213    ($($t:tt)*) => {
214        $crate::log_with_context!(::tracing::error, $crate::get_context(), $($t)*)
215    };
216}
217
218#[macro_export]
219macro_rules! debug {
220    ($($t:tt)*) => {
221        $crate::log_with_context!(::tracing::debug, $crate::get_context(), $($t)*)
222    };
223}
224
225#[macro_export]
226macro_rules! trace {
227    ($($t:tt)*) => {
228        $crate::log_with_context!(::tracing::trace, $crate::get_context(), $($t)*)
229    };
230}
231
232/// Automatically capture and preserve current context for function execution
233/// This ensures context is maintained across function boundaries without user intervention
234pub fn auto_capture_context() -> ContextGuard {
235    let current_context = get_context();
236
237    // Push to both async and sync stacks to ensure maximum compatibility
238    let _async_guard = push_async_context(current_context.clone());
239    let _sync_guard = push_context(current_context);
240
241    // Return the existing ContextGuard (empty struct)
242    ContextGuard
243}
244
245/// Capture current context and store it globally for cross-boundary persistence
246/// This function is automatically called by the macro to ensure context is preserved
247pub fn capture_context() -> ContextGuard {
248    // Merge async and sync contexts so we don't lose fields pushed to the async stack
249    let mut current_context = get_async_context();
250    let sync_ctx = get_context();
251    current_context.extend(sync_ctx);
252
253    // Store each context field globally for cross-boundary access
254    for (key, value) in &current_context {
255        set_global_context(key, value);
256    }
257
258    // Also push to context stacks for immediate access after capture
259    let _async_guard = push_async_context(current_context.clone());
260    let _sync_guard = push_context(current_context);
261
262    // Return the existing ContextGuard (empty struct)
263    ContextGuard
264}
265
266/// Get inherited context as a formatted string for automatic span propagation
267/// This function retrieves all context fields from the current span context
268/// and formats them as a string for logging
269pub fn get_inherited_context_string() -> String {
270    let mut context_parts = Vec::new();
271
272    // First, try to get context from tracing span (most reliable for cross-boundary propagation)
273    let current_span = tracing::Span::current();
274    if !current_span.is_none() {
275        // Try to extract fields from the current span
276        // This works across async boundaries when spans are properly propagated
277        // Note: Direct span field extraction is complex, so we rely on other methods
278    }
279
280    // Try async context stack (most likely to have the context)
281    if let Ok(stack) = ASYNC_CONTEXT_STACK.try_with(|stack| stack.borrow().clone()) {
282        fill_context_parts(&mut context_parts, &stack);
283    }
284
285    // Also try sync context stack and merge results
286    CONTEXT_STACK.with(|stack| {
287        let stack = stack.borrow();
288        fill_context_parts(&mut context_parts, &stack);
289    });
290
291    // If still no context, try global context store (for cross-boundary persistence)
292    if context_parts.is_empty() {
293        if let Some(global_context) = get_global_context() {
294            for (key, value) in global_context {
295                if key != "function" {
296                    context_parts.push(format!("{key}={value}"));
297                }
298            }
299        }
300    }
301
302    if context_parts.is_empty() {
303        "".to_string()
304    } else {
305        context_parts.join(",")
306    }
307}
308
309/// Get inherited context fields as individual key-value pairs
310/// This function returns a HashMap of inherited context fields for dynamic field injection
311pub fn get_inherited_fields_map() -> std::collections::HashMap<String, String> {
312    let mut context_map = std::collections::HashMap::new();
313
314    // Try async context stack first
315    if let Ok(stack) = ASYNC_CONTEXT_STACK.try_with(|stack| stack.borrow().clone()) {
316        for stack_context in stack.iter().rev() {
317            for (key, value) in stack_context {
318                // Skip function name to avoid duplication
319                if key != "function" {
320                    context_map.insert(key.clone(), value.clone());
321                }
322            }
323            if !context_map.is_empty() {
324                return context_map; // Use the most recent context
325            }
326        }
327    }
328
329    // If no async context, try sync context stack
330    if context_map.is_empty() {
331        CONTEXT_STACK.with(|stack| {
332            let stack = stack.borrow();
333            for stack_context in stack.iter().rev() {
334                for (key, value) in stack_context {
335                    // Skip function name to avoid duplication
336                    if key != "function" {
337                        context_map.insert(key.clone(), value.clone());
338                    }
339                }
340                if !context_map.is_empty() {
341                    return; // Use the most recent context
342                }
343            }
344        });
345    }
346
347    context_map
348}
349
350fn fill_context_parts(context_parts: &mut Vec<String>, stack: &[HashMap<String, String>]) {
351    for context_map in stack.iter().rev() {
352        for (key, value) in context_map {
353            // Skip function name to avoid duplication
354            if key != "function"
355                && !context_parts
356                    .iter()
357                    .any(|p: &String| p.starts_with(&format!("{key}=")))
358            {
359                context_parts.push(format!("{key}={value}"));
360            }
361        }
362    }
363}