use crate::error::Result;
use crate::middleware::{Middleware, Next};
use crate::request::Request;
use crate::response::Response;
use crate::security::random_bytes;
use async_trait::async_trait;
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct TraceContext {
pub trace_id: String,
pub span_id: String,
pub parent_span_id: Option<String>,
pub sampled: bool,
pub tracestate: Option<String>,
}
fn hex(bytes: &[u8]) -> String {
bytes.iter().map(|b| format!("{b:02x}")).collect()
}
fn random_hex(len: usize) -> Result<String> {
loop {
let bytes = random_bytes(len)?;
if bytes.iter().any(|b| *b != 0) {
return Ok(hex(&bytes));
}
}
}
fn is_hex(s: &str, len: usize) -> bool {
s.len() == len && s.bytes().all(|b| matches!(b, b'0'..=b'9' | b'a'..=b'f'))
}
impl TraceContext {
pub fn parse(header: &str) -> Option<(String, String, bool)> {
let mut parts = header.trim().split('-');
let (version, trace, span, flags) =
(parts.next()?, parts.next()?, parts.next()?, parts.next()?);
if !is_hex(version, 2) || version == "ff" || (version == "00" && parts.next().is_some()) {
return None;
}
if !is_hex(trace, 32) || trace.bytes().all(|b| b == b'0') {
return None;
}
if !is_hex(span, 16) || span.bytes().all(|b| b == b'0') {
return None;
}
if !is_hex(flags, 2) {
return None;
}
let sampled = u8::from_str_radix(flags, 16).ok()? & 1 == 1;
Some((trace.to_string(), span.to_string(), sampled))
}
pub fn traceparent(&self) -> String {
format!(
"00-{}-{}-{}",
self.trace_id,
self.span_id,
if self.sampled { "01" } else { "00" }
)
}
}
impl Request {
pub fn trace(&self) -> Option<&TraceContext> {
self.extensions.get::<TraceContext>()
}
}
#[derive(Clone, Debug)]
pub struct TraceLayer {
trust_incoming: bool,
echo: bool,
}
impl Default for TraceLayer {
fn default() -> Self {
Self::new()
}
}
impl TraceLayer {
pub fn new() -> Self {
Self {
trust_incoming: true,
echo: false,
}
}
pub fn ignore_incoming(mut self) -> Self {
self.trust_incoming = false;
self
}
pub fn echo_response(mut self) -> Self {
self.echo = true;
self
}
}
#[async_trait]
impl Middleware for TraceLayer {
async fn handle(&self, mut req: Request, next: Next) -> Result<Response> {
let incoming = self
.trust_incoming
.then(|| {
req.headers
.get("traceparent")
.and_then(|v| v.to_str().ok())
.and_then(TraceContext::parse)
})
.flatten();
let tracestate = incoming
.as_ref()
.and_then(|_| req.headers.get("tracestate"))
.and_then(|v| v.to_str().ok())
.filter(|v| v.len() <= 512)
.map(str::to_string);
let ctx = match incoming {
Some((trace_id, parent, sampled)) => TraceContext {
trace_id,
span_id: random_hex(8)?,
parent_span_id: Some(parent),
sampled,
tracestate,
},
None => TraceContext {
trace_id: random_hex(16)?,
span_id: random_hex(8)?,
parent_span_id: None,
sampled: true,
tracestate: None,
},
};
let own = ctx.traceparent();
req.extensions.insert(ctx);
let res = next.run(req).await.unwrap_or_else(Response::from_error);
Ok(if self.echo {
res.with_header("traceparent", own)
} else {
res
})
}
}
#[cfg(test)]
mod tests {
use super::*;
const TRACE: &str = "4bf92f3577b34da6a3ce929d0e0e4736";
const SPAN: &str = "00f067aa0ba902b7";
#[test]
fn parses_valid_headers() {
let (t, s, sampled) = TraceContext::parse(&format!("00-{TRACE}-{SPAN}-01")).unwrap();
assert_eq!((t.as_str(), s.as_str(), sampled), (TRACE, SPAN, true));
let (_, _, sampled) = TraceContext::parse(&format!("00-{TRACE}-{SPAN}-00")).unwrap();
assert!(!sampled);
assert!(TraceContext::parse(&format!("01-{TRACE}-{SPAN}-01-extra")).is_some());
}
#[test]
fn rejects_malformed_headers() {
for bad in [
"".to_string(),
"garbage".to_string(),
format!("ff-{TRACE}-{SPAN}-01"),
format!("00-{TRACE}-{SPAN}"),
format!("00-{TRACE}-{SPAN}-01-extra"),
format!("00-{}-{SPAN}-01", "0".repeat(32)),
format!("00-{TRACE}-{}-01", "0".repeat(16)),
format!("00-{}-{SPAN}-01", TRACE.to_uppercase()),
format!("00-{}-{SPAN}-01", &TRACE[..30]),
format!("00-{TRACE}-{SPAN}-0g"),
] {
assert!(TraceContext::parse(&bad).is_none(), "{bad}");
}
}
#[test]
fn own_traceparent_names_the_span() {
let ctx = TraceContext {
trace_id: TRACE.into(),
span_id: SPAN.into(),
parent_span_id: None,
sampled: true,
tracestate: None,
};
assert_eq!(ctx.traceparent(), format!("00-{TRACE}-{SPAN}-01"));
let unsampled = TraceContext {
sampled: false,
..ctx
};
assert_eq!(unsampled.traceparent(), format!("00-{TRACE}-{SPAN}-00"));
}
}