Files
Aether/crates/aether-admin/src/observability/monitoring.rs
T

1217 lines
41 KiB
Rust

use aether_data_contracts::repository::{
candidates::{
sanitize_request_candidate_api_formats, sanitize_request_candidate_error_type,
sanitize_request_candidate_extra_data_for_persistence,
sanitize_request_candidate_required_capabilities, sanitize_request_candidate_skip_reason,
DecisionTrace, DecisionTraceCandidate, RequestCandidateStatus,
},
provider_catalog::StoredProviderCatalogKey,
usage::StoredRequestUsageAudit,
};
use axum::{
body::Body,
http,
response::{IntoResponse, Response},
Json,
};
use serde_json::{json, Value};
use std::collections::BTreeMap;
#[derive(Clone, Debug, Default, PartialEq, Eq)]
pub struct AdminMonitoringKeyAccountDisplay {
pub label: Option<String>,
pub oauth_plan_type: Option<String>,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub 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 fn admin_monitoring_bad_request_response(detail: impl Into<String>) -> Response<Body> {
(
http::StatusCode::BAD_REQUEST,
Json(json!({ "detail": detail.into() })),
)
.into_response()
}
pub fn admin_monitoring_not_found_response(detail: &'static str) -> Response<Body> {
(
http::StatusCode::NOT_FOUND,
Json(json!({ "detail": detail })),
)
.into_response()
}
pub fn build_admin_monitoring_audit_logs_payload_response(
items: Vec<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()
}
pub fn build_admin_monitoring_suspicious_activities_payload_response(
activities: Vec<Value>,
hours: i64,
) -> Response<Body> {
let count = activities.len();
Json(json!({
"activities": activities,
"count": count,
"time_range_hours": hours,
}))
.into_response()
}
pub fn build_admin_monitoring_user_behavior_payload_response(
user_id: String,
days: i64,
event_counts: 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()
}
pub fn admin_monitoring_user_behavior_user_id_from_path(request_path: &str) -> Option<String> {
path_identifier_from_path(request_path, "/api/admin/monitoring/user-behavior/")
}
pub fn admin_monitoring_trace_request_id_from_path(request_path: &str) -> Option<String> {
path_identifier_from_path(request_path, "/api/admin/monitoring/trace/")
}
pub fn admin_monitoring_trace_provider_id_from_path(request_path: &str) -> Option<String> {
path_identifier_from_path(request_path, "/api/admin/monitoring/trace/stats/provider/")
}
pub 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()),
},
}
}
pub 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 fn build_admin_monitoring_cache_affinity_delete_success_response(
affinity_key: String,
endpoint_id: String,
model_id: String,
message_name: String,
) -> Response<Body> {
Json(json!({
"status": "ok",
"message": format!("已清除缓存亲和性: {message_name}"),
"affinity_key": affinity_key,
"endpoint_id": endpoint_id,
"model_id": model_id,
}))
.into_response()
}
pub fn build_admin_monitoring_cache_users_delete_api_key_success_response(
user_id: String,
username: Option<String>,
email: Option<String>,
api_key_id: String,
api_key_name: Option<String>,
) -> Response<Body> {
let display_name = api_key_name.clone().unwrap_or_else(|| api_key_id.clone());
Json(json!({
"status": "ok",
"message": format!("已清除 API Key {display_name} 的缓存亲和性"),
"user_info": {
"user_id": Some(user_id),
"username": username,
"email": email,
"api_key_id": api_key_id,
"api_key_name": api_key_name,
},
}))
.into_response()
}
pub fn build_admin_monitoring_cache_users_delete_user_success_response(
user_id: String,
username: String,
email: Option<String>,
) -> Response<Body> {
Json(json!({
"status": "ok",
"message": format!("已清除用户 {username} 的所有缓存亲和性"),
"user_info": {
"user_id": user_id,
"username": username,
"email": email,
},
}))
.into_response()
}
pub fn build_admin_monitoring_cache_provider_delete_success_response(
provider_id: String,
deleted_affinities: usize,
) -> Response<Body> {
Json(json!({
"status": "ok",
"message": format!("已清除 provider {provider_id} 的缓存亲和性"),
"provider_id": provider_id,
"deleted_affinities": deleted_affinities,
}))
.into_response()
}
#[allow(clippy::too_many_arguments)]
pub fn build_admin_monitoring_trace_provider_stats_payload_response(
provider_id: String,
total_attempts: usize,
success_count: usize,
failed_count: usize,
cancelled_count: usize,
skipped_count: usize,
pending_count: usize,
available_count: usize,
unused_count: usize,
failure_rate: f64,
avg_latency_ms: f64,
) -> Response<Body> {
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()
}
pub fn build_admin_monitoring_trace_request_payload_response(
trace: &DecisionTrace,
usage: Option<&StoredRequestUsageAudit>,
) -> Response<Body> {
build_admin_monitoring_trace_request_payload_response_with_key_accounts(
trace,
usage,
&BTreeMap::new(),
)
}
pub fn build_admin_monitoring_trace_request_payload_response_with_key_accounts(
trace: &DecisionTrace,
usage: Option<&StoredRequestUsageAudit>,
key_accounts: &BTreeMap<String, AdminMonitoringKeyAccountDisplay>,
) -> Response<Body> {
let usage_candidate_id =
usage.and_then(|item| resolve_admin_monitoring_usage_candidate_id(trace, item));
let candidates = trace
.candidates
.iter()
.map(|item| {
let matched_usage = usage_candidate_id
.as_deref()
.filter(|candidate_id| *candidate_id == item.candidate.id.as_str())
.and(usage);
build_admin_monitoring_trace_request_candidate_payload_with_key_accounts(
item,
matched_usage,
key_accounts,
)
})
.collect::<Vec<_>>();
Json(json!({
"request_id": trace.request_id,
"request_path": admin_monitoring_trace_request_path(usage),
"request_query_string": admin_monitoring_trace_request_query_string(usage),
"request_path_and_query": admin_monitoring_trace_request_path_and_query(usage),
"total_candidates": trace.total_candidates,
"final_status": trace.final_status,
"total_latency_ms": trace.total_latency_ms,
"candidates": candidates,
}))
.into_response()
}
pub fn build_admin_monitoring_trace_request_candidate_payload(
item: &DecisionTraceCandidate,
usage: Option<&StoredRequestUsageAudit>,
) -> Value {
build_admin_monitoring_trace_request_candidate_payload_with_key_accounts(
item,
usage,
&BTreeMap::new(),
)
}
pub fn build_admin_monitoring_trace_request_candidate_payload_with_key_accounts(
item: &DecisionTraceCandidate,
usage: Option<&StoredRequestUsageAudit>,
key_accounts: &BTreeMap<String, AdminMonitoringKeyAccountDisplay>,
) -> Value {
let mut item = item.clone();
item.sanitize_for_admin();
let candidate = &item.candidate;
let sanitized_extra_data = build_admin_monitoring_trace_candidate_extra_data(
candidate.extra_data.as_ref(),
candidate.status_code,
usage,
);
let sanitized_extra_data_ref =
(!sanitized_extra_data.is_null()).then_some(&sanitized_extra_data);
let sanitized_key_api_formats =
sanitize_request_candidate_api_formats(item.provider_key_api_formats.clone());
let sanitized_key_capabilities =
sanitize_request_candidate_required_capabilities(item.provider_key_capabilities.clone());
let sanitized_required_capabilities =
sanitize_request_candidate_required_capabilities(candidate.required_capabilities.clone());
let key_account = candidate
.key_id
.as_deref()
.and_then(|key_id| key_accounts.get(key_id));
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,
"provider_priority": item.provider_priority,
"provider_keep_priority_on_conversion": item.provider_keep_priority_on_conversion,
"provider_enable_format_conversion": item.provider_enable_format_conversion,
"endpoint_id": candidate.endpoint_id,
"endpoint_name": item.endpoint_api_format,
"endpoint_api_family": item.endpoint_api_family,
"endpoint_kind": item.endpoint_kind,
"endpoint_format_acceptance_config": serde_json::Value::Null,
"key_id": candidate.key_id,
"key_name": item.provider_key_name,
"key_account_label": key_account.and_then(|item| item.label.clone()),
"key_preview": serde_json::Value::Null,
"key_auth_type": item.provider_key_auth_type,
"key_api_formats": sanitized_key_api_formats,
"key_internal_priority": item.provider_key_internal_priority,
"key_global_priority_by_format": serde_json::Value::Null,
"key_oauth_plan_type": key_account.and_then(|item| item.oauth_plan_type.clone()),
"key_capabilities": sanitized_key_capabilities,
"required_capabilities": sanitized_required_capabilities,
"status": candidate.status,
"skip_reason": sanitize_request_candidate_skip_reason(candidate.skip_reason.clone()),
"is_cached": candidate.is_cached,
"status_code": candidate.status_code,
"error_type": sanitize_request_candidate_error_type(candidate.error_type.clone()),
"error_message": candidate.error_message,
"latency_ms": candidate.latency_ms,
"concurrent_requests": candidate.concurrent_requests,
"ranking": build_admin_monitoring_trace_candidate_ranking(sanitized_extra_data_ref),
"image_progress": sanitized_extra_data_ref
.and_then(|value| value.get("image_progress"))
.cloned()
.unwrap_or(Value::Null),
"extra_data": sanitized_extra_data,
"created_at": unix_ms_to_rfc3339(candidate.created_at_unix_ms),
"started_at": candidate.started_at_unix_ms.and_then(unix_ms_to_rfc3339),
"finished_at": candidate.finished_at_unix_ms.and_then(unix_ms_to_rfc3339),
})
}
fn resolve_admin_monitoring_usage_candidate_id(
trace: &DecisionTrace,
usage: &StoredRequestUsageAudit,
) -> Option<String> {
if !admin_monitoring_usage_matches_trace(usage, trace.request_id.as_str()) {
return None;
}
if let Some(candidate_id) = usage
.routing_candidate_id()
.map(str::trim)
.filter(|value| !value.is_empty())
{
return Some(candidate_id.to_string());
}
let candidate_index = usage.routing_candidate_index()?;
trace
.candidates
.iter()
.filter(|item| u64::from(item.candidate.candidate_index) == candidate_index)
.max_by_key(|item| {
(
item.candidate.retry_index,
item.candidate.finished_at_unix_ms.unwrap_or_default(),
item.candidate.started_at_unix_ms.unwrap_or_default(),
admin_monitoring_candidate_status_rank(item.candidate.status),
)
})
.map(|item| item.candidate.id.clone())
}
fn admin_monitoring_usage_matches_trace(
usage: &StoredRequestUsageAudit,
trace_request_id: &str,
) -> bool {
let trace_request_id = trace_request_id.trim();
if trace_request_id.is_empty() {
return false;
}
usage.request_id.trim() == trace_request_id
|| usage
.trace_id()
.is_some_and(|value| value.trim() == trace_request_id)
|| admin_monitoring_headers_contain_trace_id(
usage.request_headers.as_ref(),
trace_request_id,
)
|| admin_monitoring_headers_contain_trace_id(
usage.provider_request_headers.as_ref(),
trace_request_id,
)
}
fn admin_monitoring_headers_contain_trace_id(
headers: Option<&Value>,
trace_request_id: &str,
) -> bool {
headers.and_then(Value::as_object).is_some_and(|object| {
object.iter().any(|(key, value)| {
key.eq_ignore_ascii_case("x-trace-id")
&& value
.as_str()
.map(str::trim)
.is_some_and(|value| value == trace_request_id)
})
})
}
fn admin_monitoring_candidate_status_rank(status: RequestCandidateStatus) -> u8 {
match status {
RequestCandidateStatus::Success => 7,
RequestCandidateStatus::Streaming => 6,
RequestCandidateStatus::Pending => 5,
RequestCandidateStatus::Failed => 4,
RequestCandidateStatus::Cancelled => 3,
RequestCandidateStatus::Skipped => 2,
RequestCandidateStatus::Unused => 1,
RequestCandidateStatus::Available => 0,
}
}
fn build_admin_monitoring_trace_candidate_ranking(existing: Option<&Value>) -> Value {
let Some(object) = existing.and_then(Value::as_object) else {
return Value::Null;
};
let mut ranking = serde_json::Map::new();
if let Some(ranking_mode) = json_string_field(object, "ranking_mode") {
ranking.insert("mode".to_string(), Value::String(ranking_mode));
}
if let Some(priority_mode) = json_string_field(object, "priority_mode") {
ranking.insert("priority_mode".to_string(), Value::String(priority_mode));
}
if let Some(ranking_index) = json_u64_field(object, "ranking_index") {
ranking.insert("index".to_string(), Value::Number(ranking_index.into()));
}
if let Some(priority_slot) = json_i64_field(object, "priority_slot") {
ranking.insert(
"priority_slot".to_string(),
Value::Number(priority_slot.into()),
);
}
if let Some(promoted_by) = json_string_field(object, "promoted_by") {
ranking.insert("promoted_by".to_string(), Value::String(promoted_by));
}
if let Some(demoted_by) = json_string_field(object, "demoted_by") {
ranking.insert("demoted_by".to_string(), Value::String(demoted_by));
}
if ranking.is_empty() {
Value::Null
} else {
Value::Object(ranking)
}
}
fn build_admin_monitoring_trace_candidate_extra_data(
existing: Option<&Value>,
candidate_status_code: Option<u16>,
usage: Option<&StoredRequestUsageAudit>,
) -> Value {
let mut extra_data = sanitize_request_candidate_extra_data_for_persistence(existing.cloned())
.and_then(|value| value.as_object().cloned());
if let Some(usage) = usage {
let extra_object = extra_data.get_or_insert_with(serde_json::Map::new);
if let Some(first_byte_time_ms) = usage.first_byte_time_ms {
extra_object
.entry("first_byte_time_ms".to_string())
.or_insert_with(|| json!(first_byte_time_ms));
}
if let Some(request_path) = admin_monitoring_usage_request_path(usage) {
extra_object
.entry("request_path".to_string())
.or_insert_with(|| json!(request_path));
}
if let Some(request_query_string) = admin_monitoring_usage_request_query_string(usage) {
extra_object
.entry("request_query_string".to_string())
.or_insert_with(|| json!(request_query_string));
}
if let Some(request_path_and_query) = admin_monitoring_usage_request_path_and_query(usage) {
extra_object
.entry("request_path_and_query".to_string())
.or_insert_with(|| json!(request_path_and_query));
}
if admin_monitoring_usage_is_error_node(usage) {
if let Some(response) = admin_monitoring_trace_response_data(
"upstream_response",
candidate_status_code,
usage.response_body_state,
) {
merge_admin_monitoring_trace_response(extra_object, "upstream_response", response);
}
}
if let Some(proxy_value) = extra_object.get_mut("proxy") {
if let Some(proxy_object) = proxy_value.as_object_mut() {
let proxy_timing = parse_admin_monitoring_usage_proxy_timing(usage);
let proxy_ttfb_ms = proxy_timing
.as_ref()
.and_then(|timing| timing.get("ttfb_ms"))
.and_then(Value::as_u64)
.or(usage.first_byte_time_ms);
if let Some(ttfb_ms) = proxy_ttfb_ms {
proxy_object
.entry("ttfb_ms".to_string())
.or_insert_with(|| json!(ttfb_ms));
}
if let Some(timing) = proxy_timing {
proxy_object.entry("timing".to_string()).or_insert(timing);
}
}
}
}
sanitize_request_candidate_extra_data_for_persistence(extra_data.map(Value::Object))
.unwrap_or(Value::Null)
}
fn admin_monitoring_trace_response_data(
source: &str,
status_code: Option<u16>,
body_state: Option<aether_data_contracts::repository::usage::UsageBodyCaptureState>,
) -> Option<Value> {
if status_code.is_none() && body_state.is_none() {
return None;
}
Some(json!({
"source": source,
"status_code": status_code,
"body_state": body_state.map(|state| state.as_str()),
}))
}
fn merge_admin_monitoring_trace_response(
extra_object: &mut serde_json::Map<String, Value>,
key: &str,
response: Value,
) {
let Some(response_object) = response.as_object() else {
extra_object.insert(key.to_string(), response);
return;
};
let Some(existing_object) = extra_object.get_mut(key).and_then(Value::as_object_mut) else {
extra_object.insert(key.to_string(), Value::Object(response_object.clone()));
return;
};
for (field, value) in response_object {
if field == "status_code" && existing_object.get(field).is_some_and(Value::is_number) {
continue;
}
if admin_monitoring_trace_response_value_empty(value)
&& existing_object
.get(field)
.is_some_and(|existing| !admin_monitoring_trace_response_value_empty(existing))
{
continue;
}
existing_object.insert(field.clone(), value.clone());
}
}
fn admin_monitoring_trace_response_value_empty(value: &Value) -> bool {
value.is_null()
|| value.as_str().is_some_and(str::is_empty)
|| value.as_array().is_some_and(Vec::is_empty)
|| value.as_object().is_some_and(serde_json::Map::is_empty)
}
fn admin_monitoring_usage_is_error_node(usage: &StoredRequestUsageAudit) -> bool {
!usage.status.eq_ignore_ascii_case("completed")
|| usage
.status_code
.is_some_and(|status| !(200..300).contains(&status))
}
fn admin_monitoring_trace_request_path(usage: Option<&StoredRequestUsageAudit>) -> Option<String> {
usage.and_then(admin_monitoring_usage_request_path)
}
fn admin_monitoring_trace_request_query_string(
usage: Option<&StoredRequestUsageAudit>,
) -> Option<String> {
usage.and_then(admin_monitoring_usage_request_query_string)
}
fn admin_monitoring_trace_request_path_and_query(
usage: Option<&StoredRequestUsageAudit>,
) -> Option<String> {
usage.and_then(admin_monitoring_usage_request_path_and_query)
}
fn admin_monitoring_usage_request_path(usage: &StoredRequestUsageAudit) -> Option<String> {
admin_monitoring_usage_metadata_string(usage, "request_path")
.and_then(|value| aether_ai_formats::api::sanitize_request_path(&value))
}
fn admin_monitoring_usage_request_query_string(usage: &StoredRequestUsageAudit) -> Option<String> {
admin_monitoring_usage_metadata_string(usage, "request_query_string")
.and_then(|value| aether_ai_formats::api::sanitize_request_query_string(&value))
}
fn admin_monitoring_usage_request_path_and_query(
usage: &StoredRequestUsageAudit,
) -> Option<String> {
admin_monitoring_usage_metadata_string(usage, "request_path_and_query")
.and_then(|value| aether_ai_formats::api::sanitize_request_path_and_query(&value, None))
.or_else(|| {
let path = admin_monitoring_usage_metadata_string(usage, "request_path")?;
let query = admin_monitoring_usage_metadata_string(usage, "request_query_string")
.and_then(|value| aether_ai_formats::api::sanitize_request_query_string(&value));
aether_ai_formats::api::sanitize_request_path_and_query(&path, query.as_deref())
})
}
fn admin_monitoring_usage_metadata_string(
usage: &StoredRequestUsageAudit,
key: &str,
) -> Option<String> {
usage
.request_metadata
.as_ref()
.and_then(Value::as_object)
.and_then(|metadata| metadata.get(key))
.and_then(Value::as_str)
.map(str::trim)
.filter(|value| !value.is_empty())
.map(ToOwned::to_owned)
}
fn json_string_field(object: &serde_json::Map<String, Value>, key: &str) -> Option<String> {
object
.get(key)
.and_then(Value::as_str)
.map(str::trim)
.filter(|value| !value.is_empty())
.map(ToOwned::to_owned)
}
fn json_u64_field(object: &serde_json::Map<String, Value>, key: &str) -> Option<u64> {
match object.get(key)? {
Value::Number(value) => value.as_u64(),
Value::String(value) => value.trim().parse::<u64>().ok(),
_ => None,
}
}
fn json_i64_field(object: &serde_json::Map<String, Value>, key: &str) -> Option<i64> {
match object.get(key)? {
Value::Number(value) => value.as_i64().or_else(|| value.as_u64()?.try_into().ok()),
Value::String(value) => value.trim().parse::<i64>().ok(),
_ => None,
}
}
fn parse_admin_monitoring_usage_proxy_timing(usage: &StoredRequestUsageAudit) -> Option<Value> {
admin_monitoring_header_value(usage.response_headers.as_ref(), "x-proxy-timing")
.or_else(|| {
admin_monitoring_header_value(usage.client_response_headers.as_ref(), "x-proxy-timing")
})
.and_then(|raw| parse_admin_monitoring_proxy_timing_value(&raw))
}
fn admin_monitoring_header_value(headers: Option<&Value>, name: &str) -> Option<String> {
headers
.and_then(Value::as_object)
.and_then(|object| {
object
.iter()
.find(|(key, _)| key.eq_ignore_ascii_case(name))
.map(|(_, value)| value)
})
.and_then(|value| match value {
Value::String(text) => Some(text.trim().to_string()),
Value::Object(object) => Some(Value::Object(object.clone()).to_string()),
_ => None,
})
.filter(|value| !value.is_empty())
}
fn parse_admin_monitoring_proxy_timing_value(raw: &str) -> Option<Value> {
serde_json::from_str::<Value>(raw)
.ok()
.filter(Value::is_object)
}
#[allow(clippy::too_many_arguments)]
pub fn build_admin_monitoring_system_status_payload_response(
timestamp: chrono::DateTime<chrono::Utc>,
total_users: u64,
active_users: u64,
total_providers: usize,
active_providers: usize,
total_api_keys: u64,
active_api_keys: u64,
today_requests: usize,
today_tokens: u64,
today_cost: f64,
proxy_connections: usize,
nodes: usize,
active_streams: usize,
path_prefixes: &[&str],
recent_errors: usize,
usage_counter: Value,
) -> Response<Body> {
Json(json!({
"timestamp": timestamp.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": proxy_connections,
"nodes": nodes,
"active_streams": active_streams,
},
"internal_gateway": {
"status": "rust_native_control_plane",
"path_prefixes": path_prefixes,
},
"recent_errors": recent_errors,
"usage_counter": usage_counter,
}))
.into_response()
}
pub fn build_admin_monitoring_resilience_status_payload_response(
timestamp: chrono::DateTime<chrono::Utc>,
health_score: i64,
status: &'static str,
error_statistics: Value,
recent_errors: Vec<Value>,
recommendations: Vec<String>,
) -> Response<Body> {
Json(json!({
"timestamp": timestamp.to_rfc3339(),
"health_score": health_score,
"status": status,
"error_statistics": error_statistics,
"recent_errors": recent_errors,
"recommendations": recommendations,
}))
.into_response()
}
pub fn build_admin_monitoring_reset_error_stats_payload_response(
previous_stats: Value,
reset_by: Option<String>,
reset_at: chrono::DateTime<chrono::Utc>,
) -> Response<Body> {
Json(json!({
"message": "错误统计已重置",
"previous_stats": previous_stats,
"reset_by": reset_by,
"reset_at": reset_at.to_rfc3339(),
}))
.into_response()
}
pub 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())
}
}
}
}
pub fn build_admin_monitoring_circuit_history_items(
keys: &[StoredProviderCatalogKey],
provider_name_by_id: &BTreeMap<String, String>,
limit: usize,
) -> Vec<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 fn build_admin_monitoring_circuit_history_payload_response(
items: Vec<Value>,
) -> Response<Body> {
let count = items.len();
Json(json!({
"items": items,
"count": count,
}))
.into_response()
}
pub fn admin_monitoring_unknown_cache_category_response(category: &str) -> Response<Body> {
(
http::StatusCode::NOT_FOUND,
Json(json!({ "detail": format!("未知的缓存分类: {category}") })),
)
.into_response()
}
pub fn build_admin_monitoring_redis_keys_delete_success_response(
category: &str,
name: &str,
deleted_count: usize,
) -> Response<Body> {
Json(json!({
"status": "ok",
"message": format!("已清除 {name} 缓存"),
"category": category,
"deleted_count": deleted_count,
}))
.into_response()
}
pub fn build_admin_monitoring_cache_flush_success_response(
deleted_affinities: usize,
) -> Response<Body> {
Json(json!({
"status": "ok",
"message": "已清除全部缓存亲和性",
"deleted_affinities": deleted_affinities,
}))
.into_response()
}
pub fn admin_monitoring_cache_provider_not_found_response(provider_id: &str) -> Response<Body> {
(
http::StatusCode::NOT_FOUND,
Json(json!({
"detail": format!("未找到 provider {provider_id} 的缓存亲和性记录")
})),
)
.into_response()
}
pub fn build_admin_monitoring_model_mapping_delete_success_response(
deleted_count: usize,
) -> Response<Body> {
Json(json!({
"status": "ok",
"message": "已清除所有模型映射缓存",
"deleted_count": deleted_count,
}))
.into_response()
}
pub fn build_admin_monitoring_model_mapping_delete_model_success_response(
model_name: String,
deleted_keys: Vec<String>,
) -> Response<Body> {
Json(json!({
"status": "ok",
"message": format!("已清除模型 {model_name} 的映射缓存"),
"model_name": model_name,
"deleted_keys": deleted_keys,
}))
.into_response()
}
pub fn build_admin_monitoring_model_mapping_delete_provider_success_response(
provider_id: String,
global_model_id: String,
deleted_keys: Vec<String>,
) -> Response<Body> {
Json(json!({
"status": "ok",
"message": "已清除 Provider 模型映射缓存",
"provider_id": provider_id,
"global_model_id": global_model_id,
"deleted_keys": deleted_keys,
}))
.into_response()
}
pub 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 path_identifier_from_path(request_path: &str, prefix: &str) -> Option<String> {
let value = request_path
.strip_prefix(prefix)?
.trim()
.trim_matches('/')
.to_string();
if value.is_empty() || value.contains('/') {
None
} else {
Some(value)
}
}
fn unix_ms_to_rfc3339(unix_ms: u64) -> Option<String> {
let secs = (unix_ms / 1000) as i64;
let nanos = ((unix_ms % 1000) * 1_000_000) as u32;
chrono::DateTime::<chrono::Utc>::from_timestamp(secs, nanos)
.map(|value| value.to_rfc3339_opts(chrono::SecondsFormat::Millis, true))
}
fn query_param_value(query: Option<&str>, key: &str) -> Option<String> {
let query = query?;
for (entry_key, value) in url::form_urlencoded::parse(query.as_bytes()) {
if entry_key == key {
let value = value.trim();
if !value.is_empty() {
return Some(value.to_string());
}
}
}
None
}
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
}