diff --git a/aya-log-ebpf-macros/src/expand.rs b/aya-log-ebpf-macros/src/expand.rs index 890ffdc7..9d81d6a7 100644 --- a/aya-log-ebpf-macros/src/expand.rs +++ b/aya-log-ebpf-macros/src/expand.rs @@ -68,11 +68,11 @@ impl Parse for LogArgs { } } -pub(crate) fn log(args: LogArgs, level: Option) -> Result { +pub(crate) fn log(args: LogArgs, level_expr: Option) -> Result { let LogArgs { ctx, target, - level: level_expr, + level, format_string, formatting_args, } = args; @@ -80,14 +80,14 @@ pub(crate) fn log(args: LogArgs, level: Option) -> Result quote! { #t }, None => quote! { module_path!() }, }; - let level = match level { - Some(l) => l, + let level_expr = match level_expr { + Some(level_expr) => level_expr, None => { - let l = level_expr.ok_or(Error::new( + let level_expr = level.ok_or(Error::new( format_string.span(), "missing `level` argument: try passing an `aya_log_ebpf::Level` value", ))?; - quote! { #l } + quote! { #level_expr } } }; @@ -146,37 +146,41 @@ 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)) - })(); + let #level = #level_expr; + 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..5508046d 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 `AYA_LOG_LEVEL` 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..1eefa995 100644 --- a/aya-log/README.md +++ b/aya-log/README.md @@ -69,3 +69,33 @@ 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 `AYA_LOG_LEVEL` inside the +eBPF object allowing you to selectively enable levels before the program is +loaded. + +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 level 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..7560f6aa 100644 --- a/aya-log/src/lib.rs +++ b/aya-log/src/lib.rs @@ -67,12 +67,14 @@ use std::{ const MAP_NAME: &str = "AYA_LOGS"; +pub const LEVEL: &str = "AYA_LOG_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}; +pub use aya_log_common::Level; +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..f2398626 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: u8 = 0xff; + + /// 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 + } } diff --git a/test/integration-test/src/tests/log.rs b/test/integration-test/src/tests/log.rs index 987ec23b..91841f03 100644 --- a/test/integration-test/src/tests/log.rs +++ b/test/integration-test/src/tests/log.rs @@ -1,6 +1,6 @@ use std::{borrow::Cow, sync::Mutex}; -use aya::{Ebpf, programs::UProbe}; +use aya::{Ebpf, EbpfLoader, programs::UProbe}; use aya_log::EbpfLogger; use log::{Level, Log, Record}; @@ -173,3 +173,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