#![doc = include_str!("../README.md")]
use std::collections::BTreeMap;
use std::path::PathBuf;
use std::sync::{Arc, Once, OnceLock, RwLock};
use rivet_foundation::ConfigValue;
#[cfg(not(target_arch = "wasm32"))]
use time::macros::format_description;
#[cfg(not(target_arch = "wasm32"))]
use time::OffsetDateTime;
pub mod handlers;
mod log;
mod log_service;
pub mod logger;
pub mod processors;
pub use log::{ChannelLog, Log};
pub use log_service::LogService;
pub use logger::{
ClosureContext, DeferredValue, Git, Handler, Hostname, Introspection, Level, LoadAverage,
LoadAverageWindow, LogRecord, LogValue, Logger, LoggerError, MemoryPeakUsage, MemoryUsage,
Mercurial, ProcessId, Processor, PsrLogMessage, Tag, Uid, Web,
};
pub(crate) const LOG_LEVEL_WIDTH: usize = 5;
#[derive(Clone)]
struct ChannelBuildConfig {
log_config: ConfigValue,
base_path: PathBuf,
}
pub fn init_default_tracing() {
static TRACING_INIT: Once = Once::new();
TRACING_INIT.call_once(|| {
let subscriber = tracing_subscriber::fmt()
.with_writer(std::io::stdout)
.with_max_level(tracing::Level::DEBUG)
.finish();
let _ = tracing::subscriber::set_global_default(subscriber);
});
}
pub fn set_handler(handler: Arc<dyn handlers::Handler>) {
if let Ok(mut slot) = handler_slot().write() {
*slot = Some(handler);
}
}
pub fn set_channel_handler(channel: impl Into<String>, handler: Arc<dyn handlers::Handler>) {
if let Ok(mut slot) = channel_handler_slot().write() {
slot.insert(channel.into(), handler);
}
}
pub fn set_channel_handlers(handlers: BTreeMap<String, Arc<dyn handlers::Handler>>) {
if let Ok(mut slot) = channel_handler_slot().write() {
*slot = handlers;
}
}
pub fn set_channel_handler_build_config(log_config: ConfigValue, base_path: impl Into<PathBuf>) {
if let Ok(mut slot) = channel_build_config_slot().write() {
*slot = Some(ChannelBuildConfig {
log_config,
base_path: base_path.into(),
});
}
}
fn handler_slot() -> &'static RwLock<Option<Arc<dyn handlers::Handler>>> {
static HANDLER: OnceLock<RwLock<Option<Arc<dyn handlers::Handler>>>> = OnceLock::new();
HANDLER.get_or_init(|| RwLock::new(None))
}
fn channel_handler_slot() -> &'static RwLock<BTreeMap<String, Arc<dyn handlers::Handler>>> {
static CHANNEL_HANDLERS: OnceLock<RwLock<BTreeMap<String, Arc<dyn handlers::Handler>>>> =
OnceLock::new();
CHANNEL_HANDLERS.get_or_init(|| RwLock::new(BTreeMap::new()))
}
fn channel_build_config_slot() -> &'static RwLock<Option<ChannelBuildConfig>> {
static CHANNEL_BUILD_CONFIG: OnceLock<RwLock<Option<ChannelBuildConfig>>> = OnceLock::new();
CHANNEL_BUILD_CONFIG.get_or_init(|| RwLock::new(None))
}
fn active_handler() -> Option<Arc<dyn handlers::Handler>> {
handler_slot()
.read()
.ok()
.and_then(|slot| slot.as_ref().cloned())
}
fn active_channel_handler(channel: &str) -> Option<Arc<dyn handlers::Handler>> {
channel_handler_slot()
.read()
.ok()
.and_then(|slot| slot.get(channel).cloned())
}
pub(crate) fn write_to_handler(message: &str) {
if let Some(handler) = active_handler() {
let _ = handler.log(message);
}
}
pub(crate) fn write_to_channel_or_handler(channel: &str, message: &str) {
if let Some(handler) = active_channel_handler(channel) {
let _ = handler.log(message);
return;
}
if let Some(handler) = resolve_channel_handler(channel) {
let _ = handler.log(message);
return;
}
write_to_handler(message);
}
fn resolve_channel_handler(channel: &str) -> Option<Arc<dyn handlers::Handler>> {
let build_config = channel_build_config_slot()
.read()
.ok()
.and_then(|slot| slot.clone())?;
let handler = handlers::build_handler_for_channel_from_config(
&build_config.log_config,
&build_config.base_path,
channel,
)
.ok()?;
if let Ok(mut slot) = channel_handler_slot().write() {
if let Some(existing) = slot.get(channel) {
return Some(existing.clone());
}
slot.insert(channel.to_string(), Arc::clone(&handler));
}
Some(handler)
}
pub(crate) fn format_log_line(level: &str, message: &str) -> String {
let timestamp = timestamp_utc();
format!(
"{timestamp} {level:<width$} {message}",
width = LOG_LEVEL_WIDTH
)
}
#[cfg(not(target_arch = "wasm32"))]
fn timestamp_utc() -> String {
let ts_format =
format_description!("[year]-[month]-[day]T[hour]:[minute]:[second].[subsecond digits:6]Z");
OffsetDateTime::now_utc()
.format(ts_format)
.unwrap_or_else(|_| "1970-01-01T00:00:00.000000Z".to_string())
}
#[cfg(target_arch = "wasm32")]
fn timestamp_utc() -> String {
"1970-01-01T00:00:00.000000Z".to_string()
}
#[cfg(test)]
mod tests {
use super::format_log_line;
#[test]
fn level_column_is_fixed_width_for_alignment() {
let debug = format_log_line("DEBUG", "debug");
let info = format_log_line("INFO", "info");
let error = format_log_line("ERROR", "error");
let debug_rest = debug
.split_once(' ')
.map(|(_, rest)| rest.to_string())
.expect("log line should include separator");
let info_rest = info
.split_once(' ')
.map(|(_, rest)| rest.to_string())
.expect("log line should include separator");
let error_rest = error
.split_once(' ')
.map(|(_, rest)| rest.to_string())
.expect("log line should include separator");
assert_eq!(debug_rest.find("debug"), info_rest.find("info"));
assert_eq!(debug_rest.find("debug"), error_rest.find("error"));
}
}