use std::cell::RefCell;
thread_local! {
static CURRENT_SESSION: RefCell<Option<String>> = const { RefCell::new(None) };
static LAST_SESSION: RefCell<Option<String>> = const { RefCell::new(None) };
}
pub fn set_session(session: Option<String>) {
if let Some(sid) = session.as_deref() {
LAST_SESSION.with(|s| {
*s.borrow_mut() = Some(sid.to_string());
});
}
CURRENT_SESSION.with(|s| {
*s.borrow_mut() = session;
});
}
struct SessionGuard(Option<String>);
impl Drop for SessionGuard {
fn drop(&mut self) {
CURRENT_SESSION.with(|s| {
*s.borrow_mut() = self.0.take();
});
}
}
pub fn with_session<T>(session: Option<String>, f: impl FnOnce() -> T) -> T {
let prev = current_session();
set_session(session);
let _guard = SessionGuard(prev);
f()
}
pub fn current_session() -> Option<String> {
CURRENT_SESSION.with(|s| s.borrow().clone())
}
pub fn session_prefix() -> String {
let sid_opt = CURRENT_SESSION
.with(|s| s.borrow().clone())
.or_else(|| LAST_SESSION.with(|s| s.borrow().clone()));
match sid_opt.as_deref() {
Some(sid) if sid.starts_with("ses_") => format!("[{}] ", sid),
Some(sid) => format!("[ses_{}] ", sid),
None => String::new(),
}
}
#[macro_export]
macro_rules! slog_info {
($($arg:tt)*) => {
log::info!("{}{}", $crate::log_ctx::session_prefix(), format!($($arg)*))
};
}
#[macro_export]
macro_rules! slog_warn {
($($arg:tt)*) => {
log::warn!("{}{}", $crate::log_ctx::session_prefix(), format!($($arg)*))
};
}
#[macro_export]
macro_rules! slog_error {
($($arg:tt)*) => {
log::error!("{}{}", $crate::log_ctx::session_prefix(), format!($($arg)*))
};
}
#[macro_export]
macro_rules! slog_debug {
($($arg:tt)*) => {
log::debug!("{}{}", $crate::log_ctx::session_prefix(), format!($($arg)*))
};
}
#[cfg(test)]
mod tests {
use super::*;
fn reset_session_state() {
CURRENT_SESSION.with(|s| {
*s.borrow_mut() = None;
});
LAST_SESSION.with(|s| {
*s.borrow_mut() = None;
});
}
#[test]
fn with_session_sets_and_clears() {
reset_session_state();
CURRENT_SESSION.with(|s| {
assert!(s.borrow().is_none());
});
with_session(Some("test123".to_string()), || {
CURRENT_SESSION.with(|s| {
assert_eq!(s.borrow().as_deref(), Some("test123"));
});
});
CURRENT_SESSION.with(|s| {
assert!(s.borrow().is_none());
});
}
#[test]
fn with_session_none_is_noop() {
reset_session_state();
with_session(None, || {
CURRENT_SESSION.with(|s| {
assert!(s.borrow().is_none());
});
});
}
#[test]
fn session_prefix_format() {
reset_session_state();
with_session(Some("abcd1234".to_string()), || {
assert_eq!(session_prefix(), "[ses_abcd1234] ");
});
assert_eq!(session_prefix(), "[ses_abcd1234] ");
reset_session_state();
assert_eq!(session_prefix(), "");
}
#[test]
fn session_prefix_does_not_double_prefix_real_ids() {
reset_session_state();
with_session(Some("ses_313660571ffeZTsf4koSJwk50Q".to_string()), || {
assert_eq!(session_prefix(), "[ses_313660571ffeZTsf4koSJwk50Q] ");
});
}
#[test]
fn set_session_direct() {
reset_session_state();
set_session(Some("direct".to_string()));
CURRENT_SESSION.with(|s| {
assert_eq!(s.borrow().as_deref(), Some("direct"));
});
set_session(None);
CURRENT_SESSION.with(|s| {
assert!(s.borrow().is_none());
});
LAST_SESSION.with(|s| {
assert_eq!(s.borrow().as_deref(), Some("direct"));
});
}
#[test]
fn session_prefix_falls_back_to_last_session_after_with_session_exits() {
reset_session_state();
with_session(Some("ses_first".to_string()), || {
assert_eq!(session_prefix(), "[ses_first] ");
});
assert!(current_session().is_none());
assert_eq!(session_prefix(), "[ses_first] ");
}
#[test]
fn last_session_updates_with_each_with_session_call() {
reset_session_state();
with_session(Some("ses_first".to_string()), || {});
assert_eq!(session_prefix(), "[ses_first] ");
with_session(Some("ses_second".to_string()), || {});
assert_eq!(session_prefix(), "[ses_second] ");
}
#[test]
fn with_session_none_does_not_clear_last_session() {
reset_session_state();
with_session(Some("ses_real".to_string()), || {});
assert_eq!(session_prefix(), "[ses_real] ");
with_session(None, || {});
assert_eq!(session_prefix(), "[ses_real] ");
}
}