diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index b47ccba..da17594 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -26,6 +26,10 @@ jobs: - armv7-linux-androideabi - aarch64-linux-android - i686-linux-android + features: + - "" + - --no-default-features + - --all-features steps: - uses: actions/checkout@v3 @@ -37,9 +41,9 @@ jobs: components: rustfmt, clippy - run: cargo fmt --check - - run: cargo clippy --target=${{ matrix.target }} -- -Dwarnings - - run: cargo build --target=${{ matrix.target }} - - run: cargo doc --target=${{ matrix.target }} + - run: cargo clippy --target=${{ matrix.target }} ${{ matrix.features }} -- -Dwarnings + - run: cargo build --target=${{ matrix.target }} ${{ matrix.features }} + - run: cargo doc --target=${{ matrix.target }} ${{ matrix.features }} env: RUSTDOCFLAGS: -Dwarnings # Temporary test non-target only. diff --git a/Cargo.toml b/Cargo.toml index 07a4d12..752f6e4 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -25,12 +25,13 @@ targets = [ [features] default = ["regex"] regex = ["env_filter/regex"] +android-api-30 = [] [dependencies.log] version = "0.4" [dependencies.android_log-sys] -version = "0.3" +version = "0.3.2" [dependencies.env_filter] version = "0.1" diff --git a/README.md b/README.md index 0d97778..8a8159d 100644 --- a/README.md +++ b/README.md @@ -65,6 +65,48 @@ Therefore this library will only log a warning for subsequent `init_once` calls. This library ensures that logged messages do not overflow Android log message limits by efficiently splitting messages into chunks. +## Consistent log filtering in mixed Rust/C/C++ apps + +Android's C logging API determines the effective log level based on [a +combination](https://cs.android.com/android/platform/superproject/main/+/main:system/logging/liblog/properties.cpp;l=243;drc=b74a506c1b69f5b295a8cdfd7e2da3b16db15934) +of a process-wide global variable, [system-wide +properties](https://cs.android.com/android/platform/superproject/main/+/main:system/logging/logd/README.property;l=45;drc=99c545d3098018a544cb292e1501daca694bee0f), +and call-specific default. `log` + `android_logger` crates add another layer of +log filtering on top of that, independent from the C API. + +``` + .-----. + | app | + '-----' Rust +C/C++ | '--------------. + | v + | .-----------. filter by log::STATIC_MAX_LEVEL + + | | log crate | - log::MAX_LOG_LEVEL_FILTER, + | '-----------' overrideable via log::set_max_level + | | + | v + | .----------------------. + | | android_logger crate | - filter by Config::max_level + | '----------------------' + | | + | .------------' + v v + .--------. + | liblog | - filter by global state or system-wide properties + '--------' +``` + +`liblog` APIs introduced in Android API 30 let `android_logger` delegate log +filtering decision to `liblog`, making the log level consistent across C, C++ +and Rust calls. + +If you build `android_logger` with `android-api-30` feature enabled, the logger +will consider the process-wide global state (set via +[`__android_log_set_minimum_priority`](https://cs.android.com/android/platform/superproject/main/+/main:prebuilts/runtime/mainline/runtime/sdk/common_os/include/system/logging/liblog/include/android/log.h;l=364;drc=4cf460634134d51dba174f8af60dffb10f703f51)) +and Android system properties when deciding if a message should be logged or +not. In this case, the effective log level is the _least verbose_ of the levels +set between those and Rust log facilities. + ## License Licensed under either of diff --git a/src/config.rs b/src/config.rs index f4bd657..bd14f58 100644 --- a/src/config.rs +++ b/src/config.rs @@ -31,6 +31,56 @@ impl fmt::Debug for Config { } } +#[cfg(all(target_os = "android", feature = "android-api-30"))] +fn android_log_priority_from_level(level: Level) -> android_log_sys::LogPriority { + match level { + Level::Warn => android_log_sys::LogPriority::WARN, + Level::Info => android_log_sys::LogPriority::INFO, + Level::Debug => android_log_sys::LogPriority::DEBUG, + Level::Error => android_log_sys::LogPriority::ERROR, + Level::Trace => android_log_sys::LogPriority::VERBOSE, + } +} + +/// Asks Android liblog if a message with given `tag` and `priority` should be logged, using +/// `default_prio` as the level filter in case no system- or process-wide overrides are set. +#[cfg(all(target_os = "android", feature = "android-api-30"))] +fn android_is_loggable_len( + prio: log_ffi::LogPriority, + tag: &str, + default_prio: log_ffi::LogPriority, +) -> bool { + // SAFETY: tag points to a valid string tag.len() bytes long. + unsafe { + log_ffi::__android_log_is_loggable_len( + prio as log_ffi::c_int, + tag.as_ptr() as *const log_ffi::c_char, + tag.len() as log_ffi::c_size_t, + default_prio as log_ffi::c_int, + ) != 0 + } +} + +#[cfg(not(all(target_os = "android", feature = "android-api-30")))] +fn default_is_loggable(_tag: &str, record_level: Level, config_level: Option) -> bool { + record_level <= config_level.unwrap_or_else(log::max_level) +} + +#[cfg(all(target_os = "android", feature = "android-api-30"))] +fn android_is_loggable(tag: &str, record_level: Level, config_level: Option) -> bool { + let prio = android_log_priority_from_level(record_level); + // Priority to use in case no system-wide or process-wide overrides are set. + let default_prio = match config_level { + Some(level_filter) => match level_filter.to_level() { + Some(level) => android_log_priority_from_level(level), + // LevelFilter::to_level() returns None only for LevelFilter::Off + None => android_log_sys::LogPriority::SILENT, + }, + None => android_log_sys::LogPriority::INFO, + }; + android_is_loggable_len(prio, tag, default_prio) +} + impl Config { /// Changes the maximum log level. /// @@ -64,9 +114,13 @@ impl Config { } } - pub(crate) fn is_loggable(&self, level: Level) -> bool { - // todo: consider __android_log_is_loggable. - level <= self.log_level.unwrap_or_else(log::max_level) + pub(crate) fn is_loggable(&self, tag: &str, level: Level) -> bool { + #[cfg(all(target_os = "android", feature = "android-api-30"))] + use android_is_loggable as is_loggable; + #[cfg(not(all(target_os = "android", feature = "android-api-30")))] + use default_is_loggable as is_loggable; + + is_loggable(tag, level, self.log_level) } pub fn with_filter(mut self, filter: env_filter::Filter) -> Self { diff --git a/src/lib.rs b/src/lib.rs index a05d23d..063f4ac 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -146,7 +146,8 @@ const LOGGING_MSG_MAX_LEN: usize = 4000; impl Log for AndroidLogger { fn enabled(&self, metadata: &Metadata) -> bool { - self.config().is_loggable(metadata.level()) + self.config() + .is_loggable(metadata.target(), metadata.level()) } fn log(&self, record: &Record) {