build: add api server request context middleware
This commit is contained in:
@@ -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"] }
|
||||
|
||||
@@ -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、路由与协议装配。
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
mod app;
|
||||
mod config;
|
||||
mod logging;
|
||||
mod request_context;
|
||||
mod state;
|
||||
|
||||
use tokio::net::TcpListener;
|
||||
|
||||
65
server-rs/apps/api-server/src/request_context.rs
Normal file
65
server-rs/apps/api-server/src/request_context.rs
Normal 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)
|
||||
})
|
||||
}
|
||||
Reference in New Issue
Block a user