mirror of
https://github.com/fawney19/Aether.git
synced 2026-09-01 17:00:21 +08:00
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:
@@ -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())
|
||||
|
||||
@@ -3,10 +3,21 @@ mod types;
|
||||
pub use types::{
|
||||
parse_usage_body_ref, usage_body_ref, StoredProviderApiKeyUsageSummary,
|
||||
StoredProviderUsageSummary, StoredProviderUsageWindow, StoredRequestUsageAudit,
|
||||
StoredUsageAuditAggregation, StoredUsageAuditSummary, StoredUsageDailySummary,
|
||||
StoredUsageLeaderboardSummary, StoredUsageTimeSeriesBucket, UpsertUsageRecord,
|
||||
UsageAuditAggregationGroupBy, UsageAuditAggregationQuery, UsageAuditListQuery,
|
||||
UsageAuditSummaryQuery, UsageBodyField, UsageDailyHeatmapQuery, UsageLeaderboardGroupBy,
|
||||
UsageLeaderboardQuery, UsageReadRepository, UsageRepository, UsageTimeSeriesGranularity,
|
||||
UsageTimeSeriesQuery, UsageWriteRepository,
|
||||
StoredUsageAuditAggregation, StoredUsageAuditSummary, StoredUsageBreakdownSummaryRow,
|
||||
StoredUsageCacheAffinityHitSummary, StoredUsageCacheAffinityIntervalRow,
|
||||
StoredUsageCacheHitSummary, StoredUsageCostSavingsSummary, StoredUsageDailySummary,
|
||||
StoredUsageDashboardDailyBreakdownRow, StoredUsageDashboardProviderCount,
|
||||
StoredUsageDashboardSummary, StoredUsageErrorDistributionRow, StoredUsageLeaderboardSummary,
|
||||
StoredUsagePerformancePercentilesRow, StoredUsageSettledCostSummary,
|
||||
StoredUsageTimeSeriesBucket, UpsertUsageRecord, UsageAuditAggregationGroupBy,
|
||||
UsageAuditAggregationQuery, UsageAuditKeywordSearchQuery, UsageAuditListQuery,
|
||||
UsageAuditSummaryQuery, UsageBodyField, UsageBreakdownGroupBy, UsageBreakdownSummaryQuery,
|
||||
UsageCacheAffinityHitSummaryQuery, UsageCacheAffinityIntervalGroupBy,
|
||||
UsageCacheAffinityIntervalQuery, UsageCacheHitSummaryQuery, UsageCostSavingsSummaryQuery,
|
||||
UsageDailyHeatmapQuery, UsageDashboardDailyBreakdownQuery, UsageDashboardProviderCountsQuery,
|
||||
UsageDashboardSummaryQuery, UsageErrorDistributionQuery, UsageLeaderboardGroupBy,
|
||||
UsageLeaderboardQuery, UsageMonitoringErrorCountQuery, UsageMonitoringErrorListQuery,
|
||||
UsagePerformancePercentilesQuery, UsageReadRepository, UsageRepository,
|
||||
UsageSettledCostSummaryQuery, UsageTimeSeriesGranularity, UsageTimeSeriesQuery,
|
||||
UsageWriteRepository,
|
||||
};
|
||||
|
||||
@@ -489,6 +489,29 @@ pub struct UsageAuditListQuery {
|
||||
pub newest_first: bool,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Default, serde::Serialize, serde::Deserialize)]
|
||||
pub struct UsageAuditKeywordSearchQuery {
|
||||
pub created_from_unix_secs: Option<u64>,
|
||||
pub created_until_unix_secs: Option<u64>,
|
||||
pub user_id: Option<String>,
|
||||
pub provider_name: Option<String>,
|
||||
pub model: Option<String>,
|
||||
pub api_format: Option<String>,
|
||||
pub statuses: Option<Vec<String>>,
|
||||
pub is_stream: Option<bool>,
|
||||
pub error_only: bool,
|
||||
pub keywords: Vec<String>,
|
||||
pub matched_user_ids_by_keyword: Vec<Vec<String>>,
|
||||
pub auth_user_reader_available: bool,
|
||||
pub matched_api_key_ids_by_keyword: Vec<Vec<String>>,
|
||||
pub auth_api_key_reader_available: bool,
|
||||
pub username_keyword: Option<String>,
|
||||
pub matched_user_ids_for_username: Vec<String>,
|
||||
pub limit: Option<usize>,
|
||||
pub offset: Option<usize>,
|
||||
pub newest_first: bool,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
pub enum UsageAuditAggregationGroupBy {
|
||||
@@ -553,6 +576,244 @@ pub struct StoredUsageAuditSummary {
|
||||
pub error_requests: u64,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Default, serde::Serialize, serde::Deserialize)]
|
||||
pub struct UsageCacheHitSummaryQuery {
|
||||
pub created_from_unix_secs: u64,
|
||||
pub created_until_unix_secs: u64,
|
||||
pub user_id: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Default, serde::Serialize, serde::Deserialize)]
|
||||
pub struct StoredUsageCacheHitSummary {
|
||||
pub total_requests: u64,
|
||||
pub cache_hit_requests: u64,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Default, serde::Serialize, serde::Deserialize)]
|
||||
pub struct UsageSettledCostSummaryQuery {
|
||||
pub created_from_unix_secs: u64,
|
||||
pub created_until_unix_secs: u64,
|
||||
pub user_id: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Default, serde::Serialize, serde::Deserialize)]
|
||||
pub struct StoredUsageSettledCostSummary {
|
||||
pub total_cost_usd: f64,
|
||||
pub total_requests: u64,
|
||||
pub input_tokens: u64,
|
||||
pub output_tokens: u64,
|
||||
pub cache_creation_tokens: u64,
|
||||
pub cache_read_tokens: u64,
|
||||
pub first_finalized_at_unix_secs: Option<u64>,
|
||||
pub last_finalized_at_unix_secs: Option<u64>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Default, serde::Serialize, serde::Deserialize)]
|
||||
pub struct UsageCacheAffinityHitSummaryQuery {
|
||||
pub created_from_unix_secs: u64,
|
||||
pub created_until_unix_secs: u64,
|
||||
pub user_id: Option<String>,
|
||||
pub api_key_id: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Default, serde::Serialize, serde::Deserialize)]
|
||||
pub struct StoredUsageCacheAffinityHitSummary {
|
||||
pub total_requests: u64,
|
||||
pub requests_with_cache_hit: u64,
|
||||
pub input_tokens: u64,
|
||||
pub cache_read_tokens: u64,
|
||||
pub cache_creation_tokens: u64,
|
||||
pub total_input_context: u64,
|
||||
pub cache_read_cost_usd: f64,
|
||||
pub cache_creation_cost_usd: f64,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, serde::Serialize, serde::Deserialize)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
pub enum UsageCacheAffinityIntervalGroupBy {
|
||||
#[default]
|
||||
User,
|
||||
ApiKey,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Default, serde::Serialize, serde::Deserialize)]
|
||||
pub struct UsageCacheAffinityIntervalQuery {
|
||||
pub created_from_unix_secs: u64,
|
||||
pub created_until_unix_secs: u64,
|
||||
pub group_by: UsageCacheAffinityIntervalGroupBy,
|
||||
pub user_id: Option<String>,
|
||||
pub api_key_id: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Default, serde::Serialize, serde::Deserialize)]
|
||||
pub struct StoredUsageCacheAffinityIntervalRow {
|
||||
pub group_id: String,
|
||||
pub username: Option<String>,
|
||||
pub model: String,
|
||||
pub created_at_unix_secs: u64,
|
||||
pub interval_minutes: f64,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Default, serde::Serialize, serde::Deserialize)]
|
||||
pub struct UsageDashboardSummaryQuery {
|
||||
pub created_from_unix_secs: u64,
|
||||
pub created_until_unix_secs: u64,
|
||||
pub user_id: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Default, serde::Serialize, serde::Deserialize)]
|
||||
pub struct StoredUsageDashboardSummary {
|
||||
pub total_requests: u64,
|
||||
pub input_tokens: u64,
|
||||
pub effective_input_tokens: u64,
|
||||
pub output_tokens: u64,
|
||||
pub total_tokens: u64,
|
||||
pub cache_creation_tokens: u64,
|
||||
pub cache_read_tokens: u64,
|
||||
pub total_input_context: u64,
|
||||
pub cache_creation_cost_usd: f64,
|
||||
pub cache_read_cost_usd: f64,
|
||||
pub total_cost_usd: f64,
|
||||
pub actual_total_cost_usd: f64,
|
||||
pub error_requests: u64,
|
||||
pub response_time_sum_ms: f64,
|
||||
pub response_time_samples: u64,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
|
||||
pub struct UsageDashboardDailyBreakdownQuery {
|
||||
pub created_from_unix_secs: u64,
|
||||
pub created_until_unix_secs: u64,
|
||||
pub tz_offset_minutes: i32,
|
||||
pub user_id: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Default, serde::Serialize, serde::Deserialize)]
|
||||
pub struct StoredUsageDashboardDailyBreakdownRow {
|
||||
pub date: String,
|
||||
pub model: String,
|
||||
pub provider: String,
|
||||
pub requests: u64,
|
||||
pub total_tokens: u64,
|
||||
pub total_cost_usd: f64,
|
||||
pub response_time_sum_ms: f64,
|
||||
pub response_time_samples: u64,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
|
||||
pub struct UsageDashboardProviderCountsQuery {
|
||||
pub created_from_unix_secs: u64,
|
||||
pub created_until_unix_secs: u64,
|
||||
pub user_id: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Default, serde::Serialize, serde::Deserialize)]
|
||||
pub struct StoredUsageDashboardProviderCount {
|
||||
pub provider_name: String,
|
||||
pub request_count: u64,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, serde::Serialize, serde::Deserialize)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
pub enum UsageBreakdownGroupBy {
|
||||
#[default]
|
||||
Model,
|
||||
Provider,
|
||||
ApiFormat,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Default, serde::Serialize, serde::Deserialize)]
|
||||
pub struct UsageBreakdownSummaryQuery {
|
||||
pub created_from_unix_secs: u64,
|
||||
pub created_until_unix_secs: u64,
|
||||
pub user_id: Option<String>,
|
||||
pub group_by: UsageBreakdownGroupBy,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Default, serde::Serialize, serde::Deserialize)]
|
||||
pub struct StoredUsageBreakdownSummaryRow {
|
||||
pub group_key: String,
|
||||
pub request_count: u64,
|
||||
pub input_tokens: u64,
|
||||
pub total_tokens: u64,
|
||||
pub output_tokens: u64,
|
||||
pub effective_input_tokens: u64,
|
||||
pub total_input_context: u64,
|
||||
pub cache_creation_tokens: u64,
|
||||
pub cache_creation_ephemeral_5m_tokens: u64,
|
||||
pub cache_creation_ephemeral_1h_tokens: u64,
|
||||
pub cache_read_tokens: u64,
|
||||
pub total_cost_usd: f64,
|
||||
pub actual_total_cost_usd: f64,
|
||||
pub success_count: u64,
|
||||
pub response_time_sum_ms: f64,
|
||||
pub response_time_samples: u64,
|
||||
pub overall_response_time_sum_ms: f64,
|
||||
pub overall_response_time_samples: u64,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Default, serde::Serialize, serde::Deserialize)]
|
||||
pub struct UsageMonitoringErrorCountQuery {
|
||||
pub created_from_unix_secs: u64,
|
||||
pub created_until_unix_secs: u64,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Default, serde::Serialize, serde::Deserialize)]
|
||||
pub struct UsageMonitoringErrorListQuery {
|
||||
pub created_from_unix_secs: u64,
|
||||
pub created_until_unix_secs: u64,
|
||||
pub limit: Option<usize>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Default, serde::Serialize, serde::Deserialize)]
|
||||
pub struct UsageErrorDistributionQuery {
|
||||
pub created_from_unix_secs: u64,
|
||||
pub created_until_unix_secs: u64,
|
||||
pub tz_offset_minutes: i32,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Default, serde::Serialize, serde::Deserialize)]
|
||||
pub struct StoredUsageErrorDistributionRow {
|
||||
pub date: String,
|
||||
pub error_category: String,
|
||||
pub count: u64,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Default, serde::Serialize, serde::Deserialize)]
|
||||
pub struct UsagePerformancePercentilesQuery {
|
||||
pub created_from_unix_secs: u64,
|
||||
pub created_until_unix_secs: u64,
|
||||
pub tz_offset_minutes: i32,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Default, serde::Serialize, serde::Deserialize)]
|
||||
pub struct StoredUsagePerformancePercentilesRow {
|
||||
pub date: String,
|
||||
pub p50_response_time_ms: Option<u64>,
|
||||
pub p90_response_time_ms: Option<u64>,
|
||||
pub p99_response_time_ms: Option<u64>,
|
||||
pub p50_first_byte_time_ms: Option<u64>,
|
||||
pub p90_first_byte_time_ms: Option<u64>,
|
||||
pub p99_first_byte_time_ms: Option<u64>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Default, serde::Serialize, serde::Deserialize)]
|
||||
pub struct UsageCostSavingsSummaryQuery {
|
||||
pub created_from_unix_secs: u64,
|
||||
pub created_until_unix_secs: u64,
|
||||
pub user_id: Option<String>,
|
||||
pub provider_name: Option<String>,
|
||||
pub model: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Default, serde::Serialize, serde::Deserialize)]
|
||||
pub struct StoredUsageCostSavingsSummary {
|
||||
pub cache_read_tokens: u64,
|
||||
pub cache_read_cost_usd: f64,
|
||||
pub cache_creation_cost_usd: f64,
|
||||
pub estimated_full_cost_usd: f64,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
pub enum UsageTimeSeriesGranularity {
|
||||
@@ -694,6 +955,11 @@ pub trait UsageReadRepository: Send + Sync {
|
||||
id: &str,
|
||||
) -> Result<Option<StoredRequestUsageAudit>, crate::DataLayerError>;
|
||||
|
||||
async fn list_by_ids(
|
||||
&self,
|
||||
ids: &[String],
|
||||
) -> Result<Vec<StoredRequestUsageAudit>, crate::DataLayerError>;
|
||||
|
||||
async fn find_by_request_id(
|
||||
&self,
|
||||
request_id: &str,
|
||||
@@ -714,6 +980,16 @@ pub trait UsageReadRepository: Send + Sync {
|
||||
query: &UsageAuditListQuery,
|
||||
) -> Result<u64, crate::DataLayerError>;
|
||||
|
||||
async fn list_usage_audits_by_keyword_search(
|
||||
&self,
|
||||
query: &UsageAuditKeywordSearchQuery,
|
||||
) -> Result<Vec<StoredRequestUsageAudit>, crate::DataLayerError>;
|
||||
|
||||
async fn count_usage_audits_by_keyword_search(
|
||||
&self,
|
||||
query: &UsageAuditKeywordSearchQuery,
|
||||
) -> Result<u64, crate::DataLayerError>;
|
||||
|
||||
async fn aggregate_usage_audits(
|
||||
&self,
|
||||
query: &UsageAuditAggregationQuery,
|
||||
@@ -724,6 +1000,71 @@ pub trait UsageReadRepository: Send + Sync {
|
||||
query: &UsageAuditSummaryQuery,
|
||||
) -> Result<StoredUsageAuditSummary, crate::DataLayerError>;
|
||||
|
||||
async fn summarize_usage_cache_hit_summary(
|
||||
&self,
|
||||
query: &UsageCacheHitSummaryQuery,
|
||||
) -> Result<StoredUsageCacheHitSummary, crate::DataLayerError>;
|
||||
|
||||
async fn summarize_usage_settled_cost(
|
||||
&self,
|
||||
query: &UsageSettledCostSummaryQuery,
|
||||
) -> Result<StoredUsageSettledCostSummary, crate::DataLayerError>;
|
||||
|
||||
async fn summarize_usage_cache_affinity_hit_summary(
|
||||
&self,
|
||||
query: &UsageCacheAffinityHitSummaryQuery,
|
||||
) -> Result<StoredUsageCacheAffinityHitSummary, crate::DataLayerError>;
|
||||
|
||||
async fn list_usage_cache_affinity_intervals(
|
||||
&self,
|
||||
query: &UsageCacheAffinityIntervalQuery,
|
||||
) -> Result<Vec<StoredUsageCacheAffinityIntervalRow>, crate::DataLayerError>;
|
||||
|
||||
async fn summarize_dashboard_usage(
|
||||
&self,
|
||||
query: &UsageDashboardSummaryQuery,
|
||||
) -> Result<StoredUsageDashboardSummary, crate::DataLayerError>;
|
||||
|
||||
async fn list_dashboard_daily_breakdown(
|
||||
&self,
|
||||
query: &UsageDashboardDailyBreakdownQuery,
|
||||
) -> Result<Vec<StoredUsageDashboardDailyBreakdownRow>, crate::DataLayerError>;
|
||||
|
||||
async fn summarize_dashboard_provider_counts(
|
||||
&self,
|
||||
query: &UsageDashboardProviderCountsQuery,
|
||||
) -> Result<Vec<StoredUsageDashboardProviderCount>, crate::DataLayerError>;
|
||||
|
||||
async fn summarize_usage_breakdown(
|
||||
&self,
|
||||
query: &UsageBreakdownSummaryQuery,
|
||||
) -> Result<Vec<StoredUsageBreakdownSummaryRow>, crate::DataLayerError>;
|
||||
|
||||
async fn count_monitoring_usage_errors(
|
||||
&self,
|
||||
query: &UsageMonitoringErrorCountQuery,
|
||||
) -> Result<u64, crate::DataLayerError>;
|
||||
|
||||
async fn list_monitoring_usage_errors(
|
||||
&self,
|
||||
query: &UsageMonitoringErrorListQuery,
|
||||
) -> Result<Vec<StoredRequestUsageAudit>, crate::DataLayerError>;
|
||||
|
||||
async fn summarize_usage_error_distribution(
|
||||
&self,
|
||||
query: &UsageErrorDistributionQuery,
|
||||
) -> Result<Vec<StoredUsageErrorDistributionRow>, crate::DataLayerError>;
|
||||
|
||||
async fn summarize_usage_performance_percentiles(
|
||||
&self,
|
||||
query: &UsagePerformancePercentilesQuery,
|
||||
) -> Result<Vec<StoredUsagePerformancePercentilesRow>, crate::DataLayerError>;
|
||||
|
||||
async fn summarize_usage_cost_savings(
|
||||
&self,
|
||||
query: &UsageCostSavingsSummaryQuery,
|
||||
) -> Result<StoredUsageCostSavingsSummary, crate::DataLayerError>;
|
||||
|
||||
async fn summarize_usage_time_series(
|
||||
&self,
|
||||
query: &UsageTimeSeriesQuery,
|
||||
|
||||
@@ -188,6 +188,34 @@ impl AuthApiKeyReadRepository for InMemoryAuthApiKeySnapshotRepository {
|
||||
.collect())
|
||||
}
|
||||
|
||||
async fn list_export_api_keys_by_name_search(
|
||||
&self,
|
||||
name_search: &str,
|
||||
) -> Result<Vec<StoredAuthApiKeyExportRecord>, DataLayerError> {
|
||||
let name_search = name_search.trim().to_ascii_lowercase();
|
||||
if name_search.is_empty() {
|
||||
return Ok(Vec::new());
|
||||
}
|
||||
|
||||
let index = self
|
||||
.index
|
||||
.read()
|
||||
.expect("auth api key snapshot repository lock");
|
||||
Ok(index
|
||||
.export_by_api_key_id
|
||||
.values()
|
||||
.filter(|record| {
|
||||
record
|
||||
.name
|
||||
.as_deref()
|
||||
.unwrap_or_default()
|
||||
.to_ascii_lowercase()
|
||||
.contains(&name_search)
|
||||
})
|
||||
.cloned()
|
||||
.collect())
|
||||
}
|
||||
|
||||
async fn list_export_standalone_api_keys_page(
|
||||
&self,
|
||||
query: &StandaloneApiKeyExportListQuery,
|
||||
|
||||
@@ -182,6 +182,30 @@ WHERE api_keys.id = ANY($1::TEXT[])
|
||||
ORDER BY api_keys.id ASC
|
||||
"#;
|
||||
|
||||
const LIST_EXPORT_BY_NAME_SEARCH_SQL: &str = r#"
|
||||
SELECT
|
||||
api_keys.user_id,
|
||||
api_keys.id AS api_key_id,
|
||||
api_keys.key_hash,
|
||||
api_keys.key_encrypted,
|
||||
api_keys.name,
|
||||
api_keys.allowed_providers,
|
||||
api_keys.allowed_api_formats,
|
||||
api_keys.allowed_models,
|
||||
api_keys.rate_limit,
|
||||
api_keys.concurrent_limit,
|
||||
api_keys.force_capabilities,
|
||||
api_keys.is_active,
|
||||
CAST(EXTRACT(EPOCH FROM api_keys.expires_at) AS BIGINT) AS expires_at_unix_secs,
|
||||
api_keys.auto_delete_on_expiry,
|
||||
api_keys.total_requests,
|
||||
COALESCE(CAST(api_keys.total_cost_usd AS DOUBLE PRECISION), 0) AS total_cost_usd,
|
||||
api_keys.is_standalone
|
||||
FROM api_keys
|
||||
WHERE LOWER(COALESCE(api_keys.name, '')) LIKE $1
|
||||
ORDER BY api_keys.id ASC
|
||||
"#;
|
||||
|
||||
const LIST_EXPORT_STANDALONE_SQL: &str = r#"
|
||||
SELECT
|
||||
api_keys.user_id,
|
||||
@@ -751,6 +775,24 @@ impl SqlxAuthApiKeySnapshotReadRepository {
|
||||
.await
|
||||
}
|
||||
|
||||
pub async fn list_export_api_keys_by_name_search(
|
||||
&self,
|
||||
name_search: &str,
|
||||
) -> Result<Vec<StoredAuthApiKeyExportRecord>, DataLayerError> {
|
||||
let name_search = name_search.trim();
|
||||
if name_search.is_empty() {
|
||||
return Ok(Vec::new());
|
||||
}
|
||||
|
||||
Self::collect_query_rows(
|
||||
sqlx::query(LIST_EXPORT_BY_NAME_SEARCH_SQL)
|
||||
.bind(format!("%{}%", name_search.to_ascii_lowercase()))
|
||||
.fetch(&self.pool),
|
||||
map_auth_api_key_export_row,
|
||||
)
|
||||
.await
|
||||
}
|
||||
|
||||
pub async fn summarize_export_api_keys_by_user_ids(
|
||||
&self,
|
||||
user_ids: &[String],
|
||||
@@ -886,6 +928,13 @@ impl AuthApiKeyReadRepository for SqlxAuthApiKeySnapshotReadRepository {
|
||||
Self::list_export_api_keys_by_ids(self, api_key_ids).await
|
||||
}
|
||||
|
||||
async fn list_export_api_keys_by_name_search(
|
||||
&self,
|
||||
name_search: &str,
|
||||
) -> Result<Vec<StoredAuthApiKeyExportRecord>, DataLayerError> {
|
||||
Self::list_export_api_keys_by_name_search(self, name_search).await
|
||||
}
|
||||
|
||||
async fn list_export_standalone_api_keys_page(
|
||||
&self,
|
||||
query: &StandaloneApiKeyExportListQuery,
|
||||
|
||||
@@ -447,6 +447,11 @@ pub trait AuthApiKeyReadRepository: Send + Sync {
|
||||
api_key_ids: &[String],
|
||||
) -> Result<Vec<StoredAuthApiKeyExportRecord>, crate::DataLayerError>;
|
||||
|
||||
async fn list_export_api_keys_by_name_search(
|
||||
&self,
|
||||
name_search: &str,
|
||||
) -> Result<Vec<StoredAuthApiKeyExportRecord>, crate::DataLayerError>;
|
||||
|
||||
async fn list_export_standalone_api_keys_page(
|
||||
&self,
|
||||
query: &StandaloneApiKeyExportListQuery,
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -5,11 +5,23 @@ mod sql;
|
||||
pub(crate) use aether_data_contracts::repository::usage::{
|
||||
StoredProviderApiKeyUsageSummary, StoredProviderUsageSummary, StoredProviderUsageWindow,
|
||||
StoredRequestUsageAudit, StoredUsageAuditAggregation, StoredUsageAuditSummary,
|
||||
StoredUsageDailySummary, StoredUsageLeaderboardSummary, StoredUsageTimeSeriesBucket,
|
||||
UpsertUsageRecord, UsageAuditAggregationGroupBy, UsageAuditAggregationQuery,
|
||||
UsageAuditListQuery, UsageAuditSummaryQuery, UsageDailyHeatmapQuery, UsageLeaderboardGroupBy,
|
||||
UsageLeaderboardQuery, UsageReadRepository, UsageRepository, UsageTimeSeriesGranularity,
|
||||
UsageTimeSeriesQuery, UsageWriteRepository,
|
||||
StoredUsageBreakdownSummaryRow, StoredUsageCacheAffinityHitSummary,
|
||||
StoredUsageCacheAffinityIntervalRow, StoredUsageCacheHitSummary, StoredUsageCostSavingsSummary,
|
||||
StoredUsageDailySummary, StoredUsageDashboardDailyBreakdownRow,
|
||||
StoredUsageDashboardProviderCount, StoredUsageDashboardSummary,
|
||||
StoredUsageErrorDistributionRow, StoredUsageLeaderboardSummary,
|
||||
StoredUsagePerformancePercentilesRow, StoredUsageSettledCostSummary,
|
||||
StoredUsageTimeSeriesBucket, UpsertUsageRecord, UsageAuditAggregationGroupBy,
|
||||
UsageAuditAggregationQuery, UsageAuditKeywordSearchQuery, UsageAuditListQuery,
|
||||
UsageAuditSummaryQuery, UsageBreakdownGroupBy, UsageBreakdownSummaryQuery,
|
||||
UsageCacheAffinityHitSummaryQuery, UsageCacheAffinityIntervalGroupBy,
|
||||
UsageCacheAffinityIntervalQuery, UsageCacheHitSummaryQuery, UsageCostSavingsSummaryQuery,
|
||||
UsageDailyHeatmapQuery, UsageDashboardDailyBreakdownQuery, UsageDashboardProviderCountsQuery,
|
||||
UsageDashboardSummaryQuery, UsageErrorDistributionQuery, UsageLeaderboardGroupBy,
|
||||
UsageLeaderboardQuery, UsageMonitoringErrorCountQuery, UsageMonitoringErrorListQuery,
|
||||
UsagePerformancePercentilesQuery, UsageReadRepository, UsageRepository,
|
||||
UsageSettledCostSummaryQuery, UsageTimeSeriesGranularity, UsageTimeSeriesQuery,
|
||||
UsageWriteRepository,
|
||||
};
|
||||
pub use memory::InMemoryUsageReadRepository;
|
||||
pub use sql::SqlxUsageReadRepository;
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -95,6 +95,29 @@ impl UserReadRepository for InMemoryUserReadRepository {
|
||||
.collect())
|
||||
}
|
||||
|
||||
async fn list_users_by_username_search(
|
||||
&self,
|
||||
username_search: &str,
|
||||
) -> Result<Vec<StoredUserSummary>, DataLayerError> {
|
||||
let username_search = username_search.trim().to_ascii_lowercase();
|
||||
if username_search.is_empty() {
|
||||
return Ok(Vec::new());
|
||||
}
|
||||
|
||||
Ok(self
|
||||
.by_id
|
||||
.read()
|
||||
.expect("user repository lock")
|
||||
.values()
|
||||
.filter(|user| {
|
||||
user.username
|
||||
.to_ascii_lowercase()
|
||||
.contains(&username_search)
|
||||
})
|
||||
.cloned()
|
||||
.collect())
|
||||
}
|
||||
|
||||
async fn list_non_admin_export_users(
|
||||
&self,
|
||||
) -> Result<Vec<StoredUserExportRow>, DataLayerError> {
|
||||
|
||||
@@ -21,6 +21,20 @@ WHERE id = ANY($1::text[])
|
||||
ORDER BY id ASC
|
||||
"#;
|
||||
|
||||
const LIST_USERS_BY_USERNAME_SEARCH_SQL: &str = r#"
|
||||
SELECT
|
||||
id,
|
||||
username,
|
||||
email,
|
||||
role::text AS role,
|
||||
is_active,
|
||||
is_deleted
|
||||
FROM users
|
||||
WHERE is_deleted IS FALSE
|
||||
AND LOWER(username) LIKE $1
|
||||
ORDER BY id ASC
|
||||
"#;
|
||||
|
||||
const LIST_NON_ADMIN_EXPORT_USERS_SQL: &str = r#"
|
||||
SELECT
|
||||
id,
|
||||
@@ -199,6 +213,24 @@ impl SqlxUserReadRepository {
|
||||
.await
|
||||
}
|
||||
|
||||
pub async fn list_users_by_username_search(
|
||||
&self,
|
||||
username_search: &str,
|
||||
) -> Result<Vec<StoredUserSummary>, DataLayerError> {
|
||||
let username_search = username_search.trim();
|
||||
if username_search.is_empty() {
|
||||
return Ok(Vec::new());
|
||||
}
|
||||
|
||||
collect_query_rows(
|
||||
sqlx::query(LIST_USERS_BY_USERNAME_SEARCH_SQL)
|
||||
.bind(format!("%{}%", username_search.to_ascii_lowercase()))
|
||||
.fetch(&self.pool),
|
||||
map_user_row,
|
||||
)
|
||||
.await
|
||||
}
|
||||
|
||||
pub async fn list_non_admin_export_users(
|
||||
&self,
|
||||
) -> Result<Vec<StoredUserExportRow>, DataLayerError> {
|
||||
@@ -383,6 +415,13 @@ impl UserReadRepository for SqlxUserReadRepository {
|
||||
self.list_users_by_ids(user_ids).await
|
||||
}
|
||||
|
||||
async fn list_users_by_username_search(
|
||||
&self,
|
||||
username_search: &str,
|
||||
) -> Result<Vec<StoredUserSummary>, DataLayerError> {
|
||||
self.list_users_by_username_search(username_search).await
|
||||
}
|
||||
|
||||
async fn list_non_admin_export_users(
|
||||
&self,
|
||||
) -> Result<Vec<StoredUserExportRow>, DataLayerError> {
|
||||
|
||||
@@ -393,6 +393,11 @@ pub trait UserReadRepository: Send + Sync {
|
||||
user_ids: &[String],
|
||||
) -> Result<Vec<StoredUserSummary>, crate::DataLayerError>;
|
||||
|
||||
async fn list_users_by_username_search(
|
||||
&self,
|
||||
username_search: &str,
|
||||
) -> Result<Vec<StoredUserSummary>, crate::DataLayerError>;
|
||||
|
||||
async fn list_export_users(&self) -> Result<Vec<StoredUserExportRow>, crate::DataLayerError>;
|
||||
|
||||
async fn list_export_users_page(
|
||||
|
||||
Reference in New Issue
Block a user