lingxia 0.6.5

LingXia - Cross-platform LxApp (lightweight application) framework for Android, iOS, and HarmonyOS
use lingxia_log::{LogBuilder, LogLevel as LxLogLevel, LogManager, LogMessage, LogTag};
use log::{Level, LevelFilter, Log, Metadata, Record};
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;

const SDK_LOG_LEVEL_VERBOSE: i32 = 0;
const SDK_LOG_LEVEL_DEBUG: i32 = 1;
const SDK_LOG_LEVEL_INFO: i32 = 2;
const SDK_LOG_LEVEL_WARN: i32 = 3;
const SDK_LOG_LEVEL_ERROR: i32 = 4;

/// Error returned when installing a downstream logger fails.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum DownstreamLoggerError {
    /// A downstream logger has already been registered.
    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::Info);
    }

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

/// Registers an additional logger that receives every Rust log record emitted by LingXia.
///
/// LingXia still keeps its own platform logger and log manager. The downstream
/// logger is an observer hook for host applications that want to mirror records
/// into another sink.
pub fn register_downstream_logger(
    logger: Box<dyn Log + Send + Sync>,
) -> Result<(), DownstreamLoggerError> {
    DOWNSTREAM_LOGGER
        .set(logger)
        .map_err(|_| DownstreamLoggerError::AlreadyRegistered)
}

/// Emit a log entry from non-Rust SDK code through the Rust log pipeline.
///
/// The `level` value is the raw FFI contract: 0=verbose, 1=debug, 2=info,
/// 3=warn, 4=error. SDK-facing wrappers should hide these integer values
/// behind platform-native enums.
pub(crate) fn emit_sdk_log(
    level: i32,
    category: &str,
    appid: &str,
    path: &str,
    message: &str,
) -> bool {
    let Some(level) = map_sdk_level(level) else {
        return false;
    };
    if LogManager::get().is_none() {
        return false;
    }

    LogBuilder::new(LogTag::Native, message)
        .with_level(level)
        .with_target(category.to_string())
        .with_appid(appid.to_string())
        .with_path(path.to_string());
    true
}

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;
        }

        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 map_sdk_level(level: i32) -> Option<LxLogLevel> {
    match level {
        SDK_LOG_LEVEL_VERBOSE => Some(LxLogLevel::Verbose),
        SDK_LOG_LEVEL_DEBUG => Some(LxLogLevel::Debug),
        SDK_LOG_LEVEL_INFO => Some(LxLogLevel::Info),
        SDK_LOG_LEVEL_WARN => Some(LxLogLevel::Warn),
        SDK_LOG_LEVEL_ERROR => Some(LxLogLevel::Error),
        _ => None,
    }
}

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::Info)
                    .with_tag("Rust"),
            ),
            #[cfg(target_env = "ohos")]
            harmony: ohos_hilog::OhosLogger::new(
                ohos_hilog::Config::default()
                    .with_max_level(LevelFilter::Info)
                    .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)
}