Skip to content

Commit

Permalink
capi: Add support for tapping into traces
Browse files Browse the repository at this point in the history
The main Rust crate allows for configuration of the generic tracing
infrastructure to glean some insights into what is going on. So far the
C library didn't have any mechanism for tapping into that.
This change adds such infrastructure with the new blaze_trace()
function, which allows for registration of a callback function that is
invoked for each emitted trace line. It is intended to be used to better
understand what is going on.

Closes: #587

Signed-off-by: Daniel Müller <[email protected]>
  • Loading branch information
d-e-s-o committed Nov 15, 2024
1 parent 51384ef commit 6309aaa
Show file tree
Hide file tree
Showing 8 changed files with 335 additions and 4 deletions.
6 changes: 6 additions & 0 deletions capi/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,9 @@
Unreleased
----------
- Introduced `blaze_trace` function for tapping into the library's
tracing functionality


0.1.0-rc.2
----------
- Fixed various functions accepting `uintptr_t` addresses, when they
Expand Down
4 changes: 3 additions & 1 deletion capi/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -58,10 +58,12 @@ which = {version = "7.0.0", optional = true}
# Pinned, because we use #[doc(hidden)] APIs.
# TODO: Enable `zstd` feature once we enabled it for testing in the main
# crate.
blazesym = {version = "=0.2.0-rc.2", path = "../", features = ["apk", "demangle", "dwarf", "gsym", "zlib"]}
blazesym = {version = "=0.2.0-rc.2", path = "../", features = ["apk", "demangle", "dwarf", "gsym", "tracing", "zlib"]}
libc = "0.2.137"
# TODO: Remove dependency one MSRV is 1.77.
memoffset = "0.9"
tracing = "0.1"
tracing-subscriber = {version = "0.3", default-features = false, features = ["fmt"]}

[target.'cfg(any(target_os = "linux", target_os = "android"))'.dependencies]
blazesym = {version = "=0.2.0-rc.2", path = "../", features = ["bpf"]}
Expand Down
55 changes: 54 additions & 1 deletion capi/include/blazesym.h
Original file line number Diff line number Diff line change
Expand Up @@ -191,6 +191,35 @@ enum blaze_symbolize_reason
typedef uint8_t blaze_symbolize_reason;
#endif // __cplusplus

/**
* The level at which to emit traces.
*/
typedef enum blaze_trace_lvl {
/**
* Emit all trace events.
*
* This is the most verbose level and includes all others.
*/
BLAZE_LVL_TRACE,
/**
* Emit debug traces and above.
*
* This level excludes traces emitted with "TRACE" verbosity.
*/
BLAZE_LVL_DEBUG,
/**
* Emit info level traces and above.
*
* This level excludes traces emitted with "TRACE" or "DEBUG"
* verbosity.
*/
BLAZE_LVL_INFO,
/**
* Only emit warnings.
*/
BLAZE_LVL_WARN,
} blaze_trace_lvl;

/**
* The valid variant kind in [`blaze_user_meta`].
*/
Expand Down Expand Up @@ -871,6 +900,11 @@ typedef struct blaze_symbolize_src_gsym_file {
const char *path;
} blaze_symbolize_src_gsym_file;

/**
* The signature of a callback function as passed to [`blaze_trace`].
*/
typedef void (*blaze_trace_cb)(const char*);

#ifdef __cplusplus
extern "C" {
#endif // __cplusplus
Expand Down Expand Up @@ -1136,7 +1170,6 @@ blaze_symbolizer *blaze_symbolizer_new_opts(const struct blaze_symbolizer_opts *
* Free an instance of blazesym a symbolizer for C API.
*
* # Safety
*
* The pointer must have been returned by [`blaze_symbolizer_new`] or
* [`blaze_symbolizer_new_opts`].
*/
Expand Down Expand Up @@ -1283,6 +1316,26 @@ const struct blaze_syms *blaze_symbolize_gsym_file_virt_offsets(blaze_symbolizer
*/
void blaze_syms_free(const struct blaze_syms *syms);

/**
* Enable the main library's tracing infrastructure and invoke a
* callback function for each emitted trace line.
*
* The provided [`blaze_trace_lvl`] determines what kind of traces are
* emitted.
*
* This function should be invoked at most once. Subsequent invocations
* will not affect tracing behavior.
*
* On error the function sets the thread's last error to indicate the
* problem encountered. Use [`blaze_err_last`] to retrieve this error.
*
* # Notes
* - the format of emitted lines is unspecified and subject to change; it is
* meant for human consumption and not programmatic evaluation
*/
void blaze_trace(enum blaze_trace_lvl lvl,
blaze_trace_cb cb);

#ifdef __cplusplus
} // extern "C"
#endif // __cplusplus
Expand Down
2 changes: 2 additions & 0 deletions capi/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -89,10 +89,12 @@ mod helper;
mod inspect;
mod normalize;
mod symbolize;
mod trace;
mod util;

pub use error::*;
pub use helper::*;
pub use inspect::*;
pub use normalize::*;
pub use symbolize::*;
pub use trace::*;
1 change: 0 additions & 1 deletion capi/src/symbolize.rs
Original file line number Diff line number Diff line change
Expand Up @@ -649,7 +649,6 @@ pub unsafe extern "C" fn blaze_symbolizer_new_opts(
/// Free an instance of blazesym a symbolizer for C API.
///
/// # Safety
///
/// The pointer must have been returned by [`blaze_symbolizer_new`] or
/// [`blaze_symbolizer_new_opts`].
#[no_mangle]
Expand Down
214 changes: 214 additions & 0 deletions capi/src/trace.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,214 @@
use std::ffi::c_char;
use std::ffi::CStr;
use std::io;
use std::io::BufRead as _;
use std::io::Cursor;

use tracing::subscriber::set_global_default as set_global_subscriber;
use tracing_subscriber::filter::LevelFilter;
use tracing_subscriber::fmt;
use tracing_subscriber::fmt::format::FmtSpan;
use tracing_subscriber::fmt::time::SystemTime;
use tracing_subscriber::FmtSubscriber;

use crate::blaze_err;
#[cfg(doc)]
use crate::blaze_err_last;
use crate::set_last_err;


/// The level at which to emit traces.
#[repr(C)]
#[derive(Copy, Clone, Debug, PartialEq)]
pub enum blaze_trace_lvl {
/// Emit all trace events.
///
/// This is the most verbose level and includes all others.
BLAZE_LVL_TRACE,
/// Emit debug traces and above.
///
/// This level excludes traces emitted with "TRACE" verbosity.
BLAZE_LVL_DEBUG,
/// Emit info level traces and above.
///
/// This level excludes traces emitted with "TRACE" or "DEBUG"
/// verbosity.
BLAZE_LVL_INFO,
/// Only emit warnings.
BLAZE_LVL_WARN,
}


impl From<blaze_trace_lvl> for LevelFilter {
fn from(other: blaze_trace_lvl) -> Self {
match other {
blaze_trace_lvl::BLAZE_LVL_WARN => LevelFilter::WARN,
blaze_trace_lvl::BLAZE_LVL_INFO => LevelFilter::INFO,
blaze_trace_lvl::BLAZE_LVL_DEBUG => LevelFilter::DEBUG,
blaze_trace_lvl::BLAZE_LVL_TRACE => LevelFilter::TRACE,
}
}
}


/// The signature of a callback function as passed to [`blaze_trace`].
pub type blaze_trace_cb = extern "C" fn(*const c_char);


struct LineWriter<F> {
/// A buffer used for formatting traces.
buf: Vec<u8>,
/// The callback used for emitting formatted traces.
f: F,
}

impl<F> LineWriter<F> {
fn new(f: F) -> Self {
Self { buf: Vec::new(), f }
}
}

impl<F> io::Write for LineWriter<F>
where
F: FnMut(&CStr),
{
fn write(&mut self, buf: &[u8]) -> io::Result<usize> {
let delim = b'\n';
let mut read = 0;
let mut cursor = Cursor::new(buf);

loop {
let n = cursor.read_until(delim, &mut self.buf)?;
if n == 0 {
break Ok(read)
}
read += n;

if self.buf.last() == Some(&delim) {
// We reached a complete line. Emit it via the callback.
let () = self.buf.push(b'\0');
// SAFETY: We properly NUL terminated the C string.
let cstr = unsafe { CStr::from_ptr(self.buf.as_ptr().cast()) };
let () = (self.f)(cstr);
let () = self.buf.clear();
} else {
break Ok(read)
}
}
}

fn flush(&mut self) -> io::Result<()> {
// We flush on a per-line basis.
Ok(())
}
}


/// Enable the main library's tracing infrastructure and invoke a
/// callback function for each emitted trace line.
///
/// The provided [`blaze_trace_lvl`] determines what kind of traces are
/// emitted.
///
/// This function should be invoked at most once. Subsequent invocations
/// will not affect tracing behavior.
///
/// On error the function sets the thread's last error to indicate the
/// problem encountered. Use [`blaze_err_last`] to retrieve this error.
///
/// # Notes
/// - the format of emitted lines is unspecified and subject to change; it is
/// meant for human consumption and not programmatic evaluation
#[no_mangle]
pub extern "C" fn blaze_trace(lvl: blaze_trace_lvl, cb: blaze_trace_cb) {
let format = fmt::format().with_target(false).compact();
let subscriber = FmtSubscriber::builder()
.event_format(format)
.with_max_level(LevelFilter::from(lvl))
.with_span_events(FmtSpan::FULL)
.with_timer(SystemTime)
.with_writer(move || {
let emit = move |cstr: &CStr| cb(cstr.as_ptr());
LineWriter::new(emit)
})
.finish();

let err = set_global_subscriber(subscriber)
.map(|()| blaze_err::BLAZE_ERR_OK)
.unwrap_or(blaze_err::BLAZE_ERR_ALREADY_EXISTS);
let () = set_last_err(err);
}


#[cfg(test)]
mod tests {
use super::*;

use std::cmp::max;
use std::hash::BuildHasher as _;
use std::hash::Hasher as _;
use std::hash::RandomState;
use std::io::Write as _;

use blazesym::__private::ReadRaw;


/// Test that we can convert `blaze_trace_lvl` values into their
/// `LevelFilter` counter parts.
#[test]
fn lvl_conversions() {
use super::blaze_trace_lvl::*;

assert_eq!(LevelFilter::from(BLAZE_LVL_DEBUG), LevelFilter::DEBUG);
assert_eq!(LevelFilter::from(BLAZE_LVL_INFO), LevelFilter::INFO);
assert_eq!(LevelFilter::from(BLAZE_LVL_TRACE), LevelFilter::TRACE);
assert_eq!(LevelFilter::from(BLAZE_LVL_WARN), LevelFilter::WARN);
}

/// Check that our `CbWriter` works as expected.
#[test]
fn line_writing() {
let data = br"INFO symbolize: new src=Process(self) addrs=AbsAddr([0x0])
INFO symbolize: enter src=Process(self) addrs=AbsAddr([0x0])
INFO symbolize:handle_unknown_addr: new src=Process(self) addrs=AbsAddr([0x0]) addr=0x0
INFO symbolize:handle_unknown_addr: enter src=Process(self) addrs=AbsAddr([0x0]) addr=0x0
INFO symbolize:handle_unknown_addr: exit src=Process(self) addrs=AbsAddr([0x0]) addr=0x0
INFO symbolize:handle_unknown_addr: close src=Process(self) addrs=AbsAddr([0x0]) addr=0x0
INFO symbolize: exit src=Process(self) addrs=AbsAddr([0x0])
INFO symbolize: close src=Process(self) addrs=AbsAddr([0x0])
";
let mut to_write = &data[..];

fn rand() -> u64 {
RandomState::new().build_hasher().finish()
}

let mut bytes = Vec::new();
let mut writer = LineWriter::new(|line: &CStr| {
let data = line.to_bytes();
assert!(data.ends_with(b"\n"), "{line:?}");
assert!(
!data[..data.len().saturating_sub(1)].contains(&b'\n'),
"{line:?}"
);
let () = bytes.extend_from_slice(data);
});

// Simulate writing of all of `data` into our `LineWriter`
// instance in arbitrary length chunks and check that it emits
// back all the lines contained in the original data.
while !to_write.is_empty() {
let cnt = max(rand() % (max(to_write.len() as u64 / 2, 1)), 1) as usize;
let data = to_write.read_slice(cnt).unwrap();
let n = writer.write(data).unwrap();
assert_ne!(n, 0);

if rand() % 2 == 1 {
let () = writer.flush().unwrap();
}
}

assert_eq!(to_write, &[] as &[u8]);
assert_eq!(bytes.as_slice(), data);
}
}
56 changes: 56 additions & 0 deletions capi/tests/trace.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
//! Test capturing of trace information.
//!
//! Modifies global state; keep in separate test binary.

use std::ffi::c_char;
use std::ffi::CStr;
use std::sync::Mutex;

use blazesym::Addr;
use blazesym_c::blaze_err;
use blazesym_c::blaze_err_last;
use blazesym_c::blaze_symbolize_process_abs_addrs;
use blazesym_c::blaze_symbolize_src_process;
use blazesym_c::blaze_symbolizer_free;
use blazesym_c::blaze_symbolizer_new;
use blazesym_c::blaze_syms_free;
use blazesym_c::blaze_trace;
use blazesym_c::blaze_trace_lvl::*;


/// Check that we retrieve callbacks for traces being emitted.
#[test]
fn trace_callbacks() {
static TRACES: Mutex<Vec<String>> = Mutex::new(Vec::new());

extern "C" fn trace_cb(msg: *const c_char) {
let msg = unsafe { CStr::from_ptr(msg) };
let msg = msg.to_string_lossy().to_string();
let mut traces = TRACES.lock().unwrap();
let () = traces.push(msg);
}

let () = blaze_trace(BLAZE_LVL_TRACE, trace_cb);
assert_eq!(blaze_err_last(), blaze_err::BLAZE_ERR_OK);

// Symbolize something, which should emit traces.
{
let process_src = blaze_symbolize_src_process {
pid: 0,
..Default::default()
};
let symbolizer = blaze_symbolizer_new();
let addrs = [0x0 as Addr];
let result = unsafe {
blaze_symbolize_process_abs_addrs(symbolizer, &process_src, addrs.as_ptr(), addrs.len())
};
let () = unsafe { blaze_syms_free(result) };
let () = unsafe { blaze_symbolizer_free(symbolizer) };
}

let traces = TRACES.lock().unwrap();
assert!(traces.len() > 0, "{traces:?}");

let () = blaze_trace(BLAZE_LVL_TRACE, trace_cb);
assert_eq!(blaze_err_last(), blaze_err::BLAZE_ERR_ALREADY_EXISTS);
}
Loading

0 comments on commit 6309aaa

Please sign in to comment.