diff options
Diffstat (limited to 'xpcom/rust/gecko_logger/src/lib.rs')
-rw-r--r-- | xpcom/rust/gecko_logger/src/lib.rs | 258 |
1 files changed, 258 insertions, 0 deletions
diff --git a/xpcom/rust/gecko_logger/src/lib.rs b/xpcom/rust/gecko_logger/src/lib.rs new file mode 100644 index 0000000000..42d14a6846 --- /dev/null +++ b/xpcom/rust/gecko_logger/src/lib.rs @@ -0,0 +1,258 @@ +/* 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 https://mozilla.org/MPL/2.0/. */ + +//! This provides a way to direct rust logging into the gecko logger. + +#[macro_use] +extern crate lazy_static; + +use app_services_logger::{AppServicesLogger, LOGGERS_BY_TARGET}; +use log::Log; +use log::{Level, LevelFilter}; +use std::boxed::Box; +use std::collections::HashMap; +use std::ffi::{CStr, CString}; +use std::os::raw::c_char; +use std::os::raw::c_int; +use std::sync::atomic::{AtomicBool, Ordering}; +use std::sync::RwLock; +use std::{cmp, env}; + +extern "C" { + fn ExternMozLog(tag: *const c_char, prio: c_int, text: *const c_char); + fn gfx_critical_note(msg: *const c_char); + #[cfg(target_os = "android")] + fn __android_log_write(prio: c_int, tag: *const c_char, text: *const c_char) -> c_int; +} + +lazy_static! { + // This could be a proper static once [1] is fixed or parking_lot's const fn + // support is not nightly-only. + // + // [1]: https://github.com/rust-lang/rust/issues/73714 + static ref LOG_MODULE_MAP: RwLock<HashMap<String, (LevelFilter, bool)>> = RwLock::new(HashMap::new()); +} + +/// This tells us whether LOG_MODULE_MAP is possibly non-empty. +static LOGGING_ACTIVE: AtomicBool = AtomicBool::new(false); + +/// This function searches for the module's name in the hashmap. If that is not +/// found, it proceeds to search for the parent modules. +/// It returns a tuple containing the matched string, if the matched module +/// was a pattern match, and the level we found in the hashmap. +/// If none is found, it will return the module's name and LevelFilter::Off +fn get_level_for_module<'a>( + map: &HashMap<String, (LevelFilter, bool)>, + key: &'a str, +) -> (&'a str, bool, LevelFilter) { + if let Some((level, is_pattern_match)) = map.get(key) { + return (key, *is_pattern_match, level.clone()); + } + + let mut mod_name = &key[..]; + while let Some(pos) = mod_name.rfind("::") { + mod_name = &mod_name[..pos]; + if let Some((level, is_pattern_match)) = map.get(mod_name) { + return (mod_name, *is_pattern_match, level.clone()); + } + } + + return (key, false, LevelFilter::Off); +} + +/// This function takes a record to maybe log to Gecko. +/// It returns true if the record was handled by Gecko's logging, and false +/// otherwise. +pub fn log_to_gecko(record: &log::Record) -> bool { + if !LOGGING_ACTIVE.load(Ordering::Relaxed) { + return false; + } + + let key = match record.module_path() { + Some(key) => key, + None => return false, + }; + + let (mod_name, is_pattern_match, level) = { + let map = LOG_MODULE_MAP.read().unwrap(); + get_level_for_module(&map, &key) + }; + + if level == LevelFilter::Off { + return false; + } + + if level < record.metadata().level() { + return false; + } + + // Map the log::Level to mozilla::LogLevel. + let moz_log_level = match record.metadata().level() { + Level::Error => 1, // Error + Level::Warn => 2, // Warning + Level::Info => 3, // Info + Level::Debug => 4, // Debug + Level::Trace => 5, // Verbose + }; + + // If it was a pattern match, we need to append ::* to the matched string. + let (tag, msg) = if is_pattern_match { + ( + CString::new(format!("{}::*", mod_name)).unwrap(), + CString::new(format!("[{}] {}", key, record.args())).unwrap(), + ) + } else { + ( + CString::new(key).unwrap(), + CString::new(format!("{}", record.args())).unwrap(), + ) + }; + + unsafe { + ExternMozLog(tag.as_ptr(), moz_log_level, msg.as_ptr()); + } + + return true; +} + +#[no_mangle] +pub unsafe extern "C" fn set_rust_log_level(module: *const c_char, level: u8) { + // Convert the Gecko level to a rust LevelFilter. + let rust_level = match level { + 1 => LevelFilter::Error, + 2 => LevelFilter::Warn, + 3 => LevelFilter::Info, + 4 => LevelFilter::Debug, + 5 => LevelFilter::Trace, + _ => LevelFilter::Off, + }; + + // This is the name of the rust module that we're trying to log in Gecko. + let mut mod_name = CStr::from_ptr(module) + .to_string_lossy() + .into_owned(); + + let is_pattern_match = mod_name.ends_with("::*"); + + // If this is a pattern, remove the last "::*" from it so we can search it + // in the map. + if is_pattern_match { + let len = mod_name.len() - 3; + mod_name.truncate(len); + } + + LOGGING_ACTIVE.store(true, Ordering::Relaxed); + let mut map = LOG_MODULE_MAP.write().unwrap(); + map.insert(mod_name, (rust_level, is_pattern_match)); + + // Figure out the max level of all the modules. + let max = map + .values() + .map(|(lvl, _)| lvl) + .max() + .unwrap_or(&LevelFilter::Off); + log::set_max_level(*max); +} + +pub struct GeckoLogger { + logger: env_logger::Logger, +} + +impl GeckoLogger { + pub fn new() -> GeckoLogger { + let mut builder = env_logger::Builder::new(); + let default_level = if cfg!(debug_assertions) { + "warn" + } else { + "error" + }; + let logger = match env::var("RUST_LOG") { + Ok(v) => builder.parse_filters(&v).build(), + _ => builder.parse_filters(default_level).build(), + }; + + GeckoLogger { logger } + } + + pub fn init() -> Result<(), log::SetLoggerError> { + let gecko_logger = Self::new(); + + // The max level may have already been set by gecko_logger. Don't + // set it to a lower level. + let level = cmp::max(log::max_level(), gecko_logger.logger.filter()); + log::set_max_level(level); + log::set_boxed_logger(Box::new(gecko_logger)) + } + + fn should_log_to_app_services(target: &str) -> bool { + return AppServicesLogger::is_app_services_logger_registered(target.into()); + } + + fn maybe_log_to_app_services(&self, record: &log::Record) { + if Self::should_log_to_app_services(record.target()) { + if let Some(l) = LOGGERS_BY_TARGET.read().unwrap().get(record.target()) { + l.log(record); + } + } + } + + fn should_log_to_gfx_critical_note(record: &log::Record) -> bool { + record.level() == log::Level::Error && record.target().contains("webrender") + } + + fn maybe_log_to_gfx_critical_note(&self, record: &log::Record) { + if Self::should_log_to_gfx_critical_note(record) { + let msg = CString::new(format!("{}", record.args())).unwrap(); + unsafe { + gfx_critical_note(msg.as_ptr()); + } + } + } + + #[cfg(not(target_os = "android"))] + fn log_out(&self, record: &log::Record) { + // If the log wasn't handled by the gecko platform logger, just pass it + // to the env_logger. + if !log_to_gecko(record) { + self.logger.log(record); + } + } + + #[cfg(target_os = "android")] + fn log_out(&self, record: &log::Record) { + if !self.logger.matches(record) { + return; + } + + let msg = CString::new(format!("{}", record.args())).unwrap(); + let tag = CString::new(record.module_path().unwrap()).unwrap(); + let prio = match record.metadata().level() { + Level::Error => 6, /* ERROR */ + Level::Warn => 5, /* WARN */ + Level::Info => 4, /* INFO */ + Level::Debug => 3, /* DEBUG */ + Level::Trace => 2, /* VERBOSE */ + }; + // Output log directly to android log, since env_logger can output log + // only to stderr or stdout. + unsafe { + __android_log_write(prio, tag.as_ptr(), msg.as_ptr()); + } + } +} + +impl log::Log for GeckoLogger { + fn enabled(&self, metadata: &log::Metadata) -> bool { + self.logger.enabled(metadata) || GeckoLogger::should_log_to_app_services(metadata.target()) + } + + fn log(&self, record: &log::Record) { + // Forward log to gfxCriticalNote, if the log should be in gfx crash log. + self.maybe_log_to_gfx_critical_note(record); + self.maybe_log_to_app_services(record); + self.log_out(record); + } + + fn flush(&self) {} +} |