build: add api server request context middleware

This commit is contained in:
2026-04-21 01:20:32 +08:00
parent f3b36f15b5
commit 0ac5606a41
7 changed files with 425 additions and 7 deletions

View File

@@ -10,3 +10,4 @@ tokio = { version = "1", features = ["macros", "rt-multi-thread", "net"] }
tower-http = { version = "0.6", features = ["trace"] }
tracing = "0.1"
tracing-subscriber = { version = "0.3", features = ["env-filter", "fmt"] }
uuid = { version = "1", features = ["v4"] }

View File

@@ -28,7 +28,7 @@
后续与本 package 直接相关的任务包括:
1. [x] 接入统一日志与 tracing
2. [ ] 接入 `request_id`
2. [x] 接入 `request_id`
3. [ ] 接入统一错误处理中间件
4. [ ] 接入 response envelope
5. [ ] 接入 `/healthz`
@@ -39,6 +39,12 @@
2. 默认日志过滤器来自 `GENARRATIVE_API_LOG`,未提供时回落到 `info,tower_http=info`
3. HTTP 访问日志统一通过 Axum 路由层的 `TraceLayer` 输出,后续 `request_id`、响应头与错误中间件继续在同一层扩展。
当前 request context 约定:
1. 中间件优先读取来访 `x-request-id`,未提供时生成新的 UUID。
2. `request_id` 会统一写入请求 `extensions` 与请求头,供 tracing、错误处理中间件和响应头层复用。
3. 响应头回写 `x-request-id` 仍属于后续独立任务,本阶段只完成请求上下文准备。
## 3. 边界约束
1. `api-server` 负责 HTTP、SSE、Cookie、Header、路由与协议装配。

View File

@@ -1,8 +1,11 @@
use axum::Router;
use tower_http::trace::{DefaultMakeSpan, DefaultOnFailure, DefaultOnRequest, DefaultOnResponse, TraceLayer};
use tracing::Level;
use axum::{body::Body, http::Request, middleware, Router};
use tower_http::trace::{DefaultOnFailure, DefaultOnRequest, DefaultOnResponse, TraceLayer};
use tracing::{info_span, Level};
use crate::state::AppState;
use crate::{
request_context::{attach_request_context, resolve_request_id},
state::AppState,
};
// 统一由这里构造 Axum 路由树,后续再逐项挂接中间件与业务路由。
pub fn build_router(state: AppState) -> Router {
@@ -10,10 +13,22 @@ pub fn build_router(state: AppState) -> Router {
// 当前阶段先统一挂接 HTTP tracing后续 request_id、响应头与错误中间件继续在这里扩展。
.layer(
TraceLayer::new_for_http()
.make_span_with(DefaultMakeSpan::new().level(Level::INFO))
.make_span_with(|request: &Request<Body>| {
let request_id =
resolve_request_id(request).unwrap_or_else(|| "unknown".to_string());
info_span!(
"http.request",
method = %request.method(),
uri = %request.uri(),
request_id = %request_id,
)
})
.on_request(DefaultOnRequest::new().level(Level::INFO))
.on_response(DefaultOnResponse::new().level(Level::INFO))
.on_failure(DefaultOnFailure::new().level(Level::ERROR)),
)
// request_id 中间件先进入请求链,确保后续 tracing、错误处理和响应头层都能复用同一份请求标识。
.layer(middleware::from_fn(attach_request_context))
.with_state(state)
}

View File

@@ -1,6 +1,7 @@
mod app;
mod config;
mod logging;
mod request_context;
mod state;
use tokio::net::TcpListener;

View File

@@ -0,0 +1,65 @@
use axum::{
extract::Request,
http::{header::HeaderName, HeaderValue, Request as HttpRequest},
middleware::Next,
response::Response,
};
use uuid::Uuid;
pub const X_REQUEST_ID_HEADER: &str = "x-request-id";
// 当前阶段先把请求级元信息统一挂到 extensions后续响应头、envelope 与错误处理中间件继续复用。
#[derive(Clone, Debug)]
pub struct RequestContext {
request_id: String,
}
impl RequestContext {
pub fn new(request_id: String) -> Self {
Self { request_id }
}
pub fn request_id(&self) -> &str {
&self.request_id
}
}
pub async fn attach_request_context(mut request: Request, next: Next) -> Response {
let request_id = request
.headers()
.get(X_REQUEST_ID_HEADER)
.and_then(|value| value.to_str().ok())
.map(str::trim)
.filter(|value| !value.is_empty())
.map(ToOwned::to_owned)
.unwrap_or_else(|| Uuid::new_v4().to_string());
request
.extensions_mut()
.insert(RequestContext::new(request_id.clone()));
// 统一把 request_id 写回请求头,方便后续 tracing、响应头与 envelope 层读取同一来源。
if let Ok(header_value) = HeaderValue::from_str(&request_id) {
request
.headers_mut()
.insert(HeaderName::from_static(X_REQUEST_ID_HEADER), header_value);
}
next.run(request).await
}
pub fn resolve_request_id<B>(request: &HttpRequest<B>) -> Option<String> {
request
.extensions()
.get::<RequestContext>()
.map(|context| context.request_id().to_string())
.or_else(|| {
request
.headers()
.get(X_REQUEST_ID_HEADER)
.and_then(|value| value.to_str().ok())
.map(str::trim)
.filter(|value| !value.is_empty())
.map(ToOwned::to_owned)
})
}