//! 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>, logger: Option>, callbacks: BTreeMap, 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 { static STATE: OnceLock> = OnceLock::new(); STATE.get_or_init(|| Mutex::new(LoggerState::default())) } fn ensure_initialized() -> Arc { 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 = 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, } impl ILoggerFactory for ConsoleLoggerFactory { fn create_logger(&self, name: &str) -> Arc { 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>; struct ConsoleLogger { name: String, closed: Arc, scopes: Arc>, next_scope: Arc, } 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::>() .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 { 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>, thread_id: ThreadId, id: Option, } 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; 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 { if method != 0 { return Err(Error::Argument); } object .downcast_arc::() .map(|target| Self(Arc::clone(&target.0))) .ok_or(Error::Argument) } pub fn begin_invoke( &self, message: Object, level: LogLevel, callback: Box, object: Object, ) -> Result, 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) -> Result<(), Error> { result .downcast::() .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) -> 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, 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, |logger| logger.begin_scope(state_value), )) } pub fn begin_client_scope(mut client: GridClient) -> Result, 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, 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, Error> { Ok(Box::new(ConsoleLoggerFactory::default())) } pub fn set_logger_factory(factory: Box, name: String) -> Result<(), Error> { let factory: Arc = 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>, cancellation_token: Option, ) -> Result { 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 { 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) -> Option { 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, exception: Option, ) -> 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, 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 { Ok(ensure_initialized().is_enabled(level)) } pub fn log_with_object_log_level_grid_client( message: Object, level: LogLevel, client: Option, ) -> Result<(), Error> { Self::log(message, level, client, None) } pub fn log_with_object_log_level_grid_client_exception( message: Object, level: LogLevel, client: Option, exception: Option, ) -> 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) -> 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 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 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 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) -> Result<(), Error> { Self::log(message, $level, client, None) } pub fn $client_error( message: Object, exception: ExternalError, client: Option, ) -> 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(); } }