use std::{borrow::Cow, marker::PhantomData};
use self::stack::{SCOPE_STACK, ScopeStack};
use crate::{LogContext, LogValue};
pub mod stack;
#[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));
Self {
_marker: PhantomData,
}
}
pub fn in_scope<R>(context: LogContext, f: impl FnOnce() -> R) -> R {
let _guard = Self::enter(context);
f()
}
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() {
top.0.local.insert(key, value);
}
});
}
#[doc = include_str!("../../examples/current_context.rs")]
#[must_use]
pub fn current_context() -> LogContext {
SCOPE_STACK
.with(|stack| stack.top().map(|frame| frame.clone().into()))
.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");
frame.into()
}
}
impl Drop for LogScope {
fn drop(&mut self) {
SCOPE_STACK.with(ScopeStack::pop);
}
}
pub trait LogContextExt: Sized + crate::private::Sealed {
fn in_scope<R>(self, f: impl FnOnce() -> R) -> R;
}
impl LogContextExt for LogContext {
fn in_scope<R>(self, f: impl FnOnce() -> R) -> R {
LogScope::in_scope(self, f)
}
}
#[cfg(test)]
mod tests {
use pretty_assertions::assert_eq;
use static_assertions::assert_not_impl_any;
use super::*;
assert_not_impl_any!(LogScope: Send);
#[test]
fn test_log_context_guard_enter() {
let context = LogContext::new().with_local_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().count()),
1
);
drop(guard);
assert_eq!(SCOPE_STACK.with(ScopeStack::len), 0);
}
#[test]
fn test_log_context_nested_guards() {
let outer_context = LogContext::new().with_local_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().count()),
1
);
SCOPE_STACK.with(|stack| {
let context = &stack.top().unwrap().0;
assert_eq!(
context.local.0.get("simple_record").unwrap().to_string(),
"outer_value"
);
});
let inner_context = LogContext::new().with_local_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.0.local.find("simple_record").unwrap().to_string(),
"inner_value"
);
});
drop(inner_guard);
}
assert_eq!(
SCOPE_STACK.with(|stack| stack.top().unwrap().records().count()),
1
);
SCOPE_STACK.with(|stack| {
let frame = stack.top().unwrap();
assert_eq!(
frame.0.local.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_local_record("simple_record", "main");
let local_guard = LogScope::enter(local_context);
let first_thread_handle = std::thread::spawn(|| {
let inner_context =
LogContext::new().with_local_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.0.local.find("simple_record").unwrap().to_string(),
"first_thread"
);
});
drop(inner_guard);
});
let second_thread_handle = std::thread::spawn(|| {
let inner_context =
LogContext::new().with_local_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.0.local.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.0.local["simple_record"].to_string(), "main");
});
drop(local_guard);
}
#[test]
fn test_current_context_empty_scope() {
let context = LogScope::current_context();
assert!(context.is_empty());
}
#[test]
fn test_current_context_with_scope() {
let context = LogContext::new().with_local_record("record", 42);
{
let _guard = LogScope::enter(context);
let current_context = LogScope::current_context();
assert_eq!(current_context.local["record"].to_string(), "42");
}
assert!(LogScope::current_context().is_empty());
}
#[test]
fn test_in_scope_enters_context_and_returns_result() {
assert!(SCOPE_STACK.with(ScopeStack::is_empty));
let result = LogScope::in_scope(LogContext::new().with_local_record("record", 42), || {
let current_context = LogScope::current_context();
assert_eq!(current_context.local["record"].to_string(), "42");
40 + 2
});
assert_eq!(result, 42);
assert!(SCOPE_STACK.with(ScopeStack::is_empty));
}
#[test]
fn test_log_context_ext_in_scope_enters_context_and_returns_result() {
assert!(SCOPE_STACK.with(ScopeStack::is_empty));
let result = LogContext::new()
.with_local_record("record", 42)
.in_scope(|| {
let current_context = LogScope::current_context();
assert_eq!(current_context.local["record"].to_string(), "42");
40 + 2
});
assert_eq!(result, 42);
assert!(SCOPE_STACK.with(ScopeStack::is_empty));
}
#[test]
fn test_log_context_inherited_records() {
LogContext::new()
.with_local_record("name", "Ann")
.with_inherited_record("tag", "42")
.with_inherited_record("target", "root")
.in_scope(|| {
let ctx = LogScope::current_context();
assert_eq!(ctx.local["name"].to_string(), "Ann");
assert_eq!(ctx.inherited["tag"].to_string(), "42");
assert_eq!(ctx.inherited["target"].to_string(), "root");
LogContext::new()
.with_local_record("target", "nested")
.in_scope(|| {
let ctx = LogScope::current_context();
assert_eq!(ctx.local["target"].to_string(), "nested");
assert_eq!(ctx.inherited["tag"].to_string(), "42");
assert!(ctx.local.find("name").is_none());
});
});
}
#[test]
fn test_panic_in_child_scope_does_not_break_parent() {
let outer_context = LogContext::new()
.with_inherited_record("outer", "val")
.with_local_record("outer_local", "ol");
{
let _parent_guard = LogScope::enter(outer_context);
assert_eq!(SCOPE_STACK.with(ScopeStack::len), 1);
let result = std::panic::catch_unwind(|| {
LogContext::new().in_scope(|| panic!("inner panic"));
});
assert!(result.is_err());
}
assert_eq!(SCOPE_STACK.with(ScopeStack::len), 0);
}
#[test]
fn test_sibling_scopes_get_independent_inherited_copies() {
let parent_ctx = LogContext::new()
.with_inherited_record("parent_key", "pv")
.with_local_record("parent_local", "pl");
{
let _g1 = LogScope::enter(parent_ctx);
assert_eq!(SCOPE_STACK.with(ScopeStack::len), 1);
let child1_result = LogContext::new()
.with_inherited_record("sibling", "child1")
.with_local_record("only_in_child1", "c1")
.in_scope(|| {
let c = LogScope::current_context();
format!(
"{}|{}",
c.inherited["parent_key"], c.local["only_in_child1"]
)
});
assert_eq!(child1_result, "pv|c1");
assert_eq!(SCOPE_STACK.with(ScopeStack::len), 1);
let c2_result = LogContext::new()
.with_inherited_record("sibling", "child2")
.with_local_record("only_in_child2", "c2")
.in_scope(|| {
let c = LogScope::current_context();
format!("{}|{}", c.inherited["parent_key"], c.inherited["sibling"])
});
assert_eq!(c2_result, "pv|child2");
assert_eq!(SCOPE_STACK.with(ScopeStack::len), 1);
}
assert_eq!(SCOPE_STACK.with(ScopeStack::len), 0);
}
}