diff --git a/aya-log-ebpf-macros/src/expand.rs b/aya-log-ebpf-macros/src/expand.rs index 890ffdc7..6f7dfca1 100644 --- a/aya-log-ebpf-macros/src/expand.rs +++ b/aya-log-ebpf-macros/src/expand.rs @@ -151,32 +151,34 @@ pub(crate) fn log(args: LogArgs, level: Option) -> Result {}, - Some(::aya_log_ebpf::macro_support::LogBuf { buf: #buf }) => { - // Silence unused variable warning; we may need ctx in the future. - let _ = #ctx; - let _: Option<()> = (|| { - let #size = ::aya_log_ebpf::macro_support::write_record_header( - #buf, - #target, - #level, - module_path!(), - file!(), - line!(), - #num_args, - )?; - let mut #size = #size.get(); - #( - { - let #buf = #buf.get_mut(#size..)?; - let #len = ::aya_log_ebpf::macro_support::WriteToBuf::write(#values_iter, #buf)?; - #size += #len.get(); - } - )* - let #record = #buf.get(..#size)?; - Result::<_, i64>::ok(::aya_log_ebpf::macro_support::AYA_LOGS.output(#record, 0)) - })(); + if ::aya_log_ebpf::macro_support::level_enabled(#level) { + match ::aya_log_ebpf::macro_support::AYA_LOG_BUF.get_ptr_mut(0).and_then(|ptr| unsafe { ptr.as_mut() }) { + None => {}, + Some(::aya_log_ebpf::macro_support::LogBuf { buf: #buf }) => { + // Silence unused variable warning; we may need ctx in the future. + let _ = #ctx; + let _: Option<()> = (|| { + let #size = ::aya_log_ebpf::macro_support::write_record_header( + #buf, + #target, + #level, + module_path!(), + file!(), + line!(), + #num_args, + )?; + let mut #size = #size.get(); + #( + { + let #buf = #buf.get_mut(#size..)?; + let #len = ::aya_log_ebpf::macro_support::WriteToBuf::write(#values_iter, #buf)?; + #size += #len.get(); + } + )* + let #record = #buf.get(..#size)?; + Result::<_, i64>::ok(::aya_log_ebpf::macro_support::AYA_LOGS.output(#record, 0)) + })(); + } } } }) diff --git a/aya-log/CHANGELOG.md b/aya-log/CHANGELOG.md index 5eb0e76d..55955ef7 100644 --- a/aya-log/CHANGELOG.md +++ b/aya-log/CHANGELOG.md @@ -7,6 +7,13 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +### New Features + +- Add eBPF-side log level mask `AYA_LOG_LEVEL_MASK` and loader API `EbpfLoader::set_log_level_mask()` + allowing selective disabling of log levels at load-time. Disabled levels are + eliminated by the verifier, reducing instruction count and avoiding program + size limits when extensive logging is present. + ### Breaking Changes - The implementation is now backed by a ring buffer rather than a perf event array. This should diff --git a/aya-log/README.md b/aya-log/README.md index 765247c9..4391e0dd 100644 --- a/aya-log/README.md +++ b/aya-log/README.md @@ -69,3 +69,36 @@ fn try_xdp_firewall(ctx: XdpContext) -> Result { [aya]: https://github.com/aya-rs/aya [log]: https://docs.rs/log [env_logger]: https://docs.rs/env_logger + +## Disabling log levels at load-time + +eBPF instruction budgets are tight. Even if a log statement never executes at +runtime, the verifier must still evaluate its instructions unless it can prove +they're unreachable. `aya-log` now exposes a global bitmask `AYA_LOG_LEVEL_MASK` +inside the eBPF object allowing you to selectively enable levels before the +program is loaded. + +Bits (least-significant first) map to levels: Error=bit0, Warn=bit1, Info=bit2, +Debug=bit3, Trace=bit4. + +By default all bits are set (all logging enabled). To disable all logging: + +```rust +let mut bpf = aya::EbpfLoader::new() + .set_global(aya_log::LEVEL, &0, false /* must_exist */) + .load_file("prog.bpf.o")?; +# Ok::<(), aya::EbpfError>(()) +``` + +Enable only Error and Warn: + +```rust +let level = aya_log::Level::Warn as u8; +let mut bpf = EbpfLoader::new() + .set_global(aya_log::LEVEL, &level, false /* must_exist */) + .load_file("prog.bpf.o")?; +``` + +Because the mask is placed in global read-only data, the verifier sees the +disabled branch as unreachable and prunes the logging instructions, reducing +overall instruction count and avoiding potential instruction limit issues. diff --git a/aya-log/src/lib.rs b/aya-log/src/lib.rs index 333ddcc9..1ac840af 100644 --- a/aya-log/src/lib.rs +++ b/aya-log/src/lib.rs @@ -67,12 +67,15 @@ use std::{ const MAP_NAME: &str = "AYA_LOGS"; +pub const LEVEL: &str = "AYA_LOG_LEVEL"; +pub use aya_log_common::Level; + use aya::{ Ebpf, Pod, maps::{Map, MapData, MapError, MapInfo, RingBuf}, programs::{ProgramError, loaded_programs}, }; -use aya_log_common::{Argument, DisplayHint, LOG_FIELDS, Level, LogValueLength, RecordField}; +use aya_log_common::{Argument, DisplayHint, LOG_FIELDS, LogValueLength, RecordField}; use log::{Log, Record, error}; use thiserror::Error; diff --git a/ebpf/aya-log-ebpf/src/lib.rs b/ebpf/aya-log-ebpf/src/lib.rs index 0a3f1421..ce74df00 100644 --- a/ebpf/aya-log-ebpf/src/lib.rs +++ b/ebpf/aya-log-ebpf/src/lib.rs @@ -34,4 +34,18 @@ pub mod macro_support { // test/integration-test/Cargo.toml. #[cfg_attr(target_arch = "bpf", map)] pub static AYA_LOGS: RingBuf = RingBuf::with_byte_size((LOG_BUF_CAPACITY as u32) << 4, 0); + + /// Global log level controlling which log statements are active. + /// + /// Userspace may patch this symbol before load via `EbpfLoader::set_global`. + #[unsafe(no_mangle)] + pub static mut AYA_LOG_LEVEL: aya_log_common::Level = aya_log_common::Level::Trace; + + /// Returns `true` if the provided level is enabled according to [`AYA_LOG_LEVEL`]. + #[inline(always)] + pub fn level_enabled(level: Level) -> bool { + #[expect(static_mut_refs)] + let current_level = unsafe { core::ptr::read_volatile(&AYA_LOG_LEVEL) }; + level as u8 <= current_level as u8 + } } diff --git a/test/integration-test/src/tests/log.rs b/test/integration-test/src/tests/log.rs index 987ec23b..6adfa718 100644 --- a/test/integration-test/src/tests/log.rs +++ b/test/integration-test/src/tests/log.rs @@ -2,6 +2,7 @@ use std::{borrow::Cow, sync::Mutex}; use aya::{Ebpf, programs::UProbe}; use aya_log::EbpfLogger; +use aya::EbpfLoader; use log::{Level, Log, Record}; #[unsafe(no_mangle)] @@ -173,3 +174,54 @@ fn log() { assert_eq!(records.next(), None); } + +#[test_log::test] +fn log_level_only_error_warn() { + let level = aya_log::Level::Warn as u8; + let mut bpf = EbpfLoader::new() + .set_global(aya_log::LEVEL, &level, true /* must_exist */) + .load(crate::LOG) + .unwrap(); + + let mut captured_logs = Vec::new(); + let logger = TestingLogger { + log: Mutex::new(|record: &Record| { + captured_logs.push(CapturedLog { + body: format!("{}", record.args()).into(), + level: record.level(), + target: record.target().to_string().into(), + }); + }), + }; + let mut logger = EbpfLogger::init_with_logger(&mut bpf, &logger).unwrap(); + + let prog: &mut UProbe = bpf.program_mut("test_log").unwrap().try_into().unwrap(); + prog.load().unwrap(); + prog.attach("trigger_ebpf_program", "/proc/self/exe", None, None) + .unwrap(); + + trigger_ebpf_program(); + logger.flush(); + + let mut records = captured_logs.iter(); + + assert_eq!( + records.next(), + Some(&CapturedLog { + body: "69, 420, wao, 77616f".into(), + level: Level::Error, + target: "log".into(), + }) + ); + + assert_eq!( + records.next(), + Some(&CapturedLog { + body: "hex lc: 2f, hex uc: 2F".into(), + level: Level::Warn, + target: "log".into(), + }) + ); + + assert_eq!(records.next(), None); +} diff --git a/xtask/public-api/aya-log.txt b/xtask/public-api/aya-log.txt index 47e000d8..3583315e 100644 --- a/xtask/public-api/aya-log.txt +++ b/xtask/public-api/aya-log.txt @@ -1,4 +1,5 @@ pub mod aya_log +pub use aya_log::Level pub enum aya_log::Error pub aya_log::Error::MapError(aya::maps::MapError) pub aya_log::Error::MapNotFound @@ -297,6 +298,7 @@ impl core::borrow::BorrowMut for aya_log::UpperMacFormatter where T: ?core pub fn aya_log::UpperMacFormatter::borrow_mut(&mut self) -> &mut T impl core::convert::From for aya_log::UpperMacFormatter pub fn aya_log::UpperMacFormatter::from(t: T) -> T +pub const aya_log::LEVEL: &str pub trait aya_log::Formatter pub fn aya_log::Formatter::format(v: T) -> alloc::string::String impl aya_log::Formatter<&[u8]> for aya_log::LowerHexBytesFormatter