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