lingxia 0.5.0

LingXia - Cross-platform LxApp (lightweight application) framework for Android, iOS, and HarmonyOS
use log::{Level, LevelFilter, Log, Metadata, Record};
use lxapp::log::{LogLevel as LxLogLevel, LogManager, LogMessage, LogTag};
use std::sync::OnceLock;

static LOGGING_INIT: OnceLock<()> = OnceLock::new();
static DOWNSTREAM_LOGGER: OnceLock<Box<dyn Log + Send + Sync>> = OnceLock::new();
static SDK_LOGGER: SdkLogger = SdkLogger;

#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum DownstreamLoggerError {
    AlreadyRegistered,
}

impl std::fmt::Display for DownstreamLoggerError {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        match self {
            Self::AlreadyRegistered => write!(f, "downstream logger is already registered"),
        }
    }
}

impl std::error::Error for DownstreamLoggerError {}

pub(crate) fn init() {
    if LOGGING_INIT.get().is_some() {
        return;
    }

    let _ = LogManager::init(|message| {
        platform_logger().write(message);
    });

    if log::set_logger(&SDK_LOGGER).is_ok() {
        log::set_max_level(LevelFilter::Trace);
    }

    let _ = LOGGING_INIT.set(());
}

pub fn register_downstream_logger(
    logger: Box<dyn Log + Send + Sync>,
) -> Result<(), DownstreamLoggerError> {
    DOWNSTREAM_LOGGER
        .set(logger)
        .map_err(|_| DownstreamLoggerError::AlreadyRegistered)
}

struct SdkLogger;

impl Log for SdkLogger {
    fn enabled(&self, metadata: &Metadata<'_>) -> bool {
        metadata.level() <= Level::Trace
    }

    fn log(&self, record: &Record<'_>) {
        if !self.enabled(record.metadata()) {
            return;
        }

        lxapp::log::LogBuilder::new(LogTag::Native, format!("{}", record.args()))
            .with_level(map_level(record.level()))
            .with_target(record.target().to_string());

        if let Some(logger) = DOWNSTREAM_LOGGER.get()
            && logger.enabled(record.metadata())
        {
            logger.log(record);
        }
    }

    fn flush(&self) {
        if let Some(logger) = DOWNSTREAM_LOGGER.get() {
            logger.flush();
        }
    }
}

fn map_level(level: Level) -> LxLogLevel {
    match level {
        Level::Error => LxLogLevel::Error,
        Level::Warn => LxLogLevel::Warn,
        Level::Info => LxLogLevel::Info,
        Level::Debug => LxLogLevel::Debug,
        Level::Trace => LxLogLevel::Verbose,
    }
}

fn format_log_message(message: &LogMessage) -> String {
    let mut prefix = String::from("[");
    prefix.push_str(message.tag.as_str());
    if let Some(appid) = message.appid.as_deref()
        && !appid.is_empty()
    {
        prefix.push(':');
        prefix.push_str(appid);
    }
    if let Some(path) = message.path.as_deref()
        && !path.is_empty()
    {
        prefix.push(':');
        prefix.push_str(path);
    }
    prefix.push(']');
    if let Some(target) = message.target.as_deref()
        && !target.is_empty()
        && target != "lingxia.lxapp"
    {
        prefix.push('[');
        prefix.push_str(target);
        prefix.push(']');
    }
    format!("{prefix} {}", message.message)
}

struct PlatformLogger {
    #[cfg(target_os = "android")]
    android: android_logger::AndroidLogger,
    #[cfg(target_env = "ohos")]
    harmony: ohos_hilog::OhosLogger,
    #[cfg(any(target_os = "ios", target_os = "macos"))]
    apple: oslog::OsLog,
}

impl PlatformLogger {
    fn new() -> Self {
        Self {
            #[cfg(target_os = "android")]
            android: android_logger::AndroidLogger::new(
                android_logger::Config::default()
                    .with_max_level(LevelFilter::Trace)
                    .with_tag("Rust"),
            ),
            #[cfg(target_env = "ohos")]
            harmony: ohos_hilog::OhosLogger::new(
                ohos_hilog::Config::default()
                    .with_max_level(LevelFilter::Trace)
                    .with_tag("LingXia.Rust"),
            ),
            #[cfg(any(target_os = "ios", target_os = "macos"))]
            apple: oslog::OsLog::new("LingXia.Rust", "sdk"),
        }
    }

    fn write(&self, message: &LogMessage) {
        let formatted = format_log_message(message);
        #[cfg(target_os = "android")]
        {
            let target = message.target.as_deref().unwrap_or("lingxia");
            let args = format_args!("{formatted}");
            let record = Record::builder()
                .args(args)
                .level(map_sdk_level_to_log_level(message.level))
                .target(target)
                .module_path(Some(target))
                .build();
            self.android.log(&record);
            return;
        }

        #[cfg(target_env = "ohos")]
        {
            let target = message.target.as_deref().unwrap_or("lingxia");
            let args = format_args!("{formatted}");
            let record = Record::builder()
                .args(args)
                .level(map_sdk_level_to_log_level(message.level))
                .target(target)
                .module_path(Some(target))
                .build();
            self.harmony.log(&record);
            return;
        }

        #[cfg(any(target_os = "ios", target_os = "macos"))]
        {
            use oslog::Level as OsLevel;
            let level = match message.level {
                LxLogLevel::Verbose | LxLogLevel::Debug => OsLevel::Debug,
                LxLogLevel::Info => OsLevel::Info,
                LxLogLevel::Warn => OsLevel::Error,
                LxLogLevel::Error => OsLevel::Fault,
            };
            self.apple.with_level(level, &formatted);
            return;
        }

        #[cfg(not(any(
            target_os = "android",
            target_os = "ios",
            target_os = "macos",
            target_env = "ohos"
        )))]
        {
            eprintln!("{formatted}");
        }
    }
}

#[cfg(any(target_os = "android", target_env = "ohos"))]
fn map_sdk_level_to_log_level(level: LxLogLevel) -> Level {
    match level {
        LxLogLevel::Verbose => Level::Trace,
        LxLogLevel::Debug => Level::Debug,
        LxLogLevel::Info => Level::Info,
        LxLogLevel::Warn => Level::Warn,
        LxLogLevel::Error => Level::Error,
    }
}

fn platform_logger() -> &'static PlatformLogger {
    static PLATFORM_LOGGER: OnceLock<PlatformLogger> = OnceLock::new();
    PLATFORM_LOGGER.get_or_init(PlatformLogger::new)
}