Skip to content
Open
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
5 changes: 5 additions & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ required-features = ["json-logger"]

# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]
anyhow = {version = "1.0.100", features = ["std"], optional = true}
opentelemetry = {version = "0.31", optional = true}
opentelemetry-semantic-conventions = {version = "0.31", optional = true}
opentelemetry-otlp = {version = "0.31", features = ["http-proto", "reqwest-blocking-client"], default-features = false, optional = true}
Expand All @@ -57,10 +58,14 @@ chrono = {version = "^0.4", default-features = false, features = ["serde", "cloc
url = "2.5.0"
once_cell = "1.19.0"

# error Report
error-report = "0.0.1"

[dev-dependencies]
actix-web = "4.0.1"
opentelemetry-jaeger = {version = "0.22", features = ["integration_test"]}
prima_bridge = "0.25"
tokio = {version = "1.17", features = ["rt", "macros", "rt-multi-thread"]}
tracing-actix-web = {version = "0.7.11", features = ["opentelemetry_0_27"]}
tracing-capture = "0.1.0"
Comment on lines +62 to +70
Copy link
Contributor

Choose a reason for hiding this comment

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

I still don't understand what these definitions import, are these internal crates?

Copy link
Member Author

Choose a reason for hiding this comment

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

I'll try to explain them.

error-report -> https://docs.rs/error-report/0.0.1/error_report

Report prints an error and all its sources.

Source messages will be cleaned using CleanedErrors to remove duplication from errors that include their source’s message in their own message.

The debug implementation prints each error on a separate line, while the display implementation prints all errors on one line separated by a colon. Using alternate formatting ({:#}) is identical to the debug implementation.

tracing-capture -> https://docs.rs/tracing-capture/0.1.0/tracing_capture/

This crate provides a tracing Layer to capture tracing spans and events as they occur.

The captured spans and events can then be used for testing assertions (e.g., "Did a span with a specific name / target / … occur? What were its fields? Was the span closed? How many times the span was entered?" and so on).

Choose a reason for hiding this comment

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

Word of advise @damjack , the error-report crate used in starsky and incentives-backend is not https://docs.rs/error-report, but https://github.com/primait/intermediaries-crates/tree/master/error-report

Copy link
Contributor

Choose a reason for hiding this comment

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

Thanks for the reference

I see tracing_capture is just imported in tests, is it just a local dev library?

For error-report the fact that

  • it is 0.0.1
  • has no associated repo
    kinda scares me off of depending on it, could the code be ported directly here...?

Not sure if we need everything contained in the source, I get wanting to "pretty print" errors but we already depend on shaky libs as is

uuid = {version = "1.10", features = ["v4"]}
5 changes: 4 additions & 1 deletion src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -18,8 +18,11 @@
//! # }
//! ```

mod config;
#[cfg(feature = "traces")]
#[macro_use]
mod macros;

mod config;
mod subscriber;

#[cfg(feature = "json-logger")]
Expand Down
101 changes: 101 additions & 0 deletions src/macros.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
/// Emit a tracing error event for an error with rich, structured context.
/// It captures the error using the experimental Rust [std::error::Report](https://doc.rust-lang.org/stable/std/error/struct.Report.html)
/// and adding the type name as error.kind, the backtrace as error.trace and the error stack as error.message
///
/// # Examples
///
/// ```rust
/// use prima_tracing::trace_error;
/// # fn main() {
///
/// let error = "not a number".parse::<usize>().unwrap_err();
/// trace_error!(error, "Parsing error!");
/// trace_error!(error, "Parsing error: {}", uid="1234");
/// # }
/// ```
#[macro_export]
macro_rules! trace_error {
($error:expr, $message:expr) => {
{
$crate::trace_error!($error, $message,)
}
};
($error:expr, $message:expr, $($field:tt)*) => {
{
let kind = std::any::type_name_of_val(&$error);
let error_message = format!("{:#}", $error);
let stack = error_report::Report::new(&$error);
let trace = std::backtrace::Backtrace::force_capture();

$crate::tracing::error!(
{
error.message = error_message,
error.kind = kind,
error.stack = ?stack,
error.trace = %trace,
$($field)*
},
"{} {}",
$message,
$error
);
}
};
($error:expr, $message:expr) => {
{
$crate::trace_error!($error, $message, {})
}
};
}

/// Emit a tracing error event for anyhow error with rich, structured context.
/// It captures the error using the experimental Rust [std::error::Report](https://doc.rust-lang.org/stable/std/error/struct.Report.html)
/// and adding the type name as error.kind, the backtrace as error.trace and the error stack as error.message
///
/// # Examples
///
/// ```rust
/// use anyhow::anyhow
/// use prima_tracing::trace_error;
/// # fn main() {
///
/// let error = anyhow!("an error");
/// trace_anyhow_error!(error, "Throw error!");
/// trace_anyhow_error!(error, "Throw error!", uid="1234");
/// # }
/// ```
#[cfg(feature = "anyhow")]
#[macro_export]
macro_rules! trace_anyhow_error {
($error:expr, $message:expr) => {
{
$crate::trace_anyhow_error!($error, $message,)
}
};
($error:expr, $message:expr, $($field:tt)*) => {
{
let kind = std::any::type_name_of_val(&$error.root_cause());
let error_message = format!("{:#}", $error);
let std_err: &(dyn std::error::Error + 'static) = $error.as_ref();
let stack = error_report::Report::new(std_err);

$crate::tracing::error!(
{
error.message = error_message,
error.kind = kind,
error.stack = ?stack,
error.trace = %$error.backtrace(),
$($field)*
},
"{} {:?}",
$message,
$error
);
}
};
($error:expr, $message:expr) => {
{
$crate::trace_anyhow_error!($error, $message,)
}
};
}
84 changes: 84 additions & 0 deletions tests/macros.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
use tracing_capture::{CaptureLayer, CapturedEvent, SharedStorage};
use tracing_subscriber::layer::SubscriberExt;

#[cfg(feature = "anyhow")]
use {anyhow::anyhow, prima_tracing::trace_anyhow_error};

use prima_tracing::trace_error;

#[test]
fn produce_trace_error() {
let error = "not a number".parse::<usize>().unwrap_err();

with_test_tracing(
|| {
trace_error!(error, "Parsing error!", something = "something");
},
|events| {
let event = events.first().unwrap();

assert_eq!(event["error.message"], "invalid digit found in string");
assert_eq!(event["error.kind"], "core::num::error::ParseIntError");
assert_eq!(
event["error.stack"].as_debug_str(),
Some("invalid digit found in string")
);
assert!(event["error.trace"].as_debug_str().is_some());
assert!(event["error.trace"]
.as_debug_str()
.unwrap()
.contains("macros::produce_trace_error"));
assert_eq!(event["something"], "something");
assert_eq!(
event["message"].as_debug_str(),
Some("Parsing error! invalid digit found in string")
);
},
)
}

#[cfg(feature = "anyhow")]
#[test]
fn produce_trace_anyhow_error() {
let error = anyhow!("an error");

with_test_tracing(
|| {
trace_anyhow_error!(error, "Throw error!");
},
|events| {
let event = events.first().unwrap();

assert_eq!(event["error.message"], "an error");
assert_eq!(event["error.kind"], "&dyn core::error::Error");
assert_eq!(event["error.stack"].as_debug_str(), Some("an error"));
assert_eq!(
event["error.trace"].as_debug_str(),
Some(format!("{}", error.backtrace()).as_str())
);
assert_eq!(
event["message"].as_debug_str(),
Some(format!("Throw error! {error:?}").as_str())
);
},
)
}

fn with_test_tracing<F, T>(tracing: F, tests: T)
where
F: FnOnce(),
T: FnOnce(Vec<CapturedEvent>),
{
let subscriber = tracing_subscriber::fmt().pretty().finish();
let storage: SharedStorage = SharedStorage::default();
let subscriber = subscriber.with(CaptureLayer::new(&storage));

tracing::subscriber::with_default(subscriber, || {
tracing::info_span!("test-span").in_scope(tracing);
});

let storage = storage.lock();
let span = storage.all_spans().next().unwrap();

tests(span.events().collect());
}
1 change: 1 addition & 0 deletions tests/main.rs
Original file line number Diff line number Diff line change
@@ -1 +1,2 @@
mod e2e;
mod macros;