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:
fawney19
2026-04-09 00:10:38 +08:00
parent 4fb9882b54
commit 4fc95adfb9
663 changed files with 48471 additions and 40232 deletions
@@ -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() {
@@ -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() {
@@ -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())
}
@@ -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,
),
)
}
@@ -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))
}
@@ -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;
@@ -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,
),
)
}
@@ -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))
}
@@ -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,
))
}
@@ -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,
))
}
@@ -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,
),
)
}