Skip to main content

context_logger/
scope.rs

1//! A current logging context guard.
2
3use std::{borrow::Cow, marker::PhantomData};
4
5use crate::{
6    LogContext, LogValue,
7    stack::{SCOPE_STACK, ScopeStack},
8};
9
10/// A guard that represents an active logging context on the current thread's scope stack.
11///
12/// When the guard is dropped, the context is automatically removed from the stack.
13/// Created by [`LogScope::enter`].
14///
15/// # Examples
16///
17/// ```
18/// use context_logger::{LogContext, LogScope};
19///
20/// // Create a context with some data
21/// let context = LogContext::new().with_record("user_id", 123);
22///
23/// // Enter the context (pushes to stack)
24/// let guard = LogScope::enter(context);
25///
26/// // Log operations here will have access to the context
27/// // ...
28///
29/// // When `guard` goes out of scope, the context is automatically removed
30/// ```
31#[non_exhaustive]
32#[derive(Debug)]
33pub struct LogScope {
34    // Make this guard non-Send: LogScope manages thread-local state
35    // and must not be transferred to another thread.
36    _marker: PhantomData<*mut ()>,
37}
38
39impl LogScope {
40    /// Pushes the given context onto the current thread's scope stack and returns a guard.
41    ///
42    /// The context remains active until the returned guard is dropped, at which point
43    /// it is automatically removed from the stack.
44    ///
45    /// # In Asynchronous Code
46    ///
47    /// *Warning:* in asynchronous code [`Self::enter`] should be used very carefully or avoided entirely.
48    /// Holding the drop guard across `.await` points will result in incorrect logs:
49    ///
50    /// ```rust
51    /// use context_logger::{LogContext, LogScope};
52    ///
53    /// async fn my_async_fn() {
54    ///     let ctx = LogContext::new()
55    ///         .with_record("request_id", "req-123")
56    ///         .with_record("user_id", 42);
57    ///     // WARNING: This context will remain active until this
58    ///     // guard is dropped...
59    ///     let _guard = LogScope::enter(ctx);
60    ///     // But this code causing the runtime to switch to another task,
61    ///     // while remaining in this context.
62    ///     tokio::task::yield_now().await;
63    /// }
64    /// ```
65    ///
66    /// Please use the [`crate::FutureExt::in_log_context`] instead.
67    #[must_use]
68    pub fn enter(context: LogContext) -> Self {
69        SCOPE_STACK.with(|stack| stack.push(context.frame));
70        Self {
71            _marker: PhantomData,
72        }
73    }
74
75    /// Adds a record to the currently active scope.
76    ///
77    /// This is useful for adding records dynamically without having
78    /// direct access to the current scope.
79    ///
80    /// # Note
81    ///
82    /// If there is no active context, this operation will have no effect.
83    ///
84    /// # Ordering
85    ///
86    /// The order in which records appear in log output is **not guaranteed**.
87    /// Do not rely on any specific ordering of keys.
88    ///
89    /// # Examples
90    ///
91    /// ```
92    /// use context_logger::{LogContext, LogScope};
93    /// use log::info;
94    ///
95    /// fn process_request() {
96    ///     // Add a record to the current scope dynamically
97    ///     LogScope::add_record("processing_time_ms", 42);
98    ///     info!("Request processed");
99    /// }
100    ///
101    /// let _guard = LogScope::enter(LogContext::new()
102    ///     .with_record("request_id", "req-123"));
103    ///
104    /// process_request(); // Will log with both request_id and processing_time_ms
105    /// ```
106    pub fn add_record(key: impl Into<Cow<'static, str>>, value: impl Into<LogValue>) {
107        SCOPE_STACK.with(|stack| {
108            if let Some(mut top) = stack.top_mut() {
109                let record = (key.into(), value.into());
110                top.push(record);
111            }
112        });
113    }
114
115    /// Extracts the currently active logging context.
116    ///
117    /// This is useful for propagating context when spawning new threads or async tasks,
118    /// allowing child tasks to inherit logging information from the current scope.
119    ///
120    /// # Example
121    ///
122    /// ```no_run
123    #[doc = include_str!("../examples/current_context.rs")]
124    /// ```
125    ///
126    /// # Notes
127    ///
128    /// - Returns an empty context if there is no active scope.
129    /// - The returned [`LogContext`] is a clone of the active context, so it's safe to move into spawned tasks.
130    #[must_use]
131    pub fn current_context() -> LogContext {
132        SCOPE_STACK
133            .with(|stack| {
134                stack.top().map(|frame| LogContext {
135                    frame: frame.clone(),
136                })
137            })
138            .unwrap_or_default()
139    }
140
141    pub(crate) fn exit(self) -> LogContext {
142        // We need to prevent the destructor from being called
143        // because we're manually managing the context stack here.
144        std::mem::forget(self);
145
146        let frame = SCOPE_STACK
147            .with(ScopeStack::pop)
148            .expect("bug in LogScope::exit: expected a scope frame to exist when popping on exit");
149        LogContext { frame }
150    }
151}
152
153impl Drop for LogScope {
154    fn drop(&mut self) {
155        SCOPE_STACK.with(ScopeStack::pop);
156    }
157}
158
159#[cfg(test)]
160mod tests {
161    use pretty_assertions::assert_eq;
162    use static_assertions::assert_not_impl_any;
163
164    use super::*;
165    use crate::stack::SCOPE_STACK;
166
167    // LogScope manages thread-local state and must never be Send.
168    assert_not_impl_any!(LogScope: Send);
169
170    #[test]
171    fn test_log_context_guard_enter() {
172        let context = LogContext::new().with_record("simple", 42);
173        // Make sure the context stack is empty before entering the context.
174        assert_eq!(SCOPE_STACK.with(ScopeStack::is_empty), true);
175
176        let guard = LogScope::enter(context);
177        // Check that the record was added to the top context.
178        assert_eq!(
179            SCOPE_STACK.with(|stack| stack.top().unwrap().records().len()),
180            1
181        );
182
183        // Check that the context stack is empty after dropping the guard.
184        drop(guard);
185        assert_eq!(SCOPE_STACK.with(ScopeStack::len), 0);
186    }
187
188    #[test]
189    fn test_log_context_nested_guards() {
190        let outer_context = LogContext::new().with_record("simple_record", "outer_value");
191        assert_eq!(SCOPE_STACK.with(ScopeStack::len), 0);
192
193        let outer_guard = LogScope::enter(outer_context);
194        assert_eq!(
195            SCOPE_STACK.with(|stack| stack.top().unwrap().records().len()),
196            1
197        );
198
199        SCOPE_STACK.with(|stack| {
200            let frame = stack.top().unwrap();
201            assert_eq!(
202                frame.find("simple_record").unwrap().to_string(),
203                "outer_value"
204            );
205        });
206
207        let inner_context = LogContext::new().with_record("simple_record", "inner_value");
208        {
209            let inner_guard = LogScope::enter(inner_context);
210            // Test log context after inner guard is entered.
211            assert_eq!(SCOPE_STACK.with(ScopeStack::len), 2);
212            SCOPE_STACK.with(|stack| {
213                let frame = stack.top().unwrap();
214                assert_eq!(
215                    frame.find("simple_record").unwrap().to_string(),
216                    "inner_value"
217                );
218            });
219
220            drop(inner_guard);
221        }
222        // Test log context after inner guard is dropped.
223        assert_eq!(
224            SCOPE_STACK.with(|stack| stack.top().unwrap().records().len()),
225            1
226        );
227        SCOPE_STACK.with(|stack| {
228            let frame = stack.top().unwrap();
229            assert_eq!(
230                frame.find("simple_record").unwrap().to_string(),
231                "outer_value"
232            );
233        });
234
235        drop(outer_guard);
236        assert_eq!(SCOPE_STACK.with(ScopeStack::is_empty), true);
237    }
238
239    #[test]
240    fn test_log_context_multithread() {
241        let local_context = LogContext::new().with_record("simple_record", "main");
242        let local_guard = LogScope::enter(local_context);
243
244        let first_thread_handle = std::thread::spawn(|| {
245            let inner_context = LogContext::new().with_record("simple_record", "first_thread");
246            let inner_guard = LogScope::enter(inner_context);
247
248            // Test log context after inner guard is entered.
249            assert_eq!(SCOPE_STACK.with(ScopeStack::len), 1);
250            SCOPE_STACK.with(|stack| {
251                let frame = stack.top().unwrap();
252                assert_eq!(
253                    frame.find("simple_record").unwrap().to_string(),
254                    "first_thread"
255                );
256            });
257
258            drop(inner_guard);
259        });
260        let second_thread_handle = std::thread::spawn(|| {
261            let inner_context = LogContext::new().with_record("simple_record", "second_thread");
262            let inner_guard = LogScope::enter(inner_context);
263
264            // Test log context after inner guard is entered.
265            assert_eq!(SCOPE_STACK.with(ScopeStack::len), 1);
266            SCOPE_STACK.with(|stack| {
267                let frame = stack.top().unwrap();
268                assert_eq!(
269                    frame.find("simple_record").unwrap().to_string(),
270                    "second_thread"
271                );
272            });
273
274            drop(inner_guard);
275        });
276
277        first_thread_handle.join().unwrap();
278        second_thread_handle.join().unwrap();
279
280        SCOPE_STACK.with(|stack| {
281            let frame = stack.top().unwrap();
282            assert_eq!(frame.find("simple_record").unwrap().to_string(), "main");
283        });
284        drop(local_guard);
285    }
286
287    #[test]
288    fn test_current_context_empty_scope() {
289        let context = LogScope::current_context();
290        assert!(context.frame.is_empty());
291    }
292
293    #[test]
294    fn test_current_context_with_scope() {
295        let context = LogContext::new().with_record("record", 42);
296        {
297            let _guard = LogScope::enter(context);
298
299            let current_context = LogScope::current_context();
300            assert_eq!(
301                current_context.frame.find("record").unwrap().to_string(),
302                "42"
303            );
304        }
305
306        assert!(LogScope::current_context().frame.is_empty());
307    }
308}