diff --git a/.changelog/unreleased/improvements/3268-track-caller-events.md b/.changelog/unreleased/improvements/3268-track-caller-events.md new file mode 100644 index 0000000000..a195bc6cf8 --- /dev/null +++ b/.changelog/unreleased/improvements/3268-track-caller-events.md @@ -0,0 +1,3 @@ +- Add a new event attribute facility to track events to their origin + in Namada's source code. This is useful for debugging purposes. + ([\#3268](https://github.com/anoma/namada/pull/3268)) \ No newline at end of file diff --git a/crates/events/Cargo.toml b/crates/events/Cargo.toml index 961b40f7ef..573b82e25f 100644 --- a/crates/events/Cargo.toml +++ b/crates/events/Cargo.toml @@ -14,12 +14,13 @@ version.workspace = true [features] default = [] +debug = [] mainnet = [] migrations = [ "namada_migrations", "linkme", ] -testing = [] +testing = ["debug"] [dependencies] namada_core = {path = "../core"} diff --git a/crates/events/src/lib.rs b/crates/events/src/lib.rs index 3c8db7d82b..acbe9d64d6 100644 --- a/crates/events/src/lib.rs +++ b/crates/events/src/lib.rs @@ -20,6 +20,8 @@ pub mod extend; #[cfg(any(test, feature = "testing"))] pub mod testing; +#[cfg(any(test, feature = "debug"))] +pub mod tracer; use std::borrow::Cow; use std::collections::BTreeMap; diff --git a/crates/events/src/tracer.rs b/crates/events/src/tracer.rs new file mode 100644 index 0000000000..91ca8d147b --- /dev/null +++ b/crates/events/src/tracer.rs @@ -0,0 +1,302 @@ +//! Trace the origin of emitted events. +//! +//! ## Example +//! +//! ``` +//! #[track_caller] +//! fn emit_event(event: crate::Event, events: &mut impl EmitEvents) { +//! let mut tracer = EventTracer::trace(events); +//! tracer.emit(event); +//! } +//! ``` + +use std::borrow::Cow; +use std::fmt; +use std::mem::{self, MaybeUninit}; +use std::ops::DerefMut; +use std::panic::Location; +use std::str::FromStr; + +use namada_core::booleans::BoolResultUnitExt; + +use super::{EmitEvents, EventToEmit}; +use crate::extend::{ComposeEvent, EventAttributeEntry}; + +/// The origin of an event in source code. +#[derive(Clone, Debug, Eq, PartialEq, Ord, PartialOrd, Hash)] +pub struct EventTrace<'a> { + pkg_name: Cow<'a, str>, + pkg_version: Cow<'a, str>, + file: Cow<'a, str>, + line: u32, + column: u32, +} + +impl<'a> FromStr for EventTrace<'a> { + type Err = String; + + fn from_str(s: &str) -> Result { + let bindings = s.split(',').map(|binding| { + binding.split_once('=').ok_or_else(|| { + format!("Invalid event trace binding: {binding}") + }) + }); + + mod bits { + pub const DONE: i32 = PKG_NAME | PKG_VERSION | FILE | LINE | COLUMN; + + pub const PKG_NAME: i32 = 0b1; + pub const PKG_VERSION: i32 = 0b10; + pub const FILE: i32 = 0b100; + pub const LINE: i32 = 0b1000; + pub const COLUMN: i32 = 0b10000; + } + + macro_rules! init_trace_field { + ($trace:expr => $field:ident : $type:ty = $value:expr) => { + $trace + .as_mut_ptr() + .cast::() + .wrapping_add(mem::offset_of!(Self, $field)) + .cast::<$type>() + .write($value); + }; + } + + let mut init_state = 0i32; + let mut trace: MaybeUninit> = MaybeUninit::uninit(); + + for maybe_binding in bindings { + let (field, value) = maybe_binding?; + + match field { + "pkg_name" => { + unsafe { + init_trace_field!(trace => pkg_name: Cow<'static, str> = Cow::Owned(value.to_owned())); + } + + init_state |= bits::PKG_NAME; + } + "pkg_version" => { + unsafe { + init_trace_field!(trace => pkg_version: Cow<'static, str> = Cow::Owned(value.to_owned())); + } + + init_state |= bits::PKG_VERSION; + } + "file" => { + unsafe { + init_trace_field!(trace => file: Cow<'static, str> = Cow::Owned(value.to_owned())); + } + + init_state |= bits::FILE; + } + "line" => { + let line = value.parse().map_err(|err| { + format!( + "Failed to parse event trace file line: {value}: \ + {err}" + ) + })?; + unsafe { + init_trace_field!(trace => line: u32 = line); + } + + init_state |= bits::LINE; + } + "column" => { + let column = value.parse().map_err(|err| { + format!( + "Failed to parse event trace file column: \ + {value}: {err}" + ) + })?; + unsafe { + init_trace_field!(trace => column: u32 = column); + } + + init_state |= bits::COLUMN; + } + _ => return Err(format!("Unknown event trace field: {field}")), + } + } + + (init_state == bits::DONE).ok_or_else(|| { + "Some fields were not initialized in the event trace".to_owned() + })?; + + Ok(unsafe { trace.assume_init() }) + } +} + +impl fmt::Display for EventTrace<'_> { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + let Self { + pkg_name, + pkg_version, + file, + line, + column, + } = self; + write!( + f, + "pkg_name={pkg_name},pkg_version={pkg_version},file={file},\ + line={line},column={column}" + ) + } +} + +/// Tracer of emitted events. +pub struct EventTracer { + wrapped: W, + pkg_name: &'static str, + pkg_version: &'static str, +} + +impl EventTracer { + /// Build a new [`EventTracer`]. + pub const fn trace(wrapped: W) -> Self { + Self { + wrapped, + pkg_name: env!("CARGO_PKG_NAME"), + pkg_version: env!("CARGO_PKG_VERSION"), + } + } +} + +impl EmitEvents for EventTracer +where + EE: EmitEvents, + W: DerefMut, +{ + #[track_caller] + fn emit(&mut self, event: E) + where + E: EventToEmit, + { + let caller = Location::caller(); + + self.wrapped.emit(event.with(EventOrigin(EventTrace { + pkg_name: Cow::Borrowed(self.pkg_name), + pkg_version: Cow::Borrowed(self.pkg_version), + file: Cow::Borrowed(caller.file()), + line: caller.line(), + column: caller.column(), + }))); + } + + #[track_caller] + fn emit_many(&mut self, event_batch: B) + where + B: IntoIterator, + E: EventToEmit, + { + let caller = Location::caller(); + + self.wrapped.emit_many(event_batch.into_iter().map(|event| { + event.with(EventOrigin(EventTrace { + pkg_name: Cow::Borrowed(self.pkg_name), + pkg_version: Cow::Borrowed(self.pkg_version), + file: Cow::Borrowed(caller.file()), + line: caller.line(), + column: caller.column(), + })) + })); + } +} + +/// Extend an [`Event`](super::Event) with data pertaining to its origin in +/// source code. +pub struct EventOrigin<'a>(pub EventTrace<'a>); + +impl<'a> EventAttributeEntry<'a> for EventOrigin<'a> { + type Value = EventTrace<'a>; + type ValueOwned = EventTrace<'static>; + + const KEY: &'static str = "event-origin"; + + fn into_value(self) -> Self::Value { + self.0 + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::{Event, EventLevel, EventTypeBuilder}; + + const fn dummy_trace() -> EventTrace<'static> { + EventTrace { + pkg_name: Cow::Borrowed("pkg"), + pkg_version: Cow::Borrowed("ver"), + file: Cow::Borrowed("src/file.rs"), + line: 1, + column: 2, + } + } + + #[test] + fn test_event_trace_emit_event() { + let (event, start_line, end_line) = { + let ev = Event::new( + EventTypeBuilder::new_with_type("test").build(), + EventLevel::Tx, + ); + let mut events = Vec::with_capacity(1); + + const START_LINE: u32 = line!(); + emit_event(ev, &mut events); + const END_LINE: u32 = line!(); + + (events.pop().unwrap(), START_LINE, END_LINE) + }; + + let trace = event.read_attribute::>().unwrap(); + + assert!(trace.line > start_line && trace.line < end_line); + assert_eq!(trace.file, file!()); + assert_eq!(trace.pkg_name, env!("CARGO_PKG_NAME")); + assert_eq!(trace.pkg_version, env!("CARGO_PKG_VERSION")); + } + + #[test] + fn test_event_trace_roundtrip() { + let serialized = dummy_trace().to_string(); + let deserialized: EventTrace<'static> = serialized.parse().unwrap(); + + assert_eq!(deserialized, dummy_trace()); + } + + #[test] + fn test_event_trace_fields_missing() { + let serialized = "pkg_name=pkg,pkg_version=ver"; + let result: Result, _> = serialized.parse(); + + assert_eq!( + result, + Err("Some fields were not initialized in the event trace" + .to_owned()) + ); + } + + #[test] + fn test_event_trace_invalid_line() { + let serialized = "pkg_name=pkg,line=bruv"; + let result: Result, _> = serialized.parse(); + + assert_eq!( + result, + Err( + "Failed to parse event trace file line: bruv: invalid digit \ + found in string" + .to_owned() + ) + ); + } + + #[track_caller] + fn emit_event(event: Event, events: &mut impl EmitEvents) { + let mut tracer = EventTracer::trace(events); + tracer.emit(event); + } +}