|
1 | 1 | //! Foreign-implemented log sink. Hosts (JS, Kotlin, Swift) register |
2 | | -//! an implementation via `set_logger`; rs-core code that calls |
3 | | -//! `log_msg` forwards messages to it. Cross-target by construction — |
4 | | -//! native targets and wasm both reach the host through the same FFI |
5 | | -//! callback uniffi already builds for us. |
| 2 | +//! an implementation via `set_logger`; rs-core code logs through the |
| 3 | +//! leveled helpers below. Cross-target by construction — native targets |
| 4 | +//! and wasm both reach the host through the same FFI callback uniffi |
| 5 | +//! already builds for us. |
| 6 | +//! |
| 7 | +//! Every message crosses the FFI boundary as a synchronous host call, so a |
| 8 | +//! burst of messages can stall the host's thread (on React Native, each call |
| 9 | +//! marshals across the bridge into a `console.log`). Two guards keep that in |
| 10 | +//! check: |
| 11 | +//! * **Level filtering** — messages below the host-configured threshold |
| 12 | +//! ([`set_log_level`], default [`LogLevel::Info`]) are dropped in Rust and |
| 13 | +//! never cross FFI. The message string isn't even formatted (the helpers |
| 14 | +//! take a closure), so filtered logging is nearly free. |
| 15 | +//! * Hosts are still expected to make their sink non-blocking (batch/drop) |
| 16 | +//! for defense in depth. |
6 | 17 |
|
| 18 | +use std::sync::atomic::{AtomicU8, Ordering}; |
7 | 19 | use std::sync::{Arc, Mutex}; |
8 | 20 |
|
| 21 | +/// Severity of a log message. Hosts set a minimum threshold via |
| 22 | +/// [`set_log_level`]; anything below it is dropped before crossing FFI. |
| 23 | +#[derive(Clone, Copy, PartialEq, Eq, Debug, uniffi::Enum)] |
| 24 | +pub enum LogLevel { |
| 25 | + Trace, |
| 26 | + Debug, |
| 27 | + Info, |
| 28 | + Warn, |
| 29 | + Error, |
| 30 | + /// Disables all logging. |
| 31 | + Off, |
| 32 | +} |
| 33 | + |
| 34 | +impl LogLevel { |
| 35 | + fn rank(self) -> u8 { |
| 36 | + match self { |
| 37 | + LogLevel::Trace => 0, |
| 38 | + LogLevel::Debug => 1, |
| 39 | + LogLevel::Info => 2, |
| 40 | + LogLevel::Warn => 3, |
| 41 | + LogLevel::Error => 4, |
| 42 | + LogLevel::Off => 5, |
| 43 | + } |
| 44 | + } |
| 45 | +} |
| 46 | + |
9 | 47 | #[uniffi::export(with_foreign)] |
10 | 48 | pub trait Logger: Send + Sync { |
11 | 49 | fn log(&self, message: String); |
12 | 50 | } |
13 | 51 |
|
14 | 52 | static LOGGER: Mutex<Option<Arc<dyn Logger>>> = Mutex::new(None); |
| 53 | +/// Minimum level that crosses FFI. Defaults to `Info` so debug/trace |
| 54 | +/// floods are dropped unless a host explicitly opts in. |
| 55 | +static MIN_LEVEL: AtomicU8 = AtomicU8::new(2); |
15 | 56 |
|
16 | 57 | /// Register the foreign logger. Replaces any previously-set value. |
17 | 58 | #[uniffi::export] |
18 | 59 | pub fn set_logger(logger: Arc<dyn Logger>) { |
19 | 60 | *LOGGER.lock().unwrap() = Some(logger); |
20 | 61 | } |
21 | 62 |
|
22 | | -/// Forward `message` to the registered foreign logger (if any). The |
23 | | -/// Arc is cloned out of the mutex before the foreign call so a |
24 | | -/// re-entrant logger impl can't deadlock against `set_logger`. |
25 | | -pub(crate) fn log_msg(message: impl Into<String>) { |
| 63 | +/// Set the minimum level forwarded to the host. Messages below this are |
| 64 | +/// dropped in Rust without crossing the FFI boundary. |
| 65 | +#[uniffi::export] |
| 66 | +pub fn set_log_level(level: LogLevel) { |
| 67 | + MIN_LEVEL.store(level.rank(), Ordering::Relaxed); |
| 68 | +} |
| 69 | + |
| 70 | +/// Log at `level`. The message is only built (and only crosses FFI) when |
| 71 | +/// `level` is at or above the configured threshold — pass a closure so the |
| 72 | +/// `format!` cost is skipped for filtered messages. |
| 73 | +pub(crate) fn log_at(level: LogLevel, message: impl FnOnce() -> String) { |
| 74 | + if level.rank() < MIN_LEVEL.load(Ordering::Relaxed) { |
| 75 | + return; |
| 76 | + } |
| 77 | + // Clone the Arc out of the mutex before the foreign call so a |
| 78 | + // re-entrant logger impl can't deadlock against `set_logger`. |
26 | 79 | let logger = LOGGER.lock().unwrap().clone(); |
27 | 80 | if let Some(l) = logger { |
28 | | - l.log(message.into()); |
| 81 | + l.log(message()); |
29 | 82 | } |
30 | 83 | } |
| 84 | + |
| 85 | +pub(crate) fn log_debug(message: impl FnOnce() -> String) { |
| 86 | + log_at(LogLevel::Debug, message); |
| 87 | +} |
| 88 | + |
| 89 | +#[allow(dead_code)] |
| 90 | +pub(crate) fn log_info(message: impl FnOnce() -> String) { |
| 91 | + log_at(LogLevel::Info, message); |
| 92 | +} |
| 93 | + |
| 94 | +#[allow(dead_code)] |
| 95 | +pub(crate) fn log_warn(message: impl FnOnce() -> String) { |
| 96 | + log_at(LogLevel::Warn, message); |
| 97 | +} |
0 commit comments