use axum::{
extract::{MatchedPath, Request},
http::{HeaderMap, HeaderValue},
middleware::Next,
response::Response,
};
use opentelemetry::{Context, trace::TraceContextExt};
use std::time::Instant;
use tracing::{Instrument, debug_span};
use tracing_opentelemetry::OpenTelemetrySpanExt;
use super::{REQUEST_ID_HEADER, SERVER_TIME_HEADER};
pub async fn opentelemetry_tracing_middleware(request: Request, next: Next) -> Response {
let start_time = Instant::now();
let method = request.method().to_string();
let uri = request.uri().to_string();
let route = request
.extensions()
.get::<MatchedPath>()
.map(|matched_path| matched_path.as_str().to_owned())
.unwrap_or_else(|| request.uri().path().to_owned());
let version = format!("{:?}", request.version());
let user_agent = request
.headers()
.get("user-agent")
.and_then(|h| h.to_str().ok())
.unwrap_or("unknown");
let span = debug_span!(
"http_request",
otel.name = format!("{} {}", method, route).as_str(),
otel.kind = "server",
http.method = method.as_str(),
http.url = uri.as_str(),
http.route = route.as_str(),
http.scheme = "http",
http.version = version.as_str(),
http.user_agent = user_agent,
);
let otel_cx = Context::current();
if let Err(error) = span.set_parent(otel_cx) {
tracing::warn!(target: "telemetry", %method, %uri, ?error, "failed to attach OpenTelemetry parent context");
}
let trace_id = span.context().span().span_context().trace_id().to_string();
let trace_id = if trace_id == "00000000000000000000000000000000" {
use uuid::Uuid;
Uuid::new_v4().simple().to_string()
} else {
trace_id
};
async move {
let mut response = next.run(request).await;
let duration = start_time.elapsed();
let duration_micros = duration.as_micros();
let status_code = response.status().as_u16();
let headers = response.headers_mut();
if let Ok(trace_header) = HeaderValue::from_str(&trace_id) {
headers.insert(REQUEST_ID_HEADER, trace_header);
}
if let Ok(time_header) = HeaderValue::from_str(&duration_micros.to_string()) {
headers.insert(SERVER_TIME_HEADER, time_header);
}
tracing::debug!(
method = %method,
uri = %uri,
status = %status_code,
duration_micros = %duration_micros,
trace_id = %trace_id,
"HTTP request completed"
);
response
}
.instrument(span)
.await
}
pub fn extract_trace_id() -> String {
let current_span = tracing::Span::current();
let context = current_span.context();
let span_ref = context.span();
let span_context = span_ref.span_context();
let trace_id = if span_context.is_valid() {
span_context.trace_id().to_string()
} else {
"00000000000000000000000000000000".to_string()
};
if trace_id == "00000000000000000000000000000000" {
use uuid::Uuid;
Uuid::new_v4().simple().to_string()
} else {
trace_id
}
}
#[allow(dead_code)]
pub fn extract_trace_from_headers(headers: &HeaderMap) -> Option<String> {
if let Some(traceparent) = headers.get("traceparent")
&& let Ok(traceparent_str) = traceparent.to_str()
{
let parts: Vec<&str> = traceparent_str.split('-').collect();
if parts.len() >= 2 {
return Some(parts[1].to_string());
}
}
if let Some(trace_id) = headers.get("x-trace-id")
&& let Ok(trace_id_str) = trace_id.to_str()
{
return Some(trace_id_str.to_string());
}
None
}
#[cfg(test)]
mod tests {
use super::*;
use axum::http::{HeaderMap, HeaderValue};
#[test]
fn test_extract_trace_from_headers_traceparent() {
let mut headers = HeaderMap::new();
headers.insert(
"traceparent",
HeaderValue::from_static("00-4bf92f3577b34da6a3ce929d0e0e4736-00f067aa0ba902b7-01"),
);
let trace_id = extract_trace_from_headers(&headers);
assert_eq!(
trace_id,
Some("4bf92f3577b34da6a3ce929d0e0e4736".to_string())
);
}
#[test]
fn test_extract_trace_from_headers_custom() {
let mut headers = HeaderMap::new();
headers.insert(
"x-trace-id",
HeaderValue::from_static("custom-trace-id-123"),
);
let trace_id = extract_trace_from_headers(&headers);
assert_eq!(trace_id, Some("custom-trace-id-123".to_string()));
}
#[test]
fn test_extract_trace_from_headers_none() {
let headers = HeaderMap::new();
let trace_id = extract_trace_from_headers(&headers);
assert_eq!(trace_id, None);
}
}