use parking_lot::Mutex;
use std::cell::{Cell, RefCell};
use std::fmt;
use std::future::Future;
use std::pin::Pin;
use std::sync::atomic::{AtomicU64, Ordering};
use std::sync::Arc;
use std::task::{Context, Poll};
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
#[cfg_attr(feature = "serde", derive(serde::Serialize))]
pub enum StepPhase {
Before,
On,
After,
}
impl fmt::Display for StepPhase {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
StepPhase::Before => write!(f, "before"),
StepPhase::On => write!(f, "on"),
StepPhase::After => write!(f, "after"),
}
}
}
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
#[cfg_attr(feature = "serde", derive(serde::Serialize))]
pub enum SkipReason {
SkipCondition { label: Option<String> },
OptionalWithoutHandlers,
}
impl fmt::Display for SkipReason {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
SkipReason::SkipCondition { label: Some(label) } => write!(f, "{}", label),
SkipReason::SkipCondition { label: None } => write!(f, "skip_if condition"),
SkipReason::OptionalWithoutHandlers => write!(f, "optional without handlers"),
}
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize))]
pub enum HandlerOutcome {
Continue,
Stop,
Error(String),
}
impl fmt::Display for HandlerOutcome {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
HandlerOutcome::Continue => write!(f, "continue"),
HandlerOutcome::Stop => write!(f, "stop"),
HandlerOutcome::Error(e) => write!(f, "error: {}", e),
}
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize))]
#[non_exhaustive]
pub enum RunOutcome {
Completed,
Stopped,
Cancelled,
Errored { step: String, message: String },
}
impl fmt::Display for RunOutcome {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
RunOutcome::Completed => write!(f, "completed"),
RunOutcome::Stopped => write!(f, "stopped"),
RunOutcome::Cancelled => write!(f, "cancelled"),
RunOutcome::Errored { step, message } => write!(f, "errored at '{}': {}", step, message),
}
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize))]
pub enum TraceEventKind {
RunStarted,
StepStarted { step: String, index: usize },
StepSkipped {
step: String,
index: usize,
reason: SkipReason,
},
HandlerFinished {
step: String,
phase: StepPhase,
handler_index: usize,
outcome: HandlerOutcome,
},
StepCompleted { step: String, index: usize },
ScopeMatched { step: String, scope_index: usize },
ScopeNotMatched { step: String },
FinalizerFinished {
handler_index: usize,
outcome: HandlerOutcome,
},
RunCancelled { step: String, index: usize },
ResourcesReleased { count: usize },
RunFinished { outcome: RunOutcome },
}
impl fmt::Display for TraceEventKind {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
TraceEventKind::RunStarted => write!(f, "run started"),
TraceEventKind::StepStarted { step, index } => write!(f, "step '{}' started (index {})", step, index),
TraceEventKind::StepSkipped { step, index, reason } => {
write!(f, "step '{}' skipped (index {}): {}", step, index, reason)
}
TraceEventKind::HandlerFinished {
step,
phase,
handler_index,
outcome,
} => write!(f, "step '{}' {} handler #{}: {}", step, phase, handler_index, outcome),
TraceEventKind::StepCompleted { step, index } => write!(f, "step '{}' completed (index {})", step, index),
TraceEventKind::ScopeMatched { step, scope_index } => {
write!(f, "step '{}' matched conditional scope #{}", step, scope_index)
}
TraceEventKind::ScopeNotMatched { step } => write!(f, "step '{}' matched no conditional scope", step),
TraceEventKind::FinalizerFinished { handler_index, outcome } => {
write!(f, "finish handler #{}: {}", handler_index, outcome)
}
TraceEventKind::RunCancelled { step, index } => {
write!(f, "run cancelled before step '{}' (index {})", step, index)
}
TraceEventKind::ResourcesReleased { count } => write!(f, "released {} run-scoped resource(s)", count),
TraceEventKind::RunFinished { outcome } => write!(f, "run finished: {}", outcome),
}
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize))]
pub struct TraceEvent {
pub run_id: u64,
pub kind: TraceEventKind,
}
impl fmt::Display for TraceEvent {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "[run {}] {}", self.run_id, self.kind)
}
}
pub trait PipelineObserver: Send + Sync {
fn on_event(&self, event: &TraceEvent);
fn on_handler_error(&self, run_id: u64, step: &str, phase: StepPhase, error: &(dyn std::error::Error + 'static)) {
let _ = (run_id, step, phase, error);
}
}
pub(crate) type SharedObserver = Arc<dyn PipelineObserver>;
pub(crate) type ObserverSlot = Arc<Mutex<Option<Arc<dyn PipelineObserver>>>>;
static NEXT_RUN_ID: AtomicU64 = AtomicU64::new(1);
pub(crate) fn next_run_id() -> u64 {
NEXT_RUN_ID.fetch_add(1, Ordering::Relaxed)
}
thread_local! {
static CURRENT_RUN_ID: Cell<u64> = const { Cell::new(0) };
static CURRENT_SCOPED_OBSERVER: RefCell<Option<Arc<dyn PipelineObserver>>> =
const { RefCell::new(None) };
}
pub(crate) fn current_run_id() -> u64 {
CURRENT_RUN_ID.with(|c| c.get())
}
pub(crate) fn current_scoped_observer() -> Option<SharedObserver> {
CURRENT_SCOPED_OBSERVER.with(|c| c.borrow().clone())
}
pub(crate) fn combine_observers(a: Option<SharedObserver>, b: Option<SharedObserver>) -> Option<SharedObserver> {
match (a, b) {
(None, None) => None,
(Some(only), None) | (None, Some(only)) => Some(only),
(Some(first), Some(second)) => Some(Arc::new(CompositeObserver::with(vec![first, second]))),
}
}
pub(crate) struct HandlerScope<F> {
pub(crate) run_id: u64,
pub(crate) scoped_observer: Option<Arc<dyn PipelineObserver>>,
pub(crate) fut: F,
}
impl<F: Future + Unpin> Future for HandlerScope<F> {
type Output = F::Output;
fn poll(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<F::Output> {
let run_id = self.run_id;
let scoped = self.scoped_observer.clone();
let previous_run_id = CURRENT_RUN_ID.with(|c| c.replace(run_id));
let previous_observer = CURRENT_SCOPED_OBSERVER.with(|c| c.replace(scoped));
let result = Pin::new(&mut self.fut).poll(cx);
CURRENT_RUN_ID.with(|c| c.set(previous_run_id));
CURRENT_SCOPED_OBSERVER.with(|c| *c.borrow_mut() = previous_observer);
result
}
}
#[derive(Clone, Default)]
pub struct TraceCollector {
inner: Arc<Mutex<Vec<TraceEvent>>>,
}
impl TraceCollector {
pub fn new() -> Self {
Self::default()
}
pub fn record(&self, event: TraceEvent) {
self.inner.lock().push(event);
}
pub fn events(&self) -> Vec<TraceEvent> {
self.inner.lock().clone()
}
pub fn clear(&self) {
self.inner.lock().clear();
}
pub fn run_ids(&self) -> Vec<u64> {
let events = self.inner.lock();
let mut ids: Vec<u64> = Vec::new();
for e in events.iter() {
if !ids.contains(&e.run_id) {
ids.push(e.run_id);
}
}
ids
}
pub fn for_run(&self, run_id: u64) -> RunTrace {
let events = self
.inner
.lock()
.iter()
.filter(|e| e.run_id == run_id)
.cloned()
.collect();
RunTrace { run_id, events }
}
pub fn completed_steps(&self) -> Vec<String> {
completed_steps(&self.inner.lock())
}
pub fn skipped_steps(&self) -> Vec<String> {
skipped_steps(&self.inner.lock())
}
pub fn step_completed(&self, step: &str) -> bool {
self.completed_steps().iter().any(|s| s == step)
}
pub fn step_skipped(&self, step: &str) -> bool {
self.skipped_steps().iter().any(|s| s == step)
}
pub fn handler_finishes(&self, step: &str, phase: StepPhase) -> Vec<HandlerOutcome> {
handler_finishes(&self.inner.lock(), step, phase)
}
pub fn run_count(&self) -> usize {
self
.inner
.lock()
.iter()
.filter(|e| matches!(e.kind, TraceEventKind::RunStarted))
.count()
}
pub fn last_outcome(&self) -> Option<RunOutcome> {
last_outcome(&self.inner.lock())
}
}
impl PipelineObserver for TraceCollector {
fn on_event(&self, event: &TraceEvent) {
self.record(event.clone());
}
}
#[derive(Clone, Default)]
pub struct CompositeObserver {
observers: Vec<Arc<dyn PipelineObserver>>,
}
impl CompositeObserver {
pub fn new() -> Self {
Self::default()
}
pub fn with(observers: Vec<Arc<dyn PipelineObserver>>) -> Self {
Self { observers }
}
pub fn push(&mut self, observer: Arc<dyn PipelineObserver>) -> &mut Self {
self.observers.push(observer);
self
}
}
impl PipelineObserver for CompositeObserver {
fn on_event(&self, event: &TraceEvent) {
for observer in &self.observers {
observer.on_event(event);
}
}
fn on_handler_error(&self, run_id: u64, step: &str, phase: StepPhase, error: &(dyn std::error::Error + 'static)) {
for observer in &self.observers {
observer.on_handler_error(run_id, step, phase, error);
}
}
}
#[derive(Debug, Clone)]
pub struct RunTrace {
run_id: u64,
events: Vec<TraceEvent>,
}
impl RunTrace {
pub fn run_id(&self) -> u64 {
self.run_id
}
pub fn events(&self) -> &[TraceEvent] {
&self.events
}
pub fn completed_steps(&self) -> Vec<String> {
completed_steps(&self.events)
}
pub fn skipped_steps(&self) -> Vec<String> {
skipped_steps(&self.events)
}
pub fn step_completed(&self, step: &str) -> bool {
self.completed_steps().iter().any(|s| s == step)
}
pub fn step_skipped(&self, step: &str) -> bool {
self.skipped_steps().iter().any(|s| s == step)
}
pub fn handler_finishes(&self, step: &str, phase: StepPhase) -> Vec<HandlerOutcome> {
handler_finishes(&self.events, step, phase)
}
pub fn last_outcome(&self) -> Option<RunOutcome> {
last_outcome(&self.events)
}
}
fn completed_steps(events: &[TraceEvent]) -> Vec<String> {
events
.iter()
.filter_map(|e| match &e.kind {
TraceEventKind::StepCompleted { step, .. } => Some(step.clone()),
_ => None,
})
.collect()
}
fn skipped_steps(events: &[TraceEvent]) -> Vec<String> {
events
.iter()
.filter_map(|e| match &e.kind {
TraceEventKind::StepSkipped { step, .. } => Some(step.clone()),
_ => None,
})
.collect()
}
fn handler_finishes(events: &[TraceEvent], step: &str, phase: StepPhase) -> Vec<HandlerOutcome> {
events
.iter()
.filter_map(|e| match &e.kind {
TraceEventKind::HandlerFinished {
step: s,
phase: p,
outcome,
..
} if s == step && *p == phase => Some(outcome.clone()),
_ => None,
})
.collect()
}
fn last_outcome(events: &[TraceEvent]) -> Option<RunOutcome> {
events.iter().rev().find_map(|e| match &e.kind {
TraceEventKind::RunFinished { outcome } => Some(outcome.clone()),
_ => None,
})
}
#[cfg(test)]
mod tests {
use super::*;
#[tokio::test]
async fn nested_with_run_id_restores_the_parent_id() {
let after_nested = Arc::new(Mutex::new(u64::MAX));
let seen = after_nested.clone();
let inner = Box::pin(async {
assert_eq!(current_run_id(), 7, "the nested run sees its own id");
});
let outer = Box::pin(async move {
assert_eq!(current_run_id(), 42, "the parent sees its own id before nesting");
HandlerScope { run_id: 7, scoped_observer: None, fut: inner }.await;
*seen.lock() = current_run_id();
});
HandlerScope { run_id: 42, scoped_observer: None, fut: outer }.await;
assert_eq!(*after_nested.lock(), 42, "the parent's id survives a nested run");
assert_eq!(current_run_id(), 0, "and the top level is left clear");
}
}