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}