diff --git a/src/api/middleware.rs b/src/api/middleware.rs
index 9b90da2..763f407 100644
--- a/src/api/middleware.rs
+++ b/src/api/middleware.rs
@@ -12,7 +12,7 @@ use std::net::SocketAddr;
use std::sync::atomic::{AtomicU64, Ordering};
use std::sync::Arc;
use std::time::{Duration, Instant};
-use tracing::warn;
+use tracing::{warn, Instrument};
lazy_static::lazy_static! {
static ref START_INSTANT: Instant = Instant::now();
@@ -398,6 +398,23 @@ pub async fn rate_limit_layer(
next.run(req).await
}
+pub async fn correlation_id_layer(mut req: Request
, next: Next) -> Response {
+ let correlation_id = crate::tracing::correlation::correlation_id_from_headers(req.headers());
+ req.extensions_mut().insert(correlation_id.clone());
+
+ let span = tracing::info_span!(
+ "http.correlation",
+ correlation_id = %correlation_id.as_str()
+ );
+
+ let mut response = crate::tracing::correlation::scope(correlation_id.clone(), async move {
+ next.run(req).instrument(span).await
+ })
+ .await;
+ crate::tracing::correlation::insert_header(response.headers_mut(), &correlation_id);
+ response
+}
+
#[cfg(test)]
mod tests {
use super::*;
diff --git a/src/api/router.rs b/src/api/router.rs
index fa95ab7..d7e89b0 100644
--- a/src/api/router.rs
+++ b/src/api/router.rs
@@ -109,6 +109,9 @@ pub async fn build_router(state: AppState) -> anyhow::Result {
.layer(axum_mw::from_fn(
crate::api::middleware::slo_monitoring_layer,
))
+ .layer(axum_mw::from_fn(
+ crate::api::middleware::correlation_id_layer,
+ ))
.layer(cors)
.with_state(state);
diff --git a/src/service_mesh/client.rs b/src/service_mesh/client.rs
index 5436ffa..01cc0fa 100644
--- a/src/service_mesh/client.rs
+++ b/src/service_mesh/client.rs
@@ -144,9 +144,12 @@ impl ServiceMeshClient {
req = req.header("content-type", "application/json").body(b);
}
- req.header("x-service-name", &self.identity.service_account)
+ req = req
+ .header("x-service-name", &self.identity.service_account)
.header("x-trust-domain", &self.identity.trust_domain)
- .timeout(std::time::Duration::from_secs(endpoint.timeout_secs))
+ .timeout(std::time::Duration::from_secs(endpoint.timeout_secs));
+
+ crate::tracing::correlation::propagate_reqwest(req)
.send()
.await
.map_err(|e| ServiceMeshClientError::HttpError(e.to_string()))
@@ -164,10 +167,12 @@ impl ServiceMeshClient {
.join(endpoint.health_check_path.trim_start_matches('/'))
.map_err(|e| ServiceMeshClientError::HttpError(e.to_string()))?;
- match self
+ let req = self
.http_client
.get(url)
- .timeout(std::time::Duration::from_secs(5))
+ .timeout(std::time::Duration::from_secs(5));
+
+ match crate::tracing::correlation::propagate_reqwest(req)
.send()
.await
{
diff --git a/src/tracing/correlation.rs b/src/tracing/correlation.rs
new file mode 100644
index 0000000..da554b5
--- /dev/null
+++ b/src/tracing/correlation.rs
@@ -0,0 +1,143 @@
+use std::collections::HashMap;
+use std::future::Future;
+
+use axum::http::{HeaderMap, HeaderValue};
+use uuid::Uuid;
+
+pub const CORRELATION_ID_HEADER: &str = "x-correlation-id";
+const MAX_CORRELATION_ID_LEN: usize = 128;
+
+#[derive(Clone, Debug, Eq, PartialEq)]
+pub struct CorrelationId(String);
+
+impl CorrelationId {
+ pub fn generate() -> Self {
+ Self(Uuid::new_v4().to_string())
+ }
+
+ pub fn parse(value: &str) -> Option {
+ let trimmed = value.trim();
+ if trimmed.is_empty() || trimmed.len() > MAX_CORRELATION_ID_LEN {
+ return None;
+ }
+
+ if trimmed
+ .chars()
+ .all(|ch| ch.is_ascii_alphanumeric() || matches!(ch, '-' | '_' | '.'))
+ {
+ Some(Self(trimmed.to_string()))
+ } else {
+ None
+ }
+ }
+
+ pub fn as_str(&self) -> &str {
+ &self.0
+ }
+
+ fn to_header_value(&self) -> HeaderValue {
+ HeaderValue::from_str(self.as_str()).expect("correlation ids are sanitized")
+ }
+}
+
+tokio::task_local! {
+ static CURRENT_CORRELATION_ID: String;
+}
+
+pub async fn scope(id: CorrelationId, future: F) -> T
+where
+ F: Future