huskies: merge 768
This commit is contained in:
@@ -197,7 +197,6 @@ pub async fn run(
|
|||||||
// open supplementary mesh connections for resilience.
|
// open supplementary mesh connections for resilience.
|
||||||
{
|
{
|
||||||
let sync_handler = poem::get(crate::crdt_sync::crdt_sync_handler);
|
let sync_handler = poem::get(crate::crdt_sync::crdt_sync_handler);
|
||||||
let health_handler = poem::get(crate::http::health::health);
|
|
||||||
|
|
||||||
// Build a minimal AppContext for the crdt_sync_handler (the handler
|
// Build a minimal AppContext for the crdt_sync_handler (the handler
|
||||||
// receives it via Data<> but doesn't use it — the underscore prefix
|
// receives it via Data<> but doesn't use it — the underscore prefix
|
||||||
@@ -207,7 +206,6 @@ pub async fn run(
|
|||||||
|
|
||||||
let app = poem::Route::new()
|
let app = poem::Route::new()
|
||||||
.at("/crdt-sync", sync_handler)
|
.at("/crdt-sync", sync_handler)
|
||||||
.at("/health", health_handler)
|
|
||||||
.data(agent_ctx_arc);
|
.data(agent_ctx_arc);
|
||||||
|
|
||||||
let bind_addr = format!("0.0.0.0:{port}");
|
let bind_addr = format!("0.0.0.0:{port}");
|
||||||
|
|||||||
@@ -47,7 +47,6 @@ pub fn build_gateway_route(state_arc: Arc<GatewayState>) -> impl poem::Endpoint
|
|||||||
"/mcp",
|
"/mcp",
|
||||||
poem::post(gateway_mcp_post_handler).get(gateway_mcp_get_handler),
|
poem::post(gateway_mcp_post_handler).get(gateway_mcp_get_handler),
|
||||||
)
|
)
|
||||||
.at("/health", poem::get(gateway_health_handler))
|
|
||||||
// Agent join endpoints.
|
// Agent join endpoints.
|
||||||
.at("/gateway/mode", poem::get(gateway_mode_handler))
|
.at("/gateway/mode", poem::get(gateway_mode_handler))
|
||||||
.at(
|
.at(
|
||||||
|
|||||||
@@ -806,29 +806,6 @@ pub async fn gateway_event_push_handler(
|
|||||||
.into_response()
|
.into_response()
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── Health handler ──────────────────────────────────────────────────────────
|
|
||||||
|
|
||||||
/// HTTP GET `/health` handler for the gateway.
|
|
||||||
#[handler]
|
|
||||||
pub async fn gateway_health_handler(state: Data<&Arc<GatewayState>>) -> Response {
|
|
||||||
let (all_healthy, statuses) = gateway::health_check_all(&state).await;
|
|
||||||
|
|
||||||
let body = json!({
|
|
||||||
"status": if all_healthy { "ok" } else { "degraded" },
|
|
||||||
"projects": statuses,
|
|
||||||
});
|
|
||||||
|
|
||||||
let status = if all_healthy {
|
|
||||||
StatusCode::OK
|
|
||||||
} else {
|
|
||||||
StatusCode::SERVICE_UNAVAILABLE
|
|
||||||
};
|
|
||||||
Response::builder()
|
|
||||||
.status(status)
|
|
||||||
.header("Content-Type", "application/json")
|
|
||||||
.body(Body::from(serde_json::to_vec(&body).unwrap_or_default()))
|
|
||||||
}
|
|
||||||
|
|
||||||
// ── Gateway Web UI ──────────────────────────────────────────────────────────
|
// ── Gateway Web UI ──────────────────────────────────────────────────────────
|
||||||
|
|
||||||
/// `GET /api/gateway` — returns the list of registered projects and the active project.
|
/// `GET /api/gateway` — returns the list of registered projects and the active project.
|
||||||
|
|||||||
@@ -1,66 +0,0 @@
|
|||||||
//! Health check endpoint — thin HTTP adapter over `service::health`.
|
|
||||||
//!
|
|
||||||
//! Domain logic (the `HealthStatus` type and check function) lives in
|
|
||||||
//! `service::health`; this module is a thin adapter: call service → shape
|
|
||||||
//! response.
|
|
||||||
|
|
||||||
pub use crate::service::health::HealthStatus;
|
|
||||||
|
|
||||||
use poem::handler;
|
|
||||||
use poem_openapi::{OpenApi, Tags, payload::Json};
|
|
||||||
|
|
||||||
/// Health check endpoint.
|
|
||||||
///
|
|
||||||
/// Returns a static "ok" response to indicate the server is running.
|
|
||||||
#[handler]
|
|
||||||
pub fn health() -> &'static str {
|
|
||||||
"ok"
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Tags)]
|
|
||||||
enum HealthTags {
|
|
||||||
Health,
|
|
||||||
}
|
|
||||||
|
|
||||||
pub struct HealthApi;
|
|
||||||
|
|
||||||
#[OpenApi(tag = "HealthTags::Health")]
|
|
||||||
impl HealthApi {
|
|
||||||
/// Health check endpoint.
|
|
||||||
///
|
|
||||||
/// Returns a JSON status object to confirm the server is running.
|
|
||||||
#[oai(path = "/health", method = "get")]
|
|
||||||
async fn health(&self) -> Json<HealthStatus> {
|
|
||||||
Json(crate::service::health::check())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(test)]
|
|
||||||
mod tests {
|
|
||||||
use super::*;
|
|
||||||
|
|
||||||
#[tokio::test]
|
|
||||||
async fn handler_health_returns_ok() {
|
|
||||||
let app = poem::Route::new().at("/health", poem::get(health));
|
|
||||||
let cli = poem::test::TestClient::new(app);
|
|
||||||
let resp = cli.get("/health").send().await;
|
|
||||||
resp.assert_status_is_ok();
|
|
||||||
resp.assert_text("ok").await;
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn health_status_serializes_to_json() {
|
|
||||||
let status = HealthStatus {
|
|
||||||
status: "ok".to_string(),
|
|
||||||
};
|
|
||||||
let json = serde_json::to_value(&status).unwrap();
|
|
||||||
assert_eq!(json["status"], "ok");
|
|
||||||
}
|
|
||||||
|
|
||||||
#[tokio::test]
|
|
||||||
async fn api_health_returns_ok_status() {
|
|
||||||
let api = HealthApi;
|
|
||||||
let response = api.health().await;
|
|
||||||
assert_eq!(response.0.status, "ok");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -8,7 +8,6 @@ pub mod bot_config;
|
|||||||
pub mod chat;
|
pub mod chat;
|
||||||
pub mod context;
|
pub mod context;
|
||||||
pub mod events;
|
pub mod events;
|
||||||
pub mod health;
|
|
||||||
pub mod identity;
|
pub mod identity;
|
||||||
pub mod io;
|
pub mod io;
|
||||||
pub mod mcp;
|
pub mod mcp;
|
||||||
@@ -30,7 +29,6 @@ use bot_command::BotCommandApi;
|
|||||||
use bot_config::BotConfigApi;
|
use bot_config::BotConfigApi;
|
||||||
use chat::ChatApi;
|
use chat::ChatApi;
|
||||||
use context::AppContext;
|
use context::AppContext;
|
||||||
use health::HealthApi;
|
|
||||||
use io::IoApi;
|
use io::IoApi;
|
||||||
use model::ModelApi;
|
use model::ModelApi;
|
||||||
use poem::EndpointExt;
|
use poem::EndpointExt;
|
||||||
@@ -92,7 +90,6 @@ pub fn build_routes(
|
|||||||
"/mcp",
|
"/mcp",
|
||||||
post(mcp::mcp_post_handler).get(mcp::mcp_get_handler),
|
post(mcp::mcp_post_handler).get(mcp::mcp_get_handler),
|
||||||
)
|
)
|
||||||
.at("/health", get(health::health))
|
|
||||||
.at("/identity", get(identity::identity_handler))
|
.at("/identity", get(identity::identity_handler))
|
||||||
.at(
|
.at(
|
||||||
"/oauth/authorize",
|
"/oauth/authorize",
|
||||||
@@ -204,7 +201,6 @@ type ApiTuple = (
|
|||||||
ChatApi,
|
ChatApi,
|
||||||
AgentsApi,
|
AgentsApi,
|
||||||
SettingsApi,
|
SettingsApi,
|
||||||
HealthApi,
|
|
||||||
BotCommandApi,
|
BotCommandApi,
|
||||||
wizard::WizardApi,
|
wizard::WizardApi,
|
||||||
BotConfigApi,
|
BotConfigApi,
|
||||||
@@ -222,7 +218,6 @@ pub fn build_openapi_service(ctx: Arc<AppContext>) -> (ApiService, ApiService) {
|
|||||||
ChatApi { ctx: ctx.clone() },
|
ChatApi { ctx: ctx.clone() },
|
||||||
AgentsApi { ctx: ctx.clone() },
|
AgentsApi { ctx: ctx.clone() },
|
||||||
SettingsApi { ctx: ctx.clone() },
|
SettingsApi { ctx: ctx.clone() },
|
||||||
HealthApi,
|
|
||||||
BotCommandApi { ctx: ctx.clone() },
|
BotCommandApi { ctx: ctx.clone() },
|
||||||
wizard::WizardApi { ctx: ctx.clone() },
|
wizard::WizardApi { ctx: ctx.clone() },
|
||||||
BotConfigApi { ctx: ctx.clone() },
|
BotConfigApi { ctx: ctx.clone() },
|
||||||
@@ -239,7 +234,6 @@ pub fn build_openapi_service(ctx: Arc<AppContext>) -> (ApiService, ApiService) {
|
|||||||
ChatApi { ctx: ctx.clone() },
|
ChatApi { ctx: ctx.clone() },
|
||||||
AgentsApi { ctx: ctx.clone() },
|
AgentsApi { ctx: ctx.clone() },
|
||||||
SettingsApi { ctx: ctx.clone() },
|
SettingsApi { ctx: ctx.clone() },
|
||||||
HealthApi,
|
|
||||||
BotCommandApi { ctx: ctx.clone() },
|
BotCommandApi { ctx: ctx.clone() },
|
||||||
wizard::WizardApi { ctx: ctx.clone() },
|
wizard::WizardApi { ctx: ctx.clone() },
|
||||||
BotConfigApi { ctx },
|
BotConfigApi { ctx },
|
||||||
|
|||||||
@@ -221,10 +221,20 @@ pub async fn fetch_pipeline_status_for_project(
|
|||||||
.map_err(|e| format!("invalid upstream response: {e}"))
|
.map_err(|e| format!("invalid upstream response: {e}"))
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Check health of a single project URL.
|
/// Check health of a single project URL via the read-RPC `health.check` method.
|
||||||
|
///
|
||||||
|
/// Sends an RPC request to the project's `/mcp` endpoint. A successful
|
||||||
|
/// response (HTTP 2xx) indicates the project container is reachable and
|
||||||
|
/// serving requests.
|
||||||
pub async fn check_project_health(client: &Client, base_url: &str) -> Result<bool, String> {
|
pub async fn check_project_health(client: &Client, base_url: &str) -> Result<bool, String> {
|
||||||
let health_url = format!("{}/health", base_url.trim_end_matches('/'));
|
let mcp_url = format!("{}/mcp", base_url.trim_end_matches('/'));
|
||||||
match client.get(&health_url).send().await {
|
let rpc_body = json!({
|
||||||
|
"jsonrpc": "2.0",
|
||||||
|
"id": 1,
|
||||||
|
"method": "tools/list",
|
||||||
|
"params": {}
|
||||||
|
});
|
||||||
|
match client.post(&mcp_url).json(&rpc_body).send().await {
|
||||||
Ok(resp) => Ok(resp.status().is_success()),
|
Ok(resp) => Ok(resp.status().is_success()),
|
||||||
Err(e) => Err(format!("unreachable: {e}")),
|
Err(e) => Err(format!("unreachable: {e}")),
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -404,32 +404,6 @@ pub async fn init_project(
|
|||||||
Ok(registered_name)
|
Ok(registered_name)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Fetch aggregated health status across all projects.
|
|
||||||
pub async fn health_check_all(state: &GatewayState) -> (bool, BTreeMap<String, &'static str>) {
|
|
||||||
let mut all_healthy = true;
|
|
||||||
let mut statuses = BTreeMap::new();
|
|
||||||
|
|
||||||
let project_entries: Vec<(String, String)> = state
|
|
||||||
.projects
|
|
||||||
.read()
|
|
||||||
.await
|
|
||||||
.iter()
|
|
||||||
.map(|(n, e)| (n.clone(), e.url.clone()))
|
|
||||||
.collect();
|
|
||||||
|
|
||||||
for (name, url) in &project_entries {
|
|
||||||
let healthy = io::check_project_health(&state.client, url)
|
|
||||||
.await
|
|
||||||
.unwrap_or(false);
|
|
||||||
if !healthy {
|
|
||||||
all_healthy = false;
|
|
||||||
}
|
|
||||||
statuses.insert(name.clone(), if healthy { "ok" } else { "error" });
|
|
||||||
}
|
|
||||||
|
|
||||||
(all_healthy, statuses)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Broadcast a status event received from a project node to all local subscribers.
|
/// Broadcast a status event received from a project node to all local subscribers.
|
||||||
///
|
///
|
||||||
/// Returns the number of active receivers that received the event.
|
/// Returns the number of active receivers that received the event.
|
||||||
|
|||||||
@@ -1,38 +0,0 @@
|
|||||||
//! Pure health-check logic — no side effects.
|
|
||||||
|
|
||||||
use poem_openapi::Object;
|
|
||||||
use serde::Serialize;
|
|
||||||
|
|
||||||
/// The JSON payload returned by the health check endpoint.
|
|
||||||
#[derive(Serialize, Object)]
|
|
||||||
pub struct HealthStatus {
|
|
||||||
/// Human-readable status string, always `"ok"` when the server is healthy.
|
|
||||||
pub status: String,
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Return a healthy status response.
|
|
||||||
pub fn ok() -> HealthStatus {
|
|
||||||
HealthStatus {
|
|
||||||
status: "ok".to_string(),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(test)]
|
|
||||||
mod tests {
|
|
||||||
use super::*;
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn ok_returns_status_ok() {
|
|
||||||
let s = ok();
|
|
||||||
assert_eq!(s.status, "ok");
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn health_status_serializes() {
|
|
||||||
let s = HealthStatus {
|
|
||||||
status: "ok".to_string(),
|
|
||||||
};
|
|
||||||
let json = serde_json::to_value(&s).unwrap();
|
|
||||||
assert_eq!(json["status"], "ok");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,4 +0,0 @@
|
|||||||
//! Health I/O wrappers.
|
|
||||||
//!
|
|
||||||
//! Health has no side effects; this file exists to satisfy the
|
|
||||||
//! service-module convention (`docs/architecture/service-modules.md`).
|
|
||||||
@@ -1,39 +0,0 @@
|
|||||||
//! Health service — public API for the health domain.
|
|
||||||
//!
|
|
||||||
//! Exposes a single `check()` function that returns a [`HealthStatus`].
|
|
||||||
//! HTTP handlers call this instead of constructing the response inline.
|
|
||||||
//!
|
|
||||||
//! Conventions: `docs/architecture/service-modules.md`
|
|
||||||
|
|
||||||
pub mod check;
|
|
||||||
pub(super) mod io;
|
|
||||||
|
|
||||||
pub use check::HealthStatus;
|
|
||||||
|
|
||||||
// ── Error type ────────────────────────────────────────────────────────────────
|
|
||||||
|
|
||||||
/// Typed errors returned by `service::health` functions.
|
|
||||||
///
|
|
||||||
/// Health checks are currently infallible; this enum satisfies the module
|
|
||||||
/// convention and accommodates future error cases (e.g. dependency checks).
|
|
||||||
#[allow(dead_code)]
|
|
||||||
#[derive(Debug)]
|
|
||||||
pub enum Error {
|
|
||||||
/// An internal error occurred during the health check.
|
|
||||||
Internal(String),
|
|
||||||
}
|
|
||||||
|
|
||||||
impl std::fmt::Display for Error {
|
|
||||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
|
||||||
match self {
|
|
||||||
Self::Internal(msg) => write!(f, "Health error: {msg}"),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// ── Public API ────────────────────────────────────────────────────────────────
|
|
||||||
|
|
||||||
/// Perform a health check and return the status.
|
|
||||||
pub fn check() -> HealthStatus {
|
|
||||||
check::ok()
|
|
||||||
}
|
|
||||||
@@ -14,7 +14,6 @@ pub mod events;
|
|||||||
pub mod file_io;
|
pub mod file_io;
|
||||||
pub mod gateway;
|
pub mod gateway;
|
||||||
pub mod git_ops;
|
pub mod git_ops;
|
||||||
pub mod health;
|
|
||||||
pub mod merge;
|
pub mod merge;
|
||||||
pub mod notifications;
|
pub mod notifications;
|
||||||
pub mod oauth;
|
pub mod oauth;
|
||||||
|
|||||||
Reference in New Issue
Block a user