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