feat(gateway/data): 新增 usage 关键词搜索、缓存命中摘要、结算成本摘要及 dashboard 聚合查询能力

- 新增 UsageAuditKeywordSearchQuery,支持多关键词、用户名、API Key 交叉过滤
- 新增 UsageCacheHitSummaryQuery/StoredUsageCacheHitSummary,统计请求缓存命中率
- 新增 UsageSettledCostSummaryQuery/StoredUsageSettledCostSummary,汇总结算成本
- 新增 UsageDashboardSummaryQuery、UsageBreakdownSummaryQuery 等 dashboard 聚合类型
- SQL 层实现对应查询方法,含 list_by_ids、list_usage_audits_by_keyword_search、count_usage_audits_by_keyword_search
- 将上述能力通过 GatewayDataState / AdminAppState 暴露给 handler 层
- user_me_usage 及 dashboard_filters 切换为新查询接口,移除旧的内存过滤逻辑
- 同步更新 memory 层及测试
This commit is contained in:
fawney19
2026-04-17 00:58:46 +08:00
parent 6e5af5ef70
commit 1e0bc61526
48 changed files with 5241 additions and 1162 deletions

View File

@@ -1,7 +1,11 @@
use aether_data::repository::auth::StoredAuthApiKeySnapshot;
use aether_data_contracts::repository::{
provider_catalog::StoredProviderCatalogProvider,
usage::{StoredRequestUsageAudit, StoredUsageLeaderboardSummary, StoredUsageTimeSeriesBucket},
usage::{
StoredRequestUsageAudit, StoredUsageCostSavingsSummary, StoredUsageErrorDistributionRow,
StoredUsageLeaderboardSummary, StoredUsagePerformancePercentilesRow,
StoredUsageTimeSeriesBucket,
},
};
use axum::{
body::Body,
@@ -927,6 +931,62 @@ pub fn build_admin_stats_error_distribution_response(
.into_response()
}
pub fn build_admin_stats_error_distribution_response_from_summaries(
rows: &[StoredUsageErrorDistributionRow],
) -> Response<Body> {
let mut distribution: std::collections::BTreeMap<String, u64> =
std::collections::BTreeMap::new();
let mut trend: std::collections::BTreeMap<String, std::collections::BTreeMap<String, u64>> =
std::collections::BTreeMap::new();
for row in rows {
*distribution.entry(row.error_category.clone()).or_default() += row.count;
*trend
.entry(row.date.clone())
.or_default()
.entry(row.error_category.clone())
.or_default() += row.count;
}
let mut distribution_items: Vec<_> = distribution
.into_iter()
.map(|(category, count)| json!({ "category": category, "count": count }))
.collect();
distribution_items.sort_by(|left, right| {
let left_count = left
.get("count")
.and_then(serde_json::Value::as_u64)
.unwrap_or(0);
let right_count = right
.get("count")
.and_then(serde_json::Value::as_u64)
.unwrap_or(0);
right_count.cmp(&left_count).then_with(|| {
left.get("category")
.and_then(serde_json::Value::as_str)
.cmp(&right.get("category").and_then(serde_json::Value::as_str))
})
});
let trend_items: Vec<_> = trend
.into_iter()
.map(|(date, categories)| {
let total: u64 = categories.values().copied().sum();
json!({
"date": date,
"total": total,
"categories": categories,
})
})
.collect();
Json(json!({
"distribution": distribution_items,
"trend": trend_items,
}))
.into_response()
}
pub fn build_admin_stats_performance_percentiles_response(
time_range: &AdminStatsTimeRange,
usage: &[StoredRequestUsageAudit],
@@ -974,6 +1034,33 @@ pub fn build_admin_stats_performance_percentiles_response(
Json(serde_json::Value::Array(payload)).into_response()
}
pub fn build_admin_stats_performance_percentiles_response_from_summaries(
time_range: &AdminStatsTimeRange,
rows: &[StoredUsagePerformancePercentilesRow],
) -> Response<Body> {
let by_day: std::collections::BTreeMap<String, &StoredUsagePerformancePercentilesRow> =
rows.iter().map(|row| (row.date.clone(), row)).collect();
let payload: Vec<_> = time_range
.local_date_strings()
.into_iter()
.map(|date| {
let row = by_day.get(&date).copied();
json!({
"date": date,
"p50_response_time_ms": row.and_then(|value| value.p50_response_time_ms),
"p90_response_time_ms": row.and_then(|value| value.p90_response_time_ms),
"p99_response_time_ms": row.and_then(|value| value.p99_response_time_ms),
"p50_first_byte_time_ms": row.and_then(|value| value.p50_first_byte_time_ms),
"p90_first_byte_time_ms": row.and_then(|value| value.p90_first_byte_time_ms),
"p99_first_byte_time_ms": row.and_then(|value| value.p99_first_byte_time_ms),
})
})
.collect();
Json(serde_json::Value::Array(payload)).into_response()
}
pub fn build_admin_stats_time_series_response(
time_range: &AdminStatsTimeRange,
granularity: AdminStatsGranularity,
@@ -1199,6 +1286,53 @@ pub fn build_admin_stats_cost_forecast_response(
.into_response()
}
pub fn build_admin_stats_cost_forecast_response_from_summaries(
time_range: &AdminStatsTimeRange,
forecast_days: u32,
buckets: &[StoredUsageTimeSeriesBucket],
) -> Response<Body> {
let history: Vec<AdminStatsForecastPoint> =
build_daily_time_series_buckets_from_summaries(time_range, buckets)
.into_iter()
.map(|(date, bucket)| AdminStatsForecastPoint {
date,
total_cost: bucket.total_cost,
})
.collect();
let values: Vec<f64> = history.iter().map(|item| item.total_cost).collect();
let (slope, intercept) = linear_regression(&values);
let last_date = history
.last()
.map(|item| item.date)
.unwrap_or(time_range.end_date);
let forecast: Vec<_> = (0..forecast_days)
.map(|index| {
let idx = values.len() + index as usize;
let predicted = (slope * idx as f64 + intercept).max(0.0);
json!({
"date": last_date
.checked_add_signed(chrono::Duration::days(i64::from(index + 1)))
.unwrap_or(last_date)
.to_string(),
"total_cost": round_to(predicted, 4),
})
})
.collect();
Json(json!({
"history": history.into_iter().map(|item| json!({
"date": item.date.to_string(),
"total_cost": round_to(item.total_cost, 6),
})).collect::<Vec<_>>(),
"forecast": forecast,
"slope": round_to(slope, 6),
"intercept": round_to(intercept, 6),
"start_date": time_range.start_date.to_string(),
"end_date": time_range.end_date.to_string(),
}))
.into_response()
}
pub fn build_admin_stats_cost_savings_response(
usage: &[StoredRequestUsageAudit],
) -> Response<Body> {
@@ -1228,6 +1362,27 @@ pub fn build_admin_stats_cost_savings_response(
.into_response()
}
pub fn build_admin_stats_cost_savings_response_from_summary(
summary: &StoredUsageCostSavingsSummary,
) -> Response<Body> {
let estimated_full_cost =
if summary.estimated_full_cost_usd <= 0.0 && summary.cache_read_cost_usd > 0.0 {
summary.cache_read_cost_usd * 10.0
} else {
summary.estimated_full_cost_usd
};
let cache_savings = estimated_full_cost - summary.cache_read_cost_usd;
Json(json!({
"cache_read_tokens": summary.cache_read_tokens,
"cache_read_cost": round_to(summary.cache_read_cost_usd, 6),
"cache_creation_cost": round_to(summary.cache_creation_cost_usd, 6),
"estimated_full_cost": round_to(estimated_full_cost, 6),
"cache_savings": round_to(cache_savings, 6),
}))
.into_response()
}
fn unix_secs_to_rfc3339(unix_secs: u64) -> Option<String> {
let timestamp = chrono::DateTime::<Utc>::from_timestamp(i64::try_from(unix_secs).ok()?, 0)?;
Some(timestamp.to_rfc3339())