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;