core_crypto_ffi/core_crypto/
logger.rs

1#[cfg(not(target_family = "wasm"))]
2use std::sync::Arc;
3use std::{
4    collections::BTreeMap,
5    ops::Deref as _,
6    sync::{LazyLock, Once},
7};
8
9use log::{
10    Level, LevelFilter, Metadata, Record,
11    kv::{Key, Value, VisitSource},
12};
13use log_reload::ReloadLog;
14#[cfg(target_family = "wasm")]
15use wasm_bindgen::prelude::*;
16
17#[cfg(target_family = "wasm")]
18use crate::{CoreCrypto, CoreCryptoError, CoreCryptoResult};
19
20/// Defines the log level for a CoreCrypto
21#[derive(Debug, Clone, Copy)]
22#[cfg_attr(target_family = "wasm", wasm_bindgen)]
23#[cfg_attr(not(target_family = "wasm"), derive(uniffi::Enum))]
24#[repr(u8)]
25pub enum CoreCryptoLogLevel {
26    Off = 1,
27    Trace,
28    Debug,
29    Info,
30    Warn,
31    Error,
32}
33
34impl From<CoreCryptoLogLevel> for LevelFilter {
35    fn from(value: CoreCryptoLogLevel) -> LevelFilter {
36        match value {
37            CoreCryptoLogLevel::Off => LevelFilter::Off,
38            CoreCryptoLogLevel::Trace => LevelFilter::Trace,
39            CoreCryptoLogLevel::Debug => LevelFilter::Debug,
40            CoreCryptoLogLevel::Info => LevelFilter::Info,
41            CoreCryptoLogLevel::Warn => LevelFilter::Warn,
42            CoreCryptoLogLevel::Error => LevelFilter::Error,
43        }
44    }
45}
46
47impl From<Level> for CoreCryptoLogLevel {
48    fn from(value: Level) -> Self {
49        match value {
50            Level::Warn => CoreCryptoLogLevel::Warn,
51            Level::Error => CoreCryptoLogLevel::Error,
52            Level::Info => CoreCryptoLogLevel::Info,
53            Level::Debug => CoreCryptoLogLevel::Debug,
54            Level::Trace => CoreCryptoLogLevel::Trace,
55        }
56    }
57}
58
59#[cfg(not(target_family = "wasm"))]
60#[derive(Debug, thiserror::Error, uniffi::Error)]
61#[uniffi(flat_error)]
62pub enum LoggingError {
63    #[error("panic or otherwise unexpected error from foreign code")]
64    Ffi(#[from] uniffi::UnexpectedUniFFICallbackError),
65}
66
67/// This trait is used to provide a callback mechanism to hook up the respective platform logging system.
68#[cfg(not(target_family = "wasm"))]
69#[uniffi::export(with_foreign)]
70pub trait CoreCryptoLogger: std::fmt::Debug + Send + Sync {
71    /// Core Crypto will call this method whenever it needs to log a message.
72    ///
73    /// This function catches panics and other unexpected errors. In those cases, it writes to `stderr`.
74    fn log(&self, level: CoreCryptoLogLevel, message: String, context: Option<String>) -> Result<(), LoggingError>;
75}
76
77/// This struct stores a log function and its `this` value, which should be a class instance if defined.
78#[cfg(target_family = "wasm")]
79#[wasm_bindgen]
80#[derive(Debug, Clone, Default)]
81pub struct CoreCryptoLogger {
82    logger: js_sys::Function,
83    this: JsValue,
84}
85
86#[cfg(target_family = "wasm")]
87#[wasm_bindgen]
88impl CoreCryptoLogger {
89    /// Construct a new logger.
90    ///
91    /// The logger itself is a function which accepts three arguments:
92    /// `(level, message, context)`.
93    ///
94    /// The `this` value is the class which should be passed as `this` in the context of that function;
95    /// this is typically useful if the function is a class method instead of a free function.
96    /// It is normal and legal to pass `null` as the `this` value.
97    #[wasm_bindgen(constructor)]
98    pub fn new(logger: js_sys::Function, this: JsValue) -> CoreCryptoResult<Self> {
99        if logger.length() != 3 {
100            return Err(CoreCryptoError::ad_hoc(format!(
101                "logger function must accept 3 arguments but accepts {}",
102                logger.length()
103            )));
104        }
105        // let this = this.unwrap_or(JsValue::NULL);
106        Ok(Self { logger, this })
107    }
108}
109
110#[cfg(target_family = "wasm")]
111impl CoreCryptoLogger {
112    fn log(&self, level: CoreCryptoLogLevel, message: String, context: Option<String>) {
113        if let Err(meta_err) = self
114            .logger
115            .call3(&self.this, &level.into(), &(&message).into(), &context.into())
116        {
117            web_sys::console::error_2(&meta_err, &message.into());
118        }
119    }
120}
121
122#[cfg(target_family = "wasm")]
123// SAFETY: WASM only ever runs in a single-threaded context, so this is intrinsically thread-safe.
124// If that invariant ever varies, we may need to rethink this (but more likely that would be addressed
125// upstream where the types are defined).
126unsafe impl Send for CoreCryptoLogger {}
127#[cfg(target_family = "wasm")]
128// SAFETY: WASM only ever runs in a single-threaded context, so this is intrinsically thread-safe.
129unsafe impl Sync for CoreCryptoLogger {}
130
131/// The dummy logger is a suitable default value for the log shim
132#[cfg(not(target_family = "wasm"))]
133#[derive(Debug)]
134struct DummyLogger;
135
136#[cfg(not(target_family = "wasm"))]
137impl CoreCryptoLogger for DummyLogger {
138    #[allow(unused_variables)]
139    fn log(&self, level: CoreCryptoLogLevel, message: String, context: Option<String>) -> Result<(), LoggingError> {
140        Ok(())
141    }
142}
143
144/// The uniffi log shim is a simple wrapper around the foreign implementer of the trait
145#[cfg(not(target_family = "wasm"))]
146#[derive(Clone, derive_more::Constructor)]
147struct LogShim {
148    logger: Arc<dyn CoreCryptoLogger>,
149}
150
151#[cfg(not(target_family = "wasm"))]
152impl Default for LogShim {
153    fn default() -> Self {
154        Self {
155            logger: Arc::new(DummyLogger),
156        }
157    }
158}
159
160#[cfg(not(target_family = "wasm"))]
161impl LogShim {
162    fn adjusted_log_level(&self, metadata: &Metadata) -> Level {
163        match (metadata.level(), metadata.target()) {
164            // increase log level for refinery_core::traits since they are too verbose in transactions
165            (level, "refinery_core::traits") if level >= Level::Info => Level::Debug,
166            (level, "refinery_core::traits::sync") if level >= Level::Info => Level::Debug,
167            (level, _) => level,
168        }
169    }
170}
171
172#[cfg(target_family = "wasm")]
173#[derive(Clone, Default)]
174struct LogShim {
175    logger: CoreCryptoLogger,
176}
177
178#[cfg(target_family = "wasm")]
179impl LogShim {
180    fn new(logger: CoreCryptoLogger) -> Self {
181        Self { logger }
182    }
183}
184
185impl log::Log for LogShim {
186    #[cfg_attr(target_family = "wasm", expect(unused_variables))]
187    fn enabled(&self, metadata: &Metadata) -> bool {
188        cfg_if::cfg_if! {
189        if #[cfg(target_family = "wasm")] {
190            true
191        } else {
192            log::max_level() >= self.adjusted_log_level(metadata)
193        }}
194    }
195
196    fn log(&self, record: &Record) {
197        struct KeyValueVisitor<'kvs>(BTreeMap<Key<'kvs>, Value<'kvs>>);
198
199        impl<'kvs> VisitSource<'kvs> for KeyValueVisitor<'kvs> {
200            #[inline]
201            fn visit_pair(&mut self, key: Key<'kvs>, value: Value<'kvs>) -> Result<(), log::kv::Error> {
202                self.0.insert(key, value);
203                Ok(())
204            }
205        }
206
207        let kvs = record.key_values();
208        let mut visitor = KeyValueVisitor(BTreeMap::new());
209        let _ = kvs.visit(&mut visitor);
210
211        if !self.enabled(record.metadata()) {
212            return;
213        }
214
215        let message = format!("{}", record.args());
216        let context = serde_json::to_string(&visitor.0).ok();
217
218        // uniffi-style
219        #[cfg(not(target_family = "wasm"))]
220        {
221            let log_result = self.logger.log(
222                CoreCryptoLogLevel::from(self.adjusted_log_level(record.metadata())),
223                message.clone(),
224                context,
225            );
226            if let Err(LoggingError::Ffi(meta_err @ uniffi::UnexpectedUniFFICallbackError { .. })) = log_result {
227                eprintln!("{meta_err} while attempting to produce {message}");
228            }
229        }
230
231        // wasm-style
232        #[cfg(target_family = "wasm")]
233        {
234            self.logger.log(record.metadata().level().into(), message, context);
235        }
236    }
237
238    fn flush(&self) {}
239}
240
241static INIT_LOGGER: Once = Once::new();
242static LOGGER: LazyLock<ReloadLog<LogShim>> = LazyLock::new(|| ReloadLog::new(LogShim::default()));
243
244/// In uniffi the logger interface is a boxed trait instance
245#[cfg(not(target_family = "wasm"))]
246type Logger = Arc<dyn CoreCryptoLogger>;
247
248#[cfg(target_family = "wasm")]
249type Logger = CoreCryptoLogger;
250
251/// Initializes the logger
252///
253/// NOTE: in a future  release we will remove `level` argument.
254#[cfg(not(target_family = "wasm"))]
255#[uniffi::export]
256pub fn set_logger(logger: Arc<dyn CoreCryptoLogger>, level: CoreCryptoLogLevel) {
257    set_logger_only_inner(logger);
258    set_max_log_level(level);
259}
260
261fn set_logger_only_inner(logger: Logger) {
262    LOGGER
263        .handle()
264        .replace(LogShim::new(logger))
265        .expect("no poisoned locks should be possible as we never panic while holding the lock");
266
267    INIT_LOGGER.call_once(|| {
268        log::set_logger(LOGGER.deref())
269            .expect("no poisoned locks should be possible as we never panic while holding the lock");
270        log::set_max_level(LevelFilter::Warn);
271    });
272}
273
274/// Initializes the logger
275#[cfg(not(target_family = "wasm"))]
276#[uniffi::export]
277pub fn set_logger_only(logger: Logger) {
278    set_logger_only_inner(logger);
279}
280
281/// Set maximum log level forwarded to the logger
282#[cfg(not(target_family = "wasm"))]
283#[uniffi::export]
284pub fn set_max_log_level(level: CoreCryptoLogLevel) {
285    log::set_max_level(level.into());
286}
287
288#[cfg(target_family = "wasm")]
289#[wasm_bindgen]
290impl CoreCrypto {
291    pub fn set_logger(logger: CoreCryptoLogger) {
292        set_logger_only_inner(logger);
293    }
294
295    pub fn set_max_log_level(level: CoreCryptoLogLevel) {
296        log::set_max_level(level.into());
297    }
298}