Files
MetaCrate/crates/libremetaverse/src/logger.rs
Chili Palmer c9a1170a27
Some checks failed
API and SemVer surface / api-surface (push) Failing after 1m34s
Native code generation / deterministic (push) Has been cancelled
Concurrency and resource soak audit / soak (push) Has been cancelled
Documentation / documentation (push) Has been cancelled
performance evidence / audit (push) Has been cancelled
First release candidate / non-fuzz-release-gate (push) Has been cancelled
Release platform and feature matrix / audit (push) Has been cancelled
Release platform and feature matrix / matrix (false, linux-stable-minimal, x86_64-unknown-linux-gnu, stable) (push) Has been cancelled
Release platform and feature matrix / matrix (false, macos-stable-portable, x86_64-apple-darwin, stable) (push) Has been cancelled
Release platform and feature matrix / matrix (false, windows-stable-portable, x86_64-pc-windows-gnu, stable) (push) Has been cancelled
Release platform and feature matrix / matrix (true, linux-msrv-portable, x86_64-unknown-linux-gnu, 1.96.0) (push) Has been cancelled
Release platform and feature matrix / matrix (true, linux-stable-default, x86_64-unknown-linux-gnu, stable) (push) Has been cancelled
Release platform and feature matrix / matrix (true, linux-stable-features, x86_64-unknown-linux-gnu, stable) (push) Has been cancelled
Release platform and feature matrix / matrix (true, linux-stable-release-surface, x86_64-unknown-linux-gnu, stable) (push) Has been cancelled
Dependency and supply-chain audit / audit (push) Has been cancelled
Native release artifact audit / audit (push) Has been cancelled
Imaging and meshing gate / native (push) Failing after 53s
JPEG 2000 feature / linux (push) Successful in 2m49s
Native Rust workspace compile / compile (push) Failing after 59s
Skia feature / linux (push) Has been cancelled
Complete first release candidate audit (#107)
2026-08-12 14:44:28 +00:00

656 lines
20 KiB
Rust

//! Process-wide logging facade and callback compatibility layer.
use std::any::Any;
use std::collections::{BTreeMap, HashMap};
use std::fmt;
use std::sync::{
Arc, Mutex, OnceLock,
atomic::{AtomicBool, AtomicU64, Ordering},
};
use std::thread::ThreadId;
use std::time::Duration;
use libremetaverse_types::compat::{
CancellationToken, Close, ExternalError, Object, Subscription, Task, TaskCompletionSource,
};
use crate::callback_runtime::AsyncInvocation;
use crate::logging::{ILogger, ILoggerFactory, LogLevel};
use crate::{Error, GridClient, Settings, Simulator};
struct LoggerState {
factory: Option<Arc<dyn ILoggerFactory>>,
logger: Option<Arc<dyn ILogger>>,
callbacks: BTreeMap<u64, LoggerLogCallback>,
next_callback: u64,
}
impl Default for LoggerState {
fn default() -> Self {
Self {
factory: None,
logger: None,
callbacks: BTreeMap::new(),
next_callback: 1,
}
}
}
fn state() -> &'static Mutex<LoggerState> {
static STATE: OnceLock<Mutex<LoggerState>> = OnceLock::new();
STATE.get_or_init(|| Mutex::new(LoggerState::default()))
}
fn ensure_initialized() -> Arc<dyn ILogger> {
let mut state = state()
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner);
if let Some(logger) = state.logger.as_ref() {
return Arc::clone(logger);
}
let factory: Arc<dyn ILoggerFactory> = Arc::new(ConsoleLoggerFactory::default());
let logger = factory.create_logger("LibreMetaverse.Logger");
state.factory = Some(factory);
state.logger = Some(Arc::clone(&logger));
logger
}
#[derive(Default)]
struct ConsoleLoggerFactory {
closed: Arc<AtomicBool>,
}
impl ILoggerFactory for ConsoleLoggerFactory {
fn create_logger(&self, name: &str) -> Arc<dyn ILogger> {
Arc::new(ConsoleLogger {
name: name.to_owned(),
closed: Arc::clone(&self.closed),
scopes: Arc::new(Mutex::new(HashMap::new())),
next_scope: Arc::new(AtomicU64::new(1)),
})
}
fn shutdown(&self) -> Result<(), ExternalError> {
self.closed.store(true, Ordering::Release);
Ok(())
}
}
type ScopeMap = HashMap<ThreadId, Vec<(u64, Object)>>;
struct ConsoleLogger {
name: String,
closed: Arc<AtomicBool>,
scopes: Arc<Mutex<ScopeMap>>,
next_scope: Arc<AtomicU64>,
}
impl ILogger for ConsoleLogger {
fn is_enabled(&self, level: LogLevel) -> bool {
!self.closed.load(Ordering::Acquire) && level != LogLevel::NONE && level >= LogLevel::DEBUG
}
fn log(
&self,
level: LogLevel,
message: &Object,
exception: Option<&ExternalError>,
client_name: Option<&str>,
) {
if !self.is_enabled(level) {
return;
}
let scopes = self
.scopes
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner)
.get(&std::thread::current().id())
.cloned()
.unwrap_or_default();
let scope_text = scopes
.iter()
.map(|(_, state)| format!("{state:?}"))
.collect::<Vec<_>>()
.join(" ");
let level_name = match level {
LogLevel::TRACE => "TRACE",
LogLevel::DEBUG => "DEBUG",
LogLevel::INFORMATION => "INFO",
LogLevel::WARNING => "WARN",
LogLevel::ERROR => "ERROR",
LogLevel::CRITICAL => "CRITICAL",
_ => "LOG",
};
let client = client_name
.filter(|name| !name.is_empty())
.map_or(String::new(), |name| format!(" [{name}]"));
let scope = if scope_text.is_empty() {
String::new()
} else {
format!(" [{scope_text}]")
};
let exception = exception.map_or(String::new(), |error| format!(": {}", error.0));
eprintln!(
"[{level_name}] [{}]{client}{scope} {message:?}{exception}",
self.name
);
}
fn begin_scope(&self, state: Object) -> Box<dyn Close> {
let thread_id = std::thread::current().id();
let id = self.next_scope.fetch_add(1, Ordering::Relaxed);
self.scopes
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner)
.entry(thread_id)
.or_default()
.push((id, state));
Box::new(ConsoleScope {
scopes: Arc::clone(&self.scopes),
thread_id,
id: Some(id),
})
}
}
struct ConsoleScope {
scopes: Arc<Mutex<ScopeMap>>,
thread_id: ThreadId,
id: Option<u64>,
}
impl Close for ConsoleScope {
fn close(&mut self) -> Result<(), ExternalError> {
let Some(id) = self.id.take() else {
return Ok(());
};
let mut scopes = self
.scopes
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner);
if let Some(thread_scopes) = scopes.get_mut(&self.thread_id) {
thread_scopes.retain(|(scope_id, _)| *scope_id != id);
if thread_scopes.is_empty() {
scopes.remove(&self.thread_id);
}
}
Ok(())
}
}
impl Drop for ConsoleScope {
fn drop(&mut self) {
let _ = self.close();
}
}
struct NoopScope;
impl Close for NoopScope {
fn close(&mut self) -> Result<(), ExternalError> {
Ok(())
}
}
type LogHandler = Arc<dyn Fn(Object, LogLevel) + Send + Sync>;
pub struct LoggerCallbackTarget(pub LogHandler);
impl fmt::Debug for LoggerCallbackTarget {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
formatter.write_str("LoggerCallbackTarget(..)")
}
}
#[derive(Clone)]
pub struct LoggerLogCallback(LogHandler);
impl LoggerLogCallback {
pub fn from_callback(callback: impl Fn(Object, LogLevel) + Send + Sync + 'static) -> Self {
Self(Arc::new(callback))
}
pub fn new(object: Object, method: isize) -> Result<Self, Error> {
if method != 0 {
return Err(Error::Argument);
}
object
.downcast_arc::<LoggerCallbackTarget>()
.map(|target| Self(Arc::clone(&target.0)))
.ok_or(Error::Argument)
}
pub fn begin_invoke(
&self,
message: Object,
level: LogLevel,
callback: Box<dyn Fn(&dyn Any) + Send + Sync>,
object: Object,
) -> Result<Box<dyn Any + Send + Sync>, Error> {
let invocation = AsyncInvocation::pending(object);
let worker_invocation = invocation.clone();
let handler = Arc::clone(&self.0);
std::thread::Builder::new()
.name("logger-callback".to_owned())
.spawn(move || {
let result = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
handler(message, level);
}))
.map_err(|_| Error::InvalidOperation);
worker_invocation.complete(result);
let _ = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
callback(&worker_invocation);
}));
})
.map_err(|_| Error::InvalidOperation)?;
Ok(Box::new(invocation))
}
pub fn end_invoke(&self, result: Box<dyn Any + Send + Sync>) -> Result<(), Error> {
result
.downcast::<AsyncInvocation>()
.map_err(|_| Error::Argument)?
.wait()
}
pub fn invoke(&self, message: Object, level: LogLevel) -> Result<(), Error> {
(self.0)(message, level);
Ok(())
}
}
pub struct Logger;
impl Logger {
pub fn subscribe_on_log_message(handler: Option<LoggerLogCallback>) -> Subscription {
let Some(handler) = handler else {
return Subscription::detached();
};
let id = {
let mut state = state()
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner);
let id = state.next_callback;
state.next_callback = state.next_callback.wrapping_add(1).max(1);
state.callbacks.insert(id, handler);
id
};
Subscription::new(move || {
state()
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner)
.callbacks
.remove(&id);
})
}
pub fn begin_scope(state_value: Object) -> Result<Box<dyn Close>, Error> {
let logger = state()
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner)
.logger
.clone();
Ok(logger.map_or_else(
|| Box::new(NoopScope) as Box<dyn Close>,
|logger| logger.begin_scope(state_value),
))
}
pub fn begin_client_scope(mut client: GridClient) -> Result<Box<dyn Close>, Error> {
let log_names = client.settings().logging.log_names;
if !log_names {
return Ok(Box::new(NoopScope));
}
let name = client.self_().name();
if name.is_empty() {
return Ok(Box::new(NoopScope));
}
Self::begin_scope(Object::Map(HashMap::from([(
"Client".to_owned(),
Object::String(name),
)])))
}
pub fn begin_region_scope(simulator: Simulator) -> Result<Box<dyn Close>, Error> {
let name = if simulator.name.is_empty() {
simulator.handle.to_string()
} else {
simulator.name.clone()
};
Self::begin_scope(Object::Map(HashMap::from([(
"Region".to_owned(),
Object::String(name),
)])))
}
pub fn create_default_console_logger_factory() -> Result<Box<dyn ILoggerFactory>, Error> {
Ok(Box::new(ConsoleLoggerFactory::default()))
}
pub fn set_logger_factory(factory: Box<dyn ILoggerFactory>, name: String) -> Result<(), Error> {
let factory: Arc<dyn ILoggerFactory> = Arc::from(factory);
let logger = factory.create_logger(&name);
let previous = {
let mut state = state()
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner);
let previous = state.factory.replace(Arc::clone(&factory));
state.logger = Some(logger);
previous
};
if let Some(previous) = previous {
let _ = previous.shutdown();
}
Ok(())
}
pub fn shutdown_with_method() -> Result<(), Error> {
let factory = {
let mut state = state()
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner);
state.logger = None;
state.factory.take()
};
if let Some(factory) = factory {
let _ = factory.shutdown();
}
Ok(())
}
pub async fn shutdown_with_nullable_cancellation_token(
timeout: Option<Option<Duration>>,
cancellation_token: Option<CancellationToken>,
) -> Result<bool, Error> {
let factory = {
let mut state = state()
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner);
state.logger = None;
state.factory.take()
};
let Some(factory) = factory else {
return Ok(true);
};
let completion = TaskCompletionSource::new();
let shutdown_completion = completion.clone();
if std::thread::Builder::new()
.name("logger-shutdown".to_owned())
.spawn(move || {
let _ = shutdown_completion.try_set_result(factory.shutdown().is_ok());
})
.is_err()
{
return Ok(false);
}
let cancellation = cancellation_token.map(|token| {
let completion = completion.clone();
token.register_callback(Arc::new(move || {
let _ = completion.try_set_result(false);
}))
});
if let Some(timeout) = timeout.flatten() {
let timeout_completion = completion.clone();
let _ = std::thread::Builder::new()
.name("logger-shutdown-timeout".to_owned())
.spawn(move || {
std::thread::sleep(timeout);
let _ = timeout_completion.try_set_result(false);
});
}
let result = completion.future().await;
drop(cancellation);
result
}
fn callbacks() -> Vec<LoggerLogCallback> {
state()
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner)
.callbacks
.values()
.cloned()
.collect()
}
fn raise(message: &Object, level: LogLevel) {
for callback in Self::callbacks() {
let message = message.clone();
let _ = std::thread::Builder::new()
.name("logger-event".to_owned())
.spawn(move || {
let _ = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
let _ = callback.invoke(message, level);
}));
});
}
}
fn client_name(mut client: Option<GridClient>) -> Option<String> {
let client = client.as_mut()?;
if !client.settings().logging.log_names {
return None;
}
let name = client.self_().name();
(!name.is_empty()).then_some(name)
}
fn log_to_sink(
message: &Object,
level: LogLevel,
exception: Option<&ExternalError>,
client_name: Option<&str>,
) {
let logger = ensure_initialized();
if logger.is_enabled(level) {
logger.log(level, message, exception, client_name);
}
}
fn log(
message: Object,
level: LogLevel,
client: Option<GridClient>,
exception: Option<ExternalError>,
) -> Result<(), Error> {
Self::raise(&message, level);
let configured = Settings::log_level();
if configured == LogLevel::NONE || configured > level {
return Ok(());
}
let client_name = Self::client_name(client);
Self::log_to_sink(&message, level, exception.as_ref(), client_name.as_deref());
Ok(())
}
fn log_named(
message: Object,
level: LogLevel,
exception: Option<ExternalError>,
client_name: String,
) -> Result<(), Error> {
Self::log_to_sink(&message, level, exception.as_ref(), Some(&client_name));
Ok(())
}
pub fn is_enabled(level: LogLevel) -> Result<bool, Error> {
Ok(ensure_initialized().is_enabled(level))
}
pub fn log_with_object_log_level_grid_client(
message: Object,
level: LogLevel,
client: Option<GridClient>,
) -> Result<(), Error> {
Self::log(message, level, client, None)
}
pub fn log_with_object_log_level_grid_client_exception(
message: Object,
level: LogLevel,
client: Option<GridClient>,
exception: Option<ExternalError>,
) -> Result<(), Error> {
Self::log(message, level, client, exception)
}
pub fn log_with_object_log_level_exception(
message: Object,
level: LogLevel,
exception: ExternalError,
) -> Result<(), Error> {
Self::log(message, level, None, Some(exception))
}
pub fn debug_log(message: Object, client: Option<GridClient>) -> Result<(), Error> {
let level = Settings::log_level();
if level > LogLevel::DEBUG {
return Ok(());
}
Self::log(message, level, client, None)
}
pub async fn use_scope(
state: Object,
action: Box<dyn Fn() -> Task<()> + Send + Sync>,
) -> Result<(), Error> {
let mut scope = Self::begin_scope(state)?;
let result = action().await;
let close = scope.close().map_err(|_| Error::InvalidOperation);
result.and(close)
}
pub async fn use_client_scope(
client: GridClient,
action: Box<dyn Fn() -> Task<()> + Send + Sync>,
) -> Result<(), Error> {
let mut scope = Self::begin_client_scope(client)?;
let result = action().await;
let close = scope.close().map_err(|_| Error::InvalidOperation);
result.and(close)
}
pub async fn use_region_scope(
simulator: Simulator,
action: Box<dyn Fn() -> Task<()> + Send + Sync>,
) -> Result<(), Error> {
let mut scope = Self::begin_region_scope(simulator)?;
let result = action().await;
let close = scope.close().map_err(|_| Error::InvalidOperation);
result.and(close)
}
}
macro_rules! level_methods {
($client:ident, $client_error:ident, $named:ident, $named_error:ident, $level:expr) => {
impl Logger {
pub fn $client(message: Object, client: Option<GridClient>) -> Result<(), Error> {
Self::log(message, $level, client, None)
}
pub fn $client_error(
message: Object,
exception: ExternalError,
client: Option<GridClient>,
) -> Result<(), Error> {
Self::log(message, $level, client, Some(exception))
}
pub fn $named(message: Object, client_name: String) -> Result<(), Error> {
Self::log_named(message, $level, None, client_name)
}
pub fn $named_error(
message: Object,
exception: ExternalError,
client_name: String,
) -> Result<(), Error> {
Self::log_named(message, $level, Some(exception), client_name)
}
}
};
}
level_methods!(
trace_with_object_grid_client,
trace_with_object_exception_grid_client,
trace_with_object_string,
trace_with_object_exception_string,
LogLevel::TRACE
);
level_methods!(
debug_with_object_grid_client,
debug_with_object_exception_grid_client,
debug_with_object_string,
debug_with_object_exception_string,
LogLevel::DEBUG
);
level_methods!(
info_with_object_grid_client,
info_with_object_exception_grid_client,
info_with_object_string,
info_with_object_exception_string,
LogLevel::INFORMATION
);
level_methods!(
warn_with_object_grid_client,
warn_with_object_exception_grid_client,
warn_with_object_string,
warn_with_object_exception_string,
LogLevel::WARNING
);
level_methods!(
error_with_object_grid_client,
error_with_object_exception_grid_client,
error_with_object_string,
error_with_object_exception_string,
LogLevel::ERROR
);
level_methods!(
critical_with_object_grid_client,
critical_with_object_exception_grid_client,
critical_with_object_string,
critical_with_object_exception_string,
LogLevel::CRITICAL
);
#[cfg(test)]
mod tests {
use super::*;
use std::sync::atomic::AtomicUsize;
#[test]
fn event_subscription_delivers_and_unregisters() {
Logger::shutdown_with_method().unwrap();
let calls = Arc::new(AtomicUsize::new(0));
let observed = Arc::clone(&calls);
let subscription = Logger::subscribe_on_log_message(Some(
LoggerLogCallback::from_callback(move |_, _| {
observed.fetch_add(1, Ordering::Relaxed);
}),
));
Logger::info_with_object_grid_client(Object::String("one".to_owned()), None).unwrap();
for _ in 0..100 {
if calls.load(Ordering::Relaxed) == 1 {
break;
}
std::thread::sleep(Duration::from_millis(1));
}
assert_eq!(calls.load(Ordering::Relaxed), 1);
drop(subscription);
Logger::info_with_object_grid_client(Object::String("two".to_owned()), None).unwrap();
std::thread::sleep(Duration::from_millis(10));
assert_eq!(calls.load(Ordering::Relaxed), 1);
Logger::shutdown_with_method().unwrap();
}
#[test]
fn scope_close_is_idempotent() {
let _ = ensure_initialized();
let mut scope = Logger::begin_scope(Object::String("test".to_owned())).unwrap();
scope.close().unwrap();
scope.close().unwrap();
Logger::shutdown_with_method().unwrap();
}
}