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
656 lines
20 KiB
Rust
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();
|
|
}
|
|
}
|