mirror of
https://github.com/fawney19/Aether.git
synced 2026-10-04 08:27:46 +08:00
@@ -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>,
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -10,10 +10,11 @@ pub(crate) use self::catalog_helpers::{
|
||||
admin_requested_force_stream, api_format_display_name, build_api_format_health_monitor_payload,
|
||||
build_model_health_monitor_payload, build_provider_health_monitor_payload,
|
||||
build_public_catalog_models_payload, build_public_catalog_search_models_payload,
|
||||
build_public_health_timeline, build_public_providers_payload, normalize_admin_base_url,
|
||||
build_public_health_timeline, build_public_health_timeline_details,
|
||||
build_public_providers_payload, build_related_health_monitor_payload, normalize_admin_base_url,
|
||||
provider_key_api_formats, request_candidate_event_unix_ms, request_candidate_status_label,
|
||||
sanitize_public_model_config_for_user, ApiFormatHealthMonitorOptions,
|
||||
ModelHealthMonitorOptions,
|
||||
HealthMonitorRelationDimension, ModelHealthMonitorOptions,
|
||||
};
|
||||
pub(crate) use self::system_modules_helpers::{
|
||||
build_admin_keys_grouped_by_format_payload, build_public_auth_modules_status_payload,
|
||||
|
||||
@@ -2,9 +2,10 @@ use super::{
|
||||
build_api_format_health_monitor_payload, build_model_health_monitor_payload,
|
||||
build_public_auth_modules_status_payload, build_public_catalog_models_payload,
|
||||
build_public_catalog_search_models_payload, build_public_providers_payload,
|
||||
capability_detail_by_name, ldap_module_config_is_valid, sanitize_public_model_config_for_user,
|
||||
serialize_public_capability, supported_capability_names, ApiFormatHealthMonitorOptions,
|
||||
ModelHealthMonitorOptions, PUBLIC_CAPABILITY_DEFINITIONS,
|
||||
build_related_health_monitor_payload, capability_detail_by_name, ldap_module_config_is_valid,
|
||||
sanitize_public_model_config_for_user, serialize_public_capability, supported_capability_names,
|
||||
ApiFormatHealthMonitorOptions, HealthMonitorRelationDimension, ModelHealthMonitorOptions,
|
||||
PUBLIC_CAPABILITY_DEFINITIONS,
|
||||
};
|
||||
use crate::control::GatewayPublicRequestContext;
|
||||
use crate::handlers::shared::{
|
||||
@@ -477,6 +478,77 @@ pub(crate) async fn maybe_build_local_public_support_response(
|
||||
.await?;
|
||||
return Some(Json(payload).into_response());
|
||||
}
|
||||
|
||||
if decision.route_kind.as_deref() == Some("health_related")
|
||||
&& request_context.request_path == "/api/public/health/related"
|
||||
{
|
||||
let Some(dimension) =
|
||||
query_param_value(request_context.request_query_string.as_deref(), "dimension")
|
||||
.and_then(|value| HealthMonitorRelationDimension::parse(&value))
|
||||
else {
|
||||
return Some(
|
||||
(
|
||||
http::StatusCode::BAD_REQUEST,
|
||||
Json(json!({ "detail": "dimension 必须是 endpoint 或 model" })),
|
||||
)
|
||||
.into_response(),
|
||||
);
|
||||
};
|
||||
if matches!(dimension, HealthMonitorRelationDimension::Provider) {
|
||||
return Some(
|
||||
(
|
||||
http::StatusCode::BAD_REQUEST,
|
||||
Json(json!({ "detail": "公开健康监控不支持 provider 维度" })),
|
||||
)
|
||||
.into_response(),
|
||||
);
|
||||
}
|
||||
let Some(value) =
|
||||
query_param_value(request_context.request_query_string.as_deref(), "value")
|
||||
.map(|value| value.trim().to_string())
|
||||
.filter(|value| !value.is_empty())
|
||||
else {
|
||||
return Some(
|
||||
(
|
||||
http::StatusCode::BAD_REQUEST,
|
||||
Json(json!({ "detail": "value 不能为空" })),
|
||||
)
|
||||
.into_response(),
|
||||
);
|
||||
};
|
||||
let lookback_hours = query_param_value(
|
||||
request_context.request_query_string.as_deref(),
|
||||
"lookback_hours",
|
||||
)
|
||||
.and_then(|value| value.parse::<u64>().ok())
|
||||
.filter(|value| (1..=168).contains(value))
|
||||
.unwrap_or(6);
|
||||
let related_limit = query_param_value(
|
||||
request_context.request_query_string.as_deref(),
|
||||
"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.request_query_string.as_deref(),
|
||||
"per_item_limit",
|
||||
)
|
||||
.and_then(|value| value.parse::<usize>().ok())
|
||||
.filter(|value| (10..=500).contains(value))
|
||||
.unwrap_or(100);
|
||||
let payload = build_related_health_monitor_payload(
|
||||
state,
|
||||
lookback_hours,
|
||||
dimension,
|
||||
&value,
|
||||
related_limit,
|
||||
per_item_limit,
|
||||
false,
|
||||
)
|
||||
.await?;
|
||||
return Some(Json(payload).into_response());
|
||||
}
|
||||
}
|
||||
|
||||
if decision.route_family.as_deref() == Some("capabilities") {
|
||||
|
||||
@@ -19,6 +19,10 @@ use chrono::Datelike;
|
||||
use serde_json::json;
|
||||
use std::collections::{BTreeMap, BTreeSet};
|
||||
|
||||
const DASHBOARD_SITE_RATE_WINDOW_SECS: u64 = 60;
|
||||
const DASHBOARD_ONLINE_USER_WINDOW_SECS: u64 = 300;
|
||||
const DASHBOARD_ONLINE_USER_AGGREGATION_LIMIT: usize = 100_000;
|
||||
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
struct DashboardDateRange {
|
||||
start_date: chrono::NaiveDate,
|
||||
@@ -822,6 +826,22 @@ async fn dashboard_load_user_counts(
|
||||
Ok((count, count))
|
||||
}
|
||||
|
||||
async fn dashboard_load_online_user_count(
|
||||
state: &AppState,
|
||||
now_unix_secs: u64,
|
||||
) -> Result<u64, GatewayError> {
|
||||
let rows = state
|
||||
.aggregate_usage_audits(&UsageAuditAggregationQuery {
|
||||
created_from_unix_secs: now_unix_secs.saturating_sub(DASHBOARD_ONLINE_USER_WINDOW_SECS),
|
||||
created_until_unix_secs: now_unix_secs.saturating_add(1),
|
||||
group_by: UsageAuditAggregationGroupBy::User,
|
||||
limit: DASHBOARD_ONLINE_USER_AGGREGATION_LIMIT,
|
||||
exclude_reserved_provider_labels: false,
|
||||
})
|
||||
.await?;
|
||||
Ok(rows.len() as u64)
|
||||
}
|
||||
|
||||
fn dashboard_cache_savings_usd(summary: &StoredUsageCostSavingsSummary) -> f64 {
|
||||
let estimated_full_cost =
|
||||
if summary.estimated_full_cost_usd <= 0.0 && summary.cache_read_cost_usd > 0.0 {
|
||||
@@ -966,6 +986,30 @@ pub(super) async fn handle_dashboard_stats_get(
|
||||
});
|
||||
|
||||
if is_admin {
|
||||
let now_unix_secs = chrono::Utc::now().timestamp().max(0) as u64;
|
||||
let site_rate_summary = match dashboard_summary_for_unix_range_raw(
|
||||
state,
|
||||
now_unix_secs.saturating_sub(DASHBOARD_SITE_RATE_WINDOW_SECS),
|
||||
now_unix_secs.saturating_add(1),
|
||||
None,
|
||||
"dashboard realtime site stats lookup failed",
|
||||
)
|
||||
.await
|
||||
{
|
||||
Ok(value) => value,
|
||||
Err(response) => return response,
|
||||
};
|
||||
let site_rate_totals = dashboard_usage_totals_from_summary(&site_rate_summary);
|
||||
let online_users = match dashboard_load_online_user_count(state, now_unix_secs).await {
|
||||
Ok(value) => value,
|
||||
Err(err) => {
|
||||
return build_auth_error_response(
|
||||
http::StatusCode::INTERNAL_SERVER_ERROR,
|
||||
format!("dashboard online user stats lookup failed: {err:?}"),
|
||||
false,
|
||||
);
|
||||
}
|
||||
};
|
||||
let (total_users, active_users) =
|
||||
match dashboard_load_user_counts(state, summary_range).await {
|
||||
Ok(value) => value,
|
||||
@@ -1010,9 +1054,17 @@ pub(super) async fn handle_dashboard_stats_get(
|
||||
};
|
||||
let stats = json!([
|
||||
{
|
||||
"name": "今日请求",
|
||||
"value": dashboard_format_integer(today_totals.requests),
|
||||
"subValue": format!("成功率 {}", dashboard_format_percentage(success_rate)),
|
||||
"name": "今日请求 / 费用",
|
||||
"value": format!(
|
||||
"{} / {}",
|
||||
dashboard_format_integer(today_totals.requests),
|
||||
dashboard_format_usd(today_totals.total_cost_usd)
|
||||
),
|
||||
"subValue": format!(
|
||||
"成功率 {} / 节省 {}",
|
||||
dashboard_format_percentage(success_rate),
|
||||
dashboard_format_usd(today_cost_savings.max(0.0))
|
||||
),
|
||||
"icon": "Activity",
|
||||
},
|
||||
{
|
||||
@@ -1022,15 +1074,26 @@ pub(super) async fn handle_dashboard_stats_get(
|
||||
"icon": "Zap",
|
||||
},
|
||||
{
|
||||
"name": "今日费用",
|
||||
"value": dashboard_format_usd(today_totals.total_cost_usd),
|
||||
"subValue": format!("节省 {}", dashboard_format_usd(today_cost_savings.max(0.0))),
|
||||
"icon": "DollarSign",
|
||||
"name": "全站 RPM / TPM",
|
||||
"value": format!(
|
||||
"{} / {}",
|
||||
dashboard_format_integer(site_rate_totals.requests),
|
||||
dashboard_format_token_compact(site_rate_totals.total_tokens)
|
||||
),
|
||||
"subValue": "最近 60 秒",
|
||||
"icon": "Activity",
|
||||
},
|
||||
{
|
||||
"name": "活跃用户",
|
||||
"value": dashboard_format_integer(active_users),
|
||||
"subValue": format!("总用户 {}", dashboard_format_integer(total_users)),
|
||||
"name": "在线 / 启用用户",
|
||||
"value": format!(
|
||||
"{} / {}",
|
||||
dashboard_format_integer(online_users),
|
||||
dashboard_format_integer(active_users)
|
||||
),
|
||||
"subValue": format!(
|
||||
"最近 5 分钟 / 总用户 {}",
|
||||
dashboard_format_integer(total_users)
|
||||
),
|
||||
"icon": "Users",
|
||||
}
|
||||
]);
|
||||
@@ -1060,6 +1123,7 @@ pub(super) async fn handle_dashboard_stats_get(
|
||||
"users": {
|
||||
"total": total_users,
|
||||
"active": active_users,
|
||||
"online": online_users,
|
||||
},
|
||||
"token_breakdown": token_breakdown,
|
||||
});
|
||||
|
||||
@@ -965,6 +965,9 @@ pub(super) async fn handle_users_me_usage_get(
|
||||
created_until_unix_secs,
|
||||
user_id: Some(auth.user.id.clone()),
|
||||
provider_name: None,
|
||||
model: None,
|
||||
api_format: None,
|
||||
exclude_status_codes: Vec::new(),
|
||||
group_by: UsageBreakdownGroupBy::Model,
|
||||
})
|
||||
.await
|
||||
@@ -985,6 +988,9 @@ pub(super) async fn handle_users_me_usage_get(
|
||||
created_until_unix_secs,
|
||||
user_id: Some(auth.user.id.clone()),
|
||||
provider_name: None,
|
||||
model: None,
|
||||
api_format: None,
|
||||
exclude_status_codes: Vec::new(),
|
||||
group_by: UsageBreakdownGroupBy::Provider,
|
||||
})
|
||||
.await
|
||||
@@ -1005,6 +1011,9 @@ pub(super) async fn handle_users_me_usage_get(
|
||||
created_until_unix_secs,
|
||||
user_id: Some(auth.user.id.clone()),
|
||||
provider_name: None,
|
||||
model: None,
|
||||
api_format: None,
|
||||
exclude_status_codes: Vec::new(),
|
||||
group_by: UsageBreakdownGroupBy::ApiFormat,
|
||||
})
|
||||
.await
|
||||
@@ -1045,6 +1054,7 @@ pub(super) async fn handle_users_me_usage_get(
|
||||
client_family: None,
|
||||
exclude_unknown_model_or_provider: false,
|
||||
statuses: None,
|
||||
exclude_status_codes: Vec::new(),
|
||||
is_stream: None,
|
||||
error_only: false,
|
||||
keywords,
|
||||
@@ -1100,6 +1110,7 @@ pub(super) async fn handle_users_me_usage_get(
|
||||
client_family: None,
|
||||
exclude_unknown_model_or_provider: false,
|
||||
statuses: None,
|
||||
exclude_status_codes: Vec::new(),
|
||||
is_stream: None,
|
||||
error_only: false,
|
||||
limit: None,
|
||||
@@ -1128,6 +1139,7 @@ pub(super) async fn handle_users_me_usage_get(
|
||||
client_family: None,
|
||||
exclude_unknown_model_or_provider: false,
|
||||
statuses: None,
|
||||
exclude_status_codes: Vec::new(),
|
||||
is_stream: None,
|
||||
error_only: false,
|
||||
limit: Some(limit),
|
||||
@@ -1266,6 +1278,7 @@ pub(super) async fn handle_users_me_usage_active_get(
|
||||
client_family: None,
|
||||
exclude_unknown_model_or_provider: false,
|
||||
statuses: Some(vec!["pending".to_string(), "streaming".to_string()]),
|
||||
exclude_status_codes: Vec::new(),
|
||||
is_stream: None,
|
||||
error_only: false,
|
||||
limit: Some(50),
|
||||
|
||||
Reference in New Issue
Block a user