mirror of
https://github.com/fawney19/Aether.git
synced 2026-09-03 01:40: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:
@@ -6,8 +6,8 @@ mod usage;
|
||||
pub(super) use self::monitoring::maybe_build_local_admin_monitoring_response;
|
||||
pub(super) use self::routes::maybe_build_local_admin_observability_response;
|
||||
pub(crate) use self::stats::{
|
||||
admin_stats_bad_request_response, list_usage_for_optional_range, list_usage_for_range,
|
||||
maybe_build_local_admin_stats_response, parse_bounded_u32, round_to,
|
||||
admin_stats_bad_request_response, maybe_build_local_admin_stats_response, parse_bounded_u32,
|
||||
round_to,
|
||||
};
|
||||
pub(crate) use self::stats::{AdminStatsTimeRange, AdminStatsUsageFilter};
|
||||
pub(crate) use self::usage::maybe_build_local_admin_usage_response;
|
||||
|
||||
@@ -4,7 +4,6 @@ use super::route_filters::{
|
||||
parse_admin_monitoring_limit, parse_admin_monitoring_offset,
|
||||
parse_admin_monitoring_username_filter,
|
||||
};
|
||||
use super::usage_helpers::admin_monitoring_usage_is_error;
|
||||
use crate::constants::INTERNAL_GATEWAY_PATH_PREFIXES;
|
||||
use crate::handlers::admin::request::{AdminAppState, AdminRequestContext};
|
||||
use crate::query::monitoring as monitoring_query;
|
||||
@@ -16,7 +15,9 @@ use aether_admin::observability::monitoring::{
|
||||
build_admin_monitoring_system_status_payload_response,
|
||||
build_admin_monitoring_user_behavior_payload_response,
|
||||
};
|
||||
use aether_data_contracts::repository::usage::UsageAuditListQuery;
|
||||
use aether_data_contracts::repository::usage::{
|
||||
UsageAuditSummaryQuery, UsageMonitoringErrorCountQuery,
|
||||
};
|
||||
use axum::{body::Body, response::Response};
|
||||
|
||||
pub(super) async fn build_admin_monitoring_audit_logs_response(
|
||||
@@ -196,30 +197,27 @@ pub(super) async fn build_admin_monitoring_system_status_response(
|
||||
.saturating_add(standalone_api_key_summary.active);
|
||||
|
||||
let today_usage = state
|
||||
.list_usage_audits(&UsageAuditListQuery {
|
||||
created_from_unix_secs: Some(today_start.timestamp().max(0) as u64),
|
||||
..Default::default()
|
||||
.summarize_usage_audits(&UsageAuditSummaryQuery {
|
||||
created_from_unix_secs: today_start.timestamp().max(0) as u64,
|
||||
created_until_unix_secs: now_unix_secs.saturating_add(1),
|
||||
user_id: None,
|
||||
provider_name: None,
|
||||
model: None,
|
||||
})
|
||||
.await?;
|
||||
let today_requests = today_usage.len();
|
||||
let today_tokens = today_usage
|
||||
.iter()
|
||||
.map(|item| item.total_tokens)
|
||||
.sum::<u64>();
|
||||
let today_cost = today_usage
|
||||
.iter()
|
||||
.map(|item| item.total_cost_usd)
|
||||
.sum::<f64>();
|
||||
let today_requests = usize::try_from(today_usage.total_requests).unwrap_or(usize::MAX);
|
||||
let today_tokens = today_usage.recorded_total_tokens;
|
||||
let today_cost = today_usage.total_cost_usd;
|
||||
|
||||
let recent_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)
|
||||
.count();
|
||||
let recent_errors = usize::try_from(
|
||||
state
|
||||
.count_monitoring_usage_errors(&UsageMonitoringErrorCountQuery {
|
||||
created_from_unix_secs: recent_error_from.timestamp().max(0) as u64,
|
||||
created_until_unix_secs: now_unix_secs.saturating_add(1),
|
||||
})
|
||||
.await?,
|
||||
)
|
||||
.unwrap_or(usize::MAX);
|
||||
let tunnel = state.tunnel.stats();
|
||||
|
||||
Ok(build_admin_monitoring_system_status_payload_response(
|
||||
|
||||
@@ -8,7 +8,7 @@ use crate::handlers::admin::observability::stats::round_to;
|
||||
use crate::handlers::admin::request::AdminAppState;
|
||||
use crate::scheduler::affinity::SCHEDULER_AFFINITY_TTL;
|
||||
use crate::GatewayError;
|
||||
use aether_data_contracts::repository::usage::UsageAuditListQuery;
|
||||
use aether_data_contracts::repository::usage::UsageCacheHitSummaryQuery;
|
||||
|
||||
async fn count_admin_monitoring_cache_affinity_entries(state: &AdminAppState<'_>) -> usize {
|
||||
list_admin_monitoring_cache_affinity_records(state)
|
||||
@@ -325,27 +325,26 @@ pub(super) async fn build_admin_monitoring_cache_snapshot(
|
||||
.unwrap_or_else(|| "provider".to_string());
|
||||
|
||||
let now = chrono::Utc::now();
|
||||
let usage = if state.has_usage_data_reader() {
|
||||
let usage_summary = if state.has_usage_data_reader() {
|
||||
state
|
||||
.list_usage_audits(&UsageAuditListQuery {
|
||||
created_from_unix_secs: Some(
|
||||
(now - chrono::Duration::hours(24)).timestamp().max(0) as u64,
|
||||
),
|
||||
..Default::default()
|
||||
.summarize_usage_cache_hit_summary(&UsageCacheHitSummaryQuery {
|
||||
created_from_unix_secs: (now - chrono::Duration::hours(24)).timestamp().max(0)
|
||||
as u64,
|
||||
created_until_unix_secs: now.timestamp().max(0) as u64,
|
||||
user_id: None,
|
||||
})
|
||||
.await?
|
||||
} else {
|
||||
Vec::new()
|
||||
Default::default()
|
||||
};
|
||||
let cache_hits = usage
|
||||
.iter()
|
||||
.filter(|item| item.cache_read_input_tokens > 0)
|
||||
.count();
|
||||
let cache_misses = usage.len().saturating_sub(cache_hits);
|
||||
let cache_hit_rate = if usage.is_empty() {
|
||||
let cache_hits = usage_summary.cache_hit_requests as usize;
|
||||
let cache_misses = usage_summary
|
||||
.total_requests
|
||||
.saturating_sub(usage_summary.cache_hit_requests) as usize;
|
||||
let cache_hit_rate = if usage_summary.total_requests == 0 {
|
||||
0.0
|
||||
} else {
|
||||
round_to(cache_hits as f64 / usage.len() as f64, 4)
|
||||
round_to(cache_hits as f64 / usage_summary.total_requests as f64, 4)
|
||||
};
|
||||
let total_affinities = count_admin_monitoring_cache_affinity_entries(state).await;
|
||||
let storage_type = if state.redis_kv_runner().is_some() {
|
||||
|
||||
@@ -1,9 +1,8 @@
|
||||
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,
|
||||
provider_catalog::StoredProviderCatalogKey, usage::UsageMonitoringErrorListQuery,
|
||||
};
|
||||
use serde_json::json;
|
||||
use std::collections::BTreeMap;
|
||||
@@ -152,14 +151,12 @@ pub(super) async fn build_admin_monitoring_resilience_snapshot(
|
||||
}
|
||||
|
||||
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()
|
||||
.list_monitoring_usage_errors(&UsageMonitoringErrorListQuery {
|
||||
created_from_unix_secs: recent_error_from.timestamp().max(0) as u64,
|
||||
created_until_unix_secs: (now.timestamp().max(0) as u64).saturating_add(1),
|
||||
limit: None,
|
||||
})
|
||||
.await?
|
||||
.into_iter()
|
||||
.filter(admin_monitoring_usage_is_error)
|
||||
.collect::<Vec<_>>();
|
||||
.await?;
|
||||
recent_usage_errors
|
||||
.sort_by(|left, right| right.created_at_unix_ms.cmp(&left.created_at_unix_ms));
|
||||
|
||||
|
||||
@@ -8,13 +8,14 @@ use aether_admin::observability::stats::{
|
||||
admin_stats_error_distribution_empty_response,
|
||||
admin_stats_performance_percentiles_empty_response, admin_stats_time_series_empty_response,
|
||||
build_admin_stats_comparison_response_from_aggregates,
|
||||
build_admin_stats_error_distribution_response,
|
||||
build_admin_stats_performance_percentiles_response,
|
||||
build_admin_stats_error_distribution_response_from_summaries,
|
||||
build_admin_stats_performance_percentiles_response_from_summaries,
|
||||
build_admin_stats_time_series_response_from_summaries, AdminStatsAggregate,
|
||||
AdminStatsComparisonType, AdminStatsGranularity, AdminStatsTimeRange, AdminStatsUsageFilter,
|
||||
};
|
||||
use aether_data_contracts::repository::usage::{
|
||||
UsageAuditSummaryQuery, UsageTimeSeriesGranularity, UsageTimeSeriesQuery,
|
||||
UsageAuditSummaryQuery, UsageErrorDistributionQuery, UsagePerformancePercentilesQuery,
|
||||
UsageTimeSeriesGranularity, UsageTimeSeriesQuery,
|
||||
};
|
||||
use axum::{body::Body, http, response::Response};
|
||||
|
||||
@@ -124,13 +125,20 @@ pub(super) async fn maybe_build_local_admin_stats_analytics_response(
|
||||
return Ok(Some(admin_stats_error_distribution_empty_response()));
|
||||
}
|
||||
|
||||
let usage = state
|
||||
.list_admin_usage_for_range(&time_range, &AdminStatsUsageFilter::default())
|
||||
let Some((created_from_unix_secs, created_until_unix_secs)) = time_range.to_unix_bounds()
|
||||
else {
|
||||
return Ok(Some(admin_stats_error_distribution_empty_response()));
|
||||
};
|
||||
let rows = state
|
||||
.summarize_usage_error_distribution(&UsageErrorDistributionQuery {
|
||||
created_from_unix_secs,
|
||||
created_until_unix_secs,
|
||||
tz_offset_minutes: time_range.tz_offset_minutes,
|
||||
})
|
||||
.await?;
|
||||
return Ok(Some(build_admin_stats_error_distribution_response(
|
||||
&time_range,
|
||||
&usage,
|
||||
)));
|
||||
return Ok(Some(
|
||||
build_admin_stats_error_distribution_response_from_summaries(&rows),
|
||||
));
|
||||
}
|
||||
|
||||
if request_context.route_kind() == Some("performance_percentiles")
|
||||
@@ -149,13 +157,20 @@ pub(super) async fn maybe_build_local_admin_stats_analytics_response(
|
||||
return Ok(Some(admin_stats_performance_percentiles_empty_response()));
|
||||
}
|
||||
|
||||
let usage = state
|
||||
.list_admin_usage_for_range(&time_range, &AdminStatsUsageFilter::default())
|
||||
let Some((created_from_unix_secs, created_until_unix_secs)) = time_range.to_unix_bounds()
|
||||
else {
|
||||
return Ok(Some(admin_stats_performance_percentiles_empty_response()));
|
||||
};
|
||||
let rows = state
|
||||
.summarize_usage_performance_percentiles(&UsagePerformancePercentilesQuery {
|
||||
created_from_unix_secs,
|
||||
created_until_unix_secs,
|
||||
tz_offset_minutes: time_range.tz_offset_minutes,
|
||||
})
|
||||
.await?;
|
||||
return Ok(Some(build_admin_stats_performance_percentiles_response(
|
||||
&time_range,
|
||||
&usage,
|
||||
)));
|
||||
return Ok(Some(
|
||||
build_admin_stats_performance_percentiles_response_from_summaries(&time_range, &rows),
|
||||
));
|
||||
}
|
||||
|
||||
if request_context.route_kind() == Some("time_series")
|
||||
|
||||
@@ -6,7 +6,12 @@ use crate::GatewayError;
|
||||
use aether_admin::observability::stats::{
|
||||
admin_stats_bad_request_response, admin_stats_cost_forecast_empty_response,
|
||||
admin_stats_cost_savings_empty_response, build_admin_stats_cost_forecast_response,
|
||||
build_admin_stats_cost_savings_response, AdminStatsGranularity, AdminStatsUsageFilter,
|
||||
build_admin_stats_cost_forecast_response_from_summaries,
|
||||
build_admin_stats_cost_savings_response, build_admin_stats_cost_savings_response_from_summary,
|
||||
AdminStatsGranularity, AdminStatsUsageFilter,
|
||||
};
|
||||
use aether_data_contracts::repository::usage::{
|
||||
UsageCostSavingsSummaryQuery, UsageTimeSeriesGranularity, UsageTimeSeriesQuery,
|
||||
};
|
||||
use axum::{body::Body, http, response::Response};
|
||||
|
||||
@@ -61,14 +66,28 @@ pub(super) async fn maybe_build_local_admin_stats_cost_response(
|
||||
return Ok(Some(admin_stats_bad_request_response(detail)));
|
||||
}
|
||||
|
||||
let usage = state
|
||||
.list_admin_usage_for_range(&time_range, &AdminStatsUsageFilter::default())
|
||||
let Some((created_from_unix_secs, created_until_unix_secs)) = time_range.to_unix_bounds()
|
||||
else {
|
||||
return Ok(Some(admin_stats_cost_forecast_empty_response()));
|
||||
};
|
||||
let buckets = state
|
||||
.summarize_usage_time_series(&UsageTimeSeriesQuery {
|
||||
created_from_unix_secs,
|
||||
created_until_unix_secs,
|
||||
granularity: UsageTimeSeriesGranularity::Day,
|
||||
tz_offset_minutes: time_range.tz_offset_minutes,
|
||||
user_id: None,
|
||||
provider_name: None,
|
||||
model: None,
|
||||
})
|
||||
.await?;
|
||||
return Ok(Some(build_admin_stats_cost_forecast_response(
|
||||
&time_range,
|
||||
forecast_days,
|
||||
&usage,
|
||||
)));
|
||||
return Ok(Some(
|
||||
build_admin_stats_cost_forecast_response_from_summaries(
|
||||
&time_range,
|
||||
forecast_days,
|
||||
&buckets,
|
||||
),
|
||||
));
|
||||
}
|
||||
|
||||
if request_context
|
||||
@@ -94,11 +113,23 @@ pub(super) async fn maybe_build_local_admin_stats_cost_response(
|
||||
provider_name: query_param_value(query, "provider_name"),
|
||||
model: query_param_value(query, "model"),
|
||||
};
|
||||
let usage = state
|
||||
.list_admin_usage_for_range(&time_range, &filters)
|
||||
let Some((created_from_unix_secs, created_until_unix_secs)) = time_range.to_unix_bounds()
|
||||
else {
|
||||
return Ok(Some(admin_stats_cost_savings_empty_response()));
|
||||
};
|
||||
let summary = state
|
||||
.summarize_usage_cost_savings(&UsageCostSavingsSummaryQuery {
|
||||
created_from_unix_secs,
|
||||
created_until_unix_secs,
|
||||
user_id: None,
|
||||
provider_name: filters.provider_name,
|
||||
model: filters.model,
|
||||
})
|
||||
.await?;
|
||||
|
||||
return Ok(Some(build_admin_stats_cost_savings_response(&usage)));
|
||||
return Ok(Some(build_admin_stats_cost_savings_response_from_summary(
|
||||
&summary,
|
||||
)));
|
||||
}
|
||||
|
||||
Ok(None)
|
||||
|
||||
@@ -8,10 +8,7 @@ mod leaderboard;
|
||||
mod leaderboard_routes;
|
||||
mod provider_quota_routes;
|
||||
mod range;
|
||||
pub(crate) use self::range::{
|
||||
list_usage_for_optional_range, list_usage_for_range, parse_bounded_u32,
|
||||
resolve_admin_usage_time_range,
|
||||
};
|
||||
pub(crate) use self::range::{parse_bounded_u32, resolve_admin_usage_time_range};
|
||||
pub(crate) use aether_admin::observability::stats::{
|
||||
admin_stats_bad_request_response, aggregate_usage_stats, round_to, AdminStatsTimeRange,
|
||||
AdminStatsUsageFilter,
|
||||
|
||||
@@ -1,12 +1,9 @@
|
||||
use crate::handlers::admin::request::AdminAppState;
|
||||
use crate::GatewayError;
|
||||
pub(crate) use aether_admin::observability::stats::parse_bounded_u32;
|
||||
use aether_admin::observability::stats::AdminStatsTimeRange;
|
||||
pub(super) use aether_admin::observability::stats::{
|
||||
admin_usage_default_days, build_comparison_range, build_time_range_from_days, parse_naive_date,
|
||||
parse_nonnegative_usize, parse_tz_offset_minutes, resolve_preset_dates, user_today,
|
||||
};
|
||||
use aether_admin::observability::stats::{AdminStatsTimeRange, AdminStatsUsageFilter};
|
||||
use aether_data_contracts::repository::usage::{StoredRequestUsageAudit, UsageAuditListQuery};
|
||||
|
||||
pub(crate) fn resolve_admin_usage_time_range(
|
||||
query: Option<&str>,
|
||||
@@ -23,46 +20,3 @@ pub(crate) fn resolve_admin_usage_time_range(
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) async fn list_usage_for_range(
|
||||
state: &AdminAppState<'_>,
|
||||
time_range: &AdminStatsTimeRange,
|
||||
filters: &AdminStatsUsageFilter,
|
||||
) -> Result<Vec<StoredRequestUsageAudit>, GatewayError> {
|
||||
let Some((created_from_unix_secs, created_until_unix_secs)) = time_range.to_unix_bounds()
|
||||
else {
|
||||
return Ok(Vec::new());
|
||||
};
|
||||
|
||||
state
|
||||
.list_usage_audits(&UsageAuditListQuery {
|
||||
created_from_unix_secs: Some(created_from_unix_secs),
|
||||
created_until_unix_secs: Some(created_until_unix_secs),
|
||||
user_id: filters.user_id.clone(),
|
||||
provider_name: filters.provider_name.clone(),
|
||||
model: filters.model.clone(),
|
||||
api_format: None,
|
||||
statuses: None,
|
||||
is_stream: None,
|
||||
error_only: false,
|
||||
limit: None,
|
||||
offset: None,
|
||||
newest_first: false,
|
||||
})
|
||||
.await
|
||||
}
|
||||
|
||||
pub(crate) async fn list_usage_for_optional_range(
|
||||
state: &AdminAppState<'_>,
|
||||
time_range: Option<&AdminStatsTimeRange>,
|
||||
filters: &AdminStatsUsageFilter,
|
||||
) -> Result<Vec<StoredRequestUsageAudit>, GatewayError> {
|
||||
match time_range {
|
||||
Some(time_range) => list_usage_for_range(state, time_range, filters).await,
|
||||
None => {
|
||||
let default_time_range = build_time_range_from_days(1, 0)
|
||||
.map_err(|detail| GatewayError::Internal(detail.to_string()))?;
|
||||
list_usage_for_range(state, &default_time_range, filters).await
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,35 +1,25 @@
|
||||
use crate::handlers::admin::request::AdminAppState;
|
||||
use crate::GatewayError;
|
||||
use aether_data_contracts::repository::usage::{StoredRequestUsageAudit, UsageAuditListQuery};
|
||||
use aether_data_contracts::repository::usage::{
|
||||
StoredUsageCacheAffinityIntervalRow, UsageCacheAffinityIntervalGroupBy,
|
||||
UsageCacheAffinityIntervalQuery,
|
||||
};
|
||||
|
||||
pub(in super::super) async fn list_recent_completed_usage_for_cache_affinity(
|
||||
pub(in super::super) async fn list_usage_cache_affinity_intervals(
|
||||
state: &AdminAppState<'_>,
|
||||
hours: u32,
|
||||
group_by: UsageCacheAffinityIntervalGroupBy,
|
||||
user_id: Option<&str>,
|
||||
) -> Result<Vec<StoredRequestUsageAudit>, GatewayError> {
|
||||
api_key_id: Option<&str>,
|
||||
) -> Result<Vec<StoredUsageCacheAffinityIntervalRow>, GatewayError> {
|
||||
let now_unix_secs = u64::try_from(chrono::Utc::now().timestamp()).unwrap_or_default();
|
||||
let created_from_unix_secs = now_unix_secs.saturating_sub(u64::from(hours) * 3600);
|
||||
let mut items = state
|
||||
.list_usage_audits(&UsageAuditListQuery {
|
||||
created_from_unix_secs: Some(created_from_unix_secs),
|
||||
created_until_unix_secs: None,
|
||||
state
|
||||
.list_usage_cache_affinity_intervals(&UsageCacheAffinityIntervalQuery {
|
||||
created_from_unix_secs: now_unix_secs.saturating_sub(u64::from(hours) * 3600),
|
||||
created_until_unix_secs: now_unix_secs.saturating_add(1),
|
||||
group_by,
|
||||
user_id: user_id.map(ToOwned::to_owned),
|
||||
provider_name: None,
|
||||
model: None,
|
||||
api_format: None,
|
||||
statuses: None,
|
||||
is_stream: None,
|
||||
error_only: false,
|
||||
limit: None,
|
||||
offset: None,
|
||||
newest_first: false,
|
||||
api_key_id: api_key_id.map(ToOwned::to_owned),
|
||||
})
|
||||
.await?;
|
||||
items.retain(|item| item.status == "completed");
|
||||
items.sort_by(|left, right| {
|
||||
left.created_at_unix_ms
|
||||
.cmp(&right.created_at_unix_ms)
|
||||
.then_with(|| left.id.cmp(&right.id))
|
||||
});
|
||||
Ok(items)
|
||||
.await
|
||||
}
|
||||
|
||||
@@ -3,6 +3,6 @@ mod cache_affinity;
|
||||
mod filters;
|
||||
|
||||
pub(super) use aggregations::admin_usage_aggregation_by_user_json;
|
||||
pub(super) use cache_affinity::list_recent_completed_usage_for_cache_affinity;
|
||||
pub(super) use cache_affinity::list_usage_cache_affinity_intervals;
|
||||
pub(super) use filters::admin_usage_api_key_names;
|
||||
pub(super) use filters::admin_usage_provider_key_names;
|
||||
|
||||
@@ -1,14 +1,12 @@
|
||||
use super::super::super::stats::round_to;
|
||||
use super::super::analytics::list_recent_completed_usage_for_cache_affinity;
|
||||
use crate::handlers::admin::request::{AdminAppState, AdminRequestContext};
|
||||
use crate::handlers::admin::shared::query_param_value;
|
||||
use crate::GatewayError;
|
||||
use aether_admin::observability::usage::{
|
||||
admin_usage_bad_request_response, admin_usage_cache_creation_tokens,
|
||||
admin_usage_data_unavailable_response, admin_usage_matches_optional_id,
|
||||
admin_usage_parse_recent_hours, admin_usage_total_input_context,
|
||||
ADMIN_USAGE_DATA_UNAVAILABLE_DETAIL,
|
||||
admin_usage_bad_request_response, admin_usage_data_unavailable_response,
|
||||
admin_usage_parse_recent_hours, ADMIN_USAGE_DATA_UNAVAILABLE_DETAIL,
|
||||
};
|
||||
use aether_data_contracts::repository::usage::UsageCacheAffinityHitSummaryQuery;
|
||||
use axum::{
|
||||
body::Body,
|
||||
response::{IntoResponse, Response},
|
||||
@@ -33,65 +31,44 @@ pub(super) async fn build_admin_usage_cache_affinity_hit_analysis_response(
|
||||
};
|
||||
let user_id = query_param_value(query, "user_id");
|
||||
let api_key_id = query_param_value(query, "api_key_id");
|
||||
let usage =
|
||||
list_recent_completed_usage_for_cache_affinity(state, hours, user_id.as_deref()).await?;
|
||||
let filtered: Vec<_> = usage
|
||||
.into_iter()
|
||||
.filter(|item| {
|
||||
admin_usage_matches_optional_id(item.user_id.as_deref(), user_id.as_deref())
|
||||
&& admin_usage_matches_optional_id(
|
||||
item.api_key_id.as_deref(),
|
||||
api_key_id.as_deref(),
|
||||
)
|
||||
let now_unix_secs = u64::try_from(chrono::Utc::now().timestamp()).unwrap_or_default();
|
||||
let summary = state
|
||||
.summarize_usage_cache_affinity_hit_summary(&UsageCacheAffinityHitSummaryQuery {
|
||||
created_from_unix_secs: now_unix_secs.saturating_sub(u64::from(hours) * 3600),
|
||||
created_until_unix_secs: now_unix_secs.saturating_add(1),
|
||||
user_id,
|
||||
api_key_id,
|
||||
})
|
||||
.collect();
|
||||
let total_requests = filtered.len();
|
||||
let total_input_tokens: u64 = filtered.iter().map(|item| item.input_tokens).sum();
|
||||
let total_cache_read_tokens: u64 = filtered
|
||||
.iter()
|
||||
.map(|item| item.cache_read_input_tokens)
|
||||
.sum();
|
||||
let total_cache_creation_tokens: u64 =
|
||||
filtered.iter().map(admin_usage_cache_creation_tokens).sum();
|
||||
let total_input_context: u64 = filtered.iter().map(admin_usage_total_input_context).sum();
|
||||
let total_cache_read_cost: f64 = filtered.iter().map(|item| item.cache_read_cost_usd).sum();
|
||||
let total_cache_creation_cost: f64 = filtered
|
||||
.iter()
|
||||
.map(|item| item.cache_creation_cost_usd)
|
||||
.sum();
|
||||
let requests_with_cache_hit = filtered
|
||||
.iter()
|
||||
.filter(|item| item.cache_read_input_tokens > 0)
|
||||
.count();
|
||||
let token_cache_hit_rate = if total_input_context == 0 {
|
||||
.await?;
|
||||
let token_cache_hit_rate = if summary.total_input_context == 0 {
|
||||
0.0
|
||||
} else {
|
||||
round_to(
|
||||
total_cache_read_tokens as f64 / total_input_context as f64 * 100.0,
|
||||
summary.cache_read_tokens as f64 / summary.total_input_context as f64 * 100.0,
|
||||
2,
|
||||
)
|
||||
};
|
||||
let request_cache_hit_rate = if total_requests == 0 {
|
||||
let request_cache_hit_rate = if summary.total_requests == 0 {
|
||||
0.0
|
||||
} else {
|
||||
round_to(
|
||||
requests_with_cache_hit as f64 / total_requests as f64 * 100.0,
|
||||
summary.requests_with_cache_hit as f64 / summary.total_requests as f64 * 100.0,
|
||||
2,
|
||||
)
|
||||
};
|
||||
|
||||
Ok(Json(json!({
|
||||
"analysis_period_hours": hours,
|
||||
"total_requests": total_requests,
|
||||
"requests_with_cache_hit": requests_with_cache_hit,
|
||||
"total_requests": summary.total_requests,
|
||||
"requests_with_cache_hit": summary.requests_with_cache_hit,
|
||||
"request_cache_hit_rate": request_cache_hit_rate,
|
||||
"total_input_tokens": total_input_tokens,
|
||||
"total_cache_read_tokens": total_cache_read_tokens,
|
||||
"total_cache_creation_tokens": total_cache_creation_tokens,
|
||||
"total_input_tokens": summary.input_tokens,
|
||||
"total_cache_read_tokens": summary.cache_read_tokens,
|
||||
"total_cache_creation_tokens": summary.cache_creation_tokens,
|
||||
"token_cache_hit_rate": token_cache_hit_rate,
|
||||
"total_cache_read_cost_usd": round_to(total_cache_read_cost, 4),
|
||||
"total_cache_creation_cost_usd": round_to(total_cache_creation_cost, 4),
|
||||
"estimated_savings_usd": round_to(total_cache_read_cost * 9.0, 4),
|
||||
"total_cache_read_cost_usd": round_to(summary.cache_read_cost_usd, 4),
|
||||
"total_cache_creation_cost_usd": round_to(summary.cache_creation_cost_usd, 4),
|
||||
"estimated_savings_usd": round_to(summary.cache_read_cost_usd * 9.0, 4),
|
||||
}))
|
||||
.into_response())
|
||||
}
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
use super::super::analytics::list_recent_completed_usage_for_cache_affinity;
|
||||
use super::super::analytics::list_usage_cache_affinity_intervals;
|
||||
use crate::handlers::admin::request::{AdminAppState, AdminRequestContext};
|
||||
use crate::handlers::admin::shared::{query_param_bool, query_param_value, unix_secs_to_rfc3339};
|
||||
use crate::GatewayError;
|
||||
use aether_admin::observability::usage::{
|
||||
admin_usage_bad_request_response, admin_usage_data_unavailable_response,
|
||||
admin_usage_group_completed_by_user, admin_usage_parse_recent_hours,
|
||||
admin_usage_parse_timeline_limit, admin_usage_point_sort_key, admin_usage_proportional_limits,
|
||||
ADMIN_USAGE_DATA_UNAVAILABLE_DETAIL,
|
||||
admin_usage_parse_recent_hours, admin_usage_parse_timeline_limit, admin_usage_point_sort_key,
|
||||
admin_usage_proportional_limits, ADMIN_USAGE_DATA_UNAVAILABLE_DETAIL,
|
||||
};
|
||||
use aether_data_contracts::repository::usage::UsageCacheAffinityIntervalGroupBy;
|
||||
use axum::{
|
||||
body::Body,
|
||||
response::{IntoResponse, Response},
|
||||
@@ -51,50 +51,40 @@ pub(super) async fn build_admin_usage_cache_affinity_interval_timeline_response(
|
||||
};
|
||||
let user_id = query_param_value(query, "user_id");
|
||||
let include_user_info = query_param_bool(query, "include_user_info", false);
|
||||
let usage =
|
||||
list_recent_completed_usage_for_cache_affinity(state, hours, user_id.as_deref()).await?;
|
||||
let intervals = list_usage_cache_affinity_intervals(
|
||||
state,
|
||||
hours,
|
||||
UsageCacheAffinityIntervalGroupBy::User,
|
||||
user_id.as_deref(),
|
||||
None,
|
||||
)
|
||||
.await?;
|
||||
let mut grouped: BTreeMap<String, Vec<serde_json::Value>> = BTreeMap::new();
|
||||
let mut models = BTreeSet::new();
|
||||
let mut legacy_usernames_by_user_id = BTreeMap::new();
|
||||
let mut usernames_by_user_id = BTreeMap::new();
|
||||
|
||||
for (group_user_id, items) in admin_usage_group_completed_by_user(&usage) {
|
||||
if let Some(ref requested_user_id) = user_id {
|
||||
if &group_user_id != requested_user_id {
|
||||
continue;
|
||||
for row in intervals {
|
||||
if row.interval_minutes > 120.0 {
|
||||
continue;
|
||||
}
|
||||
let mut point = json!({
|
||||
"x": unix_secs_to_rfc3339(row.created_at_unix_secs),
|
||||
"y": ((row.interval_minutes * 100.0).round()) / 100.0,
|
||||
});
|
||||
if !row.model.trim().is_empty() {
|
||||
point["model"] = json!(row.model.clone());
|
||||
models.insert(row.model);
|
||||
}
|
||||
if include_user_info && user_id.is_none() {
|
||||
point["user_id"] = json!(row.group_id.clone());
|
||||
if let Some(username) = row.username {
|
||||
legacy_usernames_by_user_id
|
||||
.entry(row.group_id.clone())
|
||||
.or_insert(username);
|
||||
}
|
||||
}
|
||||
|
||||
let mut previous_created_at_unix_ms = None;
|
||||
for item in items {
|
||||
if let Some(previous) = previous_created_at_unix_ms {
|
||||
let interval_minutes =
|
||||
item.created_at_unix_ms.saturating_sub(previous) as f64 / 60.0;
|
||||
if interval_minutes <= 120.0 {
|
||||
let mut point = json!({
|
||||
"x": unix_secs_to_rfc3339(item.created_at_unix_ms),
|
||||
"y": ((interval_minutes * 100.0).round()) / 100.0,
|
||||
});
|
||||
if !item.model.trim().is_empty() {
|
||||
point["model"] = json!(item.model.clone());
|
||||
models.insert(item.model.clone());
|
||||
}
|
||||
if include_user_info && user_id.is_none() {
|
||||
point["user_id"] = json!(group_user_id.clone());
|
||||
if let Some(username) = item.username.clone() {
|
||||
legacy_usernames_by_user_id
|
||||
.entry(group_user_id.clone())
|
||||
.or_insert(username);
|
||||
}
|
||||
}
|
||||
grouped
|
||||
.entry(group_user_id.clone())
|
||||
.or_default()
|
||||
.push(point);
|
||||
}
|
||||
}
|
||||
previous_created_at_unix_ms = Some(item.created_at_unix_ms);
|
||||
}
|
||||
grouped.entry(row.group_id).or_default().push(point);
|
||||
}
|
||||
|
||||
if include_user_info && user_id.is_none() {
|
||||
|
||||
@@ -1,15 +1,15 @@
|
||||
use super::super::super::stats::round_to;
|
||||
use super::super::analytics::list_recent_completed_usage_for_cache_affinity;
|
||||
use super::super::analytics::list_usage_cache_affinity_intervals;
|
||||
use crate::handlers::admin::request::{AdminAppState, AdminRequestContext};
|
||||
use crate::handlers::admin::shared::query_param_value;
|
||||
use crate::GatewayError;
|
||||
use aether_admin::observability::usage::{
|
||||
admin_usage_bad_request_response, admin_usage_calculate_recommended_ttl,
|
||||
admin_usage_collect_request_intervals_minutes, admin_usage_data_unavailable_response,
|
||||
admin_usage_group_completed_by_api_key, admin_usage_group_completed_by_user,
|
||||
admin_usage_matches_optional_id, admin_usage_parse_recent_hours, admin_usage_percentile_cont,
|
||||
admin_usage_ttl_recommendation_reason, ADMIN_USAGE_DATA_UNAVAILABLE_DETAIL,
|
||||
admin_usage_data_unavailable_response, admin_usage_parse_recent_hours,
|
||||
admin_usage_percentile_cont, admin_usage_ttl_recommendation_reason,
|
||||
ADMIN_USAGE_DATA_UNAVAILABLE_DETAIL,
|
||||
};
|
||||
use aether_data_contracts::repository::usage::UsageCacheAffinityIntervalGroupBy;
|
||||
use axum::{
|
||||
body::Body,
|
||||
response::{IntoResponse, Response},
|
||||
@@ -36,19 +36,28 @@ pub(super) async fn build_admin_usage_cache_affinity_ttl_analysis_response(
|
||||
let user_id = query_param_value(query, "user_id");
|
||||
let api_key_id = query_param_value(query, "api_key_id");
|
||||
let group_by_api_key = api_key_id.is_some();
|
||||
let usage =
|
||||
list_recent_completed_usage_for_cache_affinity(state, hours, user_id.as_deref()).await?;
|
||||
|
||||
let grouped = if group_by_api_key {
|
||||
admin_usage_group_completed_by_api_key(&usage, api_key_id.as_deref())
|
||||
} else {
|
||||
admin_usage_group_completed_by_user(&usage)
|
||||
let intervals = list_usage_cache_affinity_intervals(
|
||||
state,
|
||||
hours,
|
||||
if group_by_api_key {
|
||||
UsageCacheAffinityIntervalGroupBy::ApiKey
|
||||
} else {
|
||||
UsageCacheAffinityIntervalGroupBy::User
|
||||
},
|
||||
user_id.as_deref(),
|
||||
api_key_id.as_deref(),
|
||||
)
|
||||
.await?;
|
||||
let grouped =
|
||||
intervals
|
||||
.into_iter()
|
||||
.filter(|(group_user_id, _)| {
|
||||
admin_usage_matches_optional_id(Some(group_user_id.as_str()), user_id.as_deref())
|
||||
})
|
||||
.collect()
|
||||
};
|
||||
.fold(BTreeMap::<String, Vec<f64>>::new(), |mut grouped, row| {
|
||||
grouped
|
||||
.entry(row.group_id)
|
||||
.or_default()
|
||||
.push(row.interval_minutes);
|
||||
grouped
|
||||
});
|
||||
|
||||
let user_map: BTreeMap<String, aether_data::repository::users::StoredUserSummary> =
|
||||
if !group_by_api_key && state.has_user_data_reader() {
|
||||
@@ -71,8 +80,7 @@ pub(super) async fn build_admin_usage_cache_affinity_ttl_analysis_response(
|
||||
});
|
||||
let mut users = Vec::new();
|
||||
|
||||
for (group_id, items) in grouped {
|
||||
let intervals = admin_usage_collect_request_intervals_minutes(&items);
|
||||
for (group_id, intervals) in grouped {
|
||||
if intervals.len() < 2 {
|
||||
continue;
|
||||
}
|
||||
|
||||
@@ -5,14 +5,14 @@ use crate::handlers::admin::request::{AdminAppState, AdminRequestContext};
|
||||
use crate::handlers::admin::shared::query_param_value;
|
||||
use crate::GatewayError;
|
||||
use aether_admin::observability::usage::{
|
||||
admin_usage_bad_request_response, admin_usage_data_unavailable_response,
|
||||
admin_usage_matches_search, admin_usage_matches_username, admin_usage_parse_ids,
|
||||
admin_usage_bad_request_response, admin_usage_data_unavailable_response, admin_usage_parse_ids,
|
||||
admin_usage_parse_limit, admin_usage_parse_offset, build_admin_usage_active_requests_response,
|
||||
build_admin_usage_records_response, build_admin_usage_summary_stats_response_from_summary,
|
||||
ADMIN_USAGE_DATA_UNAVAILABLE_DETAIL,
|
||||
};
|
||||
use aether_data_contracts::repository::usage::{
|
||||
StoredRequestUsageAudit, UsageAuditListQuery, UsageAuditSummaryQuery,
|
||||
StoredRequestUsageAudit, UsageAuditKeywordSearchQuery, UsageAuditListQuery,
|
||||
UsageAuditSummaryQuery,
|
||||
};
|
||||
use axum::{body::Body, http, response::Response};
|
||||
use std::collections::{BTreeMap, BTreeSet};
|
||||
@@ -23,13 +23,8 @@ async fn load_admin_usage_by_ids(
|
||||
state: &AdminAppState<'_>,
|
||||
requested_ids: &BTreeSet<String>,
|
||||
) -> Result<Vec<StoredRequestUsageAudit>, GatewayError> {
|
||||
let mut items = Vec::with_capacity(requested_ids.len());
|
||||
for usage_id in requested_ids {
|
||||
if let Some(item) = state.find_request_usage_by_id(usage_id).await? {
|
||||
items.push(item);
|
||||
}
|
||||
}
|
||||
Ok(items)
|
||||
let usage_ids = requested_ids.iter().cloned().collect::<Vec<_>>();
|
||||
state.list_request_usage_by_ids(&usage_ids).await
|
||||
}
|
||||
|
||||
fn sort_usage_newest_first(items: &mut [StoredRequestUsageAudit]) {
|
||||
@@ -89,6 +84,134 @@ fn build_admin_usage_records_query(
|
||||
list_query
|
||||
}
|
||||
|
||||
fn parse_admin_usage_search_keywords(search: &str) -> Vec<String> {
|
||||
search
|
||||
.split_whitespace()
|
||||
.map(str::trim)
|
||||
.filter(|value| !value.is_empty())
|
||||
.map(|value| value.to_ascii_lowercase())
|
||||
.collect()
|
||||
}
|
||||
|
||||
#[derive(Default)]
|
||||
struct AdminUsageSearchContext {
|
||||
matched_user_ids_by_keyword: Vec<Vec<String>>,
|
||||
matched_api_key_ids_by_keyword: Vec<Vec<String>>,
|
||||
matched_user_ids_for_username: Vec<String>,
|
||||
}
|
||||
|
||||
async fn resolve_admin_usage_search_context(
|
||||
state: &AdminAppState<'_>,
|
||||
keywords: &[String],
|
||||
username_filter: Option<&str>,
|
||||
) -> Result<AdminUsageSearchContext, GatewayError> {
|
||||
let auth_user_reader_available = state.has_auth_user_data_reader();
|
||||
let auth_api_key_reader_available = state.has_auth_api_key_data_reader();
|
||||
let username_filter = username_filter
|
||||
.map(str::trim)
|
||||
.filter(|value| !value.is_empty())
|
||||
.map(|value| value.to_ascii_lowercase());
|
||||
let mut matched_user_ids_cache = BTreeMap::<String, Vec<String>>::new();
|
||||
let mut matched_api_key_ids_cache = BTreeMap::<String, Vec<String>>::new();
|
||||
|
||||
if auth_user_reader_available {
|
||||
for keyword in keywords {
|
||||
matched_user_ids_cache.entry(keyword.clone()).or_insert(
|
||||
state
|
||||
.search_auth_user_summaries_by_username(keyword)
|
||||
.await?
|
||||
.into_iter()
|
||||
.map(|user| user.id)
|
||||
.collect(),
|
||||
);
|
||||
}
|
||||
if let Some(username_keyword) = username_filter.as_ref() {
|
||||
matched_user_ids_cache
|
||||
.entry(username_keyword.clone())
|
||||
.or_insert(
|
||||
state
|
||||
.search_auth_user_summaries_by_username(username_keyword)
|
||||
.await?
|
||||
.into_iter()
|
||||
.map(|user| user.id)
|
||||
.collect(),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
if auth_api_key_reader_available {
|
||||
for keyword in keywords {
|
||||
matched_api_key_ids_cache.entry(keyword.clone()).or_insert(
|
||||
state
|
||||
.list_auth_api_key_export_records_by_name_search(keyword)
|
||||
.await?
|
||||
.into_iter()
|
||||
.map(|record| record.api_key_id)
|
||||
.collect(),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Ok(AdminUsageSearchContext {
|
||||
matched_user_ids_by_keyword: keywords
|
||||
.iter()
|
||||
.map(|keyword| {
|
||||
matched_user_ids_cache
|
||||
.get(keyword)
|
||||
.cloned()
|
||||
.unwrap_or_default()
|
||||
})
|
||||
.collect(),
|
||||
matched_api_key_ids_by_keyword: keywords
|
||||
.iter()
|
||||
.map(|keyword| {
|
||||
matched_api_key_ids_cache
|
||||
.get(keyword)
|
||||
.cloned()
|
||||
.unwrap_or_default()
|
||||
})
|
||||
.collect(),
|
||||
matched_user_ids_for_username: username_filter
|
||||
.as_ref()
|
||||
.and_then(|keyword| matched_user_ids_cache.get(keyword))
|
||||
.cloned()
|
||||
.unwrap_or_default(),
|
||||
})
|
||||
}
|
||||
|
||||
fn build_admin_usage_keyword_search_query(
|
||||
base_query: &UsageAuditListQuery,
|
||||
keywords: Vec<String>,
|
||||
username_keyword: Option<String>,
|
||||
search_context: AdminUsageSearchContext,
|
||||
auth_user_reader_available: bool,
|
||||
auth_api_key_reader_available: bool,
|
||||
limit: Option<usize>,
|
||||
offset: Option<usize>,
|
||||
) -> UsageAuditKeywordSearchQuery {
|
||||
UsageAuditKeywordSearchQuery {
|
||||
created_from_unix_secs: base_query.created_from_unix_secs,
|
||||
created_until_unix_secs: base_query.created_until_unix_secs,
|
||||
user_id: base_query.user_id.clone(),
|
||||
provider_name: base_query.provider_name.clone(),
|
||||
model: base_query.model.clone(),
|
||||
api_format: base_query.api_format.clone(),
|
||||
statuses: base_query.statuses.clone(),
|
||||
is_stream: base_query.is_stream,
|
||||
error_only: base_query.error_only,
|
||||
keywords,
|
||||
matched_user_ids_by_keyword: search_context.matched_user_ids_by_keyword,
|
||||
auth_user_reader_available,
|
||||
matched_api_key_ids_by_keyword: search_context.matched_api_key_ids_by_keyword,
|
||||
auth_api_key_reader_available,
|
||||
username_keyword,
|
||||
matched_user_ids_for_username: search_context.matched_user_ids_for_username,
|
||||
limit,
|
||||
offset,
|
||||
newest_first: true,
|
||||
}
|
||||
}
|
||||
|
||||
pub(super) async fn maybe_build_local_admin_usage_summary_response(
|
||||
state: &AdminAppState<'_>,
|
||||
request_context: &AdminRequestContext<'_>,
|
||||
@@ -150,8 +273,10 @@ pub(super) async fn maybe_build_local_admin_usage_summary_response(
|
||||
|
||||
let query = request_context.request_query_string.as_deref();
|
||||
let requested_ids = admin_usage_parse_ids(query);
|
||||
let mut items = if let Some(requested_ids) = requested_ids.as_ref() {
|
||||
load_admin_usage_by_ids(state, requested_ids).await?
|
||||
let items = if let Some(requested_ids) = requested_ids.as_ref() {
|
||||
let mut items = load_admin_usage_by_ids(state, requested_ids).await?;
|
||||
sort_usage_newest_first(&mut items);
|
||||
items
|
||||
} else {
|
||||
let time_range = match resolve_admin_usage_time_range(query) {
|
||||
Ok(value) => value,
|
||||
@@ -178,10 +303,6 @@ pub(super) async fn maybe_build_local_admin_usage_summary_response(
|
||||
})
|
||||
.await?
|
||||
};
|
||||
sort_usage_newest_first(&mut items);
|
||||
if requested_ids.is_none() && items.len() > ADMIN_USAGE_ACTIVE_LIMIT {
|
||||
items.truncate(ADMIN_USAGE_ACTIVE_LIMIT);
|
||||
}
|
||||
let api_key_names = admin_usage_api_key_names(state, &items).await?;
|
||||
let provider_key_names = admin_usage_provider_key_names(state, &items).await?;
|
||||
|
||||
@@ -242,44 +363,46 @@ pub(super) async fn maybe_build_local_admin_usage_summary_response(
|
||||
None,
|
||||
None,
|
||||
);
|
||||
let use_metadata_fallback = search.is_some() || username_filter.is_some();
|
||||
let (usage, total) = if use_metadata_fallback {
|
||||
let mut usage = state.list_usage_audits(&base_query).await?;
|
||||
let user_ids: Vec<String> = usage
|
||||
.iter()
|
||||
.filter_map(|item| item.user_id.clone())
|
||||
.collect::<BTreeSet<_>>()
|
||||
.into_iter()
|
||||
.collect();
|
||||
let users_by_id: BTreeMap<
|
||||
String,
|
||||
aether_data::repository::users::StoredUserSummary,
|
||||
> = state.resolve_auth_user_summaries_by_ids(&user_ids).await?;
|
||||
let api_key_names = admin_usage_api_key_names(state, &usage).await?;
|
||||
|
||||
usage.retain(|item| {
|
||||
admin_usage_matches_search(
|
||||
item,
|
||||
search.as_deref(),
|
||||
&users_by_id,
|
||||
&api_key_names,
|
||||
state.has_auth_user_data_reader(),
|
||||
state.has_auth_api_key_data_reader(),
|
||||
) && admin_usage_matches_username(
|
||||
item,
|
||||
username_filter.as_deref(),
|
||||
&users_by_id,
|
||||
state.has_auth_user_data_reader(),
|
||||
)
|
||||
});
|
||||
sort_usage_newest_first(&mut usage);
|
||||
let total = usage.len();
|
||||
let records = usage
|
||||
.into_iter()
|
||||
.skip(offset)
|
||||
.take(limit)
|
||||
.collect::<Vec<_>>();
|
||||
(records, total)
|
||||
let active_search = search.as_deref().filter(|value| !value.trim().is_empty());
|
||||
let active_username_filter = username_filter
|
||||
.as_deref()
|
||||
.filter(|value| !value.trim().is_empty());
|
||||
let (usage, total) = if active_search.is_some() || active_username_filter.is_some() {
|
||||
let keywords = active_search
|
||||
.map(parse_admin_usage_search_keywords)
|
||||
.unwrap_or_default();
|
||||
let auth_user_reader_available = state.has_auth_user_data_reader();
|
||||
let auth_api_key_reader_available = state.has_auth_api_key_data_reader();
|
||||
let search_context =
|
||||
resolve_admin_usage_search_context(state, &keywords, active_username_filter)
|
||||
.await?;
|
||||
let keyword_query = build_admin_usage_keyword_search_query(
|
||||
&base_query,
|
||||
keywords,
|
||||
active_username_filter.map(str::to_owned),
|
||||
search_context,
|
||||
auth_user_reader_available,
|
||||
auth_api_key_reader_available,
|
||||
None,
|
||||
None,
|
||||
);
|
||||
let total = usize::try_from(
|
||||
state
|
||||
.count_usage_audits_by_keyword_search(&keyword_query)
|
||||
.await?,
|
||||
)
|
||||
.unwrap_or(usize::MAX);
|
||||
let paged_query = UsageAuditKeywordSearchQuery {
|
||||
limit: Some(limit),
|
||||
offset: Some(offset),
|
||||
..keyword_query
|
||||
};
|
||||
(
|
||||
state
|
||||
.list_usage_audits_by_keyword_search(&paged_query)
|
||||
.await?,
|
||||
total,
|
||||
)
|
||||
} else {
|
||||
let total = usize::try_from(state.count_usage_audits(&base_query).await?)
|
||||
.unwrap_or(usize::MAX);
|
||||
|
||||
Reference in New Issue
Block a user