mirror of
https://github.com/fawney19/Aether.git
synced 2026-10-08 02:17:46 +08:00
refactor: 大规模模块拆分与重组,新增 aether-admin crate
- 新建独立 aether-admin crate 承载 admin 相关共享契约与纯辅助函数 - 拆分 ai_pipeline 下 kiro/private_envelope/conversion/planner 等大文件为子模块目录 - 重组 admin handlers 各业务域(billing/oauth/provider/system/users 等)为目录结构,移除 shared.rs/builders.rs 等反模式 - 移除 ai_pipeline runtime adapters 旧实现(claude/openai/gemini/kiro/vertex/antigravity 等),改由 provider transport 统一承载 - 移除 control_facade/execution_facade/auth_snapshot_facade 等冗余 facade 层 - 拆分 query/billing 与 query/monitoring 模块、state/runtime/payments 与 security 模块 - 扩展架构测试覆盖 admin_billing/admin_model/admin_users 等新模块 - 删除 docs/architecture/refactor-execution-plan.md 已完成的执行计划文档
This commit is contained in:
@@ -1,4 +1,3 @@
|
||||
use super::responses::admin_monitoring_bad_request_response;
|
||||
use super::route_filters::{
|
||||
admin_monitoring_escape_like_pattern, parse_admin_monitoring_days,
|
||||
parse_admin_monitoring_event_type_filter, parse_admin_monitoring_hours,
|
||||
@@ -7,102 +6,24 @@ use super::route_filters::{
|
||||
};
|
||||
use super::usage_helpers::admin_monitoring_usage_is_error;
|
||||
use crate::constants::INTERNAL_GATEWAY_PATH_PREFIXES;
|
||||
use crate::control::GatewayPublicRequestContext;
|
||||
use crate::handlers::admin::request::{AdminAppState, AdminRequestContext};
|
||||
use crate::query::monitoring as monitoring_query;
|
||||
use crate::{AppState, GatewayError};
|
||||
use aether_data_contracts::repository::usage::UsageAuditListQuery;
|
||||
use axum::{
|
||||
body::Body,
|
||||
response::{IntoResponse, Response},
|
||||
Json,
|
||||
use crate::GatewayError;
|
||||
use aether_admin::observability::monitoring::{
|
||||
admin_monitoring_bad_request_response, admin_monitoring_user_behavior_user_id_from_path,
|
||||
build_admin_monitoring_audit_logs_payload_response,
|
||||
build_admin_monitoring_suspicious_activities_payload_response,
|
||||
build_admin_monitoring_system_status_payload_response,
|
||||
build_admin_monitoring_user_behavior_payload_response,
|
||||
};
|
||||
use serde_json::json;
|
||||
|
||||
fn build_admin_monitoring_audit_logs_payload(
|
||||
items: Vec<serde_json::Value>,
|
||||
total: usize,
|
||||
limit: usize,
|
||||
offset: usize,
|
||||
username: Option<String>,
|
||||
event_type: Option<String>,
|
||||
days: i64,
|
||||
) -> Response<Body> {
|
||||
let count = items.len();
|
||||
Json(json!({
|
||||
"items": items,
|
||||
"meta": {
|
||||
"total": total,
|
||||
"limit": limit,
|
||||
"offset": offset,
|
||||
"count": count,
|
||||
},
|
||||
"filters": {
|
||||
"username": username,
|
||||
"event_type": event_type,
|
||||
"days": days,
|
||||
},
|
||||
}))
|
||||
.into_response()
|
||||
}
|
||||
|
||||
fn build_admin_monitoring_suspicious_activities_payload(
|
||||
activities: Vec<serde_json::Value>,
|
||||
hours: i64,
|
||||
) -> Response<Body> {
|
||||
let count = activities.len();
|
||||
Json(json!({
|
||||
"activities": activities,
|
||||
"count": count,
|
||||
"time_range_hours": hours,
|
||||
}))
|
||||
.into_response()
|
||||
}
|
||||
|
||||
fn build_admin_monitoring_user_behavior_payload(
|
||||
user_id: String,
|
||||
days: i64,
|
||||
event_counts: std::collections::BTreeMap<String, u64>,
|
||||
failed_requests: u64,
|
||||
success_requests: u64,
|
||||
suspicious_activities: u64,
|
||||
) -> Response<Body> {
|
||||
let total_requests = success_requests.saturating_add(failed_requests);
|
||||
let success_rate = if total_requests == 0 {
|
||||
0.0
|
||||
} else {
|
||||
success_requests as f64 / total_requests as f64
|
||||
};
|
||||
|
||||
Json(json!({
|
||||
"user_id": user_id,
|
||||
"period_days": days,
|
||||
"event_counts": event_counts,
|
||||
"failed_requests": failed_requests,
|
||||
"success_requests": success_requests,
|
||||
"success_rate": success_rate,
|
||||
"suspicious_activities": suspicious_activities,
|
||||
"analysis_time": chrono::Utc::now().to_rfc3339(),
|
||||
}))
|
||||
.into_response()
|
||||
}
|
||||
|
||||
fn admin_monitoring_user_behavior_user_id_from_path(request_path: &str) -> Option<String> {
|
||||
let value = request_path
|
||||
.strip_prefix("/api/admin/monitoring/user-behavior/")?
|
||||
.trim()
|
||||
.trim_matches('/')
|
||||
.to_string();
|
||||
if value.is_empty() || value.contains('/') {
|
||||
None
|
||||
} else {
|
||||
Some(value)
|
||||
}
|
||||
}
|
||||
use aether_data_contracts::repository::usage::UsageAuditListQuery;
|
||||
use axum::{body::Body, response::Response};
|
||||
|
||||
pub(super) async fn build_admin_monitoring_audit_logs_response(
|
||||
state: &AppState,
|
||||
request_context: &GatewayPublicRequestContext,
|
||||
state: &AdminAppState<'_>,
|
||||
request_context: &AdminRequestContext<'_>,
|
||||
) -> Result<Response<Body>, GatewayError> {
|
||||
let state = state.as_ref();
|
||||
let query = request_context.request_query_string.as_deref();
|
||||
let username = parse_admin_monitoring_username_filter(query);
|
||||
let event_type = parse_admin_monitoring_event_type_filter(query);
|
||||
@@ -120,7 +41,7 @@ pub(super) async fn build_admin_monitoring_audit_logs_response(
|
||||
};
|
||||
|
||||
let Some(pool) = state.postgres_pool() else {
|
||||
return Ok(build_admin_monitoring_audit_logs_payload(
|
||||
return Ok(build_admin_monitoring_audit_logs_payload_response(
|
||||
Vec::new(),
|
||||
0,
|
||||
limit,
|
||||
@@ -147,15 +68,16 @@ pub(super) async fn build_admin_monitoring_audit_logs_response(
|
||||
)
|
||||
.await?;
|
||||
|
||||
Ok(build_admin_monitoring_audit_logs_payload(
|
||||
Ok(build_admin_monitoring_audit_logs_payload_response(
|
||||
items, total, limit, offset, username, event_type, days,
|
||||
))
|
||||
}
|
||||
|
||||
pub(super) async fn build_admin_monitoring_suspicious_activities_response(
|
||||
state: &AppState,
|
||||
request_context: &GatewayPublicRequestContext,
|
||||
state: &AdminAppState<'_>,
|
||||
request_context: &AdminRequestContext<'_>,
|
||||
) -> Result<Response<Body>, GatewayError> {
|
||||
let state = state.as_ref();
|
||||
let query = request_context.request_query_string.as_deref();
|
||||
let hours = match parse_admin_monitoring_hours(query) {
|
||||
Ok(value) => value,
|
||||
@@ -163,24 +85,22 @@ pub(super) async fn build_admin_monitoring_suspicious_activities_response(
|
||||
};
|
||||
|
||||
let Some(pool) = state.postgres_pool() else {
|
||||
return Ok(build_admin_monitoring_suspicious_activities_payload(
|
||||
Vec::new(),
|
||||
hours,
|
||||
));
|
||||
return Ok(
|
||||
build_admin_monitoring_suspicious_activities_payload_response(Vec::new(), hours),
|
||||
);
|
||||
};
|
||||
|
||||
let cutoff_time = chrono::Utc::now() - chrono::Duration::hours(hours);
|
||||
let activities = monitoring_query::list_admin_suspicious_activities(&pool, cutoff_time).await?;
|
||||
|
||||
Ok(build_admin_monitoring_suspicious_activities_payload(
|
||||
activities, hours,
|
||||
))
|
||||
Ok(build_admin_monitoring_suspicious_activities_payload_response(activities, hours))
|
||||
}
|
||||
|
||||
pub(super) async fn build_admin_monitoring_user_behavior_response(
|
||||
state: &AppState,
|
||||
request_context: &GatewayPublicRequestContext,
|
||||
state: &AdminAppState<'_>,
|
||||
request_context: &AdminRequestContext<'_>,
|
||||
) -> Result<Response<Body>, GatewayError> {
|
||||
let state = state.as_ref();
|
||||
let Some(user_id) =
|
||||
admin_monitoring_user_behavior_user_id_from_path(&request_context.request_path)
|
||||
else {
|
||||
@@ -192,7 +112,7 @@ pub(super) async fn build_admin_monitoring_user_behavior_response(
|
||||
};
|
||||
|
||||
let Some(pool) = state.postgres_pool() else {
|
||||
return Ok(build_admin_monitoring_user_behavior_payload(
|
||||
return Ok(build_admin_monitoring_user_behavior_payload_response(
|
||||
user_id,
|
||||
days,
|
||||
std::collections::BTreeMap::new(),
|
||||
@@ -227,7 +147,7 @@ pub(super) async fn build_admin_monitoring_user_behavior_response(
|
||||
.unwrap_or_default(),
|
||||
);
|
||||
|
||||
Ok(build_admin_monitoring_user_behavior_payload(
|
||||
Ok(build_admin_monitoring_user_behavior_payload_response(
|
||||
user_id,
|
||||
days,
|
||||
event_counts,
|
||||
@@ -238,8 +158,9 @@ pub(super) async fn build_admin_monitoring_user_behavior_response(
|
||||
}
|
||||
|
||||
pub(super) async fn build_admin_monitoring_system_status_response(
|
||||
state: &AppState,
|
||||
state: &AdminAppState<'_>,
|
||||
) -> Result<Response<Body>, GatewayError> {
|
||||
let state = state.as_ref();
|
||||
let now = chrono::Utc::now();
|
||||
let today_start = now
|
||||
.date_naive()
|
||||
@@ -301,35 +222,21 @@ pub(super) async fn build_admin_monitoring_system_status_response(
|
||||
.count();
|
||||
let tunnel = state.tunnel.stats();
|
||||
|
||||
Ok(Json(json!({
|
||||
"timestamp": now.to_rfc3339(),
|
||||
"users": {
|
||||
"total": total_users,
|
||||
"active": active_users,
|
||||
},
|
||||
"providers": {
|
||||
"total": total_providers,
|
||||
"active": active_providers,
|
||||
},
|
||||
"api_keys": {
|
||||
"total": total_api_keys,
|
||||
"active": active_api_keys,
|
||||
},
|
||||
"today_stats": {
|
||||
"requests": today_requests,
|
||||
"tokens": today_tokens,
|
||||
"cost_usd": format!("${today_cost:.4}"),
|
||||
},
|
||||
"tunnel": {
|
||||
"proxy_connections": tunnel.proxy_connections,
|
||||
"nodes": tunnel.nodes,
|
||||
"active_streams": tunnel.active_streams,
|
||||
},
|
||||
"internal_gateway": {
|
||||
"status": "rust_native_control_plane",
|
||||
"path_prefixes": INTERNAL_GATEWAY_PATH_PREFIXES,
|
||||
},
|
||||
"recent_errors": recent_errors,
|
||||
}))
|
||||
.into_response())
|
||||
Ok(build_admin_monitoring_system_status_payload_response(
|
||||
now,
|
||||
total_users,
|
||||
active_users,
|
||||
total_providers,
|
||||
active_providers,
|
||||
total_api_keys,
|
||||
active_api_keys,
|
||||
today_requests,
|
||||
today_tokens,
|
||||
today_cost,
|
||||
tunnel.proxy_connections,
|
||||
tunnel.nodes,
|
||||
tunnel.active_streams,
|
||||
INTERNAL_GATEWAY_PATH_PREFIXES,
|
||||
recent_errors,
|
||||
))
|
||||
}
|
||||
|
||||
@@ -8,7 +8,8 @@ use super::cache_config::{
|
||||
ADMIN_MONITORING_DYNAMIC_RESERVATION_STABLE_MIN_RESERVATION,
|
||||
};
|
||||
use super::cache_store::build_admin_monitoring_cache_snapshot;
|
||||
use crate::{AppState, GatewayError};
|
||||
use crate::handlers::admin::request::AdminAppState;
|
||||
use crate::GatewayError;
|
||||
use axum::{
|
||||
body::Body,
|
||||
http,
|
||||
@@ -18,7 +19,7 @@ use axum::{
|
||||
use serde_json::json;
|
||||
|
||||
pub(super) async fn build_admin_monitoring_cache_stats_response(
|
||||
state: &AppState,
|
||||
state: &AdminAppState<'_>,
|
||||
) -> Result<Response<Body>, GatewayError> {
|
||||
let snapshot = build_admin_monitoring_cache_snapshot(state).await?;
|
||||
|
||||
@@ -64,7 +65,7 @@ pub(super) async fn build_admin_monitoring_cache_stats_response(
|
||||
}
|
||||
|
||||
pub(super) async fn build_admin_monitoring_cache_metrics_response(
|
||||
state: &AppState,
|
||||
state: &AdminAppState<'_>,
|
||||
) -> Result<Response<Body>, GatewayError> {
|
||||
let snapshot = build_admin_monitoring_cache_snapshot(state).await?;
|
||||
let metrics = [
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
use super::cache_types::AdminMonitoringCacheAffinityRecord;
|
||||
use crate::{AppState, GatewayError};
|
||||
use crate::handlers::admin::request::AdminAppState;
|
||||
use crate::GatewayError;
|
||||
|
||||
fn parse_admin_monitoring_cache_affinity_key(raw_key: &str) -> Option<(String, String, String)> {
|
||||
let parts = raw_key.split(':').collect::<Vec<_>>();
|
||||
@@ -92,7 +93,7 @@ pub(super) fn admin_monitoring_cache_affinity_record(
|
||||
}
|
||||
|
||||
pub(super) fn clear_admin_monitoring_scheduler_affinity_entries(
|
||||
state: &AppState,
|
||||
state: &AdminAppState<'_>,
|
||||
records: &[AdminMonitoringCacheAffinityRecord],
|
||||
) {
|
||||
let scheduler_keys = records
|
||||
@@ -100,28 +101,32 @@ pub(super) fn clear_admin_monitoring_scheduler_affinity_entries(
|
||||
.filter_map(admin_monitoring_scheduler_affinity_cache_key)
|
||||
.collect::<std::collections::BTreeSet<_>>();
|
||||
for scheduler_key in scheduler_keys {
|
||||
let _ = state.remove_scheduler_affinity_cache_entry(&scheduler_key);
|
||||
let _ = state
|
||||
.as_ref()
|
||||
.remove_scheduler_affinity_cache_entry(&scheduler_key);
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
pub(super) fn delete_admin_monitoring_cache_affinity_entries_for_tests(
|
||||
state: &AppState,
|
||||
state: &AdminAppState<'_>,
|
||||
raw_keys: &[String],
|
||||
) -> usize {
|
||||
state.remove_admin_monitoring_cache_affinity_entries_for_tests(raw_keys)
|
||||
state
|
||||
.as_ref()
|
||||
.remove_admin_monitoring_cache_affinity_entries_for_tests(raw_keys)
|
||||
}
|
||||
|
||||
#[cfg(not(test))]
|
||||
pub(super) fn delete_admin_monitoring_cache_affinity_entries_for_tests(
|
||||
_state: &AppState,
|
||||
_state: &AdminAppState<'_>,
|
||||
_raw_keys: &[String],
|
||||
) -> usize {
|
||||
0
|
||||
}
|
||||
|
||||
pub(super) async fn delete_admin_monitoring_cache_affinity_raw_keys(
|
||||
state: &AppState,
|
||||
state: &AdminAppState<'_>,
|
||||
raw_keys: &[String],
|
||||
) -> Result<usize, GatewayError> {
|
||||
if raw_keys.is_empty() {
|
||||
|
||||
+12
-18
@@ -15,10 +15,10 @@ use super::cache_store::{
|
||||
list_admin_monitoring_cache_affinity_records,
|
||||
list_admin_monitoring_cache_affinity_records_by_affinity_keys,
|
||||
};
|
||||
use super::responses::admin_monitoring_bad_request_response;
|
||||
use super::route_filters::{parse_admin_monitoring_limit, parse_admin_monitoring_offset};
|
||||
use crate::control::GatewayPublicRequestContext;
|
||||
use crate::{AppState, GatewayError};
|
||||
use crate::handlers::admin::request::{AdminAppState, AdminRequestContext};
|
||||
use crate::GatewayError;
|
||||
use aether_admin::observability::monitoring::admin_monitoring_bad_request_response;
|
||||
use axum::{
|
||||
body::Body,
|
||||
response::{IntoResponse, Response},
|
||||
@@ -31,8 +31,8 @@ fn normalize_keyword<'a>(keyword: Option<&'a String>) -> Option<String> {
|
||||
}
|
||||
|
||||
pub(super) async fn build_admin_monitoring_cache_affinities_response(
|
||||
state: &AppState,
|
||||
request_context: &GatewayPublicRequestContext,
|
||||
state: &AdminAppState<'_>,
|
||||
request_context: &AdminRequestContext<'_>,
|
||||
) -> Result<Response<Body>, GatewayError> {
|
||||
let limit = match parse_admin_monitoring_limit(request_context.request_query_string.as_deref())
|
||||
{
|
||||
@@ -106,26 +106,20 @@ pub(super) async fn build_admin_monitoring_cache_affinities_response(
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
let provider_by_id = state
|
||||
.data
|
||||
.list_provider_catalog_providers_by_ids(&provider_ids)
|
||||
.await
|
||||
.map_err(|err| GatewayError::Internal(err.to_string()))?
|
||||
.read_provider_catalog_providers_by_ids(&provider_ids)
|
||||
.await?
|
||||
.into_iter()
|
||||
.map(|item| (item.id.clone(), item))
|
||||
.collect::<std::collections::BTreeMap<_, _>>();
|
||||
let endpoint_by_id = state
|
||||
.data
|
||||
.list_provider_catalog_endpoints_by_ids(&endpoint_ids)
|
||||
.await
|
||||
.map_err(|err| GatewayError::Internal(err.to_string()))?
|
||||
.read_provider_catalog_endpoints_by_ids(&endpoint_ids)
|
||||
.await?
|
||||
.into_iter()
|
||||
.map(|item| (item.id.clone(), item))
|
||||
.collect::<std::collections::BTreeMap<_, _>>();
|
||||
let key_by_id = state
|
||||
.data
|
||||
.list_provider_catalog_keys_by_ids(&key_ids)
|
||||
.await
|
||||
.map_err(|err| GatewayError::Internal(err.to_string()))?
|
||||
.await?
|
||||
.into_iter()
|
||||
.map(|item| (item.id.clone(), item))
|
||||
.collect::<std::collections::BTreeMap<_, _>>();
|
||||
@@ -242,8 +236,8 @@ pub(super) async fn build_admin_monitoring_cache_affinities_response(
|
||||
}
|
||||
|
||||
pub(super) async fn build_admin_monitoring_cache_affinity_response(
|
||||
state: &AppState,
|
||||
request_context: &GatewayPublicRequestContext,
|
||||
state: &AdminAppState<'_>,
|
||||
request_context: &AdminRequestContext<'_>,
|
||||
) -> Result<Response<Body>, GatewayError> {
|
||||
let Some(user_identifier) =
|
||||
admin_monitoring_cache_affinity_user_identifier_from_path(&request_context.request_path)
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
use super::cache_types::AdminMonitoringCacheAffinityRecord;
|
||||
use crate::{AppState, GatewayError};
|
||||
use crate::handlers::admin::request::AdminAppState;
|
||||
use crate::GatewayError;
|
||||
|
||||
pub(super) async fn admin_monitoring_list_export_api_key_records_by_ids(
|
||||
state: &AppState,
|
||||
state: &AdminAppState<'_>,
|
||||
api_key_ids: &[String],
|
||||
) -> Result<
|
||||
std::collections::BTreeMap<String, aether_data::repository::auth::StoredAuthApiKeyExportRecord>,
|
||||
@@ -21,7 +22,7 @@ pub(super) async fn admin_monitoring_list_export_api_key_records_by_ids(
|
||||
}
|
||||
|
||||
async fn admin_monitoring_list_user_summaries_by_ids(
|
||||
state: &AppState,
|
||||
state: &AdminAppState<'_>,
|
||||
user_ids: &[String],
|
||||
) -> Result<
|
||||
std::collections::BTreeMap<String, aether_data::repository::users::StoredUserSummary>,
|
||||
@@ -40,7 +41,7 @@ async fn admin_monitoring_list_user_summaries_by_ids(
|
||||
}
|
||||
|
||||
pub(super) async fn admin_monitoring_load_affinity_identity_maps(
|
||||
state: &AppState,
|
||||
state: &AdminAppState<'_>,
|
||||
affinities: &[AdminMonitoringCacheAffinityRecord],
|
||||
) -> Result<
|
||||
(
|
||||
@@ -71,7 +72,7 @@ pub(super) async fn admin_monitoring_load_affinity_identity_maps(
|
||||
}
|
||||
|
||||
pub(super) async fn admin_monitoring_find_user_summary_by_id(
|
||||
state: &AppState,
|
||||
state: &AdminAppState<'_>,
|
||||
user_id: &str,
|
||||
) -> Result<Option<aether_data::repository::users::StoredUserSummary>, GatewayError> {
|
||||
if user_id.trim().is_empty() {
|
||||
|
||||
+3
-3
@@ -2,7 +2,7 @@ use super::cache_config::ADMIN_MONITORING_REDIS_CACHE_CATEGORIES;
|
||||
use super::cache_store::{
|
||||
admin_monitoring_has_test_redis_keys, list_admin_monitoring_namespaced_keys,
|
||||
};
|
||||
use crate::AppState;
|
||||
use crate::handlers::admin::request::AdminAppState;
|
||||
use crate::GatewayError;
|
||||
use axum::{
|
||||
body::Body,
|
||||
@@ -12,7 +12,7 @@ use axum::{
|
||||
use serde_json::json;
|
||||
|
||||
pub(super) async fn build_admin_monitoring_model_mapping_stats_response(
|
||||
state: &AppState,
|
||||
state: &AdminAppState<'_>,
|
||||
) -> Result<Response<Body>, GatewayError> {
|
||||
if state.redis_kv_runner().is_none() && !admin_monitoring_has_test_redis_keys(state) {
|
||||
return Ok(Json(json!({
|
||||
@@ -67,7 +67,7 @@ pub(super) async fn build_admin_monitoring_model_mapping_stats_response(
|
||||
}
|
||||
|
||||
pub(super) async fn build_admin_monitoring_redis_cache_categories_response(
|
||||
state: &AppState,
|
||||
state: &AdminAppState<'_>,
|
||||
) -> Result<Response<Body>, GatewayError> {
|
||||
if state.redis_kv_runner().is_none() && !admin_monitoring_has_test_redis_keys(state) {
|
||||
return Ok(Json(json!({
|
||||
|
||||
@@ -1,400 +0,0 @@
|
||||
use super::cache_affinity::{
|
||||
clear_admin_monitoring_scheduler_affinity_entries,
|
||||
delete_admin_monitoring_cache_affinity_raw_keys,
|
||||
};
|
||||
use super::cache_config::ADMIN_MONITORING_REDIS_CACHE_CATEGORIES;
|
||||
use super::cache_identity::{
|
||||
admin_monitoring_find_user_summary_by_id, admin_monitoring_list_export_api_key_records_by_ids,
|
||||
};
|
||||
use super::cache_route_helpers::{
|
||||
admin_monitoring_cache_affinity_delete_params_from_path,
|
||||
admin_monitoring_cache_affinity_unavailable_response,
|
||||
admin_monitoring_cache_model_mapping_provider_params_from_path,
|
||||
admin_monitoring_cache_model_name_from_path, admin_monitoring_cache_provider_id_from_path,
|
||||
admin_monitoring_cache_redis_category_from_path,
|
||||
admin_monitoring_cache_users_not_found_response,
|
||||
admin_monitoring_cache_users_user_identifier_from_path,
|
||||
admin_monitoring_redis_unavailable_response,
|
||||
};
|
||||
use super::cache_store::{
|
||||
admin_monitoring_has_test_redis_keys, delete_admin_monitoring_namespaced_keys,
|
||||
list_admin_monitoring_cache_affinity_records,
|
||||
list_admin_monitoring_cache_affinity_records_by_affinity_keys,
|
||||
list_admin_monitoring_namespaced_keys, load_admin_monitoring_cache_affinity_entries_for_tests,
|
||||
};
|
||||
use super::responses::{
|
||||
admin_monitoring_bad_request_response, admin_monitoring_not_found_response,
|
||||
};
|
||||
use crate::control::GatewayPublicRequestContext;
|
||||
use crate::{AppState, GatewayError};
|
||||
use axum::{
|
||||
body::Body,
|
||||
http,
|
||||
response::{IntoResponse, Response},
|
||||
Json,
|
||||
};
|
||||
use serde_json::json;
|
||||
|
||||
pub(super) async fn build_admin_monitoring_cache_users_delete_response(
|
||||
state: &AppState,
|
||||
request_context: &GatewayPublicRequestContext,
|
||||
) -> Result<Response<Body>, GatewayError> {
|
||||
let Some(user_identifier) =
|
||||
admin_monitoring_cache_users_user_identifier_from_path(&request_context.request_path)
|
||||
else {
|
||||
return Ok(admin_monitoring_bad_request_response(
|
||||
"缺少 user_identifier",
|
||||
));
|
||||
};
|
||||
|
||||
if state.redis_kv_runner().is_none()
|
||||
&& load_admin_monitoring_cache_affinity_entries_for_tests(state).is_empty()
|
||||
{
|
||||
return Ok(admin_monitoring_cache_affinity_unavailable_response());
|
||||
}
|
||||
|
||||
let direct_api_key_by_id =
|
||||
admin_monitoring_list_export_api_key_records_by_ids(state, &[user_identifier.clone()])
|
||||
.await?;
|
||||
|
||||
if let Some(api_key) = direct_api_key_by_id.get(&user_identifier) {
|
||||
let target_affinity_keys =
|
||||
std::iter::once(user_identifier.clone()).collect::<std::collections::BTreeSet<_>>();
|
||||
let target_affinities = list_admin_monitoring_cache_affinity_records_by_affinity_keys(
|
||||
state,
|
||||
&target_affinity_keys,
|
||||
)
|
||||
.await?;
|
||||
let raw_keys = target_affinities
|
||||
.iter()
|
||||
.map(|item| item.raw_key.clone())
|
||||
.collect::<Vec<_>>();
|
||||
let _ = delete_admin_monitoring_cache_affinity_raw_keys(state, &raw_keys).await?;
|
||||
clear_admin_monitoring_scheduler_affinity_entries(state, &target_affinities);
|
||||
|
||||
let user = admin_monitoring_find_user_summary_by_id(state, &api_key.user_id).await?;
|
||||
let api_key_name = api_key
|
||||
.name
|
||||
.clone()
|
||||
.unwrap_or_else(|| user_identifier.clone());
|
||||
return Ok(Json(json!({
|
||||
"status": "ok",
|
||||
"message": format!("已清除 API Key {api_key_name} 的缓存亲和性"),
|
||||
"user_info": {
|
||||
"user_id": Some(api_key.user_id.clone()),
|
||||
"username": user.as_ref().map(|item| item.username.clone()),
|
||||
"email": user.and_then(|item| item.email),
|
||||
"api_key_id": user_identifier,
|
||||
"api_key_name": api_key.name.clone(),
|
||||
},
|
||||
}))
|
||||
.into_response());
|
||||
}
|
||||
|
||||
let Some(user) = state.find_user_auth_by_identifier(&user_identifier).await? else {
|
||||
return Ok(admin_monitoring_cache_users_not_found_response(
|
||||
&user_identifier,
|
||||
));
|
||||
};
|
||||
|
||||
let user_api_key_ids = state
|
||||
.list_auth_api_key_export_records_by_user_ids(std::slice::from_ref(&user.id))
|
||||
.await?
|
||||
.into_iter()
|
||||
.map(|item| item.api_key_id.clone())
|
||||
.collect::<std::collections::BTreeSet<_>>();
|
||||
let target_affinities =
|
||||
list_admin_monitoring_cache_affinity_records_by_affinity_keys(state, &user_api_key_ids)
|
||||
.await?;
|
||||
let raw_keys = target_affinities
|
||||
.iter()
|
||||
.map(|item| item.raw_key.clone())
|
||||
.collect::<Vec<_>>();
|
||||
let _ = delete_admin_monitoring_cache_affinity_raw_keys(state, &raw_keys).await?;
|
||||
clear_admin_monitoring_scheduler_affinity_entries(state, &target_affinities);
|
||||
|
||||
Ok(Json(json!({
|
||||
"status": "ok",
|
||||
"message": format!("已清除用户 {} 的所有缓存亲和性", user.username),
|
||||
"user_info": {
|
||||
"user_id": user.id,
|
||||
"username": user.username,
|
||||
"email": user.email,
|
||||
},
|
||||
}))
|
||||
.into_response())
|
||||
}
|
||||
|
||||
pub(super) async fn build_admin_monitoring_cache_affinity_delete_response(
|
||||
state: &AppState,
|
||||
request_context: &GatewayPublicRequestContext,
|
||||
) -> Result<Response<Body>, GatewayError> {
|
||||
let Some((affinity_key, endpoint_id, model_id, api_format)) =
|
||||
admin_monitoring_cache_affinity_delete_params_from_path(&request_context.request_path)
|
||||
else {
|
||||
return Ok(admin_monitoring_bad_request_response(
|
||||
"缺少 affinity_key、endpoint_id、model_id 或 api_format",
|
||||
));
|
||||
};
|
||||
|
||||
if state.redis_kv_runner().is_none()
|
||||
&& load_admin_monitoring_cache_affinity_entries_for_tests(state).is_empty()
|
||||
{
|
||||
return Ok(admin_monitoring_cache_affinity_unavailable_response());
|
||||
}
|
||||
|
||||
let target_affinity_keys =
|
||||
std::iter::once(affinity_key.clone()).collect::<std::collections::BTreeSet<_>>();
|
||||
let target_affinity =
|
||||
list_admin_monitoring_cache_affinity_records_by_affinity_keys(state, &target_affinity_keys)
|
||||
.await?
|
||||
.into_iter()
|
||||
.find(|item| {
|
||||
item.affinity_key == affinity_key
|
||||
&& item.endpoint_id.as_deref() == Some(endpoint_id.as_str())
|
||||
&& item.model_name == model_id
|
||||
&& item.api_format.eq_ignore_ascii_case(&api_format)
|
||||
});
|
||||
let Some(target_affinity) = target_affinity else {
|
||||
return Ok(admin_monitoring_not_found_response(
|
||||
"未找到指定的缓存亲和性记录",
|
||||
));
|
||||
};
|
||||
|
||||
let _ = delete_admin_monitoring_cache_affinity_raw_keys(
|
||||
state,
|
||||
std::slice::from_ref(&target_affinity.raw_key),
|
||||
)
|
||||
.await?;
|
||||
clear_admin_monitoring_scheduler_affinity_entries(
|
||||
state,
|
||||
std::slice::from_ref(&target_affinity),
|
||||
);
|
||||
|
||||
let mut api_key_by_id = admin_monitoring_list_export_api_key_records_by_ids(
|
||||
state,
|
||||
std::slice::from_ref(&affinity_key),
|
||||
)
|
||||
.await?;
|
||||
let api_key_name = api_key_by_id
|
||||
.remove(&affinity_key)
|
||||
.and_then(|item| item.name)
|
||||
.unwrap_or_else(|| affinity_key.chars().take(8).collect::<String>());
|
||||
|
||||
Ok(Json(json!({
|
||||
"status": "ok",
|
||||
"message": format!("已清除缓存亲和性: {api_key_name}"),
|
||||
"affinity_key": affinity_key,
|
||||
"endpoint_id": endpoint_id,
|
||||
"model_id": model_id,
|
||||
}))
|
||||
.into_response())
|
||||
}
|
||||
|
||||
pub(super) async fn build_admin_monitoring_cache_flush_response(
|
||||
state: &AppState,
|
||||
) -> Result<Response<Body>, GatewayError> {
|
||||
let raw_affinities = list_admin_monitoring_cache_affinity_records(state).await?;
|
||||
if state.redis_kv_runner().is_none() && raw_affinities.is_empty() {
|
||||
return Ok(admin_monitoring_cache_affinity_unavailable_response());
|
||||
}
|
||||
|
||||
let raw_keys = raw_affinities
|
||||
.iter()
|
||||
.map(|item| item.raw_key.clone())
|
||||
.collect::<Vec<_>>();
|
||||
let deleted = delete_admin_monitoring_cache_affinity_raw_keys(state, &raw_keys).await?;
|
||||
clear_admin_monitoring_scheduler_affinity_entries(state, &raw_affinities);
|
||||
|
||||
Ok(Json(json!({
|
||||
"status": "ok",
|
||||
"message": "已清除全部缓存亲和性",
|
||||
"deleted_affinities": deleted,
|
||||
}))
|
||||
.into_response())
|
||||
}
|
||||
|
||||
pub(super) async fn build_admin_monitoring_cache_provider_delete_response(
|
||||
state: &AppState,
|
||||
request_context: &GatewayPublicRequestContext,
|
||||
) -> Result<Response<Body>, GatewayError> {
|
||||
let Some(provider_id) =
|
||||
admin_monitoring_cache_provider_id_from_path(&request_context.request_path)
|
||||
else {
|
||||
return Ok(admin_monitoring_bad_request_response("缺少 provider_id"));
|
||||
};
|
||||
|
||||
let raw_affinities = list_admin_monitoring_cache_affinity_records(state).await?;
|
||||
if state.redis_kv_runner().is_none() && raw_affinities.is_empty() {
|
||||
return Ok(admin_monitoring_cache_affinity_unavailable_response());
|
||||
}
|
||||
|
||||
let target_affinities = raw_affinities
|
||||
.into_iter()
|
||||
.filter(|item| item.provider_id.as_deref() == Some(provider_id.as_str()))
|
||||
.collect::<Vec<_>>();
|
||||
if target_affinities.is_empty() {
|
||||
return Ok((
|
||||
http::StatusCode::NOT_FOUND,
|
||||
Json(json!({
|
||||
"detail": format!("未找到 provider {provider_id} 的缓存亲和性记录")
|
||||
})),
|
||||
)
|
||||
.into_response());
|
||||
}
|
||||
|
||||
let raw_keys = target_affinities
|
||||
.iter()
|
||||
.map(|item| item.raw_key.clone())
|
||||
.collect::<Vec<_>>();
|
||||
let deleted = delete_admin_monitoring_cache_affinity_raw_keys(state, &raw_keys).await?;
|
||||
clear_admin_monitoring_scheduler_affinity_entries(state, &target_affinities);
|
||||
|
||||
Ok(Json(json!({
|
||||
"status": "ok",
|
||||
"message": format!("已清除 provider {provider_id} 的缓存亲和性"),
|
||||
"provider_id": provider_id,
|
||||
"deleted_affinities": deleted,
|
||||
}))
|
||||
.into_response())
|
||||
}
|
||||
|
||||
pub(super) async fn build_admin_monitoring_model_mapping_delete_response(
|
||||
state: &AppState,
|
||||
) -> Result<Response<Body>, GatewayError> {
|
||||
if state.redis_kv_runner().is_none() && !admin_monitoring_has_test_redis_keys(state) {
|
||||
return Ok(admin_monitoring_redis_unavailable_response());
|
||||
}
|
||||
|
||||
let mut raw_keys = list_admin_monitoring_namespaced_keys(state, "model:*").await?;
|
||||
raw_keys.extend(list_admin_monitoring_namespaced_keys(state, "global_model:*").await?);
|
||||
raw_keys.sort();
|
||||
raw_keys.dedup();
|
||||
let deleted_count = delete_admin_monitoring_namespaced_keys(state, &raw_keys).await?;
|
||||
|
||||
Ok(Json(json!({
|
||||
"status": "ok",
|
||||
"message": "已清除所有模型映射缓存",
|
||||
"deleted_count": deleted_count,
|
||||
}))
|
||||
.into_response())
|
||||
}
|
||||
|
||||
pub(super) async fn build_admin_monitoring_model_mapping_delete_model_response(
|
||||
state: &AppState,
|
||||
request_context: &GatewayPublicRequestContext,
|
||||
) -> Result<Response<Body>, GatewayError> {
|
||||
let Some(model_name) =
|
||||
admin_monitoring_cache_model_name_from_path(&request_context.request_path)
|
||||
else {
|
||||
return Ok(admin_monitoring_bad_request_response("缺少 model_name"));
|
||||
};
|
||||
if state.redis_kv_runner().is_none() && !admin_monitoring_has_test_redis_keys(state) {
|
||||
return Ok(admin_monitoring_redis_unavailable_response());
|
||||
}
|
||||
|
||||
let candidate_keys = [
|
||||
format!("global_model:resolve:{model_name}"),
|
||||
format!("global_model:name:{model_name}"),
|
||||
];
|
||||
let mut existing_keys = Vec::new();
|
||||
for key in candidate_keys {
|
||||
let matches = list_admin_monitoring_namespaced_keys(state, key.as_str()).await?;
|
||||
existing_keys.extend(matches);
|
||||
}
|
||||
existing_keys.sort();
|
||||
existing_keys.dedup();
|
||||
|
||||
let deleted_count = delete_admin_monitoring_namespaced_keys(state, &existing_keys).await?;
|
||||
let deleted_keys = if deleted_count == 0 {
|
||||
Vec::new()
|
||||
} else {
|
||||
existing_keys
|
||||
};
|
||||
|
||||
Ok(Json(json!({
|
||||
"status": "ok",
|
||||
"message": format!("已清除模型 {model_name} 的映射缓存"),
|
||||
"model_name": model_name,
|
||||
"deleted_keys": deleted_keys,
|
||||
}))
|
||||
.into_response())
|
||||
}
|
||||
|
||||
pub(super) async fn build_admin_monitoring_model_mapping_delete_provider_response(
|
||||
state: &AppState,
|
||||
request_context: &GatewayPublicRequestContext,
|
||||
) -> Result<Response<Body>, GatewayError> {
|
||||
let Some((provider_id, global_model_id)) =
|
||||
admin_monitoring_cache_model_mapping_provider_params_from_path(
|
||||
&request_context.request_path,
|
||||
)
|
||||
else {
|
||||
return Ok(admin_monitoring_bad_request_response(
|
||||
"缺少 provider_id 或 global_model_id",
|
||||
));
|
||||
};
|
||||
if state.redis_kv_runner().is_none() && !admin_monitoring_has_test_redis_keys(state) {
|
||||
return Ok(admin_monitoring_redis_unavailable_response());
|
||||
}
|
||||
|
||||
let candidate_keys = [
|
||||
format!("model:provider_global:{provider_id}:{global_model_id}"),
|
||||
format!("model:provider_global:hits:{provider_id}:{global_model_id}"),
|
||||
];
|
||||
let mut existing_keys = Vec::new();
|
||||
for key in candidate_keys {
|
||||
let matches = list_admin_monitoring_namespaced_keys(state, key.as_str()).await?;
|
||||
existing_keys.extend(matches);
|
||||
}
|
||||
existing_keys.sort();
|
||||
existing_keys.dedup();
|
||||
|
||||
let _ = delete_admin_monitoring_namespaced_keys(state, &existing_keys).await?;
|
||||
|
||||
Ok(Json(json!({
|
||||
"status": "ok",
|
||||
"message": "已清除 Provider 模型映射缓存",
|
||||
"provider_id": provider_id,
|
||||
"global_model_id": global_model_id,
|
||||
"deleted_keys": existing_keys,
|
||||
}))
|
||||
.into_response())
|
||||
}
|
||||
|
||||
pub(super) async fn build_admin_monitoring_redis_keys_delete_response(
|
||||
state: &AppState,
|
||||
request_context: &GatewayPublicRequestContext,
|
||||
) -> Result<Response<Body>, GatewayError> {
|
||||
let Some(category) =
|
||||
admin_monitoring_cache_redis_category_from_path(&request_context.request_path)
|
||||
else {
|
||||
return Ok(admin_monitoring_bad_request_response("缺少 category"));
|
||||
};
|
||||
|
||||
let Some((cat_key, name, pattern, _description)) = ADMIN_MONITORING_REDIS_CACHE_CATEGORIES
|
||||
.iter()
|
||||
.find(|(cat_key, _, _, _)| *cat_key == category)
|
||||
else {
|
||||
return Ok((
|
||||
http::StatusCode::NOT_FOUND,
|
||||
Json(json!({ "detail": format!("未知的缓存分类: {category}") })),
|
||||
)
|
||||
.into_response());
|
||||
};
|
||||
|
||||
if state.redis_kv_runner().is_none() && !admin_monitoring_has_test_redis_keys(state) {
|
||||
return Ok(admin_monitoring_redis_unavailable_response());
|
||||
}
|
||||
|
||||
let raw_keys = list_admin_monitoring_namespaced_keys(state, pattern).await?;
|
||||
let deleted_count = delete_admin_monitoring_namespaced_keys(state, &raw_keys).await?;
|
||||
|
||||
Ok(Json(json!({
|
||||
"status": "ok",
|
||||
"message": format!("已清除 {name} 缓存"),
|
||||
"category": cat_key,
|
||||
"deleted_count": deleted_count,
|
||||
}))
|
||||
.into_response())
|
||||
}
|
||||
+86
@@ -0,0 +1,86 @@
|
||||
use super::super::cache_affinity::{
|
||||
clear_admin_monitoring_scheduler_affinity_entries,
|
||||
delete_admin_monitoring_cache_affinity_raw_keys,
|
||||
};
|
||||
use super::super::cache_identity::admin_monitoring_list_export_api_key_records_by_ids;
|
||||
use super::super::cache_route_helpers::{
|
||||
admin_monitoring_cache_affinity_delete_params_from_path,
|
||||
admin_monitoring_cache_affinity_unavailable_response,
|
||||
};
|
||||
use super::super::cache_store::{
|
||||
list_admin_monitoring_cache_affinity_records_by_affinity_keys,
|
||||
load_admin_monitoring_cache_affinity_entries_for_tests,
|
||||
};
|
||||
use crate::handlers::admin::request::{AdminAppState, AdminRequestContext};
|
||||
use crate::GatewayError;
|
||||
use aether_admin::observability::monitoring::{
|
||||
admin_monitoring_bad_request_response, admin_monitoring_not_found_response,
|
||||
build_admin_monitoring_cache_affinity_delete_success_response,
|
||||
};
|
||||
use axum::{body::Body, response::Response};
|
||||
|
||||
pub(in super::super) async fn build_admin_monitoring_cache_affinity_delete_response(
|
||||
state: &AdminAppState<'_>,
|
||||
request_context: &AdminRequestContext<'_>,
|
||||
) -> Result<Response<Body>, GatewayError> {
|
||||
let Some((affinity_key, endpoint_id, model_id, api_format)) =
|
||||
admin_monitoring_cache_affinity_delete_params_from_path(&request_context.request_path)
|
||||
else {
|
||||
return Ok(admin_monitoring_bad_request_response(
|
||||
"缺少 affinity_key、endpoint_id、model_id 或 api_format",
|
||||
));
|
||||
};
|
||||
|
||||
if state.redis_kv_runner().is_none()
|
||||
&& load_admin_monitoring_cache_affinity_entries_for_tests(state).is_empty()
|
||||
{
|
||||
return Ok(admin_monitoring_cache_affinity_unavailable_response());
|
||||
}
|
||||
|
||||
let target_affinity_keys =
|
||||
std::iter::once(affinity_key.clone()).collect::<std::collections::BTreeSet<_>>();
|
||||
let target_affinity =
|
||||
list_admin_monitoring_cache_affinity_records_by_affinity_keys(state, &target_affinity_keys)
|
||||
.await?
|
||||
.into_iter()
|
||||
.find(|item| {
|
||||
item.affinity_key == affinity_key
|
||||
&& item.endpoint_id.as_deref() == Some(endpoint_id.as_str())
|
||||
&& item.model_name == model_id
|
||||
&& item.api_format.eq_ignore_ascii_case(&api_format)
|
||||
});
|
||||
let Some(target_affinity) = target_affinity else {
|
||||
return Ok(admin_monitoring_not_found_response(
|
||||
"未找到指定的缓存亲和性记录",
|
||||
));
|
||||
};
|
||||
|
||||
let _ = delete_admin_monitoring_cache_affinity_raw_keys(
|
||||
state,
|
||||
std::slice::from_ref(&target_affinity.raw_key),
|
||||
)
|
||||
.await?;
|
||||
clear_admin_monitoring_scheduler_affinity_entries(
|
||||
state,
|
||||
std::slice::from_ref(&target_affinity),
|
||||
);
|
||||
|
||||
let mut api_key_by_id = admin_monitoring_list_export_api_key_records_by_ids(
|
||||
state,
|
||||
std::slice::from_ref(&affinity_key),
|
||||
)
|
||||
.await?;
|
||||
let api_key_name = api_key_by_id
|
||||
.remove(&affinity_key)
|
||||
.and_then(|item| item.name)
|
||||
.unwrap_or_else(|| affinity_key.chars().take(8).collect::<String>());
|
||||
|
||||
Ok(
|
||||
build_admin_monitoring_cache_affinity_delete_success_response(
|
||||
affinity_key,
|
||||
endpoint_id,
|
||||
model_id,
|
||||
api_key_name,
|
||||
),
|
||||
)
|
||||
}
|
||||
+28
@@ -0,0 +1,28 @@
|
||||
use super::super::cache_affinity::{
|
||||
clear_admin_monitoring_scheduler_affinity_entries,
|
||||
delete_admin_monitoring_cache_affinity_raw_keys,
|
||||
};
|
||||
use super::super::cache_route_helpers::admin_monitoring_cache_affinity_unavailable_response;
|
||||
use super::super::cache_store::list_admin_monitoring_cache_affinity_records;
|
||||
use crate::handlers::admin::request::AdminAppState;
|
||||
use crate::GatewayError;
|
||||
use aether_admin::observability::monitoring::build_admin_monitoring_cache_flush_success_response;
|
||||
use axum::{body::Body, response::Response};
|
||||
|
||||
pub(in super::super) async fn build_admin_monitoring_cache_flush_response(
|
||||
state: &AdminAppState<'_>,
|
||||
) -> Result<Response<Body>, GatewayError> {
|
||||
let raw_affinities = list_admin_monitoring_cache_affinity_records(state).await?;
|
||||
if state.redis_kv_runner().is_none() && raw_affinities.is_empty() {
|
||||
return Ok(admin_monitoring_cache_affinity_unavailable_response());
|
||||
}
|
||||
|
||||
let raw_keys = raw_affinities
|
||||
.iter()
|
||||
.map(|item| item.raw_key.clone())
|
||||
.collect::<Vec<_>>();
|
||||
let deleted = delete_admin_monitoring_cache_affinity_raw_keys(state, &raw_keys).await?;
|
||||
clear_admin_monitoring_scheduler_affinity_entries(state, &raw_affinities);
|
||||
|
||||
Ok(build_admin_monitoring_cache_flush_success_response(deleted))
|
||||
}
|
||||
+17
@@ -0,0 +1,17 @@
|
||||
mod affinity;
|
||||
mod flush;
|
||||
mod model_mapping;
|
||||
mod provider;
|
||||
mod redis_keys;
|
||||
mod users;
|
||||
|
||||
pub(super) use affinity::build_admin_monitoring_cache_affinity_delete_response;
|
||||
pub(super) use flush::build_admin_monitoring_cache_flush_response;
|
||||
pub(super) use model_mapping::{
|
||||
build_admin_monitoring_model_mapping_delete_model_response,
|
||||
build_admin_monitoring_model_mapping_delete_provider_response,
|
||||
build_admin_monitoring_model_mapping_delete_response,
|
||||
};
|
||||
pub(super) use provider::build_admin_monitoring_cache_provider_delete_response;
|
||||
pub(super) use redis_keys::build_admin_monitoring_redis_keys_delete_response;
|
||||
pub(super) use users::build_admin_monitoring_cache_users_delete_response;
|
||||
+113
@@ -0,0 +1,113 @@
|
||||
use super::super::cache_route_helpers::{
|
||||
admin_monitoring_cache_model_mapping_provider_params_from_path,
|
||||
admin_monitoring_cache_model_name_from_path, admin_monitoring_redis_unavailable_response,
|
||||
};
|
||||
use super::super::cache_store::{
|
||||
admin_monitoring_has_test_redis_keys, delete_admin_monitoring_namespaced_keys,
|
||||
list_admin_monitoring_namespaced_keys,
|
||||
};
|
||||
use crate::handlers::admin::request::{AdminAppState, AdminRequestContext};
|
||||
use crate::GatewayError;
|
||||
use aether_admin::observability::monitoring::{
|
||||
admin_monitoring_bad_request_response,
|
||||
build_admin_monitoring_model_mapping_delete_model_success_response,
|
||||
build_admin_monitoring_model_mapping_delete_provider_success_response,
|
||||
build_admin_monitoring_model_mapping_delete_success_response,
|
||||
};
|
||||
use axum::{body::Body, response::Response};
|
||||
|
||||
pub(in super::super) async fn build_admin_monitoring_model_mapping_delete_response(
|
||||
state: &AdminAppState<'_>,
|
||||
) -> Result<Response<Body>, GatewayError> {
|
||||
if state.redis_kv_runner().is_none() && !admin_monitoring_has_test_redis_keys(state) {
|
||||
return Ok(admin_monitoring_redis_unavailable_response());
|
||||
}
|
||||
|
||||
let mut raw_keys = list_admin_monitoring_namespaced_keys(state, "model:*").await?;
|
||||
raw_keys.extend(list_admin_monitoring_namespaced_keys(state, "global_model:*").await?);
|
||||
raw_keys.sort();
|
||||
raw_keys.dedup();
|
||||
let deleted_count = delete_admin_monitoring_namespaced_keys(state, &raw_keys).await?;
|
||||
|
||||
Ok(build_admin_monitoring_model_mapping_delete_success_response(deleted_count))
|
||||
}
|
||||
|
||||
pub(in super::super) async fn build_admin_monitoring_model_mapping_delete_model_response(
|
||||
state: &AdminAppState<'_>,
|
||||
request_context: &AdminRequestContext<'_>,
|
||||
) -> Result<Response<Body>, GatewayError> {
|
||||
let Some(model_name) =
|
||||
admin_monitoring_cache_model_name_from_path(&request_context.request_path)
|
||||
else {
|
||||
return Ok(admin_monitoring_bad_request_response("缺少 model_name"));
|
||||
};
|
||||
if state.redis_kv_runner().is_none() && !admin_monitoring_has_test_redis_keys(state) {
|
||||
return Ok(admin_monitoring_redis_unavailable_response());
|
||||
}
|
||||
|
||||
let candidate_keys = [
|
||||
format!("global_model:resolve:{model_name}"),
|
||||
format!("global_model:name:{model_name}"),
|
||||
];
|
||||
let mut existing_keys = Vec::new();
|
||||
for key in candidate_keys {
|
||||
let matches = list_admin_monitoring_namespaced_keys(state, key.as_str()).await?;
|
||||
existing_keys.extend(matches);
|
||||
}
|
||||
existing_keys.sort();
|
||||
existing_keys.dedup();
|
||||
|
||||
let deleted_count = delete_admin_monitoring_namespaced_keys(state, &existing_keys).await?;
|
||||
let deleted_keys = if deleted_count == 0 {
|
||||
Vec::new()
|
||||
} else {
|
||||
existing_keys
|
||||
};
|
||||
|
||||
Ok(
|
||||
build_admin_monitoring_model_mapping_delete_model_success_response(
|
||||
model_name,
|
||||
deleted_keys,
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
pub(in super::super) async fn build_admin_monitoring_model_mapping_delete_provider_response(
|
||||
state: &AdminAppState<'_>,
|
||||
request_context: &AdminRequestContext<'_>,
|
||||
) -> Result<Response<Body>, GatewayError> {
|
||||
let Some((provider_id, global_model_id)) =
|
||||
admin_monitoring_cache_model_mapping_provider_params_from_path(
|
||||
&request_context.request_path,
|
||||
)
|
||||
else {
|
||||
return Ok(admin_monitoring_bad_request_response(
|
||||
"缺少 provider_id 或 global_model_id",
|
||||
));
|
||||
};
|
||||
if state.redis_kv_runner().is_none() && !admin_monitoring_has_test_redis_keys(state) {
|
||||
return Ok(admin_monitoring_redis_unavailable_response());
|
||||
}
|
||||
|
||||
let candidate_keys = [
|
||||
format!("model:provider_global:{provider_id}:{global_model_id}"),
|
||||
format!("model:provider_global:hits:{provider_id}:{global_model_id}"),
|
||||
];
|
||||
let mut existing_keys = Vec::new();
|
||||
for key in candidate_keys {
|
||||
let matches = list_admin_monitoring_namespaced_keys(state, key.as_str()).await?;
|
||||
existing_keys.extend(matches);
|
||||
}
|
||||
existing_keys.sort();
|
||||
existing_keys.dedup();
|
||||
|
||||
let _ = delete_admin_monitoring_namespaced_keys(state, &existing_keys).await?;
|
||||
|
||||
Ok(
|
||||
build_admin_monitoring_model_mapping_delete_provider_success_response(
|
||||
provider_id,
|
||||
global_model_id,
|
||||
existing_keys,
|
||||
),
|
||||
)
|
||||
}
|
||||
+51
@@ -0,0 +1,51 @@
|
||||
use super::super::cache_affinity::{
|
||||
clear_admin_monitoring_scheduler_affinity_entries,
|
||||
delete_admin_monitoring_cache_affinity_raw_keys,
|
||||
};
|
||||
use super::super::cache_route_helpers::{
|
||||
admin_monitoring_cache_affinity_unavailable_response,
|
||||
admin_monitoring_cache_provider_id_from_path,
|
||||
};
|
||||
use super::super::cache_store::list_admin_monitoring_cache_affinity_records;
|
||||
use crate::handlers::admin::request::{AdminAppState, AdminRequestContext};
|
||||
use crate::GatewayError;
|
||||
use aether_admin::observability::monitoring::{
|
||||
admin_monitoring_bad_request_response, admin_monitoring_cache_provider_not_found_response,
|
||||
build_admin_monitoring_cache_provider_delete_success_response,
|
||||
};
|
||||
use axum::{body::Body, response::Response};
|
||||
|
||||
pub(in super::super) async fn build_admin_monitoring_cache_provider_delete_response(
|
||||
state: &AdminAppState<'_>,
|
||||
request_context: &AdminRequestContext<'_>,
|
||||
) -> Result<Response<Body>, GatewayError> {
|
||||
let Some(provider_id) =
|
||||
admin_monitoring_cache_provider_id_from_path(&request_context.request_path)
|
||||
else {
|
||||
return Ok(admin_monitoring_bad_request_response("缺少 provider_id"));
|
||||
};
|
||||
|
||||
let raw_affinities = list_admin_monitoring_cache_affinity_records(state).await?;
|
||||
if state.redis_kv_runner().is_none() && raw_affinities.is_empty() {
|
||||
return Ok(admin_monitoring_cache_affinity_unavailable_response());
|
||||
}
|
||||
|
||||
let target_affinities = raw_affinities
|
||||
.into_iter()
|
||||
.filter(|item| item.provider_id.as_deref() == Some(provider_id.as_str()))
|
||||
.collect::<Vec<_>>();
|
||||
if target_affinities.is_empty() {
|
||||
return Ok(admin_monitoring_cache_provider_not_found_response(
|
||||
&provider_id,
|
||||
));
|
||||
}
|
||||
|
||||
let raw_keys = target_affinities
|
||||
.iter()
|
||||
.map(|item| item.raw_key.clone())
|
||||
.collect::<Vec<_>>();
|
||||
let deleted = delete_admin_monitoring_cache_affinity_raw_keys(state, &raw_keys).await?;
|
||||
clear_admin_monitoring_scheduler_affinity_entries(state, &target_affinities);
|
||||
|
||||
Ok(build_admin_monitoring_cache_provider_delete_success_response(provider_id, deleted))
|
||||
}
|
||||
+46
@@ -0,0 +1,46 @@
|
||||
use super::super::cache_config::ADMIN_MONITORING_REDIS_CACHE_CATEGORIES;
|
||||
use super::super::cache_route_helpers::{
|
||||
admin_monitoring_cache_redis_category_from_path, admin_monitoring_redis_unavailable_response,
|
||||
};
|
||||
use super::super::cache_store::{
|
||||
admin_monitoring_has_test_redis_keys, delete_admin_monitoring_namespaced_keys,
|
||||
list_admin_monitoring_namespaced_keys,
|
||||
};
|
||||
use crate::handlers::admin::request::{AdminAppState, AdminRequestContext};
|
||||
use crate::GatewayError;
|
||||
use aether_admin::observability::monitoring::{
|
||||
admin_monitoring_bad_request_response, admin_monitoring_unknown_cache_category_response,
|
||||
build_admin_monitoring_redis_keys_delete_success_response,
|
||||
};
|
||||
use axum::{body::Body, response::Response};
|
||||
|
||||
pub(in super::super) async fn build_admin_monitoring_redis_keys_delete_response(
|
||||
state: &AdminAppState<'_>,
|
||||
request_context: &AdminRequestContext<'_>,
|
||||
) -> Result<Response<Body>, GatewayError> {
|
||||
let Some(category) =
|
||||
admin_monitoring_cache_redis_category_from_path(&request_context.request_path)
|
||||
else {
|
||||
return Ok(admin_monitoring_bad_request_response("缺少 category"));
|
||||
};
|
||||
|
||||
let Some((cat_key, name, pattern, _description)) = ADMIN_MONITORING_REDIS_CACHE_CATEGORIES
|
||||
.iter()
|
||||
.find(|(cat_key, _, _, _)| *cat_key == category)
|
||||
else {
|
||||
return Ok(admin_monitoring_unknown_cache_category_response(&category));
|
||||
};
|
||||
|
||||
if state.redis_kv_runner().is_none() && !admin_monitoring_has_test_redis_keys(state) {
|
||||
return Ok(admin_monitoring_redis_unavailable_response());
|
||||
}
|
||||
|
||||
let raw_keys = list_admin_monitoring_namespaced_keys(state, pattern).await?;
|
||||
let deleted_count = delete_admin_monitoring_namespaced_keys(state, &raw_keys).await?;
|
||||
|
||||
Ok(build_admin_monitoring_redis_keys_delete_success_response(
|
||||
cat_key,
|
||||
name,
|
||||
deleted_count,
|
||||
))
|
||||
}
|
||||
+108
@@ -0,0 +1,108 @@
|
||||
use super::super::cache_affinity::{
|
||||
clear_admin_monitoring_scheduler_affinity_entries,
|
||||
delete_admin_monitoring_cache_affinity_raw_keys,
|
||||
};
|
||||
use super::super::cache_identity::{
|
||||
admin_monitoring_find_user_summary_by_id, admin_monitoring_list_export_api_key_records_by_ids,
|
||||
};
|
||||
use super::super::cache_route_helpers::{
|
||||
admin_monitoring_cache_affinity_unavailable_response,
|
||||
admin_monitoring_cache_users_not_found_response,
|
||||
admin_monitoring_cache_users_user_identifier_from_path,
|
||||
};
|
||||
use super::super::cache_store::{
|
||||
list_admin_monitoring_cache_affinity_records_by_affinity_keys,
|
||||
load_admin_monitoring_cache_affinity_entries_for_tests,
|
||||
};
|
||||
use crate::handlers::admin::request::{AdminAppState, AdminRequestContext};
|
||||
use crate::GatewayError;
|
||||
use aether_admin::observability::monitoring::{
|
||||
admin_monitoring_bad_request_response,
|
||||
build_admin_monitoring_cache_users_delete_api_key_success_response,
|
||||
build_admin_monitoring_cache_users_delete_user_success_response,
|
||||
};
|
||||
use axum::{body::Body, response::Response};
|
||||
|
||||
pub(in super::super) async fn build_admin_monitoring_cache_users_delete_response(
|
||||
state: &AdminAppState<'_>,
|
||||
request_context: &AdminRequestContext<'_>,
|
||||
) -> Result<Response<Body>, GatewayError> {
|
||||
let Some(user_identifier) =
|
||||
admin_monitoring_cache_users_user_identifier_from_path(&request_context.request_path)
|
||||
else {
|
||||
return Ok(admin_monitoring_bad_request_response(
|
||||
"缺少 user_identifier",
|
||||
));
|
||||
};
|
||||
|
||||
if state.redis_kv_runner().is_none()
|
||||
&& load_admin_monitoring_cache_affinity_entries_for_tests(state).is_empty()
|
||||
{
|
||||
return Ok(admin_monitoring_cache_affinity_unavailable_response());
|
||||
}
|
||||
|
||||
let direct_api_key_by_id =
|
||||
admin_monitoring_list_export_api_key_records_by_ids(state, &[user_identifier.clone()])
|
||||
.await?;
|
||||
|
||||
if let Some(api_key) = direct_api_key_by_id.get(&user_identifier) {
|
||||
let target_affinity_keys =
|
||||
std::iter::once(user_identifier.clone()).collect::<std::collections::BTreeSet<_>>();
|
||||
let target_affinities = list_admin_monitoring_cache_affinity_records_by_affinity_keys(
|
||||
state,
|
||||
&target_affinity_keys,
|
||||
)
|
||||
.await?;
|
||||
let raw_keys = target_affinities
|
||||
.iter()
|
||||
.map(|item| item.raw_key.clone())
|
||||
.collect::<Vec<_>>();
|
||||
let _ = delete_admin_monitoring_cache_affinity_raw_keys(state, &raw_keys).await?;
|
||||
clear_admin_monitoring_scheduler_affinity_entries(state, &target_affinities);
|
||||
|
||||
let user = admin_monitoring_find_user_summary_by_id(state, &api_key.user_id).await?;
|
||||
let api_key_name = api_key
|
||||
.name
|
||||
.clone()
|
||||
.unwrap_or_else(|| user_identifier.clone());
|
||||
return Ok(
|
||||
build_admin_monitoring_cache_users_delete_api_key_success_response(
|
||||
api_key.user_id.clone(),
|
||||
user.as_ref().map(|item| item.username.clone()),
|
||||
user.and_then(|item| item.email),
|
||||
user_identifier,
|
||||
api_key.name.clone(),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
let Some(user) = state.find_user_auth_by_identifier(&user_identifier).await? else {
|
||||
return Ok(admin_monitoring_cache_users_not_found_response(
|
||||
&user_identifier,
|
||||
));
|
||||
};
|
||||
|
||||
let user_api_key_ids = state
|
||||
.list_auth_api_key_export_records_by_user_ids(std::slice::from_ref(&user.id))
|
||||
.await?
|
||||
.into_iter()
|
||||
.map(|item| item.api_key_id.clone())
|
||||
.collect::<std::collections::BTreeSet<_>>();
|
||||
let target_affinities =
|
||||
list_admin_monitoring_cache_affinity_records_by_affinity_keys(state, &user_api_key_ids)
|
||||
.await?;
|
||||
let raw_keys = target_affinities
|
||||
.iter()
|
||||
.map(|item| item.raw_key.clone())
|
||||
.collect::<Vec<_>>();
|
||||
let _ = delete_admin_monitoring_cache_affinity_raw_keys(state, &raw_keys).await?;
|
||||
clear_admin_monitoring_scheduler_affinity_entries(state, &target_affinities);
|
||||
|
||||
Ok(
|
||||
build_admin_monitoring_cache_users_delete_user_success_response(
|
||||
user.id,
|
||||
user.username,
|
||||
user.email,
|
||||
),
|
||||
)
|
||||
}
|
||||
@@ -1,11 +1,11 @@
|
||||
use crate::AppState;
|
||||
use crate::handlers::admin::request::AdminAppState;
|
||||
use aether_crypto::decrypt_python_fernet_ciphertext;
|
||||
#[cfg(test)]
|
||||
use aether_crypto::DEVELOPMENT_ENCRYPTION_KEY;
|
||||
use aether_data_contracts::repository::provider_catalog::StoredProviderCatalogKey;
|
||||
|
||||
pub(super) fn admin_monitoring_masked_user_api_key_prefix(
|
||||
state: &AppState,
|
||||
state: &AdminAppState<'_>,
|
||||
ciphertext: Option<&str>,
|
||||
) -> Option<String> {
|
||||
let Some(ciphertext) = ciphertext.map(str::trim).filter(|value| !value.is_empty()) else {
|
||||
@@ -23,7 +23,7 @@ pub(super) fn admin_monitoring_masked_user_api_key_prefix(
|
||||
}
|
||||
|
||||
pub(super) fn admin_monitoring_masked_provider_key_prefix(
|
||||
state: &AppState,
|
||||
state: &AdminAppState<'_>,
|
||||
key: &StoredProviderCatalogKey,
|
||||
) -> Option<String> {
|
||||
match key.auth_type.trim() {
|
||||
@@ -44,7 +44,10 @@ pub(super) fn admin_monitoring_masked_provider_key_prefix(
|
||||
}
|
||||
}
|
||||
|
||||
fn admin_monitoring_try_decrypt_secret(state: &AppState, ciphertext: &str) -> Option<String> {
|
||||
fn admin_monitoring_try_decrypt_secret(
|
||||
state: &AdminAppState<'_>,
|
||||
ciphertext: &str,
|
||||
) -> Option<String> {
|
||||
let ciphertext = ciphertext.trim();
|
||||
if ciphertext.is_empty() {
|
||||
return None;
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
use super::cache_affinity::admin_monitoring_cache_affinity_record;
|
||||
use super::cache_types::{AdminMonitoringCacheAffinityRecord, AdminMonitoringCacheSnapshot};
|
||||
use crate::handlers::admin::observability::stats::round_to;
|
||||
use crate::{AppState, GatewayError};
|
||||
use crate::handlers::admin::request::AdminAppState;
|
||||
use crate::GatewayError;
|
||||
use aether_data_contracts::repository::usage::UsageAuditListQuery;
|
||||
|
||||
async fn count_admin_monitoring_cache_affinity_entries(state: &AppState) -> usize {
|
||||
async fn count_admin_monitoring_cache_affinity_entries(state: &AdminAppState<'_>) -> usize {
|
||||
let Some(runner) = state.redis_kv_runner() else {
|
||||
return 0;
|
||||
};
|
||||
@@ -74,35 +75,45 @@ async fn scan_admin_monitoring_namespaced_keys(
|
||||
|
||||
#[cfg(test)]
|
||||
pub(super) fn load_admin_monitoring_cache_affinity_entries_for_tests(
|
||||
state: &AppState,
|
||||
state: &AdminAppState<'_>,
|
||||
) -> Vec<(String, String)> {
|
||||
state.list_admin_monitoring_cache_affinity_entries_for_tests()
|
||||
state
|
||||
.as_ref()
|
||||
.list_admin_monitoring_cache_affinity_entries_for_tests()
|
||||
}
|
||||
|
||||
#[cfg(not(test))]
|
||||
pub(super) fn load_admin_monitoring_cache_affinity_entries_for_tests(
|
||||
_state: &AppState,
|
||||
_state: &AdminAppState<'_>,
|
||||
) -> Vec<(String, String)> {
|
||||
Vec::new()
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
fn load_admin_monitoring_redis_keys_for_tests(state: &AppState) -> Vec<String> {
|
||||
state.list_admin_monitoring_redis_keys_for_tests()
|
||||
fn load_admin_monitoring_redis_keys_for_tests(state: &AdminAppState<'_>) -> Vec<String> {
|
||||
state.as_ref().list_admin_monitoring_redis_keys_for_tests()
|
||||
}
|
||||
|
||||
#[cfg(not(test))]
|
||||
fn load_admin_monitoring_redis_keys_for_tests(_state: &AppState) -> Vec<String> {
|
||||
fn load_admin_monitoring_redis_keys_for_tests(_state: &AdminAppState<'_>) -> Vec<String> {
|
||||
Vec::new()
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
fn delete_admin_monitoring_redis_keys_for_tests(state: &AppState, raw_keys: &[String]) -> usize {
|
||||
state.remove_admin_monitoring_redis_keys_for_tests(raw_keys)
|
||||
fn delete_admin_monitoring_redis_keys_for_tests(
|
||||
state: &AdminAppState<'_>,
|
||||
raw_keys: &[String],
|
||||
) -> usize {
|
||||
state
|
||||
.as_ref()
|
||||
.remove_admin_monitoring_redis_keys_for_tests(raw_keys)
|
||||
}
|
||||
|
||||
#[cfg(not(test))]
|
||||
fn delete_admin_monitoring_redis_keys_for_tests(_state: &AppState, _raw_keys: &[String]) -> usize {
|
||||
fn delete_admin_monitoring_redis_keys_for_tests(
|
||||
_state: &AdminAppState<'_>,
|
||||
_raw_keys: &[String],
|
||||
) -> usize {
|
||||
0
|
||||
}
|
||||
|
||||
@@ -113,12 +124,12 @@ fn admin_monitoring_test_key_matches_pattern(key: &str, pattern: &str) -> bool {
|
||||
}
|
||||
}
|
||||
|
||||
pub(super) fn admin_monitoring_has_test_redis_keys(state: &AppState) -> bool {
|
||||
pub(super) fn admin_monitoring_has_test_redis_keys(state: &AdminAppState<'_>) -> bool {
|
||||
!load_admin_monitoring_redis_keys_for_tests(state).is_empty()
|
||||
}
|
||||
|
||||
pub(super) async fn list_admin_monitoring_namespaced_keys(
|
||||
state: &AppState,
|
||||
state: &AdminAppState<'_>,
|
||||
pattern: &str,
|
||||
) -> Result<Vec<String>, GatewayError> {
|
||||
if let Some(runner) = state.redis_kv_runner() {
|
||||
@@ -134,7 +145,7 @@ pub(super) async fn list_admin_monitoring_namespaced_keys(
|
||||
}
|
||||
|
||||
pub(super) async fn delete_admin_monitoring_namespaced_keys(
|
||||
state: &AppState,
|
||||
state: &AdminAppState<'_>,
|
||||
raw_keys: &[String],
|
||||
) -> Result<usize, GatewayError> {
|
||||
if raw_keys.is_empty() {
|
||||
@@ -165,13 +176,13 @@ pub(super) async fn delete_admin_monitoring_namespaced_keys(
|
||||
}
|
||||
|
||||
pub(super) async fn list_admin_monitoring_cache_affinity_records(
|
||||
state: &AppState,
|
||||
state: &AdminAppState<'_>,
|
||||
) -> Result<Vec<AdminMonitoringCacheAffinityRecord>, GatewayError> {
|
||||
list_admin_monitoring_cache_affinity_records_matching(state, None).await
|
||||
}
|
||||
|
||||
pub(super) async fn list_admin_monitoring_cache_affinity_records_by_affinity_keys(
|
||||
state: &AppState,
|
||||
state: &AdminAppState<'_>,
|
||||
affinity_keys: &std::collections::BTreeSet<String>,
|
||||
) -> Result<Vec<AdminMonitoringCacheAffinityRecord>, GatewayError> {
|
||||
if affinity_keys.is_empty() {
|
||||
@@ -181,7 +192,7 @@ pub(super) async fn list_admin_monitoring_cache_affinity_records_by_affinity_key
|
||||
}
|
||||
|
||||
async fn list_admin_monitoring_cache_affinity_records_matching(
|
||||
state: &AppState,
|
||||
state: &AdminAppState<'_>,
|
||||
affinity_keys: Option<&std::collections::BTreeSet<String>>,
|
||||
) -> Result<Vec<AdminMonitoringCacheAffinityRecord>, GatewayError> {
|
||||
let mut records = Vec::new();
|
||||
@@ -272,7 +283,7 @@ async fn list_admin_monitoring_cache_affinity_records_matching(
|
||||
}
|
||||
|
||||
pub(super) async fn build_admin_monitoring_cache_snapshot(
|
||||
state: &AppState,
|
||||
state: &AdminAppState<'_>,
|
||||
) -> Result<AdminMonitoringCacheSnapshot, GatewayError> {
|
||||
let scheduling_mode = state
|
||||
.read_system_config_json_value("scheduling_mode")
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
use crate::control::GatewayPublicRequestContext;
|
||||
use crate::{AppState, GatewayError};
|
||||
use crate::handlers::admin::request::{AdminAppState, AdminRequestContext};
|
||||
use crate::GatewayError;
|
||||
use axum::{body::Body, response::Response};
|
||||
|
||||
mod activity;
|
||||
@@ -15,7 +15,6 @@ mod cache_route_helpers;
|
||||
mod cache_store;
|
||||
mod cache_types;
|
||||
mod resilience;
|
||||
mod responses;
|
||||
mod route_filters;
|
||||
mod routes;
|
||||
#[cfg(test)]
|
||||
@@ -24,8 +23,8 @@ mod trace;
|
||||
mod usage_helpers;
|
||||
|
||||
pub(crate) async fn maybe_build_local_admin_monitoring_response(
|
||||
state: &AppState,
|
||||
request_context: &GatewayPublicRequestContext,
|
||||
state: &AdminAppState<'_>,
|
||||
request_context: &AdminRequestContext<'_>,
|
||||
) -> Result<Option<Response<Body>>, GatewayError> {
|
||||
routes::maybe_build_local_admin_monitoring_response(state, request_context).await
|
||||
}
|
||||
|
||||
@@ -1,488 +0,0 @@
|
||||
use super::responses::admin_monitoring_bad_request_response;
|
||||
use super::usage_helpers::admin_monitoring_usage_is_error;
|
||||
use crate::control::GatewayPublicRequestContext;
|
||||
use crate::handlers::admin::shared::{
|
||||
provider_key_health_summary, query_param_value, unix_secs_to_rfc3339,
|
||||
};
|
||||
use crate::{AppState, GatewayError};
|
||||
use aether_data_contracts::repository::{
|
||||
provider_catalog::StoredProviderCatalogKey, usage::UsageAuditListQuery,
|
||||
};
|
||||
use axum::{
|
||||
body::Body,
|
||||
response::{IntoResponse, Response},
|
||||
Json,
|
||||
};
|
||||
use serde_json::json;
|
||||
use std::collections::BTreeMap;
|
||||
|
||||
struct AdminMonitoringResilienceSnapshot {
|
||||
timestamp: chrono::DateTime<chrono::Utc>,
|
||||
health_score: i64,
|
||||
status: &'static str,
|
||||
error_statistics: serde_json::Value,
|
||||
recent_errors: Vec<serde_json::Value>,
|
||||
recommendations: Vec<String>,
|
||||
previous_stats: serde_json::Value,
|
||||
}
|
||||
|
||||
fn build_admin_monitoring_resilience_recommendations(
|
||||
total_errors: usize,
|
||||
health_score: i64,
|
||||
open_breaker_labels: &[String],
|
||||
) -> Vec<String> {
|
||||
let mut recommendations = Vec::new();
|
||||
if health_score < 50 {
|
||||
recommendations.push("系统健康状况严重,请立即检查错误日志".to_string());
|
||||
}
|
||||
if total_errors > 100 {
|
||||
recommendations.push("错误频率过高,建议检查系统配置和外部依赖".to_string());
|
||||
}
|
||||
if !open_breaker_labels.is_empty() {
|
||||
recommendations.push(format!(
|
||||
"以下服务熔断器已打开:{}",
|
||||
open_breaker_labels.join(", ")
|
||||
));
|
||||
}
|
||||
if health_score > 90 {
|
||||
recommendations.push("系统运行良好".to_string());
|
||||
}
|
||||
recommendations
|
||||
}
|
||||
|
||||
fn parse_admin_monitoring_circuit_history_limit(query: Option<&str>) -> Result<usize, String> {
|
||||
match query_param_value(query, "limit") {
|
||||
None => Ok(50),
|
||||
Some(value) => {
|
||||
let parsed = value
|
||||
.parse::<usize>()
|
||||
.map_err(|_| "limit must be an integer between 1 and 200".to_string())?;
|
||||
if (1..=200).contains(&parsed) {
|
||||
Ok(parsed)
|
||||
} else {
|
||||
Err("limit must be an integer between 1 and 200".to_string())
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn build_admin_monitoring_circuit_history_items(
|
||||
keys: &[StoredProviderCatalogKey],
|
||||
provider_name_by_id: &BTreeMap<String, String>,
|
||||
limit: usize,
|
||||
) -> Vec<serde_json::Value> {
|
||||
let mut items = Vec::new();
|
||||
|
||||
for key in keys {
|
||||
let health_by_format = key
|
||||
.health_by_format
|
||||
.as_ref()
|
||||
.and_then(serde_json::Value::as_object)
|
||||
.cloned()
|
||||
.unwrap_or_default();
|
||||
let circuit_by_format = key
|
||||
.circuit_breaker_by_format
|
||||
.as_ref()
|
||||
.and_then(serde_json::Value::as_object)
|
||||
.cloned()
|
||||
.unwrap_or_default();
|
||||
|
||||
for (api_format, circuit_value) in circuit_by_format {
|
||||
let Some(circuit) = circuit_value.as_object() else {
|
||||
continue;
|
||||
};
|
||||
let is_open = circuit
|
||||
.get("open")
|
||||
.and_then(serde_json::Value::as_bool)
|
||||
.unwrap_or(false);
|
||||
let is_half_open = circuit
|
||||
.get("half_open_until")
|
||||
.and_then(serde_json::Value::as_str)
|
||||
.is_some();
|
||||
|
||||
if !is_open && !is_half_open {
|
||||
continue;
|
||||
}
|
||||
|
||||
let health = health_by_format
|
||||
.get(&api_format)
|
||||
.and_then(serde_json::Value::as_object);
|
||||
let timestamp = circuit
|
||||
.get("open_at")
|
||||
.and_then(serde_json::Value::as_str)
|
||||
.or_else(|| {
|
||||
circuit
|
||||
.get("half_open_until")
|
||||
.and_then(serde_json::Value::as_str)
|
||||
})
|
||||
.or_else(|| {
|
||||
health.and_then(|value| {
|
||||
value
|
||||
.get("last_failure_at")
|
||||
.and_then(serde_json::Value::as_str)
|
||||
})
|
||||
})
|
||||
.map(ToOwned::to_owned);
|
||||
let event = if is_half_open { "half_open" } else { "opened" };
|
||||
let reason = circuit
|
||||
.get("reason")
|
||||
.and_then(serde_json::Value::as_str)
|
||||
.map(ToOwned::to_owned)
|
||||
.or_else(|| {
|
||||
health
|
||||
.and_then(|value| {
|
||||
value
|
||||
.get("consecutive_failures")
|
||||
.and_then(serde_json::Value::as_i64)
|
||||
})
|
||||
.filter(|value| *value > 0)
|
||||
.map(|value| format!("连续失败 {value} 次"))
|
||||
})
|
||||
.or_else(|| {
|
||||
Some(if is_half_open {
|
||||
"熔断器处于半开状态".to_string()
|
||||
} else {
|
||||
"熔断器处于打开状态".to_string()
|
||||
})
|
||||
});
|
||||
let recovery_seconds = circuit
|
||||
.get("recovery_seconds")
|
||||
.and_then(serde_json::Value::as_i64)
|
||||
.or_else(|| {
|
||||
let open_at = circuit
|
||||
.get("open_at")
|
||||
.and_then(serde_json::Value::as_str)
|
||||
.and_then(|value| chrono::DateTime::parse_from_rfc3339(value).ok());
|
||||
let next_probe_at = circuit
|
||||
.get("next_probe_at")
|
||||
.and_then(serde_json::Value::as_str)
|
||||
.and_then(|value| chrono::DateTime::parse_from_rfc3339(value).ok());
|
||||
match (open_at, next_probe_at) {
|
||||
(Some(open_at), Some(next_probe_at)) => {
|
||||
Some((next_probe_at - open_at).num_seconds().max(0))
|
||||
}
|
||||
_ => None,
|
||||
}
|
||||
});
|
||||
|
||||
items.push(json!({
|
||||
"event": event,
|
||||
"key_id": key.id,
|
||||
"provider_id": key.provider_id,
|
||||
"provider_name": provider_name_by_id.get(&key.provider_id).cloned(),
|
||||
"key_name": key.name,
|
||||
"api_format": api_format,
|
||||
"reason": reason,
|
||||
"recovery_seconds": recovery_seconds,
|
||||
"timestamp": timestamp,
|
||||
}));
|
||||
}
|
||||
}
|
||||
|
||||
items.sort_by(|left, right| {
|
||||
let left_ts = left
|
||||
.get("timestamp")
|
||||
.and_then(serde_json::Value::as_str)
|
||||
.unwrap_or_default();
|
||||
let right_ts = right
|
||||
.get("timestamp")
|
||||
.and_then(serde_json::Value::as_str)
|
||||
.unwrap_or_default();
|
||||
right_ts.cmp(left_ts)
|
||||
});
|
||||
items.truncate(limit);
|
||||
items
|
||||
}
|
||||
|
||||
pub(super) async fn build_admin_monitoring_resilience_circuit_history_response(
|
||||
state: &AppState,
|
||||
request_context: &GatewayPublicRequestContext,
|
||||
) -> Result<Response<Body>, GatewayError> {
|
||||
let limit = match parse_admin_monitoring_circuit_history_limit(
|
||||
request_context.request_query_string.as_deref(),
|
||||
) {
|
||||
Ok(value) => value,
|
||||
Err(detail) => return Ok(admin_monitoring_bad_request_response(detail)),
|
||||
};
|
||||
|
||||
let providers = state.list_provider_catalog_providers(false).await?;
|
||||
let provider_ids = providers
|
||||
.iter()
|
||||
.map(|item| item.id.clone())
|
||||
.collect::<Vec<_>>();
|
||||
let provider_name_by_id = providers
|
||||
.iter()
|
||||
.map(|item| (item.id.clone(), item.name.clone()))
|
||||
.collect::<BTreeMap<_, _>>();
|
||||
let keys = if provider_ids.is_empty() {
|
||||
Vec::new()
|
||||
} else {
|
||||
state
|
||||
.list_provider_catalog_keys_by_provider_ids(&provider_ids)
|
||||
.await?
|
||||
};
|
||||
|
||||
let items = build_admin_monitoring_circuit_history_items(&keys, &provider_name_by_id, limit);
|
||||
let count = items.len();
|
||||
Ok(Json(json!({
|
||||
"items": items,
|
||||
"count": count,
|
||||
}))
|
||||
.into_response())
|
||||
}
|
||||
|
||||
pub(super) async fn build_admin_monitoring_resilience_status_response(
|
||||
state: &AppState,
|
||||
) -> Result<Response<Body>, GatewayError> {
|
||||
let snapshot = build_admin_monitoring_resilience_snapshot(state).await?;
|
||||
|
||||
Ok(Json(json!({
|
||||
"timestamp": snapshot.timestamp.to_rfc3339(),
|
||||
"health_score": snapshot.health_score,
|
||||
"status": snapshot.status,
|
||||
"error_statistics": snapshot.error_statistics,
|
||||
"recent_errors": snapshot.recent_errors,
|
||||
"recommendations": snapshot.recommendations,
|
||||
}))
|
||||
.into_response())
|
||||
}
|
||||
|
||||
async fn build_admin_monitoring_resilience_snapshot(
|
||||
state: &AppState,
|
||||
) -> Result<AdminMonitoringResilienceSnapshot, GatewayError> {
|
||||
let now = chrono::Utc::now();
|
||||
let recent_error_from = std::cmp::max(
|
||||
now - chrono::Duration::hours(24),
|
||||
chrono::DateTime::<chrono::Utc>::from_timestamp(
|
||||
state
|
||||
.admin_monitoring_error_stats_reset_at()
|
||||
.unwrap_or_default() as i64,
|
||||
0,
|
||||
)
|
||||
.unwrap_or_else(|| {
|
||||
chrono::DateTime::<chrono::Utc>::from_timestamp(0, 0).expect("unix epoch should exist")
|
||||
}),
|
||||
);
|
||||
|
||||
let providers = state.list_provider_catalog_providers(false).await?;
|
||||
let provider_ids = providers
|
||||
.iter()
|
||||
.map(|item| item.id.clone())
|
||||
.collect::<Vec<_>>();
|
||||
let provider_name_by_id = providers
|
||||
.iter()
|
||||
.map(|item| (item.id.clone(), item.name.clone()))
|
||||
.collect::<BTreeMap<_, _>>();
|
||||
let keys = if provider_ids.is_empty() {
|
||||
Vec::new()
|
||||
} else {
|
||||
state
|
||||
.list_provider_catalog_keys_by_provider_ids(&provider_ids)
|
||||
.await?
|
||||
};
|
||||
|
||||
let active_keys = keys.iter().filter(|item| item.is_active).count();
|
||||
let mut degraded_keys = 0usize;
|
||||
let mut unhealthy_keys = 0usize;
|
||||
let mut open_circuit_breakers = 0usize;
|
||||
let mut open_breaker_labels = Vec::new();
|
||||
let mut circuit_breakers = serde_json::Map::new();
|
||||
let mut previous_circuit_breakers = serde_json::Map::new();
|
||||
|
||||
for key in &keys {
|
||||
let (
|
||||
health_score,
|
||||
consecutive_failures,
|
||||
last_failure_at,
|
||||
circuit_breaker_open,
|
||||
circuit_by_format,
|
||||
) = provider_key_health_summary(key);
|
||||
if health_score < 0.8 {
|
||||
degraded_keys += 1;
|
||||
}
|
||||
if health_score < 0.5 {
|
||||
unhealthy_keys += 1;
|
||||
}
|
||||
|
||||
let open_formats = circuit_by_format
|
||||
.iter()
|
||||
.filter_map(|(api_format, value)| {
|
||||
value
|
||||
.get("open")
|
||||
.and_then(serde_json::Value::as_bool)
|
||||
.filter(|open| *open)
|
||||
.map(|_| api_format.clone())
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
if circuit_breaker_open {
|
||||
open_circuit_breakers += 1;
|
||||
let provider_label = provider_name_by_id
|
||||
.get(&key.provider_id)
|
||||
.cloned()
|
||||
.unwrap_or_else(|| key.provider_id.clone());
|
||||
open_breaker_labels.push(format!("{provider_label}/{}", key.name));
|
||||
}
|
||||
|
||||
if circuit_breaker_open || consecutive_failures > 0 || health_score < 1.0 {
|
||||
circuit_breakers.insert(
|
||||
key.id.clone(),
|
||||
json!({
|
||||
"state": if circuit_breaker_open { "open" } else { "closed" },
|
||||
"provider_id": key.provider_id,
|
||||
"provider_name": provider_name_by_id.get(&key.provider_id).cloned(),
|
||||
"key_name": key.name,
|
||||
"health_score": health_score,
|
||||
"consecutive_failures": consecutive_failures,
|
||||
"last_failure_at": last_failure_at,
|
||||
"open_formats": open_formats,
|
||||
}),
|
||||
);
|
||||
previous_circuit_breakers.insert(
|
||||
key.id.clone(),
|
||||
json!({
|
||||
"state": if circuit_breaker_open { "open" } else { "closed" },
|
||||
"failure_count": consecutive_failures,
|
||||
}),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
let mut recent_usage_errors = state
|
||||
.list_usage_audits(&UsageAuditListQuery {
|
||||
created_from_unix_secs: Some(recent_error_from.timestamp().max(0) as u64),
|
||||
..Default::default()
|
||||
})
|
||||
.await?
|
||||
.into_iter()
|
||||
.filter(admin_monitoring_usage_is_error)
|
||||
.collect::<Vec<_>>();
|
||||
recent_usage_errors
|
||||
.sort_by(|left, right| right.created_at_unix_secs.cmp(&left.created_at_unix_secs));
|
||||
|
||||
let total_errors = recent_usage_errors.len();
|
||||
let mut error_breakdown = std::collections::BTreeMap::<String, usize>::new();
|
||||
for item in &recent_usage_errors {
|
||||
let error_type = item
|
||||
.error_category
|
||||
.clone()
|
||||
.unwrap_or_else(|| item.status.clone());
|
||||
let operation = format!(
|
||||
"{}:{}",
|
||||
item.provider_name,
|
||||
item.api_format
|
||||
.clone()
|
||||
.unwrap_or_else(|| item.model.clone())
|
||||
);
|
||||
*error_breakdown
|
||||
.entry(format!("{error_type}:{operation}"))
|
||||
.or_default() += 1;
|
||||
}
|
||||
let recent_errors = recent_usage_errors
|
||||
.iter()
|
||||
.take(10)
|
||||
.map(|item| {
|
||||
let error_type = item
|
||||
.error_category
|
||||
.clone()
|
||||
.unwrap_or_else(|| item.status.clone());
|
||||
let operation = format!(
|
||||
"{}:{}",
|
||||
item.provider_name,
|
||||
item.api_format
|
||||
.clone()
|
||||
.unwrap_or_else(|| item.model.clone())
|
||||
);
|
||||
json!({
|
||||
"error_id": item.id,
|
||||
"error_type": error_type,
|
||||
"operation": operation,
|
||||
"timestamp": unix_secs_to_rfc3339(item.created_at_unix_secs),
|
||||
"context": {
|
||||
"request_id": item.request_id,
|
||||
"provider_id": item.provider_id,
|
||||
"provider_name": item.provider_name,
|
||||
"model": item.model,
|
||||
"api_format": item.api_format,
|
||||
"status_code": item.status_code,
|
||||
"error_message": item.error_message,
|
||||
}
|
||||
})
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
let health_score = (100_i64
|
||||
- i64::try_from(total_errors)
|
||||
.unwrap_or(i64::MAX)
|
||||
.saturating_mul(2)
|
||||
- i64::try_from(open_circuit_breakers)
|
||||
.unwrap_or(i64::MAX)
|
||||
.saturating_mul(20))
|
||||
.clamp(0, 100);
|
||||
let status = if health_score > 80 {
|
||||
"healthy"
|
||||
} else if health_score > 50 {
|
||||
"degraded"
|
||||
} else {
|
||||
"critical"
|
||||
};
|
||||
let recommendations = build_admin_monitoring_resilience_recommendations(
|
||||
total_errors,
|
||||
health_score,
|
||||
&open_breaker_labels,
|
||||
);
|
||||
|
||||
Ok(AdminMonitoringResilienceSnapshot {
|
||||
timestamp: now,
|
||||
health_score,
|
||||
status,
|
||||
error_statistics: json!({
|
||||
"total_errors": total_errors,
|
||||
"active_keys": active_keys,
|
||||
"degraded_keys": degraded_keys,
|
||||
"unhealthy_keys": unhealthy_keys,
|
||||
"open_circuit_breakers": open_circuit_breakers,
|
||||
"circuit_breakers": circuit_breakers,
|
||||
}),
|
||||
recent_errors,
|
||||
recommendations,
|
||||
previous_stats: json!({
|
||||
"total_errors": total_errors,
|
||||
"error_breakdown": error_breakdown,
|
||||
"recent_errors": total_errors,
|
||||
"circuit_breakers": previous_circuit_breakers,
|
||||
}),
|
||||
})
|
||||
}
|
||||
|
||||
pub(super) async fn build_admin_monitoring_reset_error_stats_response(
|
||||
state: &AppState,
|
||||
request_context: &GatewayPublicRequestContext,
|
||||
) -> Result<Response<Body>, GatewayError> {
|
||||
let snapshot = build_admin_monitoring_resilience_snapshot(state).await?;
|
||||
let reset_at = chrono::Utc::now();
|
||||
state.mark_admin_monitoring_error_stats_reset(reset_at.timestamp().max(0) as u64);
|
||||
|
||||
let reset_by = if let Some(user_id) = request_context
|
||||
.control_decision
|
||||
.as_ref()
|
||||
.and_then(|decision| decision.admin_principal.as_ref())
|
||||
.map(|principal| principal.user_id.clone())
|
||||
{
|
||||
state
|
||||
.find_user_auth_by_id(&user_id)
|
||||
.await?
|
||||
.and_then(|user| user.email.or(Some(user.username)))
|
||||
.or(Some(user_id))
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
Ok(Json(json!({
|
||||
"message": "错误统计已重置",
|
||||
"previous_stats": snapshot.previous_stats,
|
||||
"reset_by": reset_by,
|
||||
"reset_at": reset_at.to_rfc3339(),
|
||||
}))
|
||||
.into_response())
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
use super::snapshot::build_admin_monitoring_provider_name_by_id_and_keys;
|
||||
use crate::handlers::admin::request::{AdminAppState, AdminRequestContext};
|
||||
use crate::GatewayError;
|
||||
use aether_admin::observability::monitoring::{
|
||||
admin_monitoring_bad_request_response, build_admin_monitoring_circuit_history_items,
|
||||
build_admin_monitoring_circuit_history_payload_response,
|
||||
parse_admin_monitoring_circuit_history_limit,
|
||||
};
|
||||
use axum::{body::Body, response::Response};
|
||||
|
||||
pub(in super::super) async fn build_admin_monitoring_resilience_circuit_history_response(
|
||||
state: &AdminAppState<'_>,
|
||||
request_context: &AdminRequestContext<'_>,
|
||||
) -> Result<Response<Body>, GatewayError> {
|
||||
let limit = match parse_admin_monitoring_circuit_history_limit(
|
||||
request_context.request_query_string.as_deref(),
|
||||
) {
|
||||
Ok(value) => value,
|
||||
Err(detail) => return Ok(admin_monitoring_bad_request_response(detail)),
|
||||
};
|
||||
|
||||
let (provider_name_by_id, keys) =
|
||||
build_admin_monitoring_provider_name_by_id_and_keys(state).await?;
|
||||
let items = build_admin_monitoring_circuit_history_items(&keys, &provider_name_by_id, limit);
|
||||
Ok(build_admin_monitoring_circuit_history_payload_response(
|
||||
items,
|
||||
))
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
mod history;
|
||||
mod reset;
|
||||
mod snapshot;
|
||||
mod status;
|
||||
|
||||
pub(super) use history::build_admin_monitoring_resilience_circuit_history_response;
|
||||
pub(super) use reset::build_admin_monitoring_reset_error_stats_response;
|
||||
pub(super) use status::build_admin_monitoring_resilience_status_response;
|
||||
@@ -0,0 +1,35 @@
|
||||
use super::snapshot::build_admin_monitoring_resilience_snapshot;
|
||||
use crate::handlers::admin::request::{AdminAppState, AdminRequestContext};
|
||||
use crate::GatewayError;
|
||||
use aether_admin::observability::monitoring::build_admin_monitoring_reset_error_stats_payload_response;
|
||||
use axum::{body::Body, response::Response};
|
||||
|
||||
pub(in super::super) async fn build_admin_monitoring_reset_error_stats_response(
|
||||
state: &AdminAppState<'_>,
|
||||
request_context: &AdminRequestContext<'_>,
|
||||
) -> Result<Response<Body>, GatewayError> {
|
||||
let snapshot = build_admin_monitoring_resilience_snapshot(state).await?;
|
||||
let reset_at = chrono::Utc::now();
|
||||
state.mark_admin_monitoring_error_stats_reset(reset_at.timestamp().max(0) as u64);
|
||||
|
||||
let reset_by = if let Some(user_id) = request_context
|
||||
.control_decision
|
||||
.as_ref()
|
||||
.and_then(|decision| decision.admin_principal.as_ref())
|
||||
.map(|principal| principal.user_id.clone())
|
||||
{
|
||||
state
|
||||
.find_user_auth_by_id(&user_id)
|
||||
.await?
|
||||
.and_then(|user| user.email.or(Some(user.username)))
|
||||
.or(Some(user_id))
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
Ok(build_admin_monitoring_reset_error_stats_payload_response(
|
||||
snapshot.previous_stats,
|
||||
reset_by,
|
||||
reset_at,
|
||||
))
|
||||
}
|
||||
+259
@@ -0,0 +1,259 @@
|
||||
use super::super::usage_helpers::admin_monitoring_usage_is_error;
|
||||
use crate::handlers::admin::request::AdminAppState;
|
||||
use crate::handlers::admin::shared::{provider_key_health_summary, unix_secs_to_rfc3339};
|
||||
use crate::GatewayError;
|
||||
use aether_data_contracts::repository::{
|
||||
provider_catalog::StoredProviderCatalogKey, usage::UsageAuditListQuery,
|
||||
};
|
||||
use serde_json::json;
|
||||
use std::collections::BTreeMap;
|
||||
|
||||
pub(super) struct AdminMonitoringResilienceSnapshot {
|
||||
pub(super) timestamp: chrono::DateTime<chrono::Utc>,
|
||||
pub(super) health_score: i64,
|
||||
pub(super) status: &'static str,
|
||||
pub(super) error_statistics: serde_json::Value,
|
||||
pub(super) recent_errors: Vec<serde_json::Value>,
|
||||
pub(super) recommendations: Vec<String>,
|
||||
pub(super) previous_stats: serde_json::Value,
|
||||
}
|
||||
|
||||
fn build_admin_monitoring_resilience_recommendations(
|
||||
total_errors: usize,
|
||||
health_score: i64,
|
||||
open_breaker_labels: &[String],
|
||||
) -> Vec<String> {
|
||||
let mut recommendations = Vec::new();
|
||||
if health_score < 50 {
|
||||
recommendations.push("系统健康状况严重,请立即检查错误日志".to_string());
|
||||
}
|
||||
if total_errors > 100 {
|
||||
recommendations.push("错误频率过高,建议检查系统配置和外部依赖".to_string());
|
||||
}
|
||||
if !open_breaker_labels.is_empty() {
|
||||
recommendations.push(format!(
|
||||
"以下服务熔断器已打开:{}",
|
||||
open_breaker_labels.join(", ")
|
||||
));
|
||||
}
|
||||
if health_score > 90 {
|
||||
recommendations.push("系统运行良好".to_string());
|
||||
}
|
||||
recommendations
|
||||
}
|
||||
|
||||
pub(super) async fn build_admin_monitoring_provider_name_by_id_and_keys(
|
||||
state: &AdminAppState<'_>,
|
||||
) -> Result<(BTreeMap<String, String>, Vec<StoredProviderCatalogKey>), GatewayError> {
|
||||
let providers = state.list_provider_catalog_providers(false).await?;
|
||||
let provider_ids = providers
|
||||
.iter()
|
||||
.map(|item| item.id.clone())
|
||||
.collect::<Vec<_>>();
|
||||
let provider_name_by_id = providers
|
||||
.iter()
|
||||
.map(|item| (item.id.clone(), item.name.clone()))
|
||||
.collect::<BTreeMap<_, _>>();
|
||||
let keys = if provider_ids.is_empty() {
|
||||
Vec::new()
|
||||
} else {
|
||||
state
|
||||
.list_provider_catalog_keys_by_provider_ids(&provider_ids)
|
||||
.await?
|
||||
};
|
||||
Ok((provider_name_by_id, keys))
|
||||
}
|
||||
|
||||
pub(super) async fn build_admin_monitoring_resilience_snapshot(
|
||||
state: &AdminAppState<'_>,
|
||||
) -> Result<AdminMonitoringResilienceSnapshot, GatewayError> {
|
||||
let now = chrono::Utc::now();
|
||||
let recent_error_from = std::cmp::max(
|
||||
now - chrono::Duration::hours(24),
|
||||
chrono::DateTime::<chrono::Utc>::from_timestamp(
|
||||
state
|
||||
.admin_monitoring_error_stats_reset_at()
|
||||
.unwrap_or_default() as i64,
|
||||
0,
|
||||
)
|
||||
.unwrap_or_else(|| {
|
||||
chrono::DateTime::<chrono::Utc>::from_timestamp(0, 0).expect("unix epoch should exist")
|
||||
}),
|
||||
);
|
||||
|
||||
let (provider_name_by_id, keys) =
|
||||
build_admin_monitoring_provider_name_by_id_and_keys(state).await?;
|
||||
|
||||
let active_keys = keys.iter().filter(|item| item.is_active).count();
|
||||
let mut degraded_keys = 0usize;
|
||||
let mut unhealthy_keys = 0usize;
|
||||
let mut open_circuit_breakers = 0usize;
|
||||
let mut open_breaker_labels = Vec::new();
|
||||
let mut circuit_breakers = serde_json::Map::new();
|
||||
let mut previous_circuit_breakers = serde_json::Map::new();
|
||||
|
||||
for key in &keys {
|
||||
let (
|
||||
health_score,
|
||||
consecutive_failures,
|
||||
last_failure_at,
|
||||
circuit_breaker_open,
|
||||
circuit_by_format,
|
||||
) = provider_key_health_summary(key);
|
||||
if health_score < 0.8 {
|
||||
degraded_keys += 1;
|
||||
}
|
||||
if health_score < 0.5 {
|
||||
unhealthy_keys += 1;
|
||||
}
|
||||
|
||||
let open_formats = circuit_by_format
|
||||
.iter()
|
||||
.filter_map(|(api_format, value)| {
|
||||
value
|
||||
.get("open")
|
||||
.and_then(serde_json::Value::as_bool)
|
||||
.filter(|open| *open)
|
||||
.map(|_| api_format.clone())
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
if circuit_breaker_open {
|
||||
open_circuit_breakers += 1;
|
||||
let provider_label = provider_name_by_id
|
||||
.get(&key.provider_id)
|
||||
.cloned()
|
||||
.unwrap_or_else(|| key.provider_id.clone());
|
||||
open_breaker_labels.push(format!("{provider_label}/{}", key.name));
|
||||
}
|
||||
|
||||
if circuit_breaker_open || consecutive_failures > 0 || health_score < 1.0 {
|
||||
circuit_breakers.insert(
|
||||
key.id.clone(),
|
||||
json!({
|
||||
"state": if circuit_breaker_open { "open" } else { "closed" },
|
||||
"provider_id": key.provider_id,
|
||||
"provider_name": provider_name_by_id.get(&key.provider_id).cloned(),
|
||||
"key_name": key.name,
|
||||
"health_score": health_score,
|
||||
"consecutive_failures": consecutive_failures,
|
||||
"last_failure_at": last_failure_at,
|
||||
"open_formats": open_formats,
|
||||
}),
|
||||
);
|
||||
previous_circuit_breakers.insert(
|
||||
key.id.clone(),
|
||||
json!({
|
||||
"state": if circuit_breaker_open { "open" } else { "closed" },
|
||||
"failure_count": consecutive_failures,
|
||||
}),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
let mut recent_usage_errors = state
|
||||
.list_usage_audits(&UsageAuditListQuery {
|
||||
created_from_unix_secs: Some(recent_error_from.timestamp().max(0) as u64),
|
||||
..Default::default()
|
||||
})
|
||||
.await?
|
||||
.into_iter()
|
||||
.filter(admin_monitoring_usage_is_error)
|
||||
.collect::<Vec<_>>();
|
||||
recent_usage_errors
|
||||
.sort_by(|left, right| right.created_at_unix_secs.cmp(&left.created_at_unix_secs));
|
||||
|
||||
let total_errors = recent_usage_errors.len();
|
||||
let mut error_breakdown = BTreeMap::<String, usize>::new();
|
||||
for item in &recent_usage_errors {
|
||||
let error_type = item
|
||||
.error_category
|
||||
.clone()
|
||||
.unwrap_or_else(|| item.status.clone());
|
||||
let operation = format!(
|
||||
"{}:{}",
|
||||
item.provider_name,
|
||||
item.api_format
|
||||
.clone()
|
||||
.unwrap_or_else(|| item.model.clone())
|
||||
);
|
||||
*error_breakdown
|
||||
.entry(format!("{error_type}:{operation}"))
|
||||
.or_default() += 1;
|
||||
}
|
||||
let recent_errors = recent_usage_errors
|
||||
.iter()
|
||||
.take(10)
|
||||
.map(|item| {
|
||||
let error_type = item
|
||||
.error_category
|
||||
.clone()
|
||||
.unwrap_or_else(|| item.status.clone());
|
||||
let operation = format!(
|
||||
"{}:{}",
|
||||
item.provider_name,
|
||||
item.api_format
|
||||
.clone()
|
||||
.unwrap_or_else(|| item.model.clone())
|
||||
);
|
||||
json!({
|
||||
"error_id": item.id,
|
||||
"error_type": error_type,
|
||||
"operation": operation,
|
||||
"timestamp": unix_secs_to_rfc3339(item.created_at_unix_secs),
|
||||
"context": {
|
||||
"request_id": item.request_id,
|
||||
"provider_id": item.provider_id,
|
||||
"provider_name": item.provider_name,
|
||||
"model": item.model,
|
||||
"api_format": item.api_format,
|
||||
"status_code": item.status_code,
|
||||
"error_message": item.error_message,
|
||||
}
|
||||
})
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
let health_score = (100_i64
|
||||
- i64::try_from(total_errors)
|
||||
.unwrap_or(i64::MAX)
|
||||
.saturating_mul(2)
|
||||
- i64::try_from(open_circuit_breakers)
|
||||
.unwrap_or(i64::MAX)
|
||||
.saturating_mul(20))
|
||||
.clamp(0, 100);
|
||||
let status = if health_score > 80 {
|
||||
"healthy"
|
||||
} else if health_score > 50 {
|
||||
"degraded"
|
||||
} else {
|
||||
"critical"
|
||||
};
|
||||
let recommendations = build_admin_monitoring_resilience_recommendations(
|
||||
total_errors,
|
||||
health_score,
|
||||
&open_breaker_labels,
|
||||
);
|
||||
|
||||
Ok(AdminMonitoringResilienceSnapshot {
|
||||
timestamp: now,
|
||||
health_score,
|
||||
status,
|
||||
error_statistics: json!({
|
||||
"total_errors": total_errors,
|
||||
"active_keys": active_keys,
|
||||
"degraded_keys": degraded_keys,
|
||||
"unhealthy_keys": unhealthy_keys,
|
||||
"open_circuit_breakers": open_circuit_breakers,
|
||||
"circuit_breakers": circuit_breakers,
|
||||
}),
|
||||
recent_errors,
|
||||
recommendations,
|
||||
previous_stats: json!({
|
||||
"total_errors": total_errors,
|
||||
"error_breakdown": error_breakdown,
|
||||
"recent_errors": total_errors,
|
||||
"circuit_breakers": previous_circuit_breakers,
|
||||
}),
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
use super::snapshot::build_admin_monitoring_resilience_snapshot;
|
||||
use crate::handlers::admin::request::AdminAppState;
|
||||
use crate::GatewayError;
|
||||
use aether_admin::observability::monitoring::build_admin_monitoring_resilience_status_payload_response;
|
||||
use axum::{body::Body, response::Response};
|
||||
|
||||
pub(in super::super) async fn build_admin_monitoring_resilience_status_response(
|
||||
state: &AdminAppState<'_>,
|
||||
) -> Result<Response<Body>, GatewayError> {
|
||||
let snapshot = build_admin_monitoring_resilience_snapshot(state).await?;
|
||||
|
||||
Ok(build_admin_monitoring_resilience_status_payload_response(
|
||||
snapshot.timestamp,
|
||||
snapshot.health_score,
|
||||
snapshot.status,
|
||||
snapshot.error_statistics,
|
||||
snapshot.recent_errors,
|
||||
snapshot.recommendations,
|
||||
))
|
||||
}
|
||||
@@ -1,23 +0,0 @@
|
||||
use axum::{
|
||||
body::Body,
|
||||
http,
|
||||
response::{IntoResponse, Response},
|
||||
Json,
|
||||
};
|
||||
use serde_json::json;
|
||||
|
||||
pub(super) fn admin_monitoring_bad_request_response(detail: impl Into<String>) -> Response<Body> {
|
||||
(
|
||||
http::StatusCode::BAD_REQUEST,
|
||||
Json(json!({ "detail": detail.into() })),
|
||||
)
|
||||
.into_response()
|
||||
}
|
||||
|
||||
pub(super) fn admin_monitoring_not_found_response(detail: &'static str) -> Response<Body> {
|
||||
(
|
||||
http::StatusCode::NOT_FOUND,
|
||||
Json(json!({ "detail": detail })),
|
||||
)
|
||||
.into_response()
|
||||
}
|
||||
@@ -34,42 +34,15 @@ use super::trace::{
|
||||
build_admin_monitoring_trace_provider_stats_response,
|
||||
build_admin_monitoring_trace_request_response,
|
||||
};
|
||||
use crate::control::GatewayPublicRequestContext;
|
||||
use crate::handlers::admin::request::{AdminAppState, AdminRequestContext};
|
||||
use crate::handlers::admin::shared::attach_admin_audit_response;
|
||||
use crate::{AppState, GatewayError};
|
||||
use crate::GatewayError;
|
||||
use aether_admin::observability::monitoring::{match_admin_monitoring_route, AdminMonitoringRoute};
|
||||
use axum::{body::Body, http, response::Response};
|
||||
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
||||
pub(crate) enum AdminMonitoringRoute {
|
||||
AuditLogs,
|
||||
SystemStatus,
|
||||
SuspiciousActivities,
|
||||
UserBehavior,
|
||||
ResilienceStatus,
|
||||
ResilienceErrorStats,
|
||||
ResilienceCircuitHistory,
|
||||
TraceRequest,
|
||||
TraceProviderStats,
|
||||
CacheStats,
|
||||
CacheAffinity,
|
||||
CacheAffinities,
|
||||
CacheUsersDelete,
|
||||
CacheAffinityDelete,
|
||||
CacheFlush,
|
||||
CacheProviderDelete,
|
||||
CacheConfig,
|
||||
CacheMetrics,
|
||||
CacheModelMappingStats,
|
||||
CacheModelMappingDelete,
|
||||
CacheModelMappingDeleteModel,
|
||||
CacheModelMappingDeleteProvider,
|
||||
CacheRedisKeys,
|
||||
CacheRedisKeysDelete,
|
||||
}
|
||||
|
||||
pub(crate) async fn maybe_build_local_admin_monitoring_response(
|
||||
state: &AppState,
|
||||
request_context: &GatewayPublicRequestContext,
|
||||
state: &AdminAppState<'_>,
|
||||
request_context: &AdminRequestContext<'_>,
|
||||
) -> Result<Option<Response<Body>>, GatewayError> {
|
||||
let Some(route) = match_admin_monitoring_route(
|
||||
&request_context.request_method,
|
||||
@@ -185,7 +158,7 @@ pub(crate) async fn maybe_build_local_admin_monitoring_response(
|
||||
}
|
||||
}
|
||||
|
||||
fn admin_monitoring_audit_target_id(request_context: &GatewayPublicRequestContext) -> String {
|
||||
fn admin_monitoring_audit_target_id(request_context: &AdminRequestContext<'_>) -> String {
|
||||
match request_context.request_query_string.as_deref() {
|
||||
Some(query) if !query.trim().is_empty() => {
|
||||
format!("{}?{query}", request_context.request_path)
|
||||
@@ -193,112 +166,3 @@ fn admin_monitoring_audit_target_id(request_context: &GatewayPublicRequestContex
|
||||
_ => request_context.request_path.clone(),
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn match_admin_monitoring_route(
|
||||
method: &http::Method,
|
||||
path: &str,
|
||||
) -> Option<AdminMonitoringRoute> {
|
||||
let path = normalize_admin_monitoring_path(path);
|
||||
|
||||
match *method {
|
||||
http::Method::GET => match path {
|
||||
"/api/admin/monitoring/audit-logs" => Some(AdminMonitoringRoute::AuditLogs),
|
||||
"/api/admin/monitoring/system-status" => Some(AdminMonitoringRoute::SystemStatus),
|
||||
"/api/admin/monitoring/suspicious-activities" => {
|
||||
Some(AdminMonitoringRoute::SuspiciousActivities)
|
||||
}
|
||||
"/api/admin/monitoring/resilience-status" => {
|
||||
Some(AdminMonitoringRoute::ResilienceStatus)
|
||||
}
|
||||
"/api/admin/monitoring/resilience/circuit-history" => {
|
||||
Some(AdminMonitoringRoute::ResilienceCircuitHistory)
|
||||
}
|
||||
"/api/admin/monitoring/cache/stats" => Some(AdminMonitoringRoute::CacheStats),
|
||||
"/api/admin/monitoring/cache/affinities" => Some(AdminMonitoringRoute::CacheAffinities),
|
||||
"/api/admin/monitoring/cache/config" => Some(AdminMonitoringRoute::CacheConfig),
|
||||
"/api/admin/monitoring/cache/metrics" => Some(AdminMonitoringRoute::CacheMetrics),
|
||||
"/api/admin/monitoring/cache/model-mapping/stats" => {
|
||||
Some(AdminMonitoringRoute::CacheModelMappingStats)
|
||||
}
|
||||
"/api/admin/monitoring/cache/redis-keys" => Some(AdminMonitoringRoute::CacheRedisKeys),
|
||||
_ if matches_dynamic_segments(path, "/api/admin/monitoring/user-behavior/", 1) => {
|
||||
Some(AdminMonitoringRoute::UserBehavior)
|
||||
}
|
||||
_ if matches_dynamic_segments(
|
||||
path,
|
||||
"/api/admin/monitoring/trace/stats/provider/",
|
||||
1,
|
||||
) =>
|
||||
{
|
||||
Some(AdminMonitoringRoute::TraceProviderStats)
|
||||
}
|
||||
_ if matches_dynamic_segments(path, "/api/admin/monitoring/trace/", 1) => {
|
||||
Some(AdminMonitoringRoute::TraceRequest)
|
||||
}
|
||||
_ if matches_dynamic_segments(path, "/api/admin/monitoring/cache/affinity/", 1) => {
|
||||
Some(AdminMonitoringRoute::CacheAffinity)
|
||||
}
|
||||
_ => None,
|
||||
},
|
||||
http::Method::DELETE => match path {
|
||||
"/api/admin/monitoring/resilience/error-stats" => {
|
||||
Some(AdminMonitoringRoute::ResilienceErrorStats)
|
||||
}
|
||||
"/api/admin/monitoring/cache" => Some(AdminMonitoringRoute::CacheFlush),
|
||||
"/api/admin/monitoring/cache/model-mapping" => {
|
||||
Some(AdminMonitoringRoute::CacheModelMappingDelete)
|
||||
}
|
||||
_ if matches_dynamic_segments(path, "/api/admin/monitoring/cache/users/", 1) => {
|
||||
Some(AdminMonitoringRoute::CacheUsersDelete)
|
||||
}
|
||||
_ if matches_dynamic_segments(path, "/api/admin/monitoring/cache/providers/", 1) => {
|
||||
Some(AdminMonitoringRoute::CacheProviderDelete)
|
||||
}
|
||||
_ if matches_dynamic_segments(path, "/api/admin/monitoring/cache/redis-keys/", 1) => {
|
||||
Some(AdminMonitoringRoute::CacheRedisKeysDelete)
|
||||
}
|
||||
_ if matches_dynamic_segments(
|
||||
path,
|
||||
"/api/admin/monitoring/cache/model-mapping/provider/",
|
||||
2,
|
||||
) =>
|
||||
{
|
||||
Some(AdminMonitoringRoute::CacheModelMappingDeleteProvider)
|
||||
}
|
||||
_ if matches_dynamic_segments(
|
||||
path,
|
||||
"/api/admin/monitoring/cache/model-mapping/",
|
||||
1,
|
||||
) =>
|
||||
{
|
||||
Some(AdminMonitoringRoute::CacheModelMappingDeleteModel)
|
||||
}
|
||||
_ if matches_dynamic_segments(path, "/api/admin/monitoring/cache/affinity/", 4) => {
|
||||
Some(AdminMonitoringRoute::CacheAffinityDelete)
|
||||
}
|
||||
_ => None,
|
||||
},
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
fn normalize_admin_monitoring_path(path: &str) -> &str {
|
||||
let normalized = path.trim_end_matches('/');
|
||||
if normalized.is_empty() {
|
||||
"/"
|
||||
} else {
|
||||
normalized
|
||||
}
|
||||
}
|
||||
|
||||
fn matches_dynamic_segments(path: &str, prefix: &str, dynamic_segments: usize) -> bool {
|
||||
let Some(suffix) = path.strip_prefix(prefix) else {
|
||||
return false;
|
||||
};
|
||||
|
||||
let segments = suffix
|
||||
.split('/')
|
||||
.filter(|segment| !segment.is_empty())
|
||||
.collect::<Vec<_>>();
|
||||
segments.len() == dynamic_segments
|
||||
}
|
||||
|
||||
@@ -1,9 +1,8 @@
|
||||
use super::super::cache_config::ADMIN_MONITORING_REDIS_REQUIRED_DETAIL;
|
||||
use super::super::routes::{
|
||||
match_admin_monitoring_route, maybe_build_local_admin_monitoring_response, AdminMonitoringRoute,
|
||||
};
|
||||
use super::super::test_support::{request_context, sample_key, sample_provider, sample_usage};
|
||||
use super::local_monitoring_response;
|
||||
use crate::AppState;
|
||||
use aether_admin::observability::monitoring::{match_admin_monitoring_route, AdminMonitoringRoute};
|
||||
use aether_data::repository::provider_catalog::InMemoryProviderCatalogReadRepository;
|
||||
use aether_data::repository::usage::InMemoryUsageReadRepository;
|
||||
use axum::body::to_bytes;
|
||||
@@ -76,7 +75,7 @@ async fn admin_monitoring_model_mapping_delete_requires_redis_without_runtime_or
|
||||
http::Method::DELETE,
|
||||
"/api/admin/monitoring/cache/model-mapping",
|
||||
);
|
||||
let response = maybe_build_local_admin_monitoring_response(&state, &context)
|
||||
let response = local_monitoring_response(&state, &context)
|
||||
.await
|
||||
.expect("handler should not error")
|
||||
.expect("monitoring route should be handled locally");
|
||||
@@ -100,7 +99,7 @@ async fn admin_monitoring_user_behavior_returns_empty_local_payload_without_post
|
||||
"/api/admin/monitoring/user-behavior/user-123?days=30",
|
||||
);
|
||||
|
||||
let response = maybe_build_local_admin_monitoring_response(&state, &context)
|
||||
let response = local_monitoring_response(&state, &context)
|
||||
.await
|
||||
.expect("handler should not error")
|
||||
.expect("user behavior route should be handled locally");
|
||||
@@ -128,7 +127,7 @@ async fn admin_monitoring_audit_logs_returns_empty_local_payload_without_postgre
|
||||
"/api/admin/monitoring/audit-logs?username=alice&event_type=login_failed&days=14&limit=20&offset=5",
|
||||
);
|
||||
|
||||
let response = maybe_build_local_admin_monitoring_response(&state, &context)
|
||||
let response = local_monitoring_response(&state, &context)
|
||||
.await
|
||||
.expect("handler should not error")
|
||||
.expect("monitoring route should be handled locally");
|
||||
@@ -156,7 +155,7 @@ async fn admin_monitoring_suspicious_activities_returns_empty_local_payload_with
|
||||
"/api/admin/monitoring/suspicious-activities?hours=48",
|
||||
);
|
||||
|
||||
let response = maybe_build_local_admin_monitoring_response(&state, &context)
|
||||
let response = local_monitoring_response(&state, &context)
|
||||
.await
|
||||
.expect("handler should not error")
|
||||
.expect("monitoring route should be handled locally");
|
||||
@@ -224,7 +223,7 @@ async fn admin_monitoring_resilience_status_returns_local_payload() {
|
||||
);
|
||||
let context = request_context(http::Method::GET, "/api/admin/monitoring/resilience-status");
|
||||
|
||||
let response = maybe_build_local_admin_monitoring_response(&state, &context)
|
||||
let response = local_monitoring_response(&state, &context)
|
||||
.await
|
||||
.expect("handler should not error")
|
||||
.expect("route should be handled locally");
|
||||
@@ -296,7 +295,7 @@ async fn admin_monitoring_cache_stats_returns_local_payload() {
|
||||
);
|
||||
let context = request_context(http::Method::GET, "/api/admin/monitoring/cache/stats");
|
||||
|
||||
let response = maybe_build_local_admin_monitoring_response(&state, &context)
|
||||
let response = local_monitoring_response(&state, &context)
|
||||
.await
|
||||
.expect("handler should not error")
|
||||
.expect("route should be handled locally");
|
||||
|
||||
@@ -1,5 +1,8 @@
|
||||
use super::maybe_build_local_admin_monitoring_response;
|
||||
use super::test_support::*;
|
||||
use super::{maybe_build_local_admin_monitoring_response, AppState};
|
||||
use crate::control::GatewayPublicRequestContext;
|
||||
use crate::handlers::admin::request::{AdminAppState, AdminRequestContext};
|
||||
use crate::AppState;
|
||||
use aether_data_contracts::repository::{
|
||||
candidates::{RequestCandidateStatus, StoredRequestCandidate},
|
||||
provider_catalog::{
|
||||
@@ -23,12 +26,23 @@ use aether_data::repository::users::{
|
||||
|
||||
mod basics;
|
||||
|
||||
pub(super) async fn local_monitoring_response(
|
||||
state: &AppState,
|
||||
context: &GatewayPublicRequestContext,
|
||||
) -> crate::handlers::admin::request::AdminRouteResult {
|
||||
maybe_build_local_admin_monitoring_response(
|
||||
&AdminAppState::new(state),
|
||||
&AdminRequestContext::new(context),
|
||||
)
|
||||
.await
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn admin_monitoring_cache_affinities_returns_empty_payload_without_runtime_or_test_entries() {
|
||||
let state = AppState::new().expect("state should build");
|
||||
let context = request_context(http::Method::GET, "/api/admin/monitoring/cache/affinities");
|
||||
|
||||
let response = maybe_build_local_admin_monitoring_response(&state, &context)
|
||||
let response = local_monitoring_response(&state, &context)
|
||||
.await
|
||||
.expect("handler should not error")
|
||||
.expect("route should be handled locally");
|
||||
@@ -67,7 +81,7 @@ async fn admin_monitoring_cache_affinity_returns_not_found_without_runtime_or_te
|
||||
"/api/admin/monitoring/cache/affinity/alice",
|
||||
);
|
||||
|
||||
let response = maybe_build_local_admin_monitoring_response(&state, &context)
|
||||
let response = local_monitoring_response(&state, &context)
|
||||
.await
|
||||
.expect("handler should not error")
|
||||
.expect("route should be handled locally");
|
||||
@@ -136,7 +150,7 @@ async fn admin_monitoring_cache_affinities_and_affinity_return_local_payload_fro
|
||||
http::Method::GET,
|
||||
"/api/admin/monitoring/cache/affinities?keyword=alice&limit=20&offset=0",
|
||||
);
|
||||
let list_response = maybe_build_local_admin_monitoring_response(&state, &list_context)
|
||||
let list_response = local_monitoring_response(&state, &list_context)
|
||||
.await
|
||||
.expect("handler should not error")
|
||||
.expect("route should be handled locally");
|
||||
@@ -172,7 +186,7 @@ async fn admin_monitoring_cache_affinities_and_affinity_return_local_payload_fro
|
||||
http::Method::GET,
|
||||
"/api/admin/monitoring/cache/affinity/alice",
|
||||
);
|
||||
let detail_response = maybe_build_local_admin_monitoring_response(&state, &detail_context)
|
||||
let detail_response = local_monitoring_response(&state, &detail_context)
|
||||
.await
|
||||
.expect("handler should not error")
|
||||
.expect("route should be handled locally");
|
||||
@@ -235,7 +249,7 @@ async fn admin_monitoring_cache_users_delete_returns_local_payload_from_test_sto
|
||||
}),
|
||||
);
|
||||
|
||||
let response = maybe_build_local_admin_monitoring_response(
|
||||
let response = local_monitoring_response(
|
||||
&state,
|
||||
&request_context(
|
||||
http::Method::DELETE,
|
||||
@@ -280,7 +294,7 @@ async fn admin_monitoring_cache_users_delete_returns_not_found_for_unknown_ident
|
||||
}),
|
||||
);
|
||||
|
||||
let response = maybe_build_local_admin_monitoring_response(
|
||||
let response = local_monitoring_response(
|
||||
&state,
|
||||
&request_context(
|
||||
http::Method::DELETE,
|
||||
@@ -329,7 +343,7 @@ async fn admin_monitoring_cache_flush_returns_local_payload_from_test_store() {
|
||||
}),
|
||||
);
|
||||
|
||||
let response = maybe_build_local_admin_monitoring_response(
|
||||
let response = local_monitoring_response(
|
||||
&state,
|
||||
&request_context(http::Method::DELETE, "/api/admin/monitoring/cache"),
|
||||
)
|
||||
@@ -377,7 +391,7 @@ async fn admin_monitoring_cache_provider_delete_returns_local_payload_from_test_
|
||||
}),
|
||||
);
|
||||
|
||||
let response = maybe_build_local_admin_monitoring_response(
|
||||
let response = local_monitoring_response(
|
||||
&state,
|
||||
&request_context(
|
||||
http::Method::DELETE,
|
||||
@@ -426,7 +440,7 @@ async fn admin_monitoring_model_mapping_delete_returns_local_payload_from_test_s
|
||||
json!({"id": "model-alpha"}),
|
||||
);
|
||||
|
||||
let response = maybe_build_local_admin_monitoring_response(
|
||||
let response = local_monitoring_response(
|
||||
&state,
|
||||
&request_context(
|
||||
http::Method::DELETE,
|
||||
@@ -467,7 +481,7 @@ async fn admin_monitoring_model_mapping_delete_model_returns_local_payload_from_
|
||||
json!({"name": "model-beta"}),
|
||||
);
|
||||
|
||||
let response = maybe_build_local_admin_monitoring_response(
|
||||
let response = local_monitoring_response(
|
||||
&state,
|
||||
&request_context(
|
||||
http::Method::DELETE,
|
||||
@@ -515,7 +529,7 @@ async fn admin_monitoring_model_mapping_delete_provider_returns_local_payload_fr
|
||||
json!({"provider_id": "provider-2"}),
|
||||
);
|
||||
|
||||
let response = maybe_build_local_admin_monitoring_response(
|
||||
let response = local_monitoring_response(
|
||||
&state,
|
||||
&request_context(
|
||||
http::Method::DELETE,
|
||||
@@ -555,7 +569,7 @@ async fn admin_monitoring_redis_keys_delete_returns_local_payload_from_test_stor
|
||||
.with_admin_monitoring_redis_key_for_tests("dashboard:stats:user-1", json!({"ok": true}))
|
||||
.with_admin_monitoring_redis_key_for_tests("user:user-1", json!({"ok": true}));
|
||||
|
||||
let response = maybe_build_local_admin_monitoring_response(
|
||||
let response = local_monitoring_response(
|
||||
&state,
|
||||
&request_context(
|
||||
http::Method::DELETE,
|
||||
@@ -621,7 +635,7 @@ async fn admin_monitoring_cache_affinity_delete_returns_local_payload_from_test_
|
||||
}),
|
||||
);
|
||||
|
||||
let response = maybe_build_local_admin_monitoring_response(
|
||||
let response = local_monitoring_response(
|
||||
&state,
|
||||
&request_context(
|
||||
http::Method::DELETE,
|
||||
@@ -665,7 +679,7 @@ async fn admin_monitoring_cache_affinity_delete_returns_not_found_for_mismatched
|
||||
}),
|
||||
);
|
||||
|
||||
let response = maybe_build_local_admin_monitoring_response(
|
||||
let response = local_monitoring_response(
|
||||
&state,
|
||||
&request_context(
|
||||
http::Method::DELETE,
|
||||
@@ -721,7 +735,7 @@ async fn admin_monitoring_cache_metrics_returns_local_payload() {
|
||||
);
|
||||
let context = request_context(http::Method::GET, "/api/admin/monitoring/cache/metrics");
|
||||
|
||||
let response = maybe_build_local_admin_monitoring_response(&state, &context)
|
||||
let response = local_monitoring_response(&state, &context)
|
||||
.await
|
||||
.expect("handler should not error")
|
||||
.expect("route should be handled locally");
|
||||
@@ -752,7 +766,7 @@ async fn admin_monitoring_cache_config_returns_local_payload() {
|
||||
let state = AppState::new().expect("state should build");
|
||||
let context = request_context(http::Method::GET, "/api/admin/monitoring/cache/config");
|
||||
|
||||
let response = maybe_build_local_admin_monitoring_response(&state, &context)
|
||||
let response = local_monitoring_response(&state, &context)
|
||||
.await
|
||||
.expect("handler should not error")
|
||||
.expect("route should be handled locally");
|
||||
@@ -791,7 +805,7 @@ async fn admin_monitoring_model_mapping_stats_returns_local_payload_without_redi
|
||||
"/api/admin/monitoring/cache/model-mapping/stats",
|
||||
);
|
||||
|
||||
let response = maybe_build_local_admin_monitoring_response(&state, &context)
|
||||
let response = local_monitoring_response(&state, &context)
|
||||
.await
|
||||
.expect("handler should not error")
|
||||
.expect("route should be handled locally");
|
||||
@@ -853,7 +867,7 @@ async fn admin_monitoring_reset_error_stats_returns_local_payload_and_clears_fut
|
||||
http::Method::DELETE,
|
||||
"/api/admin/monitoring/resilience/error-stats",
|
||||
);
|
||||
let reset_response = maybe_build_local_admin_monitoring_response(&state, &reset_context)
|
||||
let reset_response = local_monitoring_response(&state, &reset_context)
|
||||
.await
|
||||
.expect("handler should not error")
|
||||
.expect("route should be handled locally");
|
||||
@@ -875,7 +889,7 @@ async fn admin_monitoring_reset_error_stats_returns_local_payload_and_clears_fut
|
||||
|
||||
let status_context =
|
||||
request_context(http::Method::GET, "/api/admin/monitoring/resilience-status");
|
||||
let status_response = maybe_build_local_admin_monitoring_response(&state, &status_context)
|
||||
let status_response = local_monitoring_response(&state, &status_context)
|
||||
.await
|
||||
.expect("handler should not error")
|
||||
.expect("route should be handled locally");
|
||||
@@ -898,7 +912,7 @@ async fn admin_monitoring_redis_keys_returns_local_payload_without_redis() {
|
||||
let state = AppState::new().expect("state should build");
|
||||
let context = request_context(http::Method::GET, "/api/admin/monitoring/cache/redis-keys");
|
||||
|
||||
let response = maybe_build_local_admin_monitoring_response(&state, &context)
|
||||
let response = local_monitoring_response(&state, &context)
|
||||
.await
|
||||
.expect("handler should not error")
|
||||
.expect("route should be handled locally");
|
||||
@@ -921,7 +935,7 @@ async fn admin_monitoring_redis_keys_delete_returns_unavailable_without_redis()
|
||||
"/api/admin/monitoring/cache/redis-keys/upstream_models",
|
||||
);
|
||||
|
||||
let response = maybe_build_local_admin_monitoring_response(&state, &context)
|
||||
let response = local_monitoring_response(&state, &context)
|
||||
.await
|
||||
.expect("handler should not error")
|
||||
.expect("route should be handled locally");
|
||||
@@ -967,7 +981,7 @@ async fn admin_monitoring_circuit_history_returns_local_payload() {
|
||||
"/api/admin/monitoring/resilience/circuit-history?limit=10",
|
||||
);
|
||||
|
||||
let response = maybe_build_local_admin_monitoring_response(&state, &context)
|
||||
let response = local_monitoring_response(&state, &context)
|
||||
.await
|
||||
.expect("handler should not error")
|
||||
.expect("route should be handled locally");
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
use super::super::routes::maybe_build_local_admin_monitoring_response;
|
||||
use super::super::test_support::{
|
||||
request_context, sample_candidate, sample_endpoint, sample_key, sample_provider,
|
||||
};
|
||||
use super::local_monitoring_response;
|
||||
use crate::AppState;
|
||||
use aether_data_contracts::repository::candidates::RequestCandidateStatus;
|
||||
use axum::body::to_bytes;
|
||||
@@ -46,7 +46,7 @@ async fn admin_monitoring_trace_request_returns_local_payload() {
|
||||
"/api/admin/monitoring/trace/request-1?attempted_only=true",
|
||||
);
|
||||
|
||||
let response = maybe_build_local_admin_monitoring_response(&state, &context)
|
||||
let response = local_monitoring_response(&state, &context)
|
||||
.await
|
||||
.expect("handler should not error")
|
||||
.expect("route should be handled locally");
|
||||
@@ -132,7 +132,7 @@ async fn admin_monitoring_trace_provider_stats_returns_local_payload() {
|
||||
"/api/admin/monitoring/trace/stats/provider/provider-1?limit=10",
|
||||
);
|
||||
|
||||
let response = maybe_build_local_admin_monitoring_response(&state, &context)
|
||||
let response = local_monitoring_response(&state, &context)
|
||||
.await
|
||||
.expect("handler should not error")
|
||||
.expect("route should be handled locally");
|
||||
@@ -163,7 +163,7 @@ async fn admin_monitoring_trace_request_returns_contextual_not_found_payload() {
|
||||
"/api/admin/monitoring/trace/provider-test-missing?attempted_only=false",
|
||||
);
|
||||
|
||||
let response = maybe_build_local_admin_monitoring_response(&state, &context)
|
||||
let response = local_monitoring_response(&state, &context)
|
||||
.await
|
||||
.expect("handler should not error")
|
||||
.expect("route should be handled locally");
|
||||
|
||||
@@ -1,75 +1,25 @@
|
||||
use super::responses::admin_monitoring_bad_request_response;
|
||||
use super::route_filters::parse_admin_monitoring_limit;
|
||||
use crate::control::GatewayPublicRequestContext;
|
||||
use crate::handlers::admin::shared::{query_param_value, unix_secs_to_rfc3339};
|
||||
use crate::handlers::admin::request::{AdminAppState, AdminRequestContext};
|
||||
use crate::log_ids::short_request_id;
|
||||
use crate::{AppState, GatewayError};
|
||||
use crate::GatewayError;
|
||||
use aether_admin::observability::monitoring::{
|
||||
admin_monitoring_bad_request_response, admin_monitoring_trace_not_found_response,
|
||||
admin_monitoring_trace_provider_id_from_path, admin_monitoring_trace_request_id_from_path,
|
||||
build_admin_monitoring_trace_provider_stats_payload_response,
|
||||
build_admin_monitoring_trace_request_payload_response, parse_admin_monitoring_attempted_only,
|
||||
};
|
||||
use aether_data_contracts::repository::candidates::RequestCandidateStatus;
|
||||
use axum::{
|
||||
body::Body,
|
||||
http,
|
||||
response::{IntoResponse, Response},
|
||||
Json,
|
||||
};
|
||||
use serde_json::json;
|
||||
use tracing::warn;
|
||||
|
||||
fn admin_monitoring_trace_request_id_from_path(request_path: &str) -> Option<String> {
|
||||
let value = request_path
|
||||
.strip_prefix("/api/admin/monitoring/trace/")?
|
||||
.trim()
|
||||
.trim_matches('/')
|
||||
.to_string();
|
||||
if value.is_empty() || value.contains('/') {
|
||||
None
|
||||
} else {
|
||||
Some(value)
|
||||
}
|
||||
}
|
||||
|
||||
fn admin_monitoring_trace_provider_id_from_path(request_path: &str) -> Option<String> {
|
||||
let value = request_path
|
||||
.strip_prefix("/api/admin/monitoring/trace/stats/provider/")?
|
||||
.trim()
|
||||
.trim_matches('/')
|
||||
.to_string();
|
||||
if value.is_empty() || value.contains('/') {
|
||||
None
|
||||
} else {
|
||||
Some(value)
|
||||
}
|
||||
}
|
||||
|
||||
fn parse_admin_monitoring_attempted_only(query: Option<&str>) -> Result<bool, String> {
|
||||
match query_param_value(query, "attempted_only") {
|
||||
None => Ok(false),
|
||||
Some(value) => match value.trim().to_ascii_lowercase().as_str() {
|
||||
"true" | "1" | "yes" => Ok(true),
|
||||
"false" | "0" | "no" => Ok(false),
|
||||
_ => Err("attempted_only must be a boolean".to_string()),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
fn admin_monitoring_trace_not_found_response(
|
||||
request_id: &str,
|
||||
attempted_only: bool,
|
||||
) -> Response<Body> {
|
||||
(
|
||||
http::StatusCode::NOT_FOUND,
|
||||
Json(json!({
|
||||
"detail": "Request trace not found",
|
||||
"request_id": request_id,
|
||||
"attempted_only": attempted_only,
|
||||
})),
|
||||
)
|
||||
.into_response()
|
||||
}
|
||||
|
||||
pub(super) async fn build_admin_monitoring_trace_request_response(
|
||||
state: &AppState,
|
||||
request_context: &GatewayPublicRequestContext,
|
||||
state: &AdminAppState<'_>,
|
||||
request_context: &AdminRequestContext<'_>,
|
||||
) -> Result<Response<Body>, GatewayError> {
|
||||
let state = state.as_ref();
|
||||
let Some(request_id) =
|
||||
admin_monitoring_trace_request_id_from_path(&request_context.request_path)
|
||||
else {
|
||||
@@ -102,59 +52,16 @@ pub(super) async fn build_admin_monitoring_trace_request_response(
|
||||
));
|
||||
};
|
||||
|
||||
let candidates = trace
|
||||
.candidates
|
||||
.iter()
|
||||
.map(|item| {
|
||||
let candidate = &item.candidate;
|
||||
json!({
|
||||
"id": candidate.id,
|
||||
"request_id": candidate.request_id,
|
||||
"candidate_index": candidate.candidate_index,
|
||||
"retry_index": candidate.retry_index,
|
||||
"provider_id": candidate.provider_id,
|
||||
"provider_name": item.provider_name,
|
||||
"provider_website": item.provider_website,
|
||||
"endpoint_id": candidate.endpoint_id,
|
||||
"endpoint_name": item.endpoint_api_format,
|
||||
"key_id": candidate.key_id,
|
||||
"key_name": item.provider_key_name,
|
||||
"key_account_label": serde_json::Value::Null,
|
||||
"key_preview": serde_json::Value::Null,
|
||||
"key_auth_type": item.provider_key_auth_type,
|
||||
"key_oauth_plan_type": serde_json::Value::Null,
|
||||
"key_capabilities": item.provider_key_capabilities,
|
||||
"required_capabilities": candidate.required_capabilities,
|
||||
"status": candidate.status,
|
||||
"skip_reason": candidate.skip_reason,
|
||||
"is_cached": candidate.is_cached,
|
||||
"status_code": candidate.status_code,
|
||||
"error_type": candidate.error_type,
|
||||
"error_message": candidate.error_message,
|
||||
"latency_ms": candidate.latency_ms,
|
||||
"concurrent_requests": candidate.concurrent_requests,
|
||||
"extra_data": candidate.extra_data,
|
||||
"created_at": unix_secs_to_rfc3339(candidate.created_at_unix_secs),
|
||||
"started_at": candidate.started_at_unix_secs.and_then(unix_secs_to_rfc3339),
|
||||
"finished_at": candidate.finished_at_unix_secs.and_then(unix_secs_to_rfc3339),
|
||||
})
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
Ok(Json(json!({
|
||||
"request_id": trace.request_id,
|
||||
"total_candidates": trace.total_candidates,
|
||||
"final_status": trace.final_status,
|
||||
"total_latency_ms": trace.total_latency_ms,
|
||||
"candidates": candidates,
|
||||
}))
|
||||
.into_response())
|
||||
Ok(build_admin_monitoring_trace_request_payload_response(
|
||||
&trace,
|
||||
))
|
||||
}
|
||||
|
||||
pub(super) async fn build_admin_monitoring_trace_provider_stats_response(
|
||||
state: &AppState,
|
||||
request_context: &GatewayPublicRequestContext,
|
||||
state: &AdminAppState<'_>,
|
||||
request_context: &AdminRequestContext<'_>,
|
||||
) -> Result<Response<Body>, GatewayError> {
|
||||
let state = state.as_ref();
|
||||
let Some(provider_id) =
|
||||
admin_monitoring_trace_provider_id_from_path(&request_context.request_path)
|
||||
else {
|
||||
@@ -215,18 +122,19 @@ pub(super) async fn build_admin_monitoring_trace_provider_stats_response(
|
||||
((total / latency_values.len() as f64) * 100.0).round() / 100.0
|
||||
};
|
||||
|
||||
Ok(Json(json!({
|
||||
"provider_id": provider_id,
|
||||
"total_attempts": total_attempts,
|
||||
"success_count": success_count,
|
||||
"failed_count": failed_count,
|
||||
"cancelled_count": cancelled_count,
|
||||
"skipped_count": skipped_count,
|
||||
"pending_count": pending_count,
|
||||
"available_count": available_count,
|
||||
"unused_count": unused_count,
|
||||
"failure_rate": failure_rate,
|
||||
"avg_latency_ms": avg_latency_ms,
|
||||
}))
|
||||
.into_response())
|
||||
Ok(
|
||||
build_admin_monitoring_trace_provider_stats_payload_response(
|
||||
provider_id,
|
||||
total_attempts,
|
||||
success_count,
|
||||
failed_count,
|
||||
cancelled_count,
|
||||
skipped_count,
|
||||
pending_count,
|
||||
available_count,
|
||||
unused_count,
|
||||
failure_rate,
|
||||
avg_latency_ms,
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user