Bram Bonné | d76ddd6 | 2021-03-25 10:29:51 +0100 | [diff] [blame] | 1 | // Copyright 2021, The Android Open Source Project |
| 2 | // |
| 3 | // Licensed under the Apache License, Version 2.0 (the "License"); |
| 4 | // you may not use this file except in compliance with the License. |
| 5 | // You may obtain a copy of the License at |
| 6 | // |
| 7 | // http://www.apache.org/licenses/LICENSE-2.0 |
| 8 | // |
| 9 | // Unless required by applicable law or agreed to in writing, software |
| 10 | // distributed under the License is distributed on an "AS IS" BASIS, |
| 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| 12 | // See the License for the specific language governing permissions and |
| 13 | // limitations under the License. |
| 14 | |
| 15 | //! Provides a universal logger interface that allows logging both on-device (using android_logger) |
| 16 | //! and on-host (using env_logger). |
| 17 | //! On-host, this allows the use of the RUST_LOG environment variable as documented in |
| 18 | //! https://docs.rs/env_logger. |
| 19 | use std::ffi::CString; |
| 20 | use std::sync::atomic::{AtomicBool, Ordering}; |
| 21 | |
| 22 | static LOGGER_INITIALIZED: AtomicBool = AtomicBool::new(false); |
| 23 | |
| 24 | type FormatFn = Box<dyn Fn(&log::Record) -> String + Sync + Send>; |
| 25 | |
| 26 | /// Logger configuration, opportunistically mapped to configuration parameters for android_logger |
| 27 | /// or env_logger where available. |
| 28 | #[derive(Default)] |
| 29 | pub struct Config<'a> { |
Jeff Vander Stoep | 6a26c18 | 2024-01-29 12:54:47 +0100 | [diff] [blame] | 30 | log_level: Option<log::LevelFilter>, |
Bram Bonné | d76ddd6 | 2021-03-25 10:29:51 +0100 | [diff] [blame] | 31 | custom_format: Option<FormatFn>, |
| 32 | filter: Option<&'a str>, |
| 33 | #[allow(dead_code)] // Field is only used on device, and ignored on host. |
| 34 | tag: Option<CString>, |
| 35 | } |
| 36 | |
| 37 | /// Based on android_logger::Config |
| 38 | impl<'a> Config<'a> { |
Jeff Vander Stoep | 6a26c18 | 2024-01-29 12:54:47 +0100 | [diff] [blame] | 39 | /// Changes the maximum log level. |
Bram Bonné | d76ddd6 | 2021-03-25 10:29:51 +0100 | [diff] [blame] | 40 | /// |
Jeff Vander Stoep | 6a26c18 | 2024-01-29 12:54:47 +0100 | [diff] [blame] | 41 | /// Note, that `Trace` is the maximum level, because it provides the |
| 42 | /// maximum amount of detail in the emitted logs. |
| 43 | /// |
| 44 | /// If `Off` level is provided, then nothing is logged at all. |
| 45 | /// |
| 46 | /// [`log::max_level()`] is considered as the default level. |
| 47 | pub fn with_max_level(mut self, level: log::LevelFilter) -> Self { |
Bram Bonné | d76ddd6 | 2021-03-25 10:29:51 +0100 | [diff] [blame] | 48 | self.log_level = Some(level); |
| 49 | self |
| 50 | } |
| 51 | |
| 52 | /// Set a log tag. Only used on device. |
| 53 | pub fn with_tag_on_device<S: Into<Vec<u8>>>(mut self, tag: S) -> Self { |
| 54 | self.tag = Some(CString::new(tag).expect("Can't convert tag to CString")); |
| 55 | self |
| 56 | } |
| 57 | |
| 58 | /// Set the format function for formatting the log output. |
| 59 | /// ``` |
| 60 | /// # use universal_logger::Config; |
| 61 | /// universal_logger::init( |
| 62 | /// Config::default() |
Jeff Vander Stoep | 6a26c18 | 2024-01-29 12:54:47 +0100 | [diff] [blame] | 63 | /// .with_max_level(log::LevelFilter::Trace) |
Bram Bonné | d76ddd6 | 2021-03-25 10:29:51 +0100 | [diff] [blame] | 64 | /// .format(|record| format!("my_app: {}", record.args())) |
| 65 | /// ) |
| 66 | /// ``` |
| 67 | pub fn format<F>(mut self, format: F) -> Self |
| 68 | where |
| 69 | F: Fn(&log::Record) -> String + Sync + Send + 'static, |
| 70 | { |
| 71 | self.custom_format = Some(Box::new(format)); |
| 72 | self |
| 73 | } |
| 74 | |
| 75 | /// Set a filter, using the format specified in https://docs.rs/env_logger. |
| 76 | pub fn with_filter(mut self, filter: &'a str) -> Self { |
| 77 | self.filter = Some(filter); |
| 78 | self |
| 79 | } |
| 80 | } |
| 81 | |
| 82 | /// Initializes logging on host. Returns false if logging is already initialized. |
| 83 | /// Config values take precedence over environment variables for host logging. |
| 84 | #[cfg(not(target_os = "android"))] |
| 85 | pub fn init(config: Config) -> bool { |
| 86 | // Return immediately if the logger is already initialized. |
| 87 | if LOGGER_INITIALIZED.fetch_or(true, Ordering::SeqCst) { |
| 88 | return false; |
| 89 | } |
| 90 | |
| 91 | let mut builder = env_logger::Builder::from_default_env(); |
| 92 | if let Some(log_level) = config.log_level { |
Jeff Vander Stoep | 6a26c18 | 2024-01-29 12:54:47 +0100 | [diff] [blame] | 93 | builder.filter_level(log_level); |
Bram Bonné | d76ddd6 | 2021-03-25 10:29:51 +0100 | [diff] [blame] | 94 | } |
| 95 | if let Some(custom_format) = config.custom_format { |
| 96 | use std::io::Write; // Trait used by write!() macro, but not in Android code |
| 97 | |
| 98 | builder.format(move |f, r| { |
| 99 | let formatted = custom_format(r); |
| 100 | writeln!(f, "{}", formatted) |
| 101 | }); |
| 102 | } |
| 103 | if let Some(filter_str) = config.filter { |
| 104 | builder.parse_filters(filter_str); |
| 105 | } |
| 106 | |
| 107 | builder.init(); |
| 108 | true |
| 109 | } |
| 110 | |
| 111 | /// Initializes logging on device. Returns false if logging is already initialized. |
| 112 | #[cfg(target_os = "android")] |
| 113 | pub fn init(config: Config) -> bool { |
| 114 | // Return immediately if the logger is already initialized. |
| 115 | if LOGGER_INITIALIZED.fetch_or(true, Ordering::SeqCst) { |
| 116 | return false; |
| 117 | } |
| 118 | |
| 119 | // We do not have access to the private variables in android_logger::Config, so we have to use |
| 120 | // the builder instead. |
| 121 | let mut builder = android_logger::Config::default(); |
| 122 | if let Some(log_level) = config.log_level { |
Jeff Vander Stoep | 6a26c18 | 2024-01-29 12:54:47 +0100 | [diff] [blame] | 123 | builder = builder.with_max_level(log_level); |
Bram Bonné | d76ddd6 | 2021-03-25 10:29:51 +0100 | [diff] [blame] | 124 | } |
| 125 | if let Some(custom_format) = config.custom_format { |
| 126 | builder = builder.format(move |f, r| { |
| 127 | let formatted = custom_format(r); |
| 128 | write!(f, "{}", formatted) |
| 129 | }); |
| 130 | } |
| 131 | if let Some(filter_str) = config.filter { |
| 132 | let filter = env_logger::filter::Builder::new().parse(filter_str).build(); |
| 133 | builder = builder.with_filter(filter); |
| 134 | } |
| 135 | if let Some(tag) = config.tag { |
| 136 | builder = builder.with_tag(tag); |
| 137 | } |
| 138 | |
| 139 | android_logger::init_once(builder); |
| 140 | true |
| 141 | } |
| 142 | |
| 143 | /// Note that the majority of tests checking behavior are under the tests/ folder, as they all |
| 144 | /// require independent initialization steps. The local test module just performs some basic crash |
| 145 | /// testing without performing initialization. |
| 146 | #[cfg(test)] |
| 147 | mod tests { |
| 148 | use super::*; |
| 149 | |
| 150 | #[test] |
Jeff Vander Stoep | 6a26c18 | 2024-01-29 12:54:47 +0100 | [diff] [blame] | 151 | fn test_with_max_level() { |
| 152 | let config = Config::default() |
| 153 | .with_max_level(log::LevelFilter::Trace) |
| 154 | .with_max_level(log::LevelFilter::Error); |
| 155 | |
| 156 | assert_eq!(config.log_level, Some(log::LevelFilter::Error)); |
Bram Bonné | d76ddd6 | 2021-03-25 10:29:51 +0100 | [diff] [blame] | 157 | } |
| 158 | |
| 159 | #[test] |
| 160 | fn test_with_filter() { |
| 161 | let filter = "debug,hello::crate=trace"; |
| 162 | let config = Config::default().with_filter(filter); |
| 163 | |
| 164 | assert_eq!(config.filter.unwrap(), filter) |
| 165 | } |
| 166 | |
| 167 | #[test] |
| 168 | fn test_with_tag_on_device() { |
| 169 | let config = Config::default().with_tag_on_device("my_app"); |
| 170 | |
| 171 | assert_eq!(config.tag.unwrap(), CString::new("my_app").unwrap()); |
| 172 | } |
| 173 | |
| 174 | #[test] |
| 175 | fn test_format() { |
| 176 | let config = Config::default().format(|record| format!("my_app: {}", record.args())); |
| 177 | |
| 178 | assert!(config.custom_format.is_some()); |
| 179 | } |
| 180 | } |