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(),
|
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(
|
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> {
|
pub async fn get_status(State(state): State<Arc<GatewayState>>) -> Result<Json<Value>, ApiError> {
|
||||||
let reload = state.reload.status();
|
let reload = state.reload.status();
|
||||||
let metrics = crate::observability::metrics::global_metrics().snapshot();
|
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 sessions_total = state.session_manager.session_count().await;
|
||||||
let active_turns = state.session_manager.active_turn_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 {
|
for name in state.channel_manager.list_channel_names().await {
|
||||||
let running = state
|
let running = state
|
||||||
.channel_manager
|
.channel_manager
|
||||||
.get_channel(&name)
|
.get_channel(&name)
|
||||||
.await
|
.await
|
||||||
.is_some_and(|channel| channel.is_running());
|
.is_some_and(|channel| channel.is_running());
|
||||||
channels.push(json!({
|
channel_states.push((name, running));
|
||||||
"name": name,
|
|
||||||
"status": if running { "connected" } else { "stopped" },
|
|
||||||
}));
|
|
||||||
}
|
}
|
||||||
|
let channels = channel_snapshot(channel_states);
|
||||||
|
|
||||||
let jobs = state
|
let jobs = state
|
||||||
.storage
|
.storage
|
||||||
.list_scheduled_jobs()
|
.list_scheduled_jobs()
|
||||||
.await
|
.await
|
||||||
.map_err(ApiError::internal)?;
|
.map_err(scheduler_status_error)?;
|
||||||
let scheduler = scheduler_snapshot(&jobs);
|
let scheduler = scheduler_snapshot(&jobs);
|
||||||
let mcp = crate::mcp::get_mcp_status()
|
let mcp = crate::mcp::get_mcp_status()
|
||||||
.into_iter()
|
.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 },
|
"control": { "depth": depths.control_depth, "cap": depths.control_cap },
|
||||||
"active_lanes": active_lanes,
|
"active_lanes": active_lanes,
|
||||||
},
|
},
|
||||||
"providers": metrics.providers,
|
"providers": sorted_providers(metrics.providers),
|
||||||
"channels": channels,
|
"channels": channels,
|
||||||
"scheduler": scheduler,
|
"scheduler": scheduler,
|
||||||
"mcp": mcp,
|
"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]
|
#[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"});
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user