Skip to content

Commit 77e1399

Browse files
committed
feat(observability): add request id middleware and access logs
1 parent 6570248 commit 77e1399

4 files changed

Lines changed: 97 additions & 2 deletions

File tree

docs/API_HTTP.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,11 @@ Authentication notes:
99
- `x-spatiad-dispatcher-token: <token>`
1010
- Dispatch endpoints are rate-limited per actor and may return `429 Too Many Requests`.
1111

12+
Request tracing notes:
13+
14+
- Every HTTP response includes `x-request-id`.
15+
- If client sends `x-request-id`, server echoes it back; otherwise server generates one.
16+
1217
## GET /health
1318

1419
Response:

rust/crates/spatiad-api/Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,5 +18,6 @@ spatiad-types = { path = "../spatiad-types" }
1818
spatiad-ws = { path = "../spatiad-ws" }
1919
spatiad-core = { path = "../spatiad-core" }
2020
tokio.workspace = true
21+
tracing.workspace = true
2122
tower = { version = "0.5", features = ["util"] }
2223
uuid.workspace = true

rust/crates/spatiad-api/src/lib.rs

Lines changed: 46 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,9 +2,11 @@ use std::{collections::{HashMap, VecDeque}, sync::Arc};
22

33
use axum::{
44
extract::ws::Message,
5+
extract::Request,
56
extract::{Path, Query, State, WebSocketUpgrade},
6-
http::{HeaderMap, StatusCode},
7-
response::IntoResponse,
7+
http::{HeaderMap, HeaderValue, StatusCode},
8+
middleware::{self, Next},
9+
response::{IntoResponse, Response},
810
routing::{get, post},
911
Json, Router,
1012
};
@@ -19,6 +21,7 @@ use spatiad_types::{Coordinates, DriverStatus, JobRequest, MatchResult, OfferSta
1921
use spatiad_ws::{DriverInbound, DriverOutbound};
2022
use tokio::sync::{mpsc, Mutex};
2123
use tokio::time::{interval, sleep, Duration};
24+
use tracing::info;
2225
use uuid::Uuid;
2326

2427
mod validation;
@@ -205,9 +208,50 @@ pub fn router(state: ApiState) -> Router {
205208
.route("/api/v1/dispatch/job/:job_id", get(dispatch_job_status))
206209
.route("/api/v1/dispatch/job/:job_id/events", get(dispatch_job_events))
207210
.route("/api/v1/stream/driver/:driver_id", get(driver_ws))
211+
.layer(middleware::from_fn(request_context_middleware))
208212
.with_state(state)
209213
}
210214

215+
async fn request_context_middleware(mut request: Request, next: Next) -> Response {
216+
let started = std::time::Instant::now();
217+
let method = request.method().clone();
218+
let path = request.uri().path().to_string();
219+
220+
let request_id = request
221+
.headers()
222+
.get("x-request-id")
223+
.and_then(|value| value.to_str().ok())
224+
.map(|value| value.to_string())
225+
.unwrap_or_else(|| Uuid::new_v4().to_string());
226+
227+
if let Ok(header) = HeaderValue::from_str(&request_id) {
228+
request
229+
.headers_mut()
230+
.insert("x-request-id", header);
231+
}
232+
233+
let mut response = next.run(request).await;
234+
let status = response.status();
235+
let elapsed_ms = started.elapsed().as_millis();
236+
237+
if let Ok(header) = HeaderValue::from_str(&request_id) {
238+
response
239+
.headers_mut()
240+
.insert("x-request-id", header);
241+
}
242+
243+
info!(
244+
request_id = %request_id,
245+
method = %method,
246+
path = %path,
247+
status = status.as_u16(),
248+
duration_ms = elapsed_ms,
249+
"http request",
250+
);
251+
252+
response
253+
}
254+
211255
async fn health() -> Json<HealthResponse> {
212256
Json(HealthResponse {
213257
status: "ok",

rust/crates/spatiad-api/tests/integration_tests.rs

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -95,6 +95,51 @@ mod tests {
9595
assert_eq!(body["active_sessions"], 0);
9696
}
9797

98+
#[tokio::test]
99+
async fn test_health_response_includes_generated_request_id() {
100+
let state = setup_test_state();
101+
let app = router(state);
102+
103+
let request = Request::builder()
104+
.method("GET")
105+
.uri("/health")
106+
.body(Body::empty())
107+
.unwrap();
108+
109+
let response = app.oneshot(request).await.unwrap();
110+
let header = response.headers().get("x-request-id").cloned();
111+
112+
assert!(header.is_some());
113+
let header_value = header.unwrap();
114+
let request_id = header_value
115+
.to_str()
116+
.expect("request id header should be valid utf8");
117+
assert!(!request_id.is_empty());
118+
}
119+
120+
#[tokio::test]
121+
async fn test_health_preserves_incoming_request_id() {
122+
let state = setup_test_state();
123+
let app = router(state);
124+
125+
let request = Request::builder()
126+
.method("GET")
127+
.uri("/health")
128+
.header("x-request-id", "req-123")
129+
.body(Body::empty())
130+
.unwrap();
131+
132+
let response = app.oneshot(request).await.unwrap();
133+
let request_id = response
134+
.headers()
135+
.get("x-request-id")
136+
.expect("request id should be present")
137+
.to_str()
138+
.expect("request id header should be valid utf8");
139+
140+
assert_eq!(request_id, "req-123");
141+
}
142+
98143
#[tokio::test]
99144
async fn test_driver_upsert() {
100145
let state = setup_test_state();

0 commit comments

Comments
 (0)