Skip to main content

leyline/
trace.rs

1use std::fmt;
2use std::future::Future;
3use std::sync::Arc;
4use std::sync::atomic::{AtomicU64, Ordering};
5use std::time::{Duration, Instant};
6
7use crate::{Error, HttpVersion};
8
9mod body;
10mod fanout;
11mod headers;
12mod metrics;
13mod notes;
14mod summary;
15
16pub(crate) use body::BodyWatch;
17pub use body::{BodyEnd, BodyOutcome};
18pub use fanout::Fanout;
19pub(crate) use headers::masked;
20pub use metrics::{Metrics, MetricsSnapshot};
21pub(crate) use notes::{note_browser, note_proxy, note_tag};
22pub use summary::Summary;
23pub(crate) use summary::{Finish, summary};
24
25#[derive(Debug)]
26#[non_exhaustive]
27pub struct Dns<'a> {
28    pub id: u64,
29    pub host: &'a str,
30    pub port: u16,
31    pub addrs: usize,
32    pub elapsed: Duration,
33}
34
35#[derive(Debug)]
36#[non_exhaustive]
37pub struct Connect<'a> {
38    pub id: u64,
39    pub host: &'a str,
40    pub port: u16,
41    pub reused: bool,
42    pub elapsed: Duration,
43}
44
45#[derive(Debug)]
46#[non_exhaustive]
47pub struct Tls<'a> {
48    pub id: u64,
49    pub host: &'a str,
50    pub version: Option<&'a str>,
51    pub cipher: Option<&'a str>,
52    pub alpn: Option<&'a str>,
53    pub elapsed: Duration,
54}
55
56#[non_exhaustive]
57pub struct Sent<'a> {
58    pub id: u64,
59    pub host: &'a str,
60    pub method: &'a str,
61    pub path: &'a str,
62    pub protocol: HttpVersion,
63    pub elapsed: Duration,
64}
65
66impl std::fmt::Debug for Sent<'_> {
67    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
68        f.debug_struct("Sent")
69            .field("id", &self.id)
70            .field("host", &self.host)
71            .field("method", &self.method)
72            .field("path", &crate::util::redact_target(self.path))
73            .field("protocol", &self.protocol)
74            .field("elapsed", &self.elapsed)
75            .finish()
76    }
77}
78
79#[non_exhaustive]
80pub struct Head<'a> {
81    pub id: u64,
82    pub host: &'a str,
83    pub status: u16,
84    pub protocol: HttpVersion,
85    pub elapsed: Duration,
86    pub headers: &'a http::HeaderMap,
87}
88
89impl fmt::Debug for Head<'_> {
90    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
91        f.debug_struct("Head")
92            .field("id", &self.id)
93            .field("host", &self.host)
94            .field("status", &self.status)
95            .field("protocol", &self.protocol)
96            .field("elapsed", &self.elapsed)
97            .field(
98                "headers",
99                &masked(self.headers.iter().map(|(k, v)| (k.as_str(), v.as_bytes()))),
100            )
101            .finish()
102    }
103}
104
105#[derive(Debug)]
106#[non_exhaustive]
107pub struct Done<'a> {
108    pub id: u64,
109    pub elapsed: Duration,
110    pub outcome: Result<(), &'a Error>,
111}
112
113#[expect(
114    unused_variables,
115    reason = "no-op defaults keep the event name visible in the rendered signature"
116)]
117pub trait Trace: Send + Sync + 'static {
118    fn dns(&self, ev: &Dns<'_>) {}
119    fn connect(&self, ev: &Connect<'_>) {}
120    fn tls(&self, ev: &Tls<'_>) {}
121    fn sent(&self, ev: &Sent<'_>) {}
122    fn head(&self, ev: &Head<'_>) {}
123    fn done(&self, ev: &Done<'_>) {}
124    fn summary(&self, ev: &Summary<'_>) {}
125    fn body(&self, ev: &BodyEnd<'_>) {}
126}
127
128#[derive(Clone)]
129pub(crate) struct Ctx {
130    hook: Arc<dyn Trace>,
131    id: u64,
132    start: Instant,
133    root: (u64, Instant),
134    notes: notes::SharedNotes,
135}
136
137tokio::task_local! {
138    static CURRENT: Ctx;
139}
140
141static NEXT: AtomicU64 = AtomicU64::new(1);
142
143pub(crate) async fn scope<F>(hook: Option<&Arc<dyn Trace>>, fut: F) -> F::Output
144where
145    F: Future,
146{
147    match hook {
148        Some(hook) => {
149            let id = NEXT.fetch_add(1, Ordering::Relaxed);
150            let start = Instant::now();
151            let root = CURRENT.try_with(|outer| outer.root).unwrap_or((id, start));
152            let ctx = Ctx {
153                hook: Arc::clone(hook),
154                id,
155                start,
156                root,
157                notes: notes::SharedNotes::default(),
158            };
159            CURRENT.scope(ctx, fut).await
160        }
161        None => fut.await,
162    }
163}
164
165pub(crate) fn carry<F>(fut: F) -> impl Future<Output = F::Output> + Send
166where
167    F: Future + Send,
168{
169    let ctx = CURRENT.try_with(Clone::clone).ok();
170    async move {
171        match ctx {
172            Some(ctx) => CURRENT.scope(ctx, fut).await,
173            None => fut.await,
174        }
175    }
176}
177
178pub(crate) fn on() -> bool {
179    CURRENT.try_with(|_| ()).is_ok()
180}
181
182fn with(emit: impl FnOnce(&Ctx)) {
183    CURRENT.try_with(emit).unwrap_or(())
184}
185
186pub(crate) fn dns(host: &str, port: u16, addrs: usize, elapsed: Duration) {
187    with(|ctx| {
188        ctx.hook.dns(&Dns {
189            id: ctx.id,
190            host,
191            port,
192            addrs,
193            elapsed,
194        });
195    });
196}
197
198pub(crate) fn connect(host: &str, port: u16, reused: bool, elapsed: Duration) {
199    with(|ctx| {
200        ctx.hook.connect(&Connect {
201            id: ctx.id,
202            host,
203            port,
204            reused,
205            elapsed,
206        });
207    });
208}
209
210pub(crate) fn tls(
211    host: &str,
212    version: Option<&str>,
213    cipher: Option<&str>,
214    alpn: Option<&str>,
215    elapsed: Duration,
216) {
217    with(|ctx| {
218        ctx.hook.tls(&Tls {
219            id: ctx.id,
220            host,
221            version,
222            cipher,
223            alpn,
224            elapsed,
225        });
226    });
227}
228
229pub(crate) fn sent(host: &str, method: &str, path: &str, protocol: HttpVersion, elapsed: Duration) {
230    with(|ctx| {
231        ctx.hook.sent(&Sent {
232            id: ctx.id,
233            host,
234            method,
235            path,
236            protocol,
237            elapsed,
238        });
239    });
240}
241
242pub(crate) fn head<I, N, V>(
243    host: &str,
244    status: u16,
245    protocol: HttpVersion,
246    elapsed: Duration,
247    headers: I,
248) where
249    I: IntoIterator<Item = (N, V)>,
250    N: AsRef<[u8]>,
251    V: Into<bytes::Bytes>,
252{
253    with(|ctx| {
254        let headers = crate::core::header_map(headers);
255        ctx.hook.head(&Head {
256            id: ctx.id,
257            host,
258            status,
259            protocol,
260            elapsed,
261            headers: &headers,
262        });
263    });
264}
265
266pub(crate) fn done(outcome: Result<(), &Error>) {
267    with(|ctx| {
268        ctx.hook.done(&Done {
269            id: ctx.id,
270            elapsed: ctx.start.elapsed(),
271            outcome,
272        });
273    });
274}
275
276#[derive(Debug, Default, Clone, Copy)]
277pub struct TracingTrace;
278
279impl Trace for TracingTrace {
280    fn dns(&self, ev: &Dns<'_>) {
281        tracing::debug!(target: "leyline::trace", id = ev.id, host = ev.host, port = ev.port, addrs = ev.addrs, elapsed_ms = ms(ev.elapsed), "dns");
282    }
283
284    fn connect(&self, ev: &Connect<'_>) {
285        tracing::debug!(target: "leyline::trace", id = ev.id, host = ev.host, port = ev.port, reused = ev.reused, elapsed_ms = ms(ev.elapsed), "connect");
286    }
287
288    fn tls(&self, ev: &Tls<'_>) {
289        tracing::debug!(target: "leyline::trace", id = ev.id, host = ev.host, version = ev.version, cipher = ev.cipher, alpn = ev.alpn, elapsed_ms = ms(ev.elapsed), "tls");
290    }
291
292    fn sent(&self, ev: &Sent<'_>) {
293        tracing::debug!(target: "leyline::trace", id = ev.id, host = ev.host, method = ev.method, path = %crate::util::redact_target(ev.path), protocol = ?ev.protocol, elapsed_ms = ms(ev.elapsed), "sent");
294    }
295
296    fn head(&self, ev: &Head<'_>) {
297        tracing::debug!(target: "leyline::trace", id = ev.id, host = ev.host, status = ev.status, protocol = ?ev.protocol, elapsed_ms = ms(ev.elapsed), "head");
298    }
299
300    fn done(&self, ev: &Done<'_>) {
301        match ev.outcome {
302            Ok(()) => {
303                tracing::debug!(target: "leyline::trace", id = ev.id, elapsed_ms = ms(ev.elapsed), "done")
304            }
305            Err(e) => {
306                tracing::debug!(target: "leyline::trace", id = ev.id, elapsed_ms = ms(ev.elapsed), kind = ?e.kind(), error = %e, "done")
307            }
308        }
309    }
310
311    fn summary(&self, ev: &Summary<'_>) {
312        summary::render(ev);
313    }
314
315    fn body(&self, ev: &BodyEnd<'_>) {
316        body::render(ev);
317    }
318}
319
320fn ms(d: Duration) -> u32 {
321    u32::try_from(d.as_millis()).unwrap_or(u32::MAX)
322}
323
324impl<T: Trace> Trace for Arc<T> {
325    fn dns(&self, ev: &Dns<'_>) {
326        T::dns(self, ev);
327    }
328
329    fn connect(&self, ev: &Connect<'_>) {
330        T::connect(self, ev);
331    }
332
333    fn tls(&self, ev: &Tls<'_>) {
334        T::tls(self, ev);
335    }
336
337    fn sent(&self, ev: &Sent<'_>) {
338        T::sent(self, ev);
339    }
340
341    fn head(&self, ev: &Head<'_>) {
342        T::head(self, ev);
343    }
344
345    fn done(&self, ev: &Done<'_>) {
346        T::done(self, ev);
347    }
348
349    fn summary(&self, ev: &Summary<'_>) {
350        T::summary(self, ev);
351    }
352
353    fn body(&self, ev: &BodyEnd<'_>) {
354        T::body(self, ev);
355    }
356}
357
358#[cfg(test)]
359mod tests;