fix(gateway): stabilize status snapshot output
This commit is contained in:
parent
b14edc40d3
commit
87cf5004b9
@ -169,6 +169,14 @@ impl ApiError {
|
||||
message: error.to_string(),
|
||||
}
|
||||
}
|
||||
|
||||
fn internal_with_message(error: impl std::fmt::Display, message: impl Into<String>) -> Self {
|
||||
tracing::error!(error = %error, "WebUI API request failed");
|
||||
Self {
|
||||
status: StatusCode::INTERNAL_SERVER_ERROR,
|
||||
message: message.into(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn upload_file(
|
||||
@ -714,6 +722,30 @@ fn scheduler_snapshot(jobs: &[crate::storage::ScheduledJob]) -> Value {
|
||||
})
|
||||
}
|
||||
|
||||
fn scheduler_status_error(error: impl std::fmt::Display) -> ApiError {
|
||||
ApiError::internal_with_message(error, "failed to load scheduler status")
|
||||
}
|
||||
|
||||
fn channel_snapshot(mut channels: Vec<(String, bool)>) -> Vec<Value> {
|
||||
channels.sort_by(|left, right| left.0.cmp(&right.0));
|
||||
channels
|
||||
.into_iter()
|
||||
.map(|(name, running)| {
|
||||
json!({
|
||||
"name": name,
|
||||
"status": if running { "connected" } else { "stopped" },
|
||||
})
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
fn sorted_providers(
|
||||
mut providers: Vec<crate::observability::metrics::ProviderSnapshot>,
|
||||
) -> Vec<crate::observability::metrics::ProviderSnapshot> {
|
||||
providers.sort_by(|left, right| left.name.cmp(&right.name));
|
||||
providers
|
||||
}
|
||||
|
||||
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();
|
||||
@ -728,24 +760,22 @@ pub async fn get_status(State(state): State<Arc<GatewayState>>) -> Result<Json<V
|
||||
let sessions_total = state.session_manager.session_count().await;
|
||||
let active_turns = state.session_manager.active_turn_count().await;
|
||||
|
||||
let mut channels = Vec::new();
|
||||
let mut channel_states = 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" },
|
||||
}));
|
||||
channel_states.push((name, running));
|
||||
}
|
||||
let channels = channel_snapshot(channel_states);
|
||||
|
||||
let jobs = state
|
||||
.storage
|
||||
.list_scheduled_jobs()
|
||||
.await
|
||||
.map_err(ApiError::internal)?;
|
||||
.map_err(scheduler_status_error)?;
|
||||
let scheduler = scheduler_snapshot(&jobs);
|
||||
let mcp = crate::mcp::get_mcp_status()
|
||||
.into_iter()
|
||||
@ -783,7 +813,7 @@ pub async fn get_status(State(state): State<Arc<GatewayState>>) -> Result<Json<V
|
||||
"control": { "depth": depths.control_depth, "cap": depths.control_cap },
|
||||
"active_lanes": active_lanes,
|
||||
},
|
||||
"providers": metrics.providers,
|
||||
"providers": sorted_providers(metrics.providers),
|
||||
"channels": channels,
|
||||
"scheduler": scheduler,
|
||||
"mcp": mcp,
|
||||
@ -943,6 +973,54 @@ mod tests {
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn channel_snapshot_is_sorted_and_maps_running_state() {
|
||||
let channels = channel_snapshot(vec![
|
||||
("zeta".to_string(), false),
|
||||
("alpha".to_string(), true),
|
||||
]);
|
||||
|
||||
assert_eq!(
|
||||
channels,
|
||||
vec![
|
||||
json!({ "name": "alpha", "status": "connected" }),
|
||||
json!({ "name": "zeta", "status": "stopped" }),
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn provider_snapshot_is_sorted_by_name() {
|
||||
let provider = |name: &str| crate::observability::metrics::ProviderSnapshot {
|
||||
name: name.to_string(),
|
||||
model: String::new(),
|
||||
status: "ok".to_string(),
|
||||
latency_ms: 0,
|
||||
latencies: Vec::new(),
|
||||
tokens_in: 0,
|
||||
tokens_out: 0,
|
||||
cost: 0.0,
|
||||
};
|
||||
|
||||
let providers = sorted_providers(vec![provider("zeta"), provider("alpha")]);
|
||||
assert_eq!(
|
||||
providers
|
||||
.iter()
|
||||
.map(|provider| provider.name.as_str())
|
||||
.collect::<Vec<_>>(),
|
||||
vec!["alpha", "zeta"]
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn scheduler_status_error_hides_internal_details() {
|
||||
let error = scheduler_status_error("database contained private payload");
|
||||
|
||||
assert_eq!(error.status, StatusCode::INTERNAL_SERVER_ERROR);
|
||||
assert_eq!(error.message, "failed to load scheduler status");
|
||||
assert!(!error.message.contains("private payload"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn secrets_are_redacted_and_restored() {
|
||||
let current = json!({"api_key":"real", "nested":{"access_token":"token"}, "safe":"yes"});
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user