Merge pull request #615 from AAEE86/main

feat: 健康监控仪表盘与关联下钻优化,完善使用记录展示
This commit is contained in:
fawney19
2026-06-21 12:48:13 +08:00
committed by GitHub
41 changed files with 4445 additions and 1602 deletions
@@ -1,7 +1,9 @@
use super::extractors::{admin_health_key_id, admin_recover_key_id};
use crate::handlers::admin::request::{AdminAppState, AdminRequestContext};
use crate::handlers::admin::shared::query_param_value;
use crate::handlers::public::{ApiFormatHealthMonitorOptions, ModelHealthMonitorOptions};
use crate::handlers::public::{
ApiFormatHealthMonitorOptions, HealthMonitorRelationDimension, ModelHealthMonitorOptions,
};
use crate::GatewayError;
use axum::{
body::Body,
@@ -22,6 +24,14 @@ fn build_admin_endpoint_health_data_unavailable_response() -> Response<Body> {
.into_response()
}
fn build_admin_endpoint_health_bad_request_response(detail: &str) -> Response<Body> {
(
http::StatusCode::BAD_REQUEST,
Json(json!({ "detail": detail })),
)
.into_response()
}
pub(super) async fn maybe_build_local_admin_endpoints_health_response(
state: &AdminAppState<'_>,
request_context: &AdminRequestContext<'_>,
@@ -231,6 +241,55 @@ pub(super) async fn maybe_build_local_admin_endpoints_health_response(
return Ok(Some(Json(payload).into_response()));
}
if decision.route_family.as_deref() == Some("endpoints_health")
&& decision.route_kind.as_deref() == Some("health_related")
&& request_context.path() == "/api/admin/endpoints/health/related"
{
if !state.has_usage_data_reader() {
return Ok(Some(build_admin_endpoint_health_data_unavailable_response()));
}
let Some(dimension) = query_param_value(request_context.query_string(), "dimension")
.and_then(|value| HealthMonitorRelationDimension::parse(&value))
else {
return Ok(Some(build_admin_endpoint_health_bad_request_response(
"dimension 必须是 endpoint、model 或 provider",
)));
};
let Some(value) = query_param_value(request_context.query_string(), "value")
.map(|value| value.trim().to_string())
.filter(|value| !value.is_empty())
else {
return Ok(Some(build_admin_endpoint_health_bad_request_response(
"value 不能为空",
)));
};
let lookback_hours = query_param_value(request_context.query_string(), "lookback_hours")
.and_then(|value| value.parse::<u64>().ok())
.filter(|value| (1..=72).contains(value))
.unwrap_or(6);
let related_limit = query_param_value(request_context.query_string(), "related_limit")
.and_then(|value| value.parse::<usize>().ok())
.filter(|value| (1..=50).contains(value))
.unwrap_or(8);
let per_item_limit = query_param_value(request_context.query_string(), "per_item_limit")
.and_then(|value| value.parse::<usize>().ok())
.filter(|value| (10..=200).contains(value))
.unwrap_or(100);
let Some(payload) = state
.build_related_health_monitor_payload(
lookback_hours,
dimension,
&value,
related_limit,
per_item_limit,
)
.await
else {
return Ok(Some(build_admin_endpoint_health_data_unavailable_response()));
};
return Ok(Some(Json(payload).into_response()));
}
if decision.route_family.as_deref() == Some("endpoints_health")
&& decision.route_kind.as_deref() == Some("health_status")
&& request_context.path() == "/api/admin/endpoints/health/status"
@@ -1,6 +1,8 @@
use crate::handlers::admin::request::AdminAppState;
use crate::handlers::admin::shared::unix_secs_to_rfc3339;
use crate::handlers::public::{api_format_display_name, build_public_health_timeline};
use crate::handlers::public::{
api_format_display_name, build_public_health_timeline, build_public_health_timeline_details,
};
use crate::handlers::shared::unix_ms_to_rfc3339;
use crate::provider_key_auth::provider_key_effective_api_formats;
use aether_data_contracts::repository::candidates::PublicHealthTimelineBucket;
@@ -11,6 +13,8 @@ use serde_json::json;
use std::collections::{BTreeMap, BTreeSet};
use std::time::{SystemTime, UNIX_EPOCH};
const ENDPOINT_HEALTH_TIMELINE_SEGMENTS: u32 = 60;
pub(crate) async fn build_admin_endpoint_health_status_payload(
state: &AdminAppState<'_>,
lookback_hours: u64,
@@ -124,7 +128,7 @@ pub(crate) async fn build_admin_endpoint_health_status_payload(
&all_endpoint_ids,
since_unix_secs,
now_unix_secs,
100,
ENDPOINT_HEALTH_TIMELINE_SEGMENTS,
)
.await
.ok()
@@ -171,7 +175,14 @@ pub(crate) async fn build_admin_endpoint_health_status_payload(
let empty_timeline = BTreeMap::new();
let timeline_source = timeline_by_format.get(&api_format).unwrap_or(&empty_timeline);
let (timeline, time_range_start, time_range_end) =
build_public_health_timeline(timeline_source, 100);
build_public_health_timeline(timeline_source, ENDPOINT_HEALTH_TIMELINE_SEGMENTS);
let timeline_details = build_public_health_timeline_details(
timeline_source,
since_unix_secs,
now_unix_secs,
ENDPOINT_HEALTH_TIMELINE_SEGMENTS,
&[],
);
let healthy_count = timeline.iter().filter(|status| **status == "healthy").count();
let warning_count = timeline.iter().filter(|status| **status == "warning").count();
let unhealthy_count = timeline.iter().filter(|status| **status == "unhealthy").count();
@@ -199,6 +210,7 @@ pub(crate) async fn build_admin_endpoint_health_status_payload(
"display_name": api_format_display_name(&api_format),
"health_score": health_score,
"timeline": timeline,
"timeline_details": timeline_details,
"time_range_start": time_range_start.and_then(unix_ms_to_rfc3339),
"time_range_end": time_range_end.map(|ms| unix_ms_to_rfc3339(ms)).unwrap_or_else(|| unix_secs_to_rfc3339(now_unix_secs)),
"total_endpoints": endpoint_ids.len(),
@@ -583,6 +583,7 @@ fn build_admin_usage_keyword_search_query(
client_family: base_query.client_family.clone(),
exclude_unknown_model_or_provider: base_query.exclude_unknown_model_or_provider,
statuses: base_query.statuses.clone(),
exclude_status_codes: base_query.exclude_status_codes.clone(),
is_stream: base_query.is_stream,
error_only: base_query.error_only,
keywords,
@@ -276,6 +276,26 @@ impl<'a> AdminAppState<'a> {
.await
}
pub(crate) async fn build_related_health_monitor_payload(
&self,
lookback_hours: u64,
dimension: crate::handlers::public::HealthMonitorRelationDimension,
value: &str,
related_limit: usize,
per_item_limit: usize,
) -> Option<serde_json::Value> {
crate::handlers::public::build_related_health_monitor_payload(
self.app,
lookback_hours,
dimension,
value,
related_limit,
per_item_limit,
true,
)
.await
}
pub(crate) async fn execute_execution_runtime_sync_plan(
&self,
trace_id: Option<&str>,