Skip to main content

aft/
log_ctx.rs

1//! Thread-local session context for log lines.
2//!
3//! AFT runs a single-threaded request loop. Each incoming request carries a
4//! `session_id` that identifies the OpenCode/Pi session. By storing it in a
5//! thread-local we can automatically prepend `[ses_xxx]` to every `slog_*`
6//! log macro call without threading the session id through every function
7//! signature.
8//!
9//! Background threads spawned during request handling (search-index pre-warm,
10//! semantic-index build) **must** capture the session id before spawning and
11//! re-install it on the new thread via [`set_session`] or [`with_session`].
12
13use std::cell::RefCell;
14
15thread_local! {
16    /// Current session id for log tagging. `None` means "no session context".
17    static CURRENT_SESSION: RefCell<Option<String>> = const { RefCell::new(None) };
18}
19
20/// Set the current thread-local session id.
21///
22/// Call this at the start of a background thread that captured the session id
23/// from the parent request loop.
24pub fn set_session(session: Option<String>) {
25    CURRENT_SESSION.with(|s| {
26        *s.borrow_mut() = session;
27    });
28}
29
30struct SessionGuard(Option<String>);
31
32impl Drop for SessionGuard {
33    fn drop(&mut self) {
34        set_session(self.0.take());
35    }
36}
37
38/// Run `f` with the given session id set on the current thread, restoring the
39/// previous value afterwards (RAII-style and panic-safe).
40///
41/// This is the primary entry point for the main request loop: wrap the
42/// dispatch call in `with_session(req.session_id.clone(), || { ... })`.
43pub fn with_session<T>(session: Option<String>, f: impl FnOnce() -> T) -> T {
44    let prev = current_session();
45    set_session(session);
46    let _guard = SessionGuard(prev);
47    f()
48}
49
50/// Return the current session id (e.g. `"abcd1234"`), or `None` if no session is set.
51pub fn current_session() -> Option<String> {
52    CURRENT_SESSION.with(|s| s.borrow().clone())
53}
54
55/// Return the current session id prefix string, e.g. `"[ses_abcd1234] "`,
56/// or an empty string if no session is set.
57pub fn session_prefix() -> String {
58    CURRENT_SESSION.with(|s| match s.borrow().as_deref() {
59        Some(sid) => format!("[ses_{}] ", sid),
60        None => String::new(),
61    })
62}
63
64/// Log at INFO level with the `[aft]` prefix and optional `[ses_xxx]` session tag.
65///
66/// Use this instead of `log::info!("[aft] ...")` in per-request code paths.
67/// The macro automatically reads the thread-local session id and formats:
68///
69/// ```text
70/// With session:    [aft] [ses_abcd1234] semantic index: rebuilding from scratch
71/// Without session: [aft] semantic index: rebuilding from scratch
72/// ```
73#[macro_export]
74macro_rules! slog_info {
75    ($($arg:tt)*) => {
76        log::info!("[aft] {}{}", $crate::log_ctx::session_prefix(), format!($($arg)*))
77    };
78}
79
80/// Log at WARN level with the `[aft]` prefix and optional `[ses_xxx]` session tag.
81///
82/// See [`slog_info!`] for format details.
83#[macro_export]
84macro_rules! slog_warn {
85    ($($arg:tt)*) => {
86        log::warn!("[aft] {}{}", $crate::log_ctx::session_prefix(), format!($($arg)*))
87    };
88}
89
90/// Log at ERROR level with the `[aft]` prefix and optional `[ses_xxx]` session tag.
91///
92/// See [`slog_info!`] for format details.
93#[macro_export]
94macro_rules! slog_error {
95    ($($arg:tt)*) => {
96        log::error!("[aft] {}{}", $crate::log_ctx::session_prefix(), format!($($arg)*))
97    };
98}
99
100#[cfg(test)]
101mod tests {
102    use super::*;
103
104    #[test]
105    fn with_session_sets_and_clears() {
106        // Initially no session
107        CURRENT_SESSION.with(|s| {
108            assert!(s.borrow().is_none());
109        });
110
111        // Set inside with_session
112        with_session(Some("test123".to_string()), || {
113            CURRENT_SESSION.with(|s| {
114                assert_eq!(s.borrow().as_deref(), Some("test123"));
115            });
116        });
117
118        // Cleared after with_session
119        CURRENT_SESSION.with(|s| {
120            assert!(s.borrow().is_none());
121        });
122    }
123
124    #[test]
125    fn with_session_none_is_noop() {
126        with_session(None, || {
127            CURRENT_SESSION.with(|s| {
128                assert!(s.borrow().is_none());
129            });
130        });
131    }
132
133    #[test]
134    fn session_prefix_format() {
135        with_session(Some("abcd1234".to_string()), || {
136            assert_eq!(session_prefix(), "[ses_abcd1234] ");
137        });
138
139        // Without session
140        assert_eq!(session_prefix(), "");
141    }
142
143    #[test]
144    fn set_session_direct() {
145        set_session(Some("direct".to_string()));
146        CURRENT_SESSION.with(|s| {
147            assert_eq!(s.borrow().as_deref(), Some("direct"));
148        });
149        set_session(None);
150        CURRENT_SESSION.with(|s| {
151            assert!(s.borrow().is_none());
152        });
153    }
154}