Skip to main content

context_logger/scope/
mod.rs

1//! A current logging context guard.
2
3use std::{borrow::Cow, marker::PhantomData};
4
5use self::stack::{SCOPE_STACK, ScopeStack};
6use crate::{LogContext, LogValue};
7
8pub mod stack;
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_local_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_local_record("request_id", "req-123")
56    ///         .with_local_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));
70        Self {
71            _marker: PhantomData,
72        }
73    }
74
75    /// Enters the given context, runs a closure, and exits the scope automatically.
76    ///
77    /// This is a convenience method for short synchronous sections where context
78    /// should be active only during closure execution.
79    ///
80    /// # Examples
81    ///
82    /// ```
83    /// use context_logger::{LogContext, LogScope};
84    ///
85    /// let context = LogContext::new().with_local_record("request_id", "req-123");
86    /// let result = LogScope::in_scope(
87    ///     context,
88    ///     || 40 + 2,
89    /// );
90    ///
91    /// assert_eq!(result, 42);
92    /// ```
93    pub fn in_scope<R>(context: LogContext, f: impl FnOnce() -> R) -> R {
94        let _guard = Self::enter(context);
95        f()
96    }
97
98    /// Adds a record to the currently active scope.
99    ///
100    /// This is useful for adding records dynamically without having
101    /// direct access to the current scope.
102    ///
103    /// # Note
104    ///
105    /// If there is no active context, this operation will have no effect.
106    ///
107    /// # Ordering
108    ///
109    /// The order in which records appear in log output is **not guaranteed**.
110    /// Do not rely on any specific ordering of keys.
111    ///
112    /// # Examples
113    ///
114    /// ```
115    /// use context_logger::{LogContext, LogScope};
116    /// use log::info;
117    ///
118    /// fn process_request() {
119    ///     // Add a record to the current scope dynamically
120    ///     LogScope::add_record("processing_time_ms", 42);
121    ///     info!("Request processed");
122    /// }
123    ///
124    /// let _guard = LogScope::enter(LogContext::new()
125    ///     .with_local_record("request_id", "req-123"));
126    ///
127    /// process_request(); // Will log with both request_id and processing_time_ms
128    /// ```
129    pub fn add_record(key: impl Into<Cow<'static, str>>, value: impl Into<LogValue>) {
130        SCOPE_STACK.with(|stack| {
131            if let Some(mut top) = stack.top_mut() {
132                top.0.local.insert(key, value);
133            }
134        });
135    }
136
137    /// Extracts the currently active logging context.
138    ///
139    /// This is useful for propagating context when spawning new threads or async tasks,
140    /// allowing child tasks to inherit logging information from the current scope.
141    ///
142    /// # Example
143    ///
144    /// ```no_run
145    #[doc = include_str!("../../examples/current_context.rs")]
146    /// ```
147    ///
148    /// # Notes
149    ///
150    /// - Returns an empty context if there is no active scope.
151    /// - The returned [`LogContext`] is a clone of the active context, so it's safe to move into spawned tasks.
152    #[must_use]
153    pub fn current_context() -> LogContext {
154        SCOPE_STACK
155            .with(|stack| stack.top().map(|frame| frame.clone().into()))
156            .unwrap_or_default()
157    }
158
159    pub(crate) fn exit(self) -> LogContext {
160        // We need to prevent the destructor from being called
161        // because we're manually managing the context stack here.
162        std::mem::forget(self);
163
164        let frame = SCOPE_STACK
165            .with(ScopeStack::pop)
166            .expect("bug in LogScope::exit: expected a scope frame to exist when popping on exit");
167        frame.into()
168    }
169}
170
171impl Drop for LogScope {
172    fn drop(&mut self) {
173        SCOPE_STACK.with(ScopeStack::pop);
174    }
175}
176
177/// Extension trait for [`LogContext`] to run code within a temporary logging scope.
178///
179/// This trait provides ergonomic, method-style access to [`LogScope::in_scope`].
180pub trait LogContextExt: Sized + crate::private::Sealed {
181    /// Enters this context, runs a closure, and exits the scope automatically.
182    ///
183    /// This is equivalent to calling [`LogScope::in_scope`] with `self`.
184    ///
185    /// # Examples
186    ///
187    /// ```
188    /// use context_logger::{LogContext, LogContextExt as _};
189    ///
190    /// let result = LogContext::new()
191    ///     .with_local_record("request_id", "req-123")
192    ///     .in_scope(|| 40 + 2);
193    ///
194    /// assert_eq!(result, 42);
195    /// ```
196    fn in_scope<R>(self, f: impl FnOnce() -> R) -> R;
197}
198
199impl LogContextExt for LogContext {
200    fn in_scope<R>(self, f: impl FnOnce() -> R) -> R {
201        LogScope::in_scope(self, f)
202    }
203}
204
205#[cfg(test)]
206mod tests {
207    use pretty_assertions::assert_eq;
208    use static_assertions::assert_not_impl_any;
209
210    use super::*;
211
212    // LogScope manages thread-local state and must never be Send.
213    assert_not_impl_any!(LogScope: Send);
214
215    #[test]
216    fn test_log_context_guard_enter() {
217        let context = LogContext::new().with_local_record("simple", 42);
218        // Make sure the context stack is empty before entering the context.
219        assert_eq!(SCOPE_STACK.with(ScopeStack::is_empty), true);
220
221        let guard = LogScope::enter(context);
222        // Check that the record was added to the top context.
223        assert_eq!(
224            SCOPE_STACK.with(|stack| stack.top().unwrap().records().count()),
225            1
226        );
227
228        // Check that the context stack is empty after dropping the guard.
229        drop(guard);
230        assert_eq!(SCOPE_STACK.with(ScopeStack::len), 0);
231    }
232
233    #[test]
234    fn test_log_context_nested_guards() {
235        let outer_context = LogContext::new().with_local_record("simple_record", "outer_value");
236        assert_eq!(SCOPE_STACK.with(ScopeStack::len), 0);
237
238        let outer_guard = LogScope::enter(outer_context);
239        assert_eq!(
240            SCOPE_STACK.with(|stack| stack.top().unwrap().records().count()),
241            1
242        );
243
244        SCOPE_STACK.with(|stack| {
245            let context = &stack.top().unwrap().0;
246            assert_eq!(
247                context.local.0.get("simple_record").unwrap().to_string(),
248                "outer_value"
249            );
250        });
251
252        let inner_context = LogContext::new().with_local_record("simple_record", "inner_value");
253        {
254            let inner_guard = LogScope::enter(inner_context);
255            // Test log context after inner guard is entered.
256            assert_eq!(SCOPE_STACK.with(ScopeStack::len), 2);
257            SCOPE_STACK.with(|stack| {
258                let frame = stack.top().unwrap();
259                assert_eq!(
260                    frame.0.local.find("simple_record").unwrap().to_string(),
261                    "inner_value"
262                );
263            });
264
265            drop(inner_guard);
266        }
267        // Test log context after inner guard is dropped.
268        assert_eq!(
269            SCOPE_STACK.with(|stack| stack.top().unwrap().records().count()),
270            1
271        );
272        SCOPE_STACK.with(|stack| {
273            let frame = stack.top().unwrap();
274            assert_eq!(
275                frame.0.local.find("simple_record").unwrap().to_string(),
276                "outer_value"
277            );
278        });
279
280        drop(outer_guard);
281        assert_eq!(SCOPE_STACK.with(ScopeStack::is_empty), true);
282    }
283
284    #[test]
285    fn test_log_context_multithread() {
286        let local_context = LogContext::new().with_local_record("simple_record", "main");
287        let local_guard = LogScope::enter(local_context);
288
289        let first_thread_handle = std::thread::spawn(|| {
290            let inner_context =
291                LogContext::new().with_local_record("simple_record", "first_thread");
292            let inner_guard = LogScope::enter(inner_context);
293
294            // Test log context after inner guard is entered.
295            assert_eq!(SCOPE_STACK.with(ScopeStack::len), 1);
296            SCOPE_STACK.with(|stack| {
297                let frame = stack.top().unwrap();
298                assert_eq!(
299                    frame.0.local.find("simple_record").unwrap().to_string(),
300                    "first_thread"
301                );
302            });
303
304            drop(inner_guard);
305        });
306        let second_thread_handle = std::thread::spawn(|| {
307            let inner_context =
308                LogContext::new().with_local_record("simple_record", "second_thread");
309            let inner_guard = LogScope::enter(inner_context);
310
311            // Test log context after inner guard is entered.
312            assert_eq!(SCOPE_STACK.with(ScopeStack::len), 1);
313            SCOPE_STACK.with(|stack| {
314                let frame = stack.top().unwrap();
315                assert_eq!(
316                    frame.0.local.find("simple_record").unwrap().to_string(),
317                    "second_thread"
318                );
319            });
320
321            drop(inner_guard);
322        });
323
324        first_thread_handle.join().unwrap();
325        second_thread_handle.join().unwrap();
326
327        SCOPE_STACK.with(|stack| {
328            let frame = stack.top().unwrap();
329            assert_eq!(frame.0.local["simple_record"].to_string(), "main");
330        });
331        drop(local_guard);
332    }
333
334    #[test]
335    fn test_current_context_empty_scope() {
336        let context = LogScope::current_context();
337        assert!(context.is_empty());
338    }
339
340    #[test]
341    fn test_current_context_with_scope() {
342        let context = LogContext::new().with_local_record("record", 42);
343        {
344            let _guard = LogScope::enter(context);
345
346            let current_context = LogScope::current_context();
347            assert_eq!(current_context.local["record"].to_string(), "42");
348        }
349
350        assert!(LogScope::current_context().is_empty());
351    }
352
353    #[test]
354    fn test_in_scope_enters_context_and_returns_result() {
355        assert!(SCOPE_STACK.with(ScopeStack::is_empty));
356
357        let result = LogScope::in_scope(LogContext::new().with_local_record("record", 42), || {
358            let current_context = LogScope::current_context();
359            assert_eq!(current_context.local["record"].to_string(), "42");
360
361            40 + 2
362        });
363
364        assert_eq!(result, 42);
365        assert!(SCOPE_STACK.with(ScopeStack::is_empty));
366    }
367
368    #[test]
369    fn test_log_context_ext_in_scope_enters_context_and_returns_result() {
370        assert!(SCOPE_STACK.with(ScopeStack::is_empty));
371
372        let result = LogContext::new()
373            .with_local_record("record", 42)
374            .in_scope(|| {
375                let current_context = LogScope::current_context();
376                assert_eq!(current_context.local["record"].to_string(), "42");
377
378                40 + 2
379            });
380
381        assert_eq!(result, 42);
382        assert!(SCOPE_STACK.with(ScopeStack::is_empty));
383    }
384
385    #[test]
386    fn test_log_context_inherited_records() {
387        LogContext::new()
388            .with_local_record("name", "Ann")
389            .with_inherited_record("tag", "42")
390            .with_inherited_record("target", "root")
391            .in_scope(|| {
392                let ctx = LogScope::current_context();
393
394                assert_eq!(ctx.local["name"].to_string(), "Ann");
395                assert_eq!(ctx.inherited["tag"].to_string(), "42");
396                assert_eq!(ctx.inherited["target"].to_string(), "root");
397
398                LogContext::new()
399                    .with_local_record("target", "nested")
400                    .in_scope(|| {
401                        let ctx = LogScope::current_context();
402
403                        assert_eq!(ctx.local["target"].to_string(), "nested");
404                        assert_eq!(ctx.inherited["tag"].to_string(), "42");
405                        assert!(ctx.local.find("name").is_none());
406                    });
407            });
408    }
409
410    // Edge case: panic in child scope doesn't break parent stack
411    #[test]
412    fn test_panic_in_child_scope_does_not_break_parent() {
413        // Push parent frame onto the stack
414        let outer_context = LogContext::new()
415            .with_inherited_record("outer", "val")
416            .with_local_record("outer_local", "ol");
417        {
418            let _parent_guard = LogScope::enter(outer_context);
419            // Verify parent is on the stack
420            assert_eq!(SCOPE_STACK.with(ScopeStack::len), 1);
421
422            // Panic in inner scope — the child guard's Drop must run
423            let result = std::panic::catch_unwind(|| {
424                LogContext::new().in_scope(|| panic!("inner panic"));
425            });
426
427            assert!(result.is_err());
428        }
429
430        // Stack must be clean: parent guard dropped + child guard's Drop ran
431        assert_eq!(SCOPE_STACK.with(ScopeStack::len), 0);
432    }
433
434    // Edge case: two siblings from one parent each get their own inherited copy
435    #[test]
436    fn test_sibling_scopes_get_independent_inherited_copies() {
437        let parent_ctx = LogContext::new()
438            .with_inherited_record("parent_key", "pv")
439            .with_local_record("parent_local", "pl");
440
441        {
442            let _g1 = LogScope::enter(parent_ctx);
443            assert_eq!(SCOPE_STACK.with(ScopeStack::len), 1);
444
445            // child1: inherits parent's `parent_key`, adds its own `sibling` and local
446            let child1_result = LogContext::new()
447                .with_inherited_record("sibling", "child1")
448                .with_local_record("only_in_child1", "c1")
449                .in_scope(|| {
450                    let c = LogScope::current_context();
451                    format!(
452                        "{}|{}",
453                        c.inherited["parent_key"], c.local["only_in_child1"]
454                    )
455                });
456            assert_eq!(child1_result, "pv|c1");
457
458            // after child1 scope ends, parent is still the only frame on the stack
459            assert_eq!(SCOPE_STACK.with(ScopeStack::len), 1);
460
461            // child2: also inherits parent's `parent_key`, but its own `sibling` wins
462            let c2_result = LogContext::new()
463                .with_inherited_record("sibling", "child2")
464                .with_local_record("only_in_child2", "c2")
465                .in_scope(|| {
466                    let c = LogScope::current_context();
467                    format!("{}|{}", c.inherited["parent_key"], c.inherited["sibling"])
468                });
469            assert_eq!(c2_result, "pv|child2");
470
471            // parent state unchanged after child2 scope ends
472            assert_eq!(SCOPE_STACK.with(ScopeStack::len), 1);
473        }
474
475        // After parent scope: stack is empty
476        assert_eq!(SCOPE_STACK.with(ScopeStack::len), 0);
477    }
478}