Skip to main content

guinea_trace/
sink.rs

1//! Where records go.
2
3use std::cell::RefCell;
4use std::rc::Rc;
5use std::sync::atomic::{AtomicUsize, Ordering};
6
7use tracing::Level;
8
9use crate::{Cause, Point, Record, Trace};
10
11pub type Observer = Rc<dyn Fn(&Trace)>;
12
13thread_local! {
14    static OBSERVER: RefCell<Option<Observer>> = const { RefCell::new(None) };
15}
16
17static OBSERVED_THREADS: AtomicUsize = AtomicUsize::new(0);
18
19/// Hands every record produced on this thread to `observer`, replacing any
20/// observer already set.
21pub fn observe(observer: impl Fn(&Trace) + 'static) {
22    let before = OBSERVER.with(|slot| slot.borrow_mut().replace(Rc::new(observer)));
23    if before.is_none() {
24        OBSERVED_THREADS.fetch_add(1, Ordering::Relaxed);
25    }
26}
27
28pub fn stop_observing() {
29    if OBSERVER.with(|slot| slot.borrow_mut().take()).is_some() {
30        OBSERVED_THREADS.fetch_sub(1, Ordering::Relaxed);
31    }
32}
33
34/// Whether devtools are watching this thread.
35pub fn is_observed() -> bool {
36    OBSERVER.with(|slot| slot.borrow().is_some())
37}
38
39/// Whether devtools are watching any thread.
40pub fn is_observed_anywhere() -> bool {
41    OBSERVED_THREADS.load(Ordering::Relaxed) > 0
42}
43
44fn observer() -> Option<Observer> {
45    OBSERVER.with(|slot| slot.borrow().clone())
46}
47
48pub(crate) fn wanted() -> bool {
49    OBSERVER.with(|slot| slot.borrow().is_some())
50        || tracing::enabled!(target: "guinea", Level::DEBUG)
51}
52
53/// Whether `target` is one [`emit`] writes points under, so a layer that
54/// turns `tracing` events into points can leave them alone.
55pub fn is_point_target(target: &str) -> bool {
56    target.starts_with("guinea::")
57}
58
59macro_rules! point {
60    ($target:literal, $record:expr $(, $($field:tt)+)?) => {
61        tracing::debug!(
62            target: $target,
63            id = $record.id.get(),
64            parent = $record.parent.map(Cause::get)
65            $(, $($field)+)?
66        )
67    };
68}
69
70fn write(record: &Record) {
71    match &record.point {
72        Point::Action { message } => point!("guinea::action", record, action = %message),
73        Point::Send { actor, message } => {
74            point!("guinea::send", record, actor = %actor, msg = %message)
75        }
76        Point::Handle { actor, message } => {
77            point!("guinea::handle", record, actor = %actor, msg = %message)
78        }
79        Point::Spawn {
80            actor,
81            actor_id,
82            output,
83        } => point!(
84            "guinea::spawn",
85            record,
86            actor = %actor,
87            actor_id,
88            output = %output
89        ),
90        Point::Settled {
91            actor,
92            actor_id,
93            output,
94            took_us,
95        } => point!(
96            "guinea::settled",
97            record,
98            actor = %actor,
99            actor_id,
100            output = %output,
101            took_us
102        ),
103        Point::Cancelled {
104            actor,
105            actor_id,
106            output,
107            took_us,
108        } => point!(
109            "guinea::cancelled",
110            record,
111            actor = %actor,
112            actor_id,
113            output = %output,
114            took_us
115        ),
116        Point::Publish {
117            event,
118            bus,
119            subscribers,
120        } => point!(
121            "guinea::publish",
122            record,
123            event = %event,
124            bus = %bus,
125            subscribers
126        ),
127        Point::Deliver { event, bus } => {
128            point!("guinea::deliver", record, event = %event, bus = %bus)
129        }
130        Point::Push { reducer } => point!("guinea::push", record, reducer = %reducer),
131        Point::Navigate { root, to } => point!("guinea::navigate", record, root = %root, to = %to),
132        Point::Tick { timer } => point!("guinea::tick", record, timer),
133        Point::Store {
134            op,
135            path,
136            field,
137            outside,
138        } => point!(
139            "guinea::store",
140            record,
141            op = %op,
142            path = %path,
143            field = field.as_deref(),
144            outside
145        ),
146        Point::Render { segment, took_us } => {
147            point!("guinea::render", record, segment = %segment, took_us)
148        }
149        Point::Note(text) => point!("guinea::note", record, "{text}"),
150        Point::Log { .. } => {}
151    }
152}
153
154pub(crate) fn emit(trace: Trace) {
155    match &trace {
156        Trace::Begin(record) | Trace::Mark(record) => write(record),
157        Trace::End { id, took } => tracing::trace!(
158            target: "guinea::end",
159            id = id.get(),
160            took_us = took.as_micros() as u64
161        ),
162    }
163
164    if let Some(observer) = observer() {
165        observer(&trace);
166    }
167}