use bytes::Bytes;
use http::{HeaderMap, Request};
use crate::middleware::request_id::REQUEST_ID_HEADER;
pub fn inject_request_headers(headers: &mut HeaderMap) {
if !headers.contains_key(REQUEST_ID_HEADER) {
let id = uuid::Uuid::new_v4().to_string();
match http::HeaderValue::from_str(&id) {
Ok(value) => {
headers.insert(REQUEST_ID_HEADER, value);
}
Err(e) => {
#[cfg(feature = "tracing")]
tracing::warn!(msg = "failed to build request id header", error = %e);
let _ = e;
}
}
}
#[cfg(feature = "tracing")]
{
use tracing_opentelemetry::OpenTelemetrySpanExt as _;
let context = tracing::Span::current().context();
opentelemetry::global::get_text_map_propagator(|propagator| {
propagator.inject_context(&context, &mut HeaderInjector(headers));
});
}
}
#[cfg(feature = "tracing")]
pub fn client_span(req: &Request<Bytes>) -> tracing::Span {
let request_id = req
.headers()
.get(REQUEST_ID_HEADER)
.and_then(|v| v.to_str().ok())
.map(str::to_string);
tracing::info_span!(
"http.client",
"otel.kind" = "client",
"http.request.method" = %req.method(),
"url.full" = %req.uri(),
"server.address" = req.uri().host(),
"request_id" = request_id,
"http.response.status_code" = tracing::field::Empty,
"otel.status_code" = tracing::field::Empty,
)
}
#[cfg(feature = "tracing")]
struct HeaderInjector<'a>(&'a mut HeaderMap);
#[cfg(feature = "tracing")]
impl opentelemetry::propagation::Injector for HeaderInjector<'_> {
fn set(&mut self, key: &str, value: String) {
let Ok(name) = http::header::HeaderName::try_from(key) else {
return;
};
let Ok(value) = http::HeaderValue::from_str(&value) else {
return;
};
self.0.insert(name, value);
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_injects_request_id_when_absent() {
let mut headers = HeaderMap::new();
inject_request_headers(&mut headers);
let value = headers
.get(REQUEST_ID_HEADER)
.and_then(|v| v.to_str().ok())
.expect("x-request-id injected");
assert!(uuid::Uuid::parse_str(value).is_ok());
}
#[test]
fn test_preserves_existing_request_id() {
let mut headers = HeaderMap::new();
headers.insert(
REQUEST_ID_HEADER,
http::HeaderValue::from_static("caller-set"),
);
inject_request_headers(&mut headers);
assert_eq!(headers.get(REQUEST_ID_HEADER).unwrap(), "caller-set");
}
#[cfg(feature = "tracing")]
#[test]
fn test_client_span_uses_request_id() {
let req = Request::builder()
.method("POST")
.uri("https://example.com/pkg.Service/Method")
.header(REQUEST_ID_HEADER, "request-123")
.body(Bytes::new())
.unwrap();
let span = client_span(&req);
drop(span);
}
#[cfg(feature = "tracing")]
#[test]
fn test_header_injector_ignores_invalid_names_and_values() {
use opentelemetry::propagation::Injector as _;
let mut headers = HeaderMap::new();
{
let mut injector = HeaderInjector(&mut headers);
injector.set("not a header", "value".to_string());
injector.set("x-valid", "bad\nvalue".to_string());
injector.set("x-valid", "good".to_string());
}
assert_eq!(headers.get("x-valid").unwrap(), "good");
}
}