Consistent Rust logger initialization for host and device
Implement a universal logger interface that allows logging both
on-device (using android_logger) and on-host (using env_logger).
Uses a configuration struct similar to the one used by android_logger.
Allows using the same logging initialization code for on-device
code and host-side tests.
Bug: 162454083
Test: atest system/logging/rust
+ For device tests: confirm logging happens in logcat
+ For host tests: confirm logging happens to stdout (for failing test)
Change-Id: If80685033d2b39c660fab881405456f629fb1f8b
diff --git a/rust/Android.bp b/rust/Android.bp
new file mode 100644
index 0000000..f4e798e
--- /dev/null
+++ b/rust/Android.bp
@@ -0,0 +1,94 @@
+rust_library {
+ name: "liblogger",
+ host_supported: true,
+ crate_name: "logger",
+ srcs: ["logger.rs"],
+ rustlibs: [
+ "libenv_logger",
+ "liblog_rust",
+ ],
+ target: {
+ android: {
+ rustlibs: [
+ "libandroid_logger",
+ ]
+ },
+ },
+}
+
+rust_defaults {
+ name: "liblogger_test_defaults",
+ crate_name: "logger",
+ test_suites: ["general-tests"],
+ auto_gen_config: true,
+ rustlibs: [
+ "liblogger",
+ "liblog_rust",
+ ]
+}
+
+rust_test {
+ name: "logger_device_unit_tests",
+ defaults: ["liblogger_test_defaults"],
+ srcs: ["logger.rs"],
+ rustlibs: [
+ "libenv_logger",
+ "libandroid_logger"
+ ]
+}
+
+rust_test_host {
+ name: "logger_host_unit_tests",
+ defaults: ["liblogger_test_defaults"],
+ srcs: ["logger.rs"],
+ rustlibs: ["libenv_logger"]
+}
+
+// The following tests are each run as separate targets because they all require a clean init state.
+rust_test {
+ name: "logger_device_test_default_init",
+ defaults: ["liblogger_test_defaults"],
+ srcs: ["tests/default_init.rs"],
+}
+
+rust_test_host {
+ name: "logger_host_test_default_init",
+ defaults: ["liblogger_test_defaults"],
+ srcs: ["tests/default_init.rs"],
+}
+
+rust_test {
+ name: "logger_device_test_env_log_level",
+ defaults: ["liblogger_test_defaults"],
+ srcs: ["tests/env_log_level.rs"],
+}
+
+rust_test_host {
+ name: "logger_host_test_env_log_level",
+ defaults: ["liblogger_test_defaults"],
+ srcs: ["tests/env_log_level.rs"],
+}
+
+rust_test {
+ name: "logger_device_test_config_log_level",
+ defaults: ["liblogger_test_defaults"],
+ srcs: ["tests/config_log_level.rs"],
+}
+
+rust_test_host {
+ name: "logger_host_test_config_log_level",
+ defaults: ["liblogger_test_defaults"],
+ srcs: ["tests/config_log_level.rs"],
+}
+
+rust_test {
+ name: "logger_device_test_multiple_init",
+ defaults: ["liblogger_test_defaults"],
+ srcs: ["tests/multiple_init.rs"],
+}
+
+rust_test_host {
+ name: "logger_host_test_multiple_init",
+ defaults: ["liblogger_test_defaults"],
+ srcs: ["tests/multiple_init.rs"],
+}
diff --git a/rust/OWNERS b/rust/OWNERS
new file mode 100644
index 0000000..19042bc
--- /dev/null
+++ b/rust/OWNERS
@@ -0,0 +1 @@
+include platform/prebuilts/rust:/OWNERS
\ No newline at end of file
diff --git a/rust/PREUPLOAD.cfg b/rust/PREUPLOAD.cfg
new file mode 100644
index 0000000..68b351e
--- /dev/null
+++ b/rust/PREUPLOAD.cfg
@@ -0,0 +1,5 @@
+[Builtin Hooks]
+rustfmt = true
+
+[Builtin Hooks Options]
+rustfmt = --config-path=rustfmt.toml
\ No newline at end of file
diff --git a/rust/TEST_MAPPING b/rust/TEST_MAPPING
new file mode 100644
index 0000000..6f4be05
--- /dev/null
+++ b/rust/TEST_MAPPING
@@ -0,0 +1,21 @@
+// Host tests are run automatically in presubmit.
+// This file includes all device tests, as they test different behavior.
+{
+ "presubmit": [
+ {
+ "name": "logger_device_unit_tests"
+ },
+ {
+ "name": "logger_device_test_default_init"
+ },
+ {
+ "name": "logger_device_test_env_log_level"
+ },
+ {
+ "name": "logger_device_test_config_log_level"
+ },
+ {
+ "name": "logger_device_test_multiple_init"
+ }
+ ]
+}
\ No newline at end of file
diff --git a/rust/logger.rs b/rust/logger.rs
new file mode 100644
index 0000000..19deda1
--- /dev/null
+++ b/rust/logger.rs
@@ -0,0 +1,176 @@
+// Copyright 2021, The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+//! Provides a universal logger interface that allows logging both on-device (using android_logger)
+//! and on-host (using env_logger).
+//! On-host, this allows the use of the RUST_LOG environment variable as documented in
+//! https://docs.rs/env_logger.
+use std::ffi::CString;
+use std::sync::atomic::{AtomicBool, Ordering};
+
+static LOGGER_INITIALIZED: AtomicBool = AtomicBool::new(false);
+
+type FormatFn = Box<dyn Fn(&log::Record) -> String + Sync + Send>;
+
+/// Logger configuration, opportunistically mapped to configuration parameters for android_logger
+/// or env_logger where available.
+#[derive(Default)]
+pub struct Config<'a> {
+ log_level: Option<log::Level>,
+ custom_format: Option<FormatFn>,
+ filter: Option<&'a str>,
+ #[allow(dead_code)] // Field is only used on device, and ignored on host.
+ tag: Option<CString>,
+}
+
+/// Based on android_logger::Config
+impl<'a> Config<'a> {
+ /// Change the minimum log level.
+ ///
+ /// All values above the set level are logged. For example, if
+ /// `Warn` is set, the `Error` is logged too, but `Info` isn't.
+ pub fn with_min_level(mut self, level: log::Level) -> Self {
+ self.log_level = Some(level);
+ self
+ }
+
+ /// Set a log tag. Only used on device.
+ pub fn with_tag_on_device<S: Into<Vec<u8>>>(mut self, tag: S) -> Self {
+ self.tag = Some(CString::new(tag).expect("Can't convert tag to CString"));
+ self
+ }
+
+ /// Set the format function for formatting the log output.
+ /// ```
+ /// # use universal_logger::Config;
+ /// universal_logger::init(
+ /// Config::default()
+ /// .with_min_level(log::Level::Trace)
+ /// .format(|record| format!("my_app: {}", record.args()))
+ /// )
+ /// ```
+ pub fn format<F>(mut self, format: F) -> Self
+ where
+ F: Fn(&log::Record) -> String + Sync + Send + 'static,
+ {
+ self.custom_format = Some(Box::new(format));
+ self
+ }
+
+ /// Set a filter, using the format specified in https://docs.rs/env_logger.
+ pub fn with_filter(mut self, filter: &'a str) -> Self {
+ self.filter = Some(filter);
+ self
+ }
+}
+
+/// Initializes logging on host. Returns false if logging is already initialized.
+/// Config values take precedence over environment variables for host logging.
+#[cfg(not(target_os = "android"))]
+pub fn init(config: Config) -> bool {
+ // Return immediately if the logger is already initialized.
+ if LOGGER_INITIALIZED.fetch_or(true, Ordering::SeqCst) {
+ return false;
+ }
+
+ let mut builder = env_logger::Builder::from_default_env();
+ if let Some(log_level) = config.log_level {
+ builder.filter_level(log_level.to_level_filter());
+ }
+ if let Some(custom_format) = config.custom_format {
+ use std::io::Write; // Trait used by write!() macro, but not in Android code
+
+ builder.format(move |f, r| {
+ let formatted = custom_format(r);
+ writeln!(f, "{}", formatted)
+ });
+ }
+ if let Some(filter_str) = config.filter {
+ builder.parse_filters(filter_str);
+ }
+
+ builder.init();
+ true
+}
+
+/// Initializes logging on device. Returns false if logging is already initialized.
+#[cfg(target_os = "android")]
+pub fn init(config: Config) -> bool {
+ // Return immediately if the logger is already initialized.
+ if LOGGER_INITIALIZED.fetch_or(true, Ordering::SeqCst) {
+ return false;
+ }
+
+ // We do not have access to the private variables in android_logger::Config, so we have to use
+ // the builder instead.
+ let mut builder = android_logger::Config::default();
+ if let Some(log_level) = config.log_level {
+ builder = builder.with_min_level(log_level);
+ }
+ if let Some(custom_format) = config.custom_format {
+ builder = builder.format(move |f, r| {
+ let formatted = custom_format(r);
+ write!(f, "{}", formatted)
+ });
+ }
+ if let Some(filter_str) = config.filter {
+ let filter = env_logger::filter::Builder::new().parse(filter_str).build();
+ builder = builder.with_filter(filter);
+ }
+ if let Some(tag) = config.tag {
+ builder = builder.with_tag(tag);
+ }
+
+ android_logger::init_once(builder);
+ true
+}
+
+/// Note that the majority of tests checking behavior are under the tests/ folder, as they all
+/// require independent initialization steps. The local test module just performs some basic crash
+/// testing without performing initialization.
+#[cfg(test)]
+mod tests {
+ use super::*;
+
+ #[test]
+ fn test_with_min_level() {
+ let config = Config::default()
+ .with_min_level(log::Level::Trace)
+ .with_min_level(log::Level::Error);
+
+ assert_eq!(config.log_level, Some(log::Level::Error));
+ }
+
+ #[test]
+ fn test_with_filter() {
+ let filter = "debug,hello::crate=trace";
+ let config = Config::default().with_filter(filter);
+
+ assert_eq!(config.filter.unwrap(), filter)
+ }
+
+ #[test]
+ fn test_with_tag_on_device() {
+ let config = Config::default().with_tag_on_device("my_app");
+
+ assert_eq!(config.tag.unwrap(), CString::new("my_app").unwrap());
+ }
+
+ #[test]
+ fn test_format() {
+ let config = Config::default().format(|record| format!("my_app: {}", record.args()));
+
+ assert!(config.custom_format.is_some());
+ }
+}
diff --git a/rust/rustfmt.toml b/rust/rustfmt.toml
new file mode 100644
index 0000000..475ba8f
--- /dev/null
+++ b/rust/rustfmt.toml
@@ -0,0 +1 @@
+../../../build/soong/scripts/rustfmt.toml
\ No newline at end of file
diff --git a/rust/tests/config_log_level.rs b/rust/tests/config_log_level.rs
new file mode 100644
index 0000000..7a9a241
--- /dev/null
+++ b/rust/tests/config_log_level.rs
@@ -0,0 +1,17 @@
+use std::env;
+
+#[test]
+fn config_log_level() {
+ // Environment variables should be overwritten by config values.
+ env::set_var("RUST_LOG", "debug");
+
+ let init_result = logger::init(
+ logger::Config::default()
+ .with_min_level(log::Level::Trace));
+
+ assert_eq!(init_result, true);
+ // Setting the level through the Config struct should impact both host and device
+ assert_eq!(log::max_level(), log::LevelFilter::Trace);
+
+ env::remove_var("RUST_LOG");
+}
\ No newline at end of file
diff --git a/rust/tests/default_init.rs b/rust/tests/default_init.rs
new file mode 100644
index 0000000..413e8c8
--- /dev/null
+++ b/rust/tests/default_init.rs
@@ -0,0 +1,12 @@
+#[test]
+fn default_init() {
+ assert_eq!(logger::init(Default::default()), true);
+
+ if cfg!(target_os = "android") {
+ // android_logger has default log level "off"
+ assert_eq!(log::max_level(), log::LevelFilter::Off);
+ } else {
+ // env_logger has default log level "error"
+ assert_eq!(log::max_level(), log::LevelFilter::Error);
+ }
+}
\ No newline at end of file
diff --git a/rust/tests/env_log_level.rs b/rust/tests/env_log_level.rs
new file mode 100644
index 0000000..eb86d97
--- /dev/null
+++ b/rust/tests/env_log_level.rs
@@ -0,0 +1,16 @@
+use std::env;
+
+#[test]
+fn env_log_level() {
+ env::set_var("RUST_LOG", "debug");
+ assert_eq!(logger::init(Default::default()), true);
+
+ if cfg!(target_os = "android") {
+ // android_logger does not read from environment variables
+ assert_eq!(log::max_level(), log::LevelFilter::Off);
+ } else {
+ // env_logger reads its log level from the "RUST_LOG" environment variable
+ assert_eq!(log::max_level(), log::LevelFilter::Debug);
+ }
+ env::remove_var("RUST_LOG");
+}
\ No newline at end of file
diff --git a/rust/tests/multiple_init.rs b/rust/tests/multiple_init.rs
new file mode 100644
index 0000000..0ac8b2f
--- /dev/null
+++ b/rust/tests/multiple_init.rs
@@ -0,0 +1,8 @@
+#[test]
+fn multiple_init() {
+ let first_init = logger::init(Default::default());
+ let second_init = logger::init(Default::default());
+
+ assert_eq!(first_init, true);
+ assert_eq!(second_init, false);
+}
\ No newline at end of file