rivet-logger 0.1.0

Rivet framework crates and adapters.
Documentation
#![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"));
    }
}