diff --git a/.buildconfig-android.yml b/.buildconfig-android.yml index 9cd18f89c..e2876abc8 100644 --- a/.buildconfig-android.yml +++ b/.buildconfig-android.yml @@ -50,6 +50,13 @@ projects: - name: rustlog type: aar description: Android hook into the log crate. + rust-log-forwarder: + path: components/support/rust-log-forwarder/android + artifactId: rust-log-forwarder + publications: + - name: rust-log-forwarder + type: aar + description: Forward logs from Rust httpconfig: path: components/viaduct/android artifactId: httpconfig diff --git a/Cargo.lock b/Cargo.lock index 7f4a9cbd2..6b3a153e3 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1792,6 +1792,7 @@ dependencies = [ "places", "push", "rc_log_ffi", + "rust-log-forwarder", "sync_manager", "tabs", "viaduct", @@ -1821,6 +1822,7 @@ dependencies = [ "places", "push", "rc_log_ffi", + "rust-log-forwarder", "sync15", "tabs", "viaduct", @@ -2923,6 +2925,15 @@ dependencies = [ "crossbeam-utils", ] +[[package]] +name = "rust-log-forwarder" +version = "0.1.0" +dependencies = [ + "log", + "parking_lot", + "uniffi", +] + [[package]] name = "rustc-demangle" version = "0.1.21" diff --git a/Cargo.toml b/Cargo.toml index deb37b6de..a735b6a00 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -23,6 +23,7 @@ members = [ "components/support/rc_crypto/nss/nss_build_common", "components/support/rc_crypto/nss/nss_sys", "components/support/rc_crypto/nss/systest", + "components/support/rust-log-forwarder", "components/support/sql", "components/support/types", "components/support/viaduct-reqwest", diff --git a/components/support/rust-log-forwarder/Cargo.toml b/components/support/rust-log-forwarder/Cargo.toml new file mode 100644 index 000000000..fbde5ad78 --- /dev/null +++ b/components/support/rust-log-forwarder/Cargo.toml @@ -0,0 +1,15 @@ +[package] +name = "rust-log-forwarder" +version = "0.1.0" +edition = "2021" +authors = ["application-services@mozilla.com"] +license = "MPL-2.0" +exclude = ["/android", "/ios"] + +[dependencies] +log = "0.4" +parking_lot = ">=0.11,<=0.12" +uniffi = "0.23" + +[build-dependencies] +uniffi = { version = "0.23", features = ["build"] } diff --git a/components/support/rust-log-forwarder/android/build.gradle b/components/support/rust-log-forwarder/android/build.gradle new file mode 100644 index 000000000..021ada54a --- /dev/null +++ b/components/support/rust-log-forwarder/android/build.gradle @@ -0,0 +1,7 @@ + +apply from: "$rootDir/build-scripts/component-common.gradle" +apply from: "$rootDir/publish.gradle" + +ext.configureUniFFIBindgen("../src/rust_log_forwarder.udl") +ext.dependsOnTheMegazord() +ext.configurePublish() diff --git a/components/support/rust-log-forwarder/android/src/main/AndroidManifest.xml b/components/support/rust-log-forwarder/android/src/main/AndroidManifest.xml new file mode 100644 index 000000000..f81fee428 --- /dev/null +++ b/components/support/rust-log-forwarder/android/src/main/AndroidManifest.xml @@ -0,0 +1,2 @@ + diff --git a/components/support/rust-log-forwarder/build.rs b/components/support/rust-log-forwarder/build.rs new file mode 100644 index 000000000..66f9679d4 --- /dev/null +++ b/components/support/rust-log-forwarder/build.rs @@ -0,0 +1,8 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. + */ + +fn main() { + uniffi::generate_scaffolding("./src/rust_log_forwarder.udl").unwrap(); +} diff --git a/components/support/rust-log-forwarder/src/foreign_logger.rs b/components/support/rust-log-forwarder/src/foreign_logger.rs new file mode 100644 index 000000000..d4066763a --- /dev/null +++ b/components/support/rust-log-forwarder/src/foreign_logger.rs @@ -0,0 +1,32 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +//! Logger interface for foreign code +//! +//! This is what the application code defines. It's responsible for taking rust log records and +//! feeding them to the application logging system. + +pub use log::Level; + +/// log::Record, except it exposes it's data as fields rather than methods +#[derive(Debug, PartialEq, Eq)] +pub struct Record { + pub level: Level, + pub target: String, + pub message: String, +} + +pub trait Logger: Sync + Send { + fn log(&self, record: Record); +} + +impl From<&log::Record<'_>> for Record { + fn from(record: &log::Record) -> Self { + Self { + level: record.level(), + target: record.target().to_string(), + message: record.args().to_string(), + } + } +} diff --git a/components/support/rust-log-forwarder/src/lib.rs b/components/support/rust-log-forwarder/src/lib.rs new file mode 100644 index 000000000..6f09afb6f --- /dev/null +++ b/components/support/rust-log-forwarder/src/lib.rs @@ -0,0 +1,114 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +use std::sync::atomic::{AtomicBool, Ordering}; +mod foreign_logger; +mod rust_logger; + +pub use foreign_logger::{Level, Logger, Record}; + +static HAVE_SET_MAX_LEVEL: AtomicBool = AtomicBool::new(false); + +/// Set the logger to forward to. +/// +/// Pass in None to disable logging. +pub fn set_logger(logger: Option>) { + // Set a default max level, if none has already been set + if !HAVE_SET_MAX_LEVEL.load(Ordering::Relaxed) { + set_max_level(Level::Debug); + } + rust_logger::set_foreign_logger(logger) +} + +/// Set the maximum log level filter. Records below this level will not be sent to the logger. +pub fn set_max_level(level: Level) { + log::set_max_level(level.to_level_filter()); + HAVE_SET_MAX_LEVEL.store(true, Ordering::Relaxed); +} + +uniffi::include_scaffolding!("rust_log_forwarder"); + +#[cfg(test)] +mod test { + use super::*; + use std::sync::{Arc, Mutex}; + + #[derive(Clone)] + struct TestLogger { + records: Arc>>, + } + + impl TestLogger { + fn new() -> Self { + Self { + records: Arc::new(Mutex::new(Vec::new())), + } + } + + fn check_records(&self, correct_records: Vec) { + assert_eq!(*self.records.lock().unwrap(), correct_records); + } + + fn clear_records(&self) { + self.records.lock().unwrap().clear() + } + } + + impl Logger for TestLogger { + fn log(&self, record: Record) { + self.records.lock().unwrap().push(record) + } + } + + #[test] + fn test_logging() { + let logger = TestLogger::new(); + set_logger(Some(Box::new(logger.clone()))); + log::info!("Test message"); + log::warn!("Test message2"); + logger.check_records(vec![ + Record { + level: Level::Info, + target: "rust_log_forwarder::test".into(), + message: "Test message".into(), + }, + Record { + level: Level::Warn, + target: "rust_log_forwarder::test".into(), + message: "Test message2".into(), + }, + ]); + logger.clear_records(); + set_logger(None); + log::info!("Test message"); + log::warn!("Test message2"); + logger.check_records(vec![]); + } + + #[test] + fn test_max_level() { + set_max_level(Level::Debug); + assert_eq!(log::max_level(), log::Level::Debug); + set_max_level(Level::Warn); + assert_eq!(log::max_level(), log::Level::Warn); + } + + #[test] + fn test_max_level_default() { + HAVE_SET_MAX_LEVEL.store(false, Ordering::Relaxed); + let logger = TestLogger::new(); + // Calling set_logger should set the level to `Debug' by default + set_logger(Some(Box::new(logger))); + assert_eq!(log::max_level(), log::Level::Debug); + } + + #[test] + fn test_max_level_default_ignored_if_set_manually() { + HAVE_SET_MAX_LEVEL.store(false, Ordering::Relaxed); + set_max_level(Level::Warn); + // Calling set_logger should not set the level if it was set manually. + set_logger(Some(Box::new(TestLogger::new()))); + assert_eq!(log::max_level(), log::Level::Warn); + } +} diff --git a/components/support/rust-log-forwarder/src/rust_log_forwarder.udl b/components/support/rust-log-forwarder/src/rust_log_forwarder.udl new file mode 100644 index 000000000..bd3825694 --- /dev/null +++ b/components/support/rust-log-forwarder/src/rust_log_forwarder.udl @@ -0,0 +1,31 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +namespace rust_log_forwarder { + // Set the logger to forward to. + // + // Pass in null to disable logging. + void set_logger(Logger? logger); + // Set the maximum log level filter. Records below this level will not be sent to the logger. + void set_max_level(Level level); +}; + +enum Level { + "Error", + "Warn", + "Info", + "Debug", + "Trace", +}; + +dictionary Record { + Level level; + // The target field from the Rust log crate. Usually the Rust module name, however log! calls can manually override the target name. + string target; + string message; +}; + +callback interface Logger { + void log(Record record); +}; diff --git a/components/support/rust-log-forwarder/src/rust_logger.rs b/components/support/rust-log-forwarder/src/rust_logger.rs new file mode 100644 index 000000000..978576f2d --- /dev/null +++ b/components/support/rust-log-forwarder/src/rust_logger.rs @@ -0,0 +1,65 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +//! Rust Logger implementation +//! +//! This is responsible for taking logs from the rust log crate and forwarding them to a +//! foreign_logger::Logger instance. + +use crate::foreign_logger::Logger as ForeignLogger; +use parking_lot::RwLock; +use std::sync::{ + atomic::{AtomicBool, Ordering}, + Once, +}; + +// ForeignLogger to forward to +static RUST_LOGGER: Logger = Logger::new(); +// Handles calling `log::set_logger`, which can only be called once. +static INIT: Once = Once::new(); + +struct Logger { + foreign_logger: RwLock>>, + is_enabled: AtomicBool, +} + +impl Logger { + const fn new() -> Self { + Self { + foreign_logger: RwLock::new(None), + is_enabled: AtomicBool::new(false), + } + } + + fn set_foreign_logger(&self, foreign_logger: Option>) { + self.is_enabled + .store(foreign_logger.is_some(), Ordering::Relaxed); + *self.foreign_logger.write() = foreign_logger; + } +} + +impl log::Log for Logger { + fn enabled(&self, _: &log::Metadata<'_>) -> bool { + self.is_enabled.load(Ordering::Relaxed) + } + + fn log(&self, record: &log::Record<'_>) { + if let Some(foreign_logger) = &*self.foreign_logger.read() { + foreign_logger.log(record.into()) + } + } + + fn flush(&self) {} +} + +pub fn set_foreign_logger(foreign_logger: Option>) { + INIT.call_once(|| { + // This should be the only component that calls `log::set_logger()`. If not, then + // panic'ing seems reasonable. + log::set_logger(&RUST_LOGGER).expect( + "Failed to initialize rust-log-forwarder::Logger, other log implementation already initialized?", + ); + }); + RUST_LOGGER.set_foreign_logger(foreign_logger); +} diff --git a/components/support/rust-log-forwarder/uniffi.toml b/components/support/rust-log-forwarder/uniffi.toml new file mode 100644 index 000000000..111c08461 --- /dev/null +++ b/components/support/rust-log-forwarder/uniffi.toml @@ -0,0 +1,8 @@ +[bindings.kotlin] +package_name = "mozilla.appservices.rust_log_forwarder" +cdylib_name = "megazord" + +[bindings.swift] +ffi_module_name = "MozillaRustComponents" +ffi_module_filename = "rustlogforwarderFFI" +generate_module_map = false diff --git a/megazords/full/Cargo.toml b/megazords/full/Cargo.toml index 96b957ee7..ee0b4e97e 100644 --- a/megazords/full/Cargo.toml +++ b/megazords/full/Cargo.toml @@ -16,6 +16,7 @@ sync_manager = { path = "../../components/sync_manager/" } places = { path = "../../components/places" } push = { path = "../../components/push" } rc_log_ffi = { path = "../../components/rc_log" } +rust-log-forwarder = { path = "../../components/support/rust-log-forwarder" } viaduct = { path = "../../components/viaduct" } nimbus-sdk = { path = "../../components/nimbus" } autofill = { path = "../../components/autofill" } diff --git a/megazords/full/src/lib.rs b/megazords/full/src/lib.rs index 7d368bf46..e99a4303a 100644 --- a/megazords/full/src/lib.rs +++ b/megazords/full/src/lib.rs @@ -16,7 +16,10 @@ pub use logins; pub use nimbus; pub use places; pub use push; +// TODO: Drop this dependency once android-components switches to using `rust_log_forwarder` for +// log forwarding. pub use rc_log_ffi; +pub use rust_log_forwarder; pub use sync_manager; pub use tabs; pub use viaduct; diff --git a/megazords/ios-rust/Cargo.toml b/megazords/ios-rust/Cargo.toml index cb01efaf9..ae489b722 100644 --- a/megazords/ios-rust/Cargo.toml +++ b/megazords/ios-rust/Cargo.toml @@ -10,6 +10,7 @@ crate-type = ["staticlib"] [dependencies] rc_log_ffi = { path = "../../components/rc_log" } +rust-log-forwarder = { path = "../../components/support/rust-log-forwarder" } viaduct = { path = "../../components/viaduct" } viaduct-reqwest = { path = "../../components/support/viaduct-reqwest" } nimbus-sdk = { path = "../../components/nimbus" } diff --git a/megazords/ios-rust/src/lib.rs b/megazords/ios-rust/src/lib.rs index eaa0230c3..49902ec7c 100644 --- a/megazords/ios-rust/src/lib.rs +++ b/megazords/ios-rust/src/lib.rs @@ -13,7 +13,10 @@ pub use logins; pub use nimbus; pub use places; pub use push; +// TODO: Drop this dependency once firefox-ios switches to using `rust_log_forwarder` for log +// forwarding. pub use rc_log_ffi; +pub use rust_log_forwarder; pub use sync15; pub use tabs; pub use viaduct_reqwest;