use std::{borrow::Cow, marker::PhantomData};
use crate::{
LogContext, LogValue,
stack::{SCOPE_STACK, ScopeStack},
};
#[non_exhaustive]
#[derive(Debug)]
pub struct LogScope {
_marker: PhantomData<*mut ()>,
}
impl LogScope {
#[must_use]
pub fn enter(context: LogContext) -> Self {
SCOPE_STACK.with(|stack| stack.push(context.frame));
Self {
_marker: PhantomData,
}
}
pub fn add_record(key: impl Into<Cow<'static, str>>, value: impl Into<LogValue>) {
SCOPE_STACK.with(|stack| {
if let Some(mut top) = stack.top_mut() {
let record = (key.into(), value.into());
top.push(record);
}
});
}
#[doc = include_str!("../examples/current_context.rs")]
#[must_use]
pub fn current_context() -> LogContext {
SCOPE_STACK
.with(|stack| {
stack.top().map(|frame| LogContext {
frame: frame.clone(),
})
})
.unwrap_or_default()
}
pub(crate) fn exit(self) -> LogContext {
std::mem::forget(self);
let frame = SCOPE_STACK
.with(ScopeStack::pop)
.expect("bug in LogScope::exit: expected a scope frame to exist when popping on exit");
LogContext { frame }
}
}
impl Drop for LogScope {
fn drop(&mut self) {
SCOPE_STACK.with(ScopeStack::pop);
}
}
#[cfg(test)]
mod tests {
use pretty_assertions::assert_eq;
use static_assertions::assert_not_impl_any;
use super::*;
use crate::stack::SCOPE_STACK;
assert_not_impl_any!(LogScope: Send);
#[test]
fn test_log_context_guard_enter() {
let context = LogContext::new().with_record("simple", 42);
assert_eq!(SCOPE_STACK.with(ScopeStack::is_empty), true);
let guard = LogScope::enter(context);
assert_eq!(
SCOPE_STACK.with(|stack| stack.top().unwrap().records().len()),
1
);
drop(guard);
assert_eq!(SCOPE_STACK.with(ScopeStack::len), 0);
}
#[test]
fn test_log_context_nested_guards() {
let outer_context = LogContext::new().with_record("simple_record", "outer_value");
assert_eq!(SCOPE_STACK.with(ScopeStack::len), 0);
let outer_guard = LogScope::enter(outer_context);
assert_eq!(
SCOPE_STACK.with(|stack| stack.top().unwrap().records().len()),
1
);
SCOPE_STACK.with(|stack| {
let frame = stack.top().unwrap();
assert_eq!(
frame.find("simple_record").unwrap().to_string(),
"outer_value"
);
});
let inner_context = LogContext::new().with_record("simple_record", "inner_value");
{
let inner_guard = LogScope::enter(inner_context);
assert_eq!(SCOPE_STACK.with(ScopeStack::len), 2);
SCOPE_STACK.with(|stack| {
let frame = stack.top().unwrap();
assert_eq!(
frame.find("simple_record").unwrap().to_string(),
"inner_value"
);
});
drop(inner_guard);
}
assert_eq!(
SCOPE_STACK.with(|stack| stack.top().unwrap().records().len()),
1
);
SCOPE_STACK.with(|stack| {
let frame = stack.top().unwrap();
assert_eq!(
frame.find("simple_record").unwrap().to_string(),
"outer_value"
);
});
drop(outer_guard);
assert_eq!(SCOPE_STACK.with(ScopeStack::is_empty), true);
}
#[test]
fn test_log_context_multithread() {
let local_context = LogContext::new().with_record("simple_record", "main");
let local_guard = LogScope::enter(local_context);
let first_thread_handle = std::thread::spawn(|| {
let inner_context = LogContext::new().with_record("simple_record", "first_thread");
let inner_guard = LogScope::enter(inner_context);
assert_eq!(SCOPE_STACK.with(ScopeStack::len), 1);
SCOPE_STACK.with(|stack| {
let frame = stack.top().unwrap();
assert_eq!(
frame.find("simple_record").unwrap().to_string(),
"first_thread"
);
});
drop(inner_guard);
});
let second_thread_handle = std::thread::spawn(|| {
let inner_context = LogContext::new().with_record("simple_record", "second_thread");
let inner_guard = LogScope::enter(inner_context);
assert_eq!(SCOPE_STACK.with(ScopeStack::len), 1);
SCOPE_STACK.with(|stack| {
let frame = stack.top().unwrap();
assert_eq!(
frame.find("simple_record").unwrap().to_string(),
"second_thread"
);
});
drop(inner_guard);
});
first_thread_handle.join().unwrap();
second_thread_handle.join().unwrap();
SCOPE_STACK.with(|stack| {
let frame = stack.top().unwrap();
assert_eq!(frame.find("simple_record").unwrap().to_string(), "main");
});
drop(local_guard);
}
#[test]
fn test_current_context_empty_scope() {
let context = LogScope::current_context();
assert!(context.frame.is_empty());
}
#[test]
fn test_current_context_with_scope() {
let context = LogContext::new().with_record("record", 42);
{
let _guard = LogScope::enter(context);
let current_context = LogScope::current_context();
assert_eq!(
current_context.frame.find("record").unwrap().to_string(),
"42"
);
}
assert!(LogScope::current_context().frame.is_empty());
}
}