tracing_web/
performance_layer.rs

1use std::marker::PhantomData;
2
3use js_sys::{JsString, Object, Reflect};
4use tracing_core::{span, Subscriber};
5use tracing_subscriber::{
6    field::RecordFields,
7    fmt::{FormatFields, FormattedFields},
8    layer::Context,
9    registry::{Extensions, ExtensionsMut, LookupSpan, SpanRef},
10    Layer,
11};
12use wasm_bindgen::{prelude::wasm_bindgen, JsCast, JsValue};
13
14#[wasm_bindgen]
15extern "C" {
16    #[wasm_bindgen(js_name = _fakeGlobal)]
17    type Global;
18    #[wasm_bindgen()]
19    type Performance;
20    #[wasm_bindgen(static_method_of = Global, js_class = "globalThis", getter)]
21    fn performance() -> Performance;
22    #[wasm_bindgen(method, catch, js_name = "mark")]
23    fn do_mark(this: &Performance, name: &str) -> Result<(), JsValue>;
24    #[wasm_bindgen(method, catch, js_name = "mark")]
25    fn do_mark_with_details(
26        this: &Performance,
27        name: &str,
28        details: &JsValue,
29    ) -> Result<(), JsValue>;
30    #[wasm_bindgen(method, catch, js_name = "measure")]
31    fn do_measure_with_start_mark_and_end_mark(
32        this: &Performance,
33        name: &str,
34        start: &str,
35        end: &str,
36    ) -> Result<(), JsValue>;
37    #[wasm_bindgen(method, catch, js_name = "measure")]
38    fn do_measure_with_details(
39        this: &Performance,
40        name: &str,
41        details: &JsValue,
42    ) -> Result<(), JsValue>;
43}
44
45impl Performance {
46    fn mark(&self, name: &str) -> Result<(), JsValue> {
47        self.do_mark(name)
48    }
49    fn mark_detailed(&self, name: &str, details: &str) -> Result<(), JsValue> {
50        let details_obj = Object::create(JsValue::NULL.unchecked_ref::<Object>());
51        let detail_prop = JsString::from(wasm_bindgen::intern("detail"));
52        Reflect::set(&details_obj, &detail_prop, &JsValue::from(details)).unwrap();
53        self.do_mark_with_details(name, &details_obj)
54    }
55    fn measure(&self, name: &str, start: &str, end: &str) -> Result<(), JsValue> {
56        self.do_measure_with_start_mark_and_end_mark(name, start, end)
57    }
58    fn measure_detailed(
59        &self,
60        name: &str,
61        start: &str,
62        end: &str,
63        details: &str,
64    ) -> Result<(), JsValue> {
65        let details_obj = Object::create(JsValue::NULL.unchecked_ref::<Object>());
66        let detail_prop = JsString::from(wasm_bindgen::intern("detail"));
67        let start_prop = JsString::from(wasm_bindgen::intern("start"));
68        let end_prop = JsString::from(wasm_bindgen::intern("end"));
69        Reflect::set(&details_obj, &detail_prop, &JsValue::from(details)).unwrap();
70        Reflect::set(&details_obj, &start_prop, &JsValue::from(start)).unwrap();
71        Reflect::set(&details_obj, &end_prop, &JsValue::from(end)).unwrap();
72        self.do_measure_with_details(name, &details_obj)
73    }
74}
75
76thread_local! {
77    static PERF: Performance = {
78        let performance = Global::performance();
79        assert!(!performance.is_undefined(), "browser seems to not support the Performance API");
80        performance
81    };
82}
83
84/// A [`Layer`] that emits span enter, exit and events as [`performance`] marks.
85///
86/// [`performance`]: https://developer.mozilla.org/en-US/docs/Web/API/Performance
87pub struct PerformanceEventsLayer<S, N = ()> {
88    fmt_details: N,
89    _inner: PhantomData<fn(S)>,
90}
91
92impl<S, N> PerformanceEventsLayer<S, N> {
93    /// Change the way additional details are attached to performance events.
94    ///
95    /// The given [`FormatFields`] is used to format a string that is attached to each event.
96    /// See the [`mod@tracing_subscriber::fmt::format`] module for an assortment of available formatters.
97    pub fn with_details_from_fields<N2>(
98        self,
99        fmt_fields: N2,
100    ) -> PerformanceEventsLayer<S, FormatSpanFromFields<N2>>
101    where
102        N2: 'static + for<'writer> FormatFields<'writer>,
103    {
104        self.with_details(FormatSpanFromFields { inner: fmt_fields })
105    }
106    /// Change the way additional details are attached to performance events.
107    ///
108    /// See also [`with_details_from_fields`](Self::with_details_from_fields) for compatibility with [`mod@tracing_subscriber::fmt::format`].
109    pub fn with_details<N2: FormatSpan>(self, fmt_details: N2) -> PerformanceEventsLayer<S, N2> {
110        PerformanceEventsLayer {
111            fmt_details,
112            _inner: PhantomData,
113        }
114    }
115}
116
117impl<S, N> PerformanceEventsLayer<S, N>
118where
119    S: Subscriber + for<'lookup> LookupSpan<'lookup>,
120    N: FormatSpan,
121{
122    fn template_name(span: &SpanRef<'_, S>, event_name: &str) -> String {
123        let span_id = span.id().into_u64();
124        let name = span.metadata().name();
125        format!("{name} [{span_id}]: {event_name}")
126    }
127    fn span_enter_name(&self, span: &SpanRef<'_, S>) -> String {
128        Self::template_name(span, "span-enter")
129    }
130    fn span_exit_name(&self, span: &SpanRef<'_, S>) -> String {
131        Self::template_name(span, "span-exit")
132    }
133    fn span_record_name(&self, span: &SpanRef<'_, S>) -> String {
134        Self::template_name(span, "span-record")
135    }
136    fn span_measure_name(&self, span: &SpanRef<'_, S>) -> String {
137        Self::template_name(span, "span-measure")
138    }
139}
140
141impl<S, N> Layer<S> for PerformanceEventsLayer<S, N>
142where
143    S: Subscriber + for<'lookup> LookupSpan<'lookup>,
144    N: FormatSpan,
145{
146    fn on_new_span(&self, attrs: &span::Attributes<'_>, span: &span::Id, ctx: Context<'_, S>) {
147        let span = ctx.span(span).expect("can't find span, this is a bug");
148
149        self.fmt_details
150            .add_details(&mut span.extensions_mut(), attrs);
151    }
152    fn on_record(&self, span: &span::Id, values: &span::Record<'_>, ctx: Context<'_, S>) {
153        let span = ctx.span(span).expect("can't find span, this is a bug");
154        self.fmt_details
155            .record_values(&mut span.extensions_mut(), values);
156
157        let mark_name = self.span_record_name(&span);
158        let _ = PERF.with(|p| {
159            if let Some(details) = self.fmt_details.find_details(&span.extensions()) {
160                p.mark_detailed(&mark_name, details)
161            } else {
162                p.mark(&mark_name)
163            }
164        }); // Ignore errors
165    }
166    fn on_enter(&self, span: &span::Id, ctx: Context<'_, S>) {
167        let span = ctx.span(span).expect("can't find span, this is a bug");
168        let mark_name = self.span_enter_name(&span);
169        let _ = PERF.with(|p| {
170            if let Some(details) = self.fmt_details.find_details(&span.extensions()) {
171                p.mark_detailed(&mark_name, details)
172            } else {
173                p.mark(&mark_name)
174            }
175        }); // Ignore errors
176    }
177    fn on_exit(&self, span: &span::Id, ctx: Context<'_, S>) {
178        let span = ctx.span(span).expect("can't find span, this is a bug");
179        let mark_enter_name = self.span_enter_name(&span);
180        let mark_exit_name = self.span_exit_name(&span);
181        let mark_measure_name = self.span_measure_name(&span);
182        let _ = PERF.with(|p| {
183            if let Some(details) = self.fmt_details.find_details(&span.extensions()) {
184                p.mark_detailed(&mark_exit_name, details)?;
185                p.measure_detailed(
186                    &mark_measure_name,
187                    &mark_enter_name,
188                    &mark_exit_name,
189                    details,
190                )?;
191            } else {
192                p.mark(&mark_exit_name)?;
193                p.measure(&mark_measure_name, &mark_enter_name, &mark_exit_name)?;
194            }
195            Result::<(), JsValue>::Ok(())
196        }); // Ignore errors
197    }
198    fn on_id_change(&self, _: &span::Id, _: &span::Id, _ctx: Context<'_, S>) {
199        web_sys::console::warn_1(&JsValue::from(
200            "A span changed id, this is currently not supported",
201        ));
202        debug_assert!(false, "A span changed id, this is currently not supported");
203    }
204}
205
206/// Construct a new layer recording performance events.
207///
208/// The default will not attach any additional field information to the events.
209pub fn performance_layer<S>() -> PerformanceEventsLayer<S, ()>
210where
211    S: Subscriber + for<'lookup> LookupSpan<'lookup>,
212{
213    PerformanceEventsLayer {
214        fmt_details: (),
215        _inner: PhantomData,
216    }
217}
218
219/// Determine what additional information will be attached to the performance events.
220pub trait FormatSpan: 'static {
221    /// Find the details in the extensions of a span that will be recorded with the event.
222    fn find_details<'ext>(&self, ext: &'ext Extensions<'_>) -> Option<&'ext str>;
223    /// Called when a span is constructed, with its initial attributes.
224    ///
225    /// This method should insert, for later consumption in [`Self::find_details`], a description of the details.
226    fn add_details(&self, ext: &mut ExtensionsMut<'_>, attrs: &span::Attributes<'_>);
227    /// Called when a span records some values.
228    ///
229    /// This method should modify, for later consumption in [`Self::find_details`], the description of the details.
230    fn record_values(&self, ext: &mut ExtensionsMut<'_>, values: &span::Record<'_>);
231}
232
233impl FormatSpan for () {
234    fn find_details<'ext>(&self, _: &'ext Extensions<'_>) -> Option<&'ext str> {
235        None
236    }
237    fn add_details(&self, _: &mut ExtensionsMut<'_>, _: &span::Attributes<'_>) {}
238    fn record_values(&self, _: &mut ExtensionsMut<'_>, _: &span::Record<'_>) {}
239}
240
241/// An adaptor for Formatters from [`mod@tracing_subscriber::fmt::format`] as a [`FormatSpan`].
242///
243/// Uses [`FormattedFields`] to store the details attachement, so it might reuse an existing extension
244/// for logging, to save some work visiting the recorded fields.
245pub struct FormatSpanFromFields<N> {
246    inner: N,
247}
248impl<N> FormatSpanFromFields<N>
249where
250    N: 'static + for<'writer> FormatFields<'writer>,
251{
252    fn add_formatted_fields(&self, ext: &mut ExtensionsMut<'_>, fields: impl RecordFields) {
253        if ext.get_mut::<FormattedFields<N>>().is_none() {
254            let mut fmt_fields = FormattedFields::<N>::new(String::new());
255            if self
256                .inner
257                .format_fields(fmt_fields.as_writer(), fields)
258                .is_ok()
259            {
260                ext.insert(fmt_fields);
261            }
262        }
263    }
264}
265
266impl<N> FormatSpan for FormatSpanFromFields<N>
267where
268    N: 'static + for<'writer> FormatFields<'writer>,
269{
270    fn find_details<'ext>(&self, ext: &'ext Extensions<'_>) -> Option<&'ext str> {
271        let fields = ext.get::<FormattedFields<N>>()?;
272        Some(&fields.fields)
273    }
274
275    fn add_details(&self, ext: &mut ExtensionsMut<'_>, attrs: &span::Attributes<'_>) {
276        self.add_formatted_fields(ext, attrs);
277    }
278
279    fn record_values(&self, ext: &mut ExtensionsMut<'_>, values: &span::Record<'_>) {
280        if let Some(fields) = ext.get_mut::<FormattedFields<N>>() {
281            let _ = self.inner.add_fields(fields, values);
282        } else {
283            self.add_formatted_fields(ext, values);
284        }
285    }
286}