build: add api server health endpoint
This commit is contained in:
@@ -110,7 +110,8 @@
|
|||||||
交付物:[../server-rs/apps/api-server/src/response_headers.rs](../server-rs/apps/api-server/src/response_headers.rs)
|
交付物:[../server-rs/apps/api-server/src/response_headers.rs](../server-rs/apps/api-server/src/response_headers.rs)
|
||||||
- [x] 接入 `x-response-time-ms`
|
- [x] 接入 `x-response-time-ms`
|
||||||
交付物:[../server-rs/apps/api-server/src/response_headers.rs](../server-rs/apps/api-server/src/response_headers.rs)、[../server-rs/apps/api-server/src/request_context.rs](../server-rs/apps/api-server/src/request_context.rs)
|
交付物:[../server-rs/apps/api-server/src/response_headers.rs](../server-rs/apps/api-server/src/response_headers.rs)、[../server-rs/apps/api-server/src/request_context.rs](../server-rs/apps/api-server/src/request_context.rs)
|
||||||
- [ ] 实现 `/healthz`
|
- [x] 实现 `/healthz`
|
||||||
|
交付物:[../server-rs/apps/api-server/src/health.rs](../server-rs/apps/api-server/src/health.rs)、[../server-rs/apps/api-server/src/app.rs](../server-rs/apps/api-server/src/app.rs)
|
||||||
|
|
||||||
### 基础工程脚本
|
### 基础工程脚本
|
||||||
|
|
||||||
@@ -123,9 +124,11 @@
|
|||||||
### 阶段验收
|
### 阶段验收
|
||||||
|
|
||||||
- [ ] Axum 服务可独立启动
|
- [ ] Axum 服务可独立启动
|
||||||
- [ ] `/healthz` 返回与当前工程兼容
|
- [x] `/healthz` 返回与当前工程兼容
|
||||||
- [ ] 基础 response envelope 与 request id 行为稳定
|
- [x] 基础 response envelope 与 request id 行为稳定
|
||||||
- [ ] Rust workspace 能完整编译通过
|
证据:`cargo test -p api-server --manifest-path server-rs/Cargo.toml` 已通过,覆盖 envelope 协商与 `/healthz` 头部回写。
|
||||||
|
- [x] Rust workspace 能完整编译通过
|
||||||
|
证据:`cargo check -p api-server --manifest-path server-rs/Cargo.toml` 已通过。
|
||||||
|
|
||||||
## M2:鉴权、会话、JWT 与 refresh cookie
|
## M2:鉴权、会话、JWT 与 refresh cookie
|
||||||
|
|
||||||
|
|||||||
2
server-rs/Cargo.lock
generated
2
server-rs/Cargo.lock
generated
@@ -22,10 +22,12 @@ name = "api-server"
|
|||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"axum",
|
"axum",
|
||||||
|
"http-body-util",
|
||||||
"serde",
|
"serde",
|
||||||
"serde_json",
|
"serde_json",
|
||||||
"time",
|
"time",
|
||||||
"tokio",
|
"tokio",
|
||||||
|
"tower",
|
||||||
"tower-http",
|
"tower-http",
|
||||||
"tracing",
|
"tracing",
|
||||||
"tracing-subscriber",
|
"tracing-subscriber",
|
||||||
|
|||||||
@@ -14,3 +14,7 @@ tower-http = { version = "0.6", features = ["trace"] }
|
|||||||
tracing = "0.1"
|
tracing = "0.1"
|
||||||
tracing-subscriber = { version = "0.3", features = ["env-filter", "fmt"] }
|
tracing-subscriber = { version = "0.3", features = ["env-filter", "fmt"] }
|
||||||
uuid = { version = "1", features = ["v4"] }
|
uuid = { version = "1", features = ["v4"] }
|
||||||
|
|
||||||
|
[dev-dependencies]
|
||||||
|
http-body-util = "0.1"
|
||||||
|
tower = { version = "0.5", features = ["util"] }
|
||||||
|
|||||||
@@ -31,7 +31,7 @@
|
|||||||
2. [x] 接入 `request_id`
|
2. [x] 接入 `request_id`
|
||||||
3. [x] 接入统一错误处理中间件
|
3. [x] 接入统一错误处理中间件
|
||||||
4. [x] 接入 response envelope
|
4. [x] 接入 response envelope
|
||||||
5. [ ] 接入 `/healthz`
|
5. [x] 接入 `/healthz`
|
||||||
|
|
||||||
当前 tracing 约定:
|
当前 tracing 约定:
|
||||||
|
|
||||||
@@ -64,6 +64,13 @@
|
|||||||
3. 所有响应都会回写 `x-route-version`,当前阶段默认与 `x-api-version` 保持一致,后续再按路由粒度细分。
|
3. 所有响应都会回写 `x-route-version`,当前阶段默认与 `x-api-version` 保持一致,后续再按路由粒度细分。
|
||||||
4. 所有响应都会回写 `x-response-time-ms`,值来源于 `RequestContext` 内记录的请求开始时间。
|
4. 所有响应都会回写 `x-response-time-ms`,值来源于 `RequestContext` 内记录的请求开始时间。
|
||||||
|
|
||||||
|
当前 `/healthz` 约定:
|
||||||
|
|
||||||
|
1. 路径固定为 `/healthz`。
|
||||||
|
2. 裸响应继续返回 `{ ok: true, service: "genarrative-node-server" }`,保持与当前 Node 工程兼容。
|
||||||
|
3. 当请求携带 `x-genarrative-response-envelope` 时,`/healthz` 会返回标准 success envelope。
|
||||||
|
4. `x-request-id`、`x-api-version`、`x-route-version`、`x-response-time-ms` 会在 `/healthz` 响应中一并回写。
|
||||||
|
|
||||||
## 3. 边界约束
|
## 3. 边界约束
|
||||||
|
|
||||||
1. `api-server` 负责 HTTP、SSE、Cookie、Header、路由与协议装配。
|
1. `api-server` 负责 HTTP、SSE、Cookie、Header、路由与协议装配。
|
||||||
|
|||||||
@@ -1,9 +1,10 @@
|
|||||||
use axum::{body::Body, http::Request, middleware, Router};
|
use axum::{body::Body, extract::Extension, http::Request, middleware, routing::get, Router};
|
||||||
use tower_http::trace::{DefaultOnFailure, DefaultOnRequest, DefaultOnResponse, TraceLayer};
|
use tower_http::trace::{DefaultOnFailure, DefaultOnRequest, DefaultOnResponse, TraceLayer};
|
||||||
use tracing::{info_span, Level};
|
use tracing::{info_span, Level};
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
error_middleware::normalize_error_response,
|
error_middleware::normalize_error_response,
|
||||||
|
health::health_check,
|
||||||
request_context::{attach_request_context, resolve_request_id},
|
request_context::{attach_request_context, resolve_request_id},
|
||||||
response_headers::propagate_request_id_header,
|
response_headers::propagate_request_id_header,
|
||||||
state::AppState,
|
state::AppState,
|
||||||
@@ -12,6 +13,12 @@ use crate::{
|
|||||||
// 统一由这里构造 Axum 路由树,后续再逐项挂接中间件与业务路由。
|
// 统一由这里构造 Axum 路由树,后续再逐项挂接中间件与业务路由。
|
||||||
pub fn build_router(state: AppState) -> Router {
|
pub fn build_router(state: AppState) -> Router {
|
||||||
Router::new()
|
Router::new()
|
||||||
|
.route(
|
||||||
|
"/healthz",
|
||||||
|
get(|Extension(request_context): Extension<_>| async move {
|
||||||
|
health_check(Extension(request_context)).await
|
||||||
|
}),
|
||||||
|
)
|
||||||
// 错误归一化层放在 tracing 里侧,让 tracing 记录到最终对外返回的状态与错误体形态。
|
// 错误归一化层放在 tracing 里侧,让 tracing 记录到最终对外返回的状态与错误体形态。
|
||||||
.layer(middleware::from_fn(normalize_error_response))
|
.layer(middleware::from_fn(normalize_error_response))
|
||||||
// 响应头回写放在错误归一化外侧,确保最终写回的是归一化后的最终响应。
|
// 响应头回写放在错误归一化外侧,确保最终写回的是归一化后的最终响应。
|
||||||
@@ -38,3 +45,102 @@ pub fn build_router(state: AppState) -> Router {
|
|||||||
.layer(middleware::from_fn(attach_request_context))
|
.layer(middleware::from_fn(attach_request_context))
|
||||||
.with_state(state)
|
.with_state(state)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use axum::{
|
||||||
|
body::Body,
|
||||||
|
http::{Request, StatusCode},
|
||||||
|
};
|
||||||
|
use http_body_util::BodyExt;
|
||||||
|
use serde_json::Value;
|
||||||
|
use tower::ServiceExt;
|
||||||
|
|
||||||
|
use crate::{config::AppConfig, state::AppState};
|
||||||
|
|
||||||
|
use super::build_router;
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn healthz_returns_legacy_compatible_payload_and_headers() {
|
||||||
|
let app = build_router(AppState::new(AppConfig::default()));
|
||||||
|
|
||||||
|
let response = app
|
||||||
|
.oneshot(
|
||||||
|
Request::builder()
|
||||||
|
.uri("/healthz")
|
||||||
|
.header("x-request-id", "req-health-legacy")
|
||||||
|
.body(Body::empty())
|
||||||
|
.expect("healthz request should build"),
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
.expect("healthz request should succeed");
|
||||||
|
|
||||||
|
assert_eq!(response.status(), StatusCode::OK);
|
||||||
|
assert_eq!(
|
||||||
|
response.headers().get("x-request-id").and_then(|value| value.to_str().ok()),
|
||||||
|
Some("req-health-legacy")
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
response.headers().get("x-api-version").and_then(|value| value.to_str().ok()),
|
||||||
|
Some("2026-04-08")
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
response.headers().get("x-route-version").and_then(|value| value.to_str().ok()),
|
||||||
|
Some("2026-04-08")
|
||||||
|
);
|
||||||
|
assert!(response.headers().contains_key("x-response-time-ms"));
|
||||||
|
|
||||||
|
let body = response
|
||||||
|
.into_body()
|
||||||
|
.collect()
|
||||||
|
.await
|
||||||
|
.expect("healthz body should collect")
|
||||||
|
.to_bytes();
|
||||||
|
let payload: Value =
|
||||||
|
serde_json::from_slice(&body).expect("healthz body should be valid json");
|
||||||
|
|
||||||
|
assert_eq!(payload["ok"], Value::Bool(true));
|
||||||
|
assert_eq!(
|
||||||
|
payload["service"],
|
||||||
|
Value::String("genarrative-node-server".to_string())
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn healthz_returns_standard_envelope_when_requested() {
|
||||||
|
let app = build_router(AppState::new(AppConfig::default()));
|
||||||
|
|
||||||
|
let response = app
|
||||||
|
.oneshot(
|
||||||
|
Request::builder()
|
||||||
|
.uri("/healthz")
|
||||||
|
.header("x-request-id", "req-health-envelope")
|
||||||
|
.header("x-genarrative-response-envelope", "v1")
|
||||||
|
.body(Body::empty())
|
||||||
|
.expect("healthz request should build"),
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
.expect("healthz request should succeed");
|
||||||
|
|
||||||
|
assert_eq!(response.status(), StatusCode::OK);
|
||||||
|
|
||||||
|
let body = response
|
||||||
|
.into_body()
|
||||||
|
.collect()
|
||||||
|
.await
|
||||||
|
.expect("healthz body should collect")
|
||||||
|
.to_bytes();
|
||||||
|
let payload: Value =
|
||||||
|
serde_json::from_slice(&body).expect("healthz body should be valid json");
|
||||||
|
|
||||||
|
assert_eq!(payload["ok"], Value::Bool(true));
|
||||||
|
assert_eq!(
|
||||||
|
payload["data"]["service"],
|
||||||
|
Value::String("genarrative-node-server".to_string())
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
payload["meta"]["requestId"],
|
||||||
|
Value::String("req-health-envelope".to_string())
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
16
server-rs/apps/api-server/src/health.rs
Normal file
16
server-rs/apps/api-server/src/health.rs
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
use axum::{extract::Extension, Json};
|
||||||
|
use serde_json::{json, Value};
|
||||||
|
|
||||||
|
use crate::{api_response::json_success_body, request_context::RequestContext};
|
||||||
|
|
||||||
|
pub async fn health_check(
|
||||||
|
Extension(request_context): Extension<RequestContext>,
|
||||||
|
) -> Json<Value> {
|
||||||
|
json_success_body(
|
||||||
|
Some(&request_context),
|
||||||
|
json!({
|
||||||
|
"ok": true,
|
||||||
|
"service": "genarrative-node-server",
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -2,6 +2,7 @@ mod app;
|
|||||||
mod api_response;
|
mod api_response;
|
||||||
mod config;
|
mod config;
|
||||||
mod error_middleware;
|
mod error_middleware;
|
||||||
|
mod health;
|
||||||
mod http_error;
|
mod http_error;
|
||||||
mod logging;
|
mod logging;
|
||||||
mod request_context;
|
mod request_context;
|
||||||
|
|||||||
Reference in New Issue
Block a user