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