-
Notifications
You must be signed in to change notification settings - Fork 25
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
capi: Add support for tapping into traces
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
Showing
8 changed files
with
335 additions
and
4 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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); | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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); | ||
} |
Oops, something went wrong.