mod config;
mod format;
mod rotation;
mod sink;
use std::io::{self, Write};
use std::path::Path;
use std::sync::{Arc, Mutex, MutexGuard, Weak};
use spdlog::sink::Sink;
use spdlog::{Logger, ThreadPool};
use uuid::Uuid;
use crate::error::{FlowError, Result};
pub use config::{
DEFAULT_FILE_FLUSH_INTERVAL_MILLIS, DEFAULT_FILE_SINK_QUEUE_ENTRIES, FileLogRotationConfig,
FileLogSinkConfig, LogFormat, LogLevel, LogSinkConfig, LoggingConfig,
MAX_FILE_SINK_QUEUE_ENTRIES, MAX_FILE_SINK_RETAINED_FILES,
};
pub(crate) use sink::build_logger;
use sink::log_level_filter;
#[cfg(test)]
pub(crate) use format::format_event_for_test;
static LOGGER_LIFECYCLE_LOCK: Mutex<()> = Mutex::new(());
static DEFAULT_LOGGING_RUNTIME: Mutex<Option<LoggingRuntime>> = Mutex::new(None);
static ACTIVE_RELAY_LOGGER: Mutex<Option<Weak<Logger>>> = Mutex::new(None);
fn lock_logger_lifecycle() -> MutexGuard<'static, ()> {
LOGGER_LIFECYCLE_LOCK
.lock()
.unwrap_or_else(|error| error.into_inner())
}
fn log_crate_proxy_is_installed() -> bool {
std::ptr::addr_eq(log::logger(), spdlog::log_crate_proxy() as &dyn log::Log)
}
fn active_relay_logger_exists() -> bool {
ACTIVE_RELAY_LOGGER
.lock()
.unwrap_or_else(|error| error.into_inner())
.as_ref()
.is_some_and(|logger| logger.upgrade().is_some())
}
fn set_active_relay_logger(logger: &Arc<Logger>) {
*ACTIVE_RELAY_LOGGER
.lock()
.unwrap_or_else(|error| error.into_inner()) = Some(Arc::downgrade(logger));
}
fn clear_active_relay_logger(logger: &Arc<Logger>) {
let mut active = ACTIVE_RELAY_LOGGER
.lock()
.unwrap_or_else(|error| error.into_inner());
if active
.as_ref()
.is_some_and(|current| Weak::ptr_eq(current, &Arc::downgrade(logger)))
{
*active = None;
}
}
fn install_log_crate_proxy() -> Result<()> {
match spdlog::init_log_crate_proxy() {
Ok(()) => Ok(()),
Err(_) if log_crate_proxy_is_installed() => Ok(()),
Err(_) => Err(FlowError::AlreadyExists(
"process-global log facade is already initialized by another logger; Relay logging cannot install its log proxy"
.into(),
)),
}
}
pub struct LoggingRuntime {
root_relay_id: String,
pub(crate) logger: Arc<Logger>,
_thread_pools: Vec<Arc<ThreadPool>>,
}
impl LoggingRuntime {
pub fn configure(config: LoggingConfig) -> Result<Self> {
let _lifecycle = lock_logger_lifecycle();
Self::configure_with_lifecycle_lock(config)
}
fn configure_with_lifecycle_lock(config: LoggingConfig) -> Result<Self> {
let root_relay_id = Uuid::now_v7().to_string();
let (logger, thread_pools) = build_logger(&config, root_relay_id.clone())?;
install_log_crate_proxy()?;
spdlog::log_crate_proxy().set_logger(Some(Arc::clone(&logger)));
spdlog::log_crate_proxy().set_filter(None);
log::set_max_level(log_level_filter(config.level));
set_active_relay_logger(&logger);
log::info!(
target: "nemo_relay.logging",
event = "logging_initialized",
file_sink_count = config.sinks.len();
"Operational logging initialized"
);
Ok(Self {
root_relay_id,
logger,
_thread_pools: thread_pools,
})
}
pub fn configure_from_file_path(path: impl AsRef<Path>) -> Result<Self> {
Self::configure(LoggingConfig::from_file_path(path)?)
}
pub fn configure_from_environment() -> Result<Self> {
Self::configure(LoggingConfig::from_environment()?.unwrap_or_default())
}
pub fn root_relay_id(&self) -> &str {
&self.root_relay_id
}
pub fn shutdown(self) {
drop(self);
}
}
impl Drop for LoggingRuntime {
fn drop(&mut self) {
log::info!(
target: "nemo_relay.logging",
event = "logging_shutdown_started";
"Operational logging shutdown started"
);
self.logger.set_flush_period(None);
for sink in self.logger.sinks() {
if let Err(error) = Sink::flush_on_exit(sink.as_ref()) {
let _ = writeln!(
io::stderr(),
"nemo-relay: logging shutdown flush failed: {error}"
);
}
}
let _lifecycle = lock_logger_lifecycle();
let detached = spdlog::log_crate_proxy().swap_logger(None);
if let Some(logger) = detached
&& !Arc::ptr_eq(&logger, &self.logger)
{
spdlog::log_crate_proxy().set_logger(Some(Arc::clone(&logger)));
set_active_relay_logger(&logger);
} else {
clear_active_relay_logger(&self.logger);
}
}
}
pub fn init_logging(config: &LoggingConfig) -> Result<LoggingRuntime> {
LoggingRuntime::configure(config.clone())
}
#[doc(hidden)]
pub fn initialize_default_logging() -> Result<()> {
let mut runtime = DEFAULT_LOGGING_RUNTIME.lock().map_err(|error| {
FlowError::Internal(format!("default logging runtime lock poisoned: {error}"))
})?;
if runtime.is_none() {
let config = LoggingConfig::from_environment()?;
let uses_default_config = config.is_none();
let _lifecycle = lock_logger_lifecycle();
if uses_default_config && active_relay_logger_exists() {
return Ok(());
}
match LoggingRuntime::configure_with_lifecycle_lock(config.unwrap_or_default()) {
Ok(configured) => *runtime = Some(configured),
Err(FlowError::AlreadyExists(_)) if uses_default_config => {}
Err(error) => return Err(error),
}
}
Ok(())
}
#[doc(hidden)]
pub fn shutdown_default_logging() -> Result<()> {
let runtime = DEFAULT_LOGGING_RUNTIME
.lock()
.map_err(|error| {
FlowError::Internal(format!("default logging runtime lock poisoned: {error}"))
})?
.take();
if let Some(runtime) = runtime {
runtime.shutdown();
}
Ok(())
}
#[cfg(test)]
#[path = "../../tests/coverage/logging_tests.rs"]
mod tests;