use std::fmt;
use std::future::Future;
use std::sync::Arc;
use std::sync::atomic::{AtomicU64, Ordering};
use std::time::{Duration, Instant};
use crate::{Error, HttpVersion};
mod body;
mod fanout;
mod headers;
mod metrics;
mod notes;
mod summary;
pub(crate) use body::BodyWatch;
pub use body::{BodyEnd, BodyOutcome};
pub use fanout::Fanout;
pub(crate) use headers::masked;
pub use metrics::{Metrics, MetricsSnapshot};
pub(crate) use notes::{note_browser, note_proxy, note_tag};
pub use summary::Summary;
pub(crate) use summary::{Finish, summary};
#[derive(Debug)]
#[non_exhaustive]
pub struct Dns<'a> {
pub id: u64,
pub host: &'a str,
pub port: u16,
pub addrs: usize,
pub elapsed: Duration,
}
#[derive(Debug)]
#[non_exhaustive]
pub struct Connect<'a> {
pub id: u64,
pub host: &'a str,
pub port: u16,
pub reused: bool,
pub elapsed: Duration,
}
#[derive(Debug)]
#[non_exhaustive]
pub struct Tls<'a> {
pub id: u64,
pub host: &'a str,
pub version: Option<&'a str>,
pub cipher: Option<&'a str>,
pub alpn: Option<&'a str>,
pub elapsed: Duration,
}
#[non_exhaustive]
pub struct Sent<'a> {
pub id: u64,
pub host: &'a str,
pub method: &'a str,
pub path: &'a str,
pub protocol: HttpVersion,
pub elapsed: Duration,
}
impl std::fmt::Debug for Sent<'_> {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("Sent")
.field("id", &self.id)
.field("host", &self.host)
.field("method", &self.method)
.field("path", &crate::util::redact_target(self.path))
.field("protocol", &self.protocol)
.field("elapsed", &self.elapsed)
.finish()
}
}
#[non_exhaustive]
pub struct Head<'a> {
pub id: u64,
pub host: &'a str,
pub status: u16,
pub protocol: HttpVersion,
pub elapsed: Duration,
pub headers: &'a http::HeaderMap,
}
impl fmt::Debug for Head<'_> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("Head")
.field("id", &self.id)
.field("host", &self.host)
.field("status", &self.status)
.field("protocol", &self.protocol)
.field("elapsed", &self.elapsed)
.field(
"headers",
&masked(self.headers.iter().map(|(k, v)| (k.as_str(), v.as_bytes()))),
)
.finish()
}
}
#[derive(Debug)]
#[non_exhaustive]
pub struct Done<'a> {
pub id: u64,
pub elapsed: Duration,
pub outcome: Result<(), &'a Error>,
}
#[expect(
unused_variables,
reason = "no-op defaults keep the event name visible in the rendered signature"
)]
pub trait Trace: Send + Sync + 'static {
fn dns(&self, ev: &Dns<'_>) {}
fn connect(&self, ev: &Connect<'_>) {}
fn tls(&self, ev: &Tls<'_>) {}
fn sent(&self, ev: &Sent<'_>) {}
fn head(&self, ev: &Head<'_>) {}
fn done(&self, ev: &Done<'_>) {}
fn summary(&self, ev: &Summary<'_>) {}
fn body(&self, ev: &BodyEnd<'_>) {}
}
#[derive(Clone)]
pub(crate) struct Ctx {
hook: Arc<dyn Trace>,
id: u64,
start: Instant,
root: (u64, Instant),
notes: notes::SharedNotes,
}
tokio::task_local! {
static CURRENT: Ctx;
}
static NEXT: AtomicU64 = AtomicU64::new(1);
pub(crate) async fn scope<F>(hook: Option<&Arc<dyn Trace>>, fut: F) -> F::Output
where
F: Future,
{
match hook {
Some(hook) => {
let id = NEXT.fetch_add(1, Ordering::Relaxed);
let start = Instant::now();
let root = CURRENT.try_with(|outer| outer.root).unwrap_or((id, start));
let ctx = Ctx {
hook: Arc::clone(hook),
id,
start,
root,
notes: notes::SharedNotes::default(),
};
CURRENT.scope(ctx, fut).await
}
None => fut.await,
}
}
pub(crate) fn carry<F>(fut: F) -> impl Future<Output = F::Output> + Send
where
F: Future + Send,
{
let ctx = CURRENT.try_with(Clone::clone).ok();
async move {
match ctx {
Some(ctx) => CURRENT.scope(ctx, fut).await,
None => fut.await,
}
}
}
pub(crate) fn on() -> bool {
CURRENT.try_with(|_| ()).is_ok()
}
fn with(emit: impl FnOnce(&Ctx)) {
CURRENT.try_with(emit).unwrap_or(())
}
pub(crate) fn dns(host: &str, port: u16, addrs: usize, elapsed: Duration) {
with(|ctx| {
ctx.hook.dns(&Dns {
id: ctx.id,
host,
port,
addrs,
elapsed,
});
});
}
pub(crate) fn connect(host: &str, port: u16, reused: bool, elapsed: Duration) {
with(|ctx| {
ctx.hook.connect(&Connect {
id: ctx.id,
host,
port,
reused,
elapsed,
});
});
}
pub(crate) fn tls(
host: &str,
version: Option<&str>,
cipher: Option<&str>,
alpn: Option<&str>,
elapsed: Duration,
) {
with(|ctx| {
ctx.hook.tls(&Tls {
id: ctx.id,
host,
version,
cipher,
alpn,
elapsed,
});
});
}
pub(crate) fn sent(host: &str, method: &str, path: &str, protocol: HttpVersion, elapsed: Duration) {
with(|ctx| {
ctx.hook.sent(&Sent {
id: ctx.id,
host,
method,
path,
protocol,
elapsed,
});
});
}
pub(crate) fn head<I, N, V>(
host: &str,
status: u16,
protocol: HttpVersion,
elapsed: Duration,
headers: I,
) where
I: IntoIterator<Item = (N, V)>,
N: AsRef<[u8]>,
V: Into<bytes::Bytes>,
{
with(|ctx| {
let headers = crate::core::header_map(headers);
ctx.hook.head(&Head {
id: ctx.id,
host,
status,
protocol,
elapsed,
headers: &headers,
});
});
}
pub(crate) fn done(outcome: Result<(), &Error>) {
with(|ctx| {
ctx.hook.done(&Done {
id: ctx.id,
elapsed: ctx.start.elapsed(),
outcome,
});
});
}
#[derive(Debug, Default, Clone, Copy)]
pub struct TracingTrace;
impl Trace for TracingTrace {
fn dns(&self, ev: &Dns<'_>) {
tracing::debug!(target: "leyline::trace", id = ev.id, host = ev.host, port = ev.port, addrs = ev.addrs, elapsed_ms = ms(ev.elapsed), "dns");
}
fn connect(&self, ev: &Connect<'_>) {
tracing::debug!(target: "leyline::trace", id = ev.id, host = ev.host, port = ev.port, reused = ev.reused, elapsed_ms = ms(ev.elapsed), "connect");
}
fn tls(&self, ev: &Tls<'_>) {
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");
}
fn sent(&self, ev: &Sent<'_>) {
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");
}
fn head(&self, ev: &Head<'_>) {
tracing::debug!(target: "leyline::trace", id = ev.id, host = ev.host, status = ev.status, protocol = ?ev.protocol, elapsed_ms = ms(ev.elapsed), "head");
}
fn done(&self, ev: &Done<'_>) {
match ev.outcome {
Ok(()) => {
tracing::debug!(target: "leyline::trace", id = ev.id, elapsed_ms = ms(ev.elapsed), "done")
}
Err(e) => {
tracing::debug!(target: "leyline::trace", id = ev.id, elapsed_ms = ms(ev.elapsed), kind = ?e.kind(), error = %e, "done")
}
}
}
fn summary(&self, ev: &Summary<'_>) {
summary::render(ev);
}
fn body(&self, ev: &BodyEnd<'_>) {
body::render(ev);
}
}
fn ms(d: Duration) -> u32 {
u32::try_from(d.as_millis()).unwrap_or(u32::MAX)
}
impl<T: Trace> Trace for Arc<T> {
fn dns(&self, ev: &Dns<'_>) {
T::dns(self, ev);
}
fn connect(&self, ev: &Connect<'_>) {
T::connect(self, ev);
}
fn tls(&self, ev: &Tls<'_>) {
T::tls(self, ev);
}
fn sent(&self, ev: &Sent<'_>) {
T::sent(self, ev);
}
fn head(&self, ev: &Head<'_>) {
T::head(self, ev);
}
fn done(&self, ev: &Done<'_>) {
T::done(self, ev);
}
fn summary(&self, ev: &Summary<'_>) {
T::summary(self, ev);
}
fn body(&self, ev: &BodyEnd<'_>) {
T::body(self, ev);
}
}
#[cfg(test)]
mod tests;