Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Track caller's emitted events #3268

Merged
merged 4 commits into from
May 31, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -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))
3 changes: 2 additions & 1 deletion crates/events/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -14,12 +14,13 @@ version.workspace = true

[features]
default = []
debug = []
sug0 marked this conversation as resolved.
Show resolved Hide resolved
mainnet = []
migrations = [
"namada_migrations",
"linkme",
]
testing = []
testing = ["debug"]

[dependencies]
namada_core = {path = "../core"}
Expand Down
2 changes: 2 additions & 0 deletions crates/events/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
302 changes: 302 additions & 0 deletions crates/events/src/tracer.rs
Original file line number Diff line number Diff line change
@@ -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<Self, Self::Err> {
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::<u8>()
.wrapping_add(mem::offset_of!(Self, $field))
.cast::<$type>()
.write($value);
};
}

let mut init_state = 0i32;
let mut trace: MaybeUninit<EventTrace<'static>> = 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<W> {
wrapped: W,
pkg_name: &'static str,
pkg_version: &'static str,
}

impl<W> EventTracer<W> {
/// 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<EE, W> EmitEvents for EventTracer<W>
where
EE: EmitEvents,
W: DerefMut<Target = EE>,
{
#[track_caller]
fn emit<E>(&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<B, E>(&mut self, event_batch: B)
where
B: IntoIterator<Item = E>,
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);
sug0 marked this conversation as resolved.
Show resolved Hide resolved
const END_LINE: u32 = line!();

(events.pop().unwrap(), START_LINE, END_LINE)
};

let trace = event.read_attribute::<EventOrigin<'_>>().unwrap();

assert!(trace.line > start_line && trace.line < end_line);
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: maybe trace.line == start_line + 1 && trace.line == END_LINE - 1?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

doesn't matter to be precise, we only care to check if the event was emitted within a certain region of the source code

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<EventTrace<'static>, _> = 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<EventTrace<'static>, _> = 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);
}
}
Loading