rings-node 0.20.0

Rings is a structured peer-to-peer network implementation using WebRTC, Chord algorithm, and full WebAssembly (WASM) support.
Documentation
//! Logging configuration contains both `node` and `browser`.
use std::fmt;
use std::panic::Location;
use std::panic::PanicHookInfo;

use backtrace::Backtrace;
use clap::ValueEnum;
use tracing::Level;
use tracing_log::LogTracer;
use tracing_subscriber::layer::SubscriberExt;
use tracing_subscriber::Registry;

#[cfg(all(feature = "browser", target_family = "wasm"))]
pub use self::browser::init_logging;
#[cfg(feature = "node")]
pub use self::node::rings_node_init_logging as init_logging;
use crate::prelude::wasm_export;

/// Logging verbosity accepted by native CLI and browser bindings.
#[repr(C)]
#[wasm_export]
#[derive(ValueEnum, Debug, Clone, Copy, Default, Eq, PartialEq)]
pub enum LogLevel {
    /// Debug-level diagnostic output.
    Debug,
    /// Informational runtime output.
    Info,
    /// Warning-level runtime output.
    Warn,
    /// Error-level runtime output.
    #[default]
    Error,
    /// Trace-level diagnostic output.
    Trace,
}

impl From<LogLevel> for Level {
    fn from(val: LogLevel) -> Self {
        match val {
            LogLevel::Trace => Level::TRACE,
            LogLevel::Debug => Level::DEBUG,
            LogLevel::Info => Level::INFO,
            LogLevel::Warn => Level::WARN,
            LogLevel::Error => Level::ERROR,
        }
    }
}

impl std::str::FromStr for LogLevel {
    type Err = crate::error::Error;
    fn from_str(s: &str) -> Result<Self, Self::Err> {
        match s.to_uppercase().as_str() {
            "TRACE" => Ok(LogLevel::Trace),
            "DEBUG" => Ok(LogLevel::Debug),
            "INFO" => Ok(LogLevel::Info),
            "WARN" => Ok(LogLevel::Warn),
            "ERROR" => Ok(LogLevel::Error),
            x => Err(crate::error::Error::InvalidLoggingLevel(x.to_string())),
        }
    }
}

/// Panic location
#[derive(Debug, Clone)]
pub struct PanicLocation {
    file: String,
    line: String,
    column: String,
}

impl<'a, T> From<T> for PanicLocation
where T: Into<Location<'a>>
{
    fn from(lo: T) -> Self {
        let lo: Location = lo.into();
        Self {
            file: lo.file().to_string(),
            line: lo.line().to_string(),
            column: lo.file().to_string(),
        }
    }
}

/// Necessary information for recording panic
#[derive(Debug, Clone)]
pub struct PanicData<'a> {
    message: &'a PanicHookInfo<'a>,
    backtrace: String,
    location: Option<PanicLocation>,
}

impl<'a, T> From<T> for PanicData<'a>
where T: Into<&'a PanicHookInfo<'a>>
{
    fn from(panic: T) -> PanicData<'a> {
        let panic = panic.into();
        let backtrace = Backtrace::new();
        let backtrace = format!("{backtrace:?}");
        let location: Option<PanicLocation> = panic.location().map(|l| PanicLocation::from(*l));
        PanicData {
            message: panic,
            backtrace,
            location,
        }
    }
}

impl fmt::Display for PanicLocation {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        write!(f, "{}:{}:{}", self.file, self.line, self.column)
    }
}

impl<'a> fmt::Display for PanicData<'a> {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        match &self.location {
            Some(l) => write!(f, "{}, {} \n\n {}", self.message, l, self.backtrace),
            None => write!(f, "{} \n\n {}", self.message, self.backtrace),
        }
    }
}

fn log_panic(panic: &PanicHookInfo) {
    let data: PanicData = panic.into();
    tracing::error!("{}", data)
}

/// Setup hooks for panic, this function works for both wasm and native.
pub fn set_panic_hook() {
    // Set a panic hook that records the panic as a `tracing` event at the
    // `ERROR` verbosity level.
    //
    // If we are currently in a span when the panic occurred, the logged event
    // will include the current span, allowing the context in which the panic
    // occurred to be recorded.
    std::panic::set_hook(Box::new(|panic| {
        log_panic(panic);
    }));
}

#[cfg(feature = "node")]
/// logging configuration about node.
pub mod node {
    use tracing_subscriber::filter;
    use tracing_subscriber::fmt;
    use tracing_subscriber::EnvFilter;
    use tracing_subscriber::Layer;

    use super::*;

    /// Initializes native tracing and panic logging for the requested level.
    #[no_mangle]
    pub extern "C" fn rings_node_init_logging(level: LogLevel) {
        set_panic_hook();

        let subscriber = Registry::default();
        let level_filter = filter::LevelFilter::from_level(level.into());
        let filter = match std::env::var("RINGS_LOG_FILTER") {
            Ok(spec) if !spec.trim().is_empty() => {
                EnvFilter::try_new(spec.trim()).unwrap_or_else(|err| {
                    eprintln!(
                        "invalid RINGS_LOG_FILTER '{}': {}; falling back to {}",
                        spec, err, level_filter
                    );
                    EnvFilter::new(level_filter.to_string())
                })
            }
            _ => EnvFilter::new(level_filter.to_string()),
        };

        // Stderr
        let subscriber = subscriber.with(
            fmt::layer()
                .with_writer(std::io::stderr)
                .with_filter(filter),
        );
        // Enable log compatible layer to convert log record to tracing span.
        // We will ignore any errors that returned by this functions.
        let _ = LogTracer::init();

        // Ignore errors returned by set_global_default.
        let _ = tracing::subscriber::set_global_default(subscriber);
    }
}

#[cfg(all(feature = "browser", target_family = "wasm"))]
/// Browser console logging configuration.
pub mod browser {
    use tracing_wasm::ConsoleConfig;
    use tracing_wasm::WASMLayer;
    use tracing_wasm::WASMLayerConfigBuilder;

    use super::*;
    /// Initializes browser console tracing and panic logging for the requested level.
    #[wasm_export]
    pub fn init_logging(level: LogLevel) {
        set_panic_hook();

        let subscriber = Registry::default();

        // Browser console and profiler
        let subscriber = subscriber.with(WASMLayer::new(
            WASMLayerConfigBuilder::new()
                .set_max_level(level.into())
                .set_console_config(ConsoleConfig::ReportWithoutConsoleColor)
                .build(),
        ));

        // Enable log compatible layer to convert log record to tracing span.
        // We will ignore any errors that returned by this functions.
        let _ = LogTracer::init();

        // Ignore errors returned by set_global_default.
        let _ = tracing::subscriber::set_global_default(subscriber);
    }
}

#[cfg(test)]
mod tests {
    use super::LogLevel;

    #[test]
    fn test_default_log_level_is_error() {
        assert_eq!(LogLevel::default(), LogLevel::Error);
    }
}