feat(gateway): GET /api/status runtime snapshot
This commit is contained in:
parent
aa989cbaf1
commit
b14edc40d3
@ -689,6 +689,107 @@ pub struct LimitQuery {
|
|||||||
limit: Option<usize>,
|
limit: Option<usize>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn scheduler_snapshot(jobs: &[crate::storage::ScheduledJob]) -> Value {
|
||||||
|
let enabled = jobs.iter().filter(|job| job.enabled).count();
|
||||||
|
let failed_jobs = jobs
|
||||||
|
.iter()
|
||||||
|
.filter(|job| {
|
||||||
|
matches!(
|
||||||
|
job.last_status.as_deref(),
|
||||||
|
Some("error" | "timeout" | "delivery_error")
|
||||||
|
)
|
||||||
|
})
|
||||||
|
.count();
|
||||||
|
let next_run_at = jobs
|
||||||
|
.iter()
|
||||||
|
.filter(|job| job.enabled)
|
||||||
|
.map(|job| job.next_run_at)
|
||||||
|
.min();
|
||||||
|
|
||||||
|
json!({
|
||||||
|
"jobs": jobs.len(),
|
||||||
|
"enabled": enabled,
|
||||||
|
"failed_jobs": failed_jobs,
|
||||||
|
"next_run_at": next_run_at,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn get_status(State(state): State<Arc<GatewayState>>) -> Result<Json<Value>, ApiError> {
|
||||||
|
let reload = state.reload.status();
|
||||||
|
let metrics = crate::observability::metrics::global_metrics().snapshot();
|
||||||
|
let depths = state.bus().queue_depths();
|
||||||
|
let active_lanes = state
|
||||||
|
.outbound_lanes
|
||||||
|
.load(std::sync::atomic::Ordering::Relaxed);
|
||||||
|
let ws_connections = state
|
||||||
|
.ws_connections
|
||||||
|
.load(std::sync::atomic::Ordering::Relaxed);
|
||||||
|
let background_tasks = state.task_supervisor.running_count();
|
||||||
|
let sessions_total = state.session_manager.session_count().await;
|
||||||
|
let active_turns = state.session_manager.active_turn_count().await;
|
||||||
|
|
||||||
|
let mut channels = Vec::new();
|
||||||
|
for name in state.channel_manager.list_channel_names().await {
|
||||||
|
let running = state
|
||||||
|
.channel_manager
|
||||||
|
.get_channel(&name)
|
||||||
|
.await
|
||||||
|
.is_some_and(|channel| channel.is_running());
|
||||||
|
channels.push(json!({
|
||||||
|
"name": name,
|
||||||
|
"status": if running { "connected" } else { "stopped" },
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
let jobs = state
|
||||||
|
.storage
|
||||||
|
.list_scheduled_jobs()
|
||||||
|
.await
|
||||||
|
.map_err(ApiError::internal)?;
|
||||||
|
let scheduler = scheduler_snapshot(&jobs);
|
||||||
|
let mcp = crate::mcp::get_mcp_status()
|
||||||
|
.into_iter()
|
||||||
|
.map(|status| {
|
||||||
|
json!({
|
||||||
|
"name": status.name,
|
||||||
|
"connected": status.connected,
|
||||||
|
"tools": status.tools.len(),
|
||||||
|
})
|
||||||
|
})
|
||||||
|
.collect::<Vec<_>>();
|
||||||
|
|
||||||
|
Ok(Json(json!({
|
||||||
|
"generation": reload.generation,
|
||||||
|
"version": env!("CARGO_PKG_VERSION"),
|
||||||
|
"uptime_secs": crate::gateway::process_uptime_secs(),
|
||||||
|
"phase": reload.phase,
|
||||||
|
"ws_connections": ws_connections,
|
||||||
|
"background_tasks": background_tasks,
|
||||||
|
"sessions": {
|
||||||
|
"total": sessions_total,
|
||||||
|
"active_turns": active_turns,
|
||||||
|
},
|
||||||
|
"metrics": {
|
||||||
|
"tokens_in": metrics.tokens_in,
|
||||||
|
"tokens_out": metrics.tokens_out,
|
||||||
|
"cost": metrics.cost,
|
||||||
|
"tool_calls": metrics.tool_calls,
|
||||||
|
"turns": metrics.turns,
|
||||||
|
"turn_latency_p95_ms": metrics.turn_latency_p95_ms,
|
||||||
|
},
|
||||||
|
"bus": {
|
||||||
|
"inbound": { "depth": depths.inbound_depth, "cap": depths.inbound_cap },
|
||||||
|
"outbound": { "depth": depths.outbound_depth, "cap": depths.outbound_cap },
|
||||||
|
"control": { "depth": depths.control_depth, "cap": depths.control_cap },
|
||||||
|
"active_lanes": active_lanes,
|
||||||
|
},
|
||||||
|
"providers": metrics.providers,
|
||||||
|
"channels": channels,
|
||||||
|
"scheduler": scheduler,
|
||||||
|
"mcp": mcp,
|
||||||
|
})))
|
||||||
|
}
|
||||||
|
|
||||||
pub async fn get_tasks(
|
pub async fn get_tasks(
|
||||||
State(state): State<Arc<GatewayState>>,
|
State(state): State<Arc<GatewayState>>,
|
||||||
Query(query): Query<LimitQuery>,
|
Query(query): Query<LimitQuery>,
|
||||||
@ -779,6 +880,69 @@ pub async fn get_memories(
|
|||||||
mod tests {
|
mod tests {
|
||||||
use super::*;
|
use super::*;
|
||||||
|
|
||||||
|
fn scheduled_job(
|
||||||
|
id: &str,
|
||||||
|
enabled: bool,
|
||||||
|
next_run_at: i64,
|
||||||
|
last_status: Option<&str>,
|
||||||
|
) -> crate::storage::ScheduledJob {
|
||||||
|
crate::storage::ScheduledJob {
|
||||||
|
id: id.to_string(),
|
||||||
|
name: id.to_string(),
|
||||||
|
schedule: crate::scheduler::Schedule::Every { every_ms: 60_000 },
|
||||||
|
prompt: String::new(),
|
||||||
|
channel: "cli_chat".to_string(),
|
||||||
|
chat_id: "test".to_string(),
|
||||||
|
model: None,
|
||||||
|
job_kind: crate::storage::JobKind::Task,
|
||||||
|
delivery_policy: crate::storage::DeliveryPolicy::Never,
|
||||||
|
enabled,
|
||||||
|
delete_after_run: false,
|
||||||
|
next_run_at,
|
||||||
|
last_run_at: None,
|
||||||
|
last_status: last_status.map(str::to_string),
|
||||||
|
last_error: None,
|
||||||
|
created_at: 0,
|
||||||
|
updated_at: 0,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn scheduler_snapshot_classifies_failures_and_next_enabled_run() {
|
||||||
|
let jobs = vec![
|
||||||
|
scheduled_job("healthy", true, 300, Some("ok")),
|
||||||
|
scheduled_job("error", true, 200, Some("error")),
|
||||||
|
scheduled_job("timeout", false, 100, Some("timeout")),
|
||||||
|
scheduled_job("delivery", true, 400, Some("delivery_error")),
|
||||||
|
scheduled_job("other", false, 50, Some("cancelled")),
|
||||||
|
];
|
||||||
|
|
||||||
|
assert_eq!(
|
||||||
|
scheduler_snapshot(&jobs),
|
||||||
|
json!({
|
||||||
|
"jobs": 5,
|
||||||
|
"enabled": 3,
|
||||||
|
"failed_jobs": 3,
|
||||||
|
"next_run_at": 200,
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn scheduler_snapshot_has_null_next_run_without_enabled_jobs() {
|
||||||
|
let jobs = vec![scheduled_job("disabled", false, 100, None)];
|
||||||
|
|
||||||
|
assert_eq!(
|
||||||
|
scheduler_snapshot(&jobs),
|
||||||
|
json!({
|
||||||
|
"jobs": 1,
|
||||||
|
"enabled": 0,
|
||||||
|
"failed_jobs": 0,
|
||||||
|
"next_run_at": null,
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn secrets_are_redacted_and_restored() {
|
fn secrets_are_redacted_and_restored() {
|
||||||
let current = json!({"api_key":"real", "nested":{"access_token":"token"}, "safe":"yes"});
|
let current = json!({"api_key":"real", "nested":{"access_token":"token"}, "safe":"yes"});
|
||||||
|
|||||||
@ -593,6 +593,7 @@ fn build_router(state: Arc<GatewayState>) -> Router {
|
|||||||
)
|
)
|
||||||
.route("/api/logs", routing::get(http::get_logs))
|
.route("/api/logs", routing::get(http::get_logs))
|
||||||
.route("/api/tasks", routing::get(http::get_tasks))
|
.route("/api/tasks", routing::get(http::get_tasks))
|
||||||
|
.route("/api/status", routing::get(http::get_status))
|
||||||
.route("/api/jobs", routing::get(http::get_jobs))
|
.route("/api/jobs", routing::get(http::get_jobs))
|
||||||
.route("/api/jobs/{id}/runs", routing::get(http::get_job_runs))
|
.route("/api/jobs/{id}/runs", routing::get(http::get_job_runs))
|
||||||
.route("/api/memories", routing::get(http::get_memories))
|
.route("/api/memories", routing::get(http::get_memories))
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user