Skip to main content

context_logger/
future.rs

1//! Future types.
2
3use std::task::Poll;
4
5use pin_project::pin_project;
6
7use crate::{LogContext, scope::LogScope};
8
9/// Extension trait for futures to propagate contextual logging information.
10///
11/// This trait adds ability to attach a [`LogContext`] for any [`Future`],
12/// ensuring that logs emitted during the future's execution will include
13/// the contextual properties even if the future is polled across different
14/// threads.
15pub trait FutureExt: Sized + crate::private::Sealed {
16    /// Attaches a log context to this future.
17    ///
18    /// The attached [context](LogContext) will be activated every time the
19    /// instrumented future is polled.
20    ///
21    /// # Examples
22    ///
23    /// ```
24    /// use context_logger::{LogContext, FutureExt};
25    /// use log::info;
26    ///
27    /// async fn process_user_data(user_id: u64) {
28    ///     // Create a context with user information
29    ///     let context = LogContext::new()
30    ///         .with_local_field("user_id", user_id)
31    ///         .with_local_field("operation", "process_data");
32    ///
33    ///     async {
34    ///         info!("Starting user data processing"); // Will include context
35    ///
36    ///         // Do some async work...
37    ///
38    ///         info!("User data processing complete"); // Still includes context
39    ///     }
40    ///     .in_log_context(context)
41    ///     .await;
42    /// }
43    /// ```
44    fn in_log_context(self, context: LogContext) -> LogContextFuture<Self>;
45}
46
47impl<F> FutureExt for F
48where
49    F: Future,
50{
51    fn in_log_context(self, context: LogContext) -> LogContextFuture<Self> {
52        LogContextFuture {
53            inner: self,
54            log_context: Some(context),
55        }
56    }
57}
58
59/// A future with an attached logging context.
60///
61/// This type is created by the [`FutureExt::in_log_context`].
62///
63/// # Note
64///
65/// If the wrapped future will panic, the next `poll` invocation will panic
66/// unconditionally.
67#[pin_project]
68#[derive(Debug)]
69pub struct LogContextFuture<F> {
70    #[pin]
71    inner: F,
72    log_context: Option<LogContext>,
73}
74
75impl<F> Future for LogContextFuture<F>
76where
77    F: Future,
78{
79    type Output = F::Output;
80
81    fn poll(self: std::pin::Pin<&mut Self>, cx: &mut std::task::Context<'_>) -> Poll<Self::Output> {
82        let this = self.project();
83
84        let log_context = this
85            .log_context
86            .take()
87            .expect("An attempt to poll panicked future");
88
89        let guard = LogScope::enter(log_context);
90        let result = this.inner.poll(cx);
91        this.log_context.replace(guard.exit());
92
93        result
94    }
95}
96
97#[cfg(test)]
98mod tests {
99    use std::panic::AssertUnwindSafe;
100
101    use futures_util::FutureExt as _;
102    use pretty_assertions::assert_eq;
103
104    use super::FutureExt;
105    use crate::{LogContext, LogValue, scope::stack::SCOPE_STACK};
106
107    fn find_local_value(key: &str) -> Option<String> {
108        SCOPE_STACK.with(|stack| {
109            let frame = stack.top()?;
110            frame.0.local.find(key).map(ToString::to_string)
111        })
112    }
113
114    async fn check_nested_different_contexts(answer: u32) {
115        let context = LogContext::new().with_local_field("answer", answer);
116
117        async {
118            tokio::task::yield_now().await;
119
120            async {
121                tokio::task::yield_now().await;
122                assert_eq!(find_local_value("answer"), Some("None".to_string()));
123            }
124            .in_log_context(LogContext::new().with_local_field("answer", LogValue::null()))
125            .await;
126
127            tokio::task::yield_now().await;
128            assert_eq!(find_local_value("answer"), Some(answer.to_string()));
129        }
130        .in_log_context(context)
131        .await;
132
133        assert_eq!(find_local_value("answer"), None);
134    }
135
136    #[tokio::test]
137    async fn test_future_with_context() {
138        let context = LogContext::new().with_local_field("answer", 42);
139
140        async {
141            tokio::task::yield_now().await;
142            assert_eq!(find_local_value("answer"), Some("42".to_string()));
143        }
144        .in_log_context(context)
145        .await;
146
147        assert_eq!(find_local_value("answer"), None);
148    }
149
150    #[tokio::test]
151    async fn test_panicked_future() {
152        let context = LogContext::new().with_local_field("answer", 42);
153
154        AssertUnwindSafe(
155            async {
156                tokio::task::yield_now().await;
157                panic!("Goodbye cruel world");
158            }
159            .in_log_context(context),
160        )
161        .catch_unwind()
162        .await
163        .unwrap_err();
164
165        assert_eq!(find_local_value("answer"), None);
166    }
167
168    #[tokio::test]
169    async fn test_nested_future_with_common_context() {
170        let context = LogContext::new().with_local_field("answer", 42);
171
172        async {
173            tokio::task::yield_now().await;
174
175            async {
176                tokio::task::yield_now().await;
177                assert_eq!(find_local_value("answer"), Some("42".to_string()));
178            }
179            .await;
180
181            assert_eq!(find_local_value("answer"), Some("42".to_string()));
182        }
183        .in_log_context(context)
184        .await;
185
186        assert_eq!(find_local_value("answer"), None);
187    }
188
189    #[tokio::test]
190    async fn test_nested_future_with_different_contexts() {
191        check_nested_different_contexts(42).await;
192    }
193
194    #[tokio::test]
195    async fn test_join_multiple_tasks_single_thread() {
196        let tasks = (0..128).map(check_nested_different_contexts);
197        futures_util::future::join_all(tasks).await;
198    }
199
200    #[tokio::test]
201    async fn test_join_multiple_tasks_multi_thread() {
202        let handles = (0..64).map(|i| {
203            tokio::spawn(futures_util::future::join_all(
204                (0..128).map(|j| check_nested_different_contexts(j * i)),
205            ))
206        });
207
208        let results = futures_util::future::join_all(handles).await;
209        for result in results {
210            result.unwrap();
211        }
212    }
213}