mirror of
https://github.com/fawney19/Aether.git
synced 2026-09-02 17:30:23 +08:00
feat(stats): 新增聚合读路径与回填机制并重构 dashboard/usage 读取链路
- 新增 stats_user_summary 及 user_daily_provider/api_format/cost_savings 等聚合表 - 扩展 stats_daily/hourly 有效 token 与响应时间等字段,maintenance runtime 同步写入 - 新增 backfill 模块与 --apply-backfills 命令补齐历史聚合数据 - 重写 dashboard_filters、usage_heatmap、user_rollups 查询改走聚合表 - 同步更新 baseline_v2.sql 与 migration 集,README/dev.sh 补充回填用法
This commit is contained in:
@@ -166,6 +166,7 @@ pub(super) async fn build_admin_create_api_key_response(
|
||||
expires_at_unix_secs,
|
||||
auto_delete_on_expiry,
|
||||
total_requests: 0,
|
||||
total_tokens: 0,
|
||||
total_cost_usd: 0.0,
|
||||
},
|
||||
)
|
||||
|
||||
@@ -222,6 +222,7 @@ pub(super) fn sample_monitoring_export_api_key(
|
||||
None,
|
||||
false,
|
||||
0,
|
||||
0,
|
||||
0.0,
|
||||
false,
|
||||
)
|
||||
|
||||
@@ -1,10 +1,13 @@
|
||||
use crate::handlers::admin::request::AdminAppState;
|
||||
use crate::query::usage_heatmap::{
|
||||
list_usage_heatmap_aggregate_rows, read_stats_daily_cutoff_date,
|
||||
};
|
||||
use crate::GatewayError;
|
||||
use aether_admin::observability::stats::round_to;
|
||||
use aether_admin::observability::usage::{
|
||||
admin_usage_data_unavailable_response, ADMIN_USAGE_DATA_UNAVAILABLE_DETAIL,
|
||||
};
|
||||
use aether_data_contracts::repository::usage::UsageDailyHeatmapQuery;
|
||||
use aether_data_contracts::repository::usage::{StoredUsageDailySummary, UsageDailyHeatmapQuery};
|
||||
use axum::{
|
||||
body::Body,
|
||||
response::{IntoResponse, Response},
|
||||
@@ -34,17 +37,13 @@ pub(super) async fn build_admin_usage_heatmap_response(
|
||||
)
|
||||
.unwrap_or_default();
|
||||
|
||||
let summaries = state
|
||||
.summarize_usage_daily_heatmap(&UsageDailyHeatmapQuery {
|
||||
created_from_unix_secs,
|
||||
user_id: None,
|
||||
admin_mode: true,
|
||||
})
|
||||
.await?;
|
||||
let summaries =
|
||||
build_admin_heatmap_summaries(state, created_from_unix_secs, start_date, today).await?;
|
||||
|
||||
let grouped: BTreeMap<String, _> = summaries.into_iter().map(|s| (s.date.clone(), s)).collect();
|
||||
|
||||
let mut max_requests = 0_u64;
|
||||
let mut active_days = 0_u64;
|
||||
let mut cursor = start_date;
|
||||
let mut days = Vec::new();
|
||||
while cursor <= today {
|
||||
@@ -61,6 +60,9 @@ pub(super) async fn build_admin_usage_heatmap_response(
|
||||
(0, 0, 0.0, 0.0)
|
||||
};
|
||||
max_requests = max_requests.max(requests);
|
||||
if requests > 0 {
|
||||
active_days = active_days.saturating_add(1);
|
||||
}
|
||||
days.push(json!({
|
||||
"date": date_str,
|
||||
"requests": requests,
|
||||
@@ -77,8 +79,59 @@ pub(super) async fn build_admin_usage_heatmap_response(
|
||||
"start_date": start_date.to_string(),
|
||||
"end_date": today.to_string(),
|
||||
"total_days": days.len(),
|
||||
"active_days": active_days,
|
||||
"max_requests": max_requests,
|
||||
"days": days,
|
||||
}))
|
||||
.into_response())
|
||||
}
|
||||
|
||||
async fn build_admin_heatmap_summaries(
|
||||
state: &AdminAppState<'_>,
|
||||
created_from_unix_secs: u64,
|
||||
start_date: chrono::NaiveDate,
|
||||
today: chrono::NaiveDate,
|
||||
) -> Result<Vec<StoredUsageDailySummary>, GatewayError> {
|
||||
let query = UsageDailyHeatmapQuery {
|
||||
created_from_unix_secs,
|
||||
user_id: None,
|
||||
admin_mode: true,
|
||||
};
|
||||
let Some(pool) = state.app().postgres_pool() else {
|
||||
return state.summarize_usage_daily_heatmap(&query).await;
|
||||
};
|
||||
|
||||
let Some(cutoff_date) = read_stats_daily_cutoff_date(&pool).await? else {
|
||||
return state.summarize_usage_daily_heatmap(&query).await;
|
||||
};
|
||||
|
||||
let cutoff_day = cutoff_date.date_naive().min(today);
|
||||
let mut summaries =
|
||||
list_usage_heatmap_aggregate_rows(&pool, start_date, cutoff_day, None).await?;
|
||||
let raw_start_date = start_date.max(cutoff_day);
|
||||
if raw_start_date <= today {
|
||||
let raw_start_of_day = raw_start_date
|
||||
.and_hms_opt(0, 0, 0)
|
||||
.expect("heatmap day start should be valid");
|
||||
let raw_created_from_unix_secs = u64::try_from(
|
||||
chrono::DateTime::<chrono::Utc>::from_naive_utc_and_offset(
|
||||
raw_start_of_day,
|
||||
chrono::Utc,
|
||||
)
|
||||
.timestamp(),
|
||||
)
|
||||
.unwrap_or_default();
|
||||
summaries.extend(
|
||||
state
|
||||
.summarize_usage_daily_heatmap(&UsageDailyHeatmapQuery {
|
||||
created_from_unix_secs: raw_created_from_unix_secs,
|
||||
user_id: None,
|
||||
admin_mode: true,
|
||||
})
|
||||
.await?,
|
||||
);
|
||||
}
|
||||
|
||||
summaries.sort_by(|left, right| left.date.cmp(&right.date));
|
||||
Ok(summaries)
|
||||
}
|
||||
|
||||
@@ -297,6 +297,7 @@ impl<'a> AdminAppState<'a> {
|
||||
json!(key.auto_delete_on_expiry),
|
||||
),
|
||||
("total_requests".to_string(), json!(key.total_requests)),
|
||||
("total_tokens".to_string(), json!(key.total_tokens)),
|
||||
("total_cost_usd".to_string(), json!(key.total_cost_usd)),
|
||||
(
|
||||
"wallet".to_string(),
|
||||
|
||||
@@ -1982,6 +1982,11 @@ impl<'a> AdminAppState<'a> {
|
||||
"total_requests"
|
||||
))
|
||||
.unwrap_or(0);
|
||||
let total_tokens = invalid_value!(imported_optional_u64(
|
||||
key.get("total_tokens"),
|
||||
"total_tokens"
|
||||
))
|
||||
.unwrap_or(0);
|
||||
let total_cost_usd = invalid_value!(imported_optional_f64(
|
||||
key.get("total_cost_usd"),
|
||||
"total_cost_usd"
|
||||
@@ -2047,6 +2052,7 @@ impl<'a> AdminAppState<'a> {
|
||||
|| key.contains_key("expires_at")
|
||||
|| key.contains_key("auto_delete_on_expiry")
|
||||
|| key.contains_key("total_requests")
|
||||
|| key.contains_key("total_tokens")
|
||||
|| key.contains_key("total_cost_usd")
|
||||
{
|
||||
stats.errors.push(format!(
|
||||
@@ -2077,6 +2083,7 @@ impl<'a> AdminAppState<'a> {
|
||||
expires_at_unix_secs,
|
||||
auto_delete_on_expiry,
|
||||
total_requests,
|
||||
total_tokens,
|
||||
total_cost_usd,
|
||||
})
|
||||
.await?;
|
||||
@@ -2169,6 +2176,11 @@ impl<'a> AdminAppState<'a> {
|
||||
"total_requests"
|
||||
))
|
||||
.unwrap_or(0);
|
||||
let total_tokens = invalid_value!(imported_optional_u64(
|
||||
key.get("total_tokens"),
|
||||
"total_tokens"
|
||||
))
|
||||
.unwrap_or(0);
|
||||
let total_cost_usd = invalid_value!(imported_optional_f64(
|
||||
key.get("total_cost_usd"),
|
||||
"total_cost_usd"
|
||||
@@ -2225,6 +2237,7 @@ impl<'a> AdminAppState<'a> {
|
||||
|| key.contains_key("auto_delete_on_expiry")
|
||||
|| key.contains_key("force_capabilities")
|
||||
|| key.contains_key("total_requests")
|
||||
|| key.contains_key("total_tokens")
|
||||
|| key.contains_key("total_cost_usd")
|
||||
{
|
||||
stats.errors.push(
|
||||
@@ -2263,6 +2276,7 @@ impl<'a> AdminAppState<'a> {
|
||||
expires_at_unix_secs,
|
||||
auto_delete_on_expiry,
|
||||
total_requests,
|
||||
total_tokens,
|
||||
total_cost_usd,
|
||||
},
|
||||
)
|
||||
|
||||
@@ -142,6 +142,7 @@ pub(crate) async fn build_admin_create_user_api_key_response(
|
||||
expires_at_unix_secs: None,
|
||||
auto_delete_on_expiry: false,
|
||||
total_requests: 0,
|
||||
total_tokens: 0,
|
||||
total_cost_usd: 0.0,
|
||||
})
|
||||
.await?
|
||||
|
||||
@@ -4,6 +4,7 @@ use super::support::{
|
||||
};
|
||||
use crate::handlers::admin::request::{AdminAppState, AdminRequestContext};
|
||||
use crate::handlers::admin::shared::{query_param_optional_bool, query_param_value};
|
||||
use crate::query::user_rollups::list_user_usage_totals_from_stats_summary;
|
||||
use crate::GatewayError;
|
||||
use axum::{
|
||||
body::Body,
|
||||
@@ -42,10 +43,19 @@ pub(in super::super) async fn build_admin_list_users_response(
|
||||
.iter()
|
||||
.map(|row| row.id.clone())
|
||||
.collect::<Vec<_>>();
|
||||
let usage_totals_future = async {
|
||||
let Some(pool) = state.app().postgres_pool() else {
|
||||
return state.summarize_usage_totals_by_user_ids(&user_ids).await;
|
||||
};
|
||||
match list_user_usage_totals_from_stats_summary(&pool, &user_ids).await? {
|
||||
Some(items) => Ok(items),
|
||||
None => state.summarize_usage_totals_by_user_ids(&user_ids).await,
|
||||
}
|
||||
};
|
||||
let (auth_rows_result, wallet_rows_result, usage_totals_result) = tokio::join!(
|
||||
state.list_user_auth_by_ids(&user_ids),
|
||||
state.list_wallet_snapshots_by_user_ids(&user_ids),
|
||||
state.summarize_usage_totals_by_user_ids(&user_ids),
|
||||
usage_totals_future,
|
||||
);
|
||||
let auth_by_user_id = auth_rows_result?
|
||||
.into_iter()
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -554,6 +554,7 @@ pub(super) async fn handle_users_me_api_key_create(
|
||||
expires_at_unix_secs: None,
|
||||
auto_delete_on_expiry: false,
|
||||
total_requests: 0,
|
||||
total_tokens: 0,
|
||||
total_cost_usd: 0.0,
|
||||
};
|
||||
let Some(created) = (match state.create_user_api_key(record).await {
|
||||
|
||||
@@ -4,9 +4,10 @@ use aether_billing::{
|
||||
normalize_input_tokens_for_billing, normalize_total_input_context_for_cache_hit_rate,
|
||||
};
|
||||
use aether_data_contracts::repository::usage::{
|
||||
StoredRequestUsageAudit, StoredUsageBreakdownSummaryRow, UsageAuditKeywordSearchQuery,
|
||||
UsageAuditListQuery, UsageBreakdownGroupBy, UsageBreakdownSummaryQuery,
|
||||
UsageCacheAffinityIntervalGroupBy, UsageCacheAffinityIntervalQuery, UsageDashboardSummaryQuery,
|
||||
StoredRequestUsageAudit, StoredUsageBreakdownSummaryRow, StoredUsageDailySummary,
|
||||
UsageAuditKeywordSearchQuery, UsageAuditListQuery, UsageBreakdownGroupBy,
|
||||
UsageBreakdownSummaryQuery, UsageCacheAffinityIntervalGroupBy, UsageCacheAffinityIntervalQuery,
|
||||
UsageDashboardSummaryQuery,
|
||||
};
|
||||
use axum::{
|
||||
body::Body,
|
||||
@@ -17,6 +18,9 @@ use axum::{
|
||||
use chrono::Utc;
|
||||
use serde_json::json;
|
||||
|
||||
use crate::query::usage_heatmap::{
|
||||
list_usage_heatmap_aggregate_rows, read_stats_daily_cutoff_date,
|
||||
};
|
||||
use crate::GatewayError;
|
||||
|
||||
use super::{
|
||||
@@ -957,15 +961,14 @@ pub(super) async fn handle_users_me_usage_heatmap_get(
|
||||
)
|
||||
.unwrap_or_default();
|
||||
|
||||
let summaries = match state
|
||||
.summarize_usage_daily_heatmap(
|
||||
&aether_data_contracts::repository::usage::UsageDailyHeatmapQuery {
|
||||
created_from_unix_secs,
|
||||
user_id: Some(auth.user.id.clone()),
|
||||
admin_mode: false,
|
||||
},
|
||||
)
|
||||
.await
|
||||
let summaries = match build_usage_heatmap_summaries(
|
||||
state,
|
||||
created_from_unix_secs,
|
||||
start_date,
|
||||
today,
|
||||
Some(auth.user.id.as_str()),
|
||||
)
|
||||
.await
|
||||
{
|
||||
Ok(value) => value,
|
||||
Err(err) => {
|
||||
@@ -982,6 +985,7 @@ pub(super) async fn handle_users_me_usage_heatmap_get(
|
||||
summaries.into_iter().map(|s| (s.date.clone(), s)).collect();
|
||||
|
||||
let mut max_requests = 0_u64;
|
||||
let mut active_days = 0_u64;
|
||||
let mut cursor = start_date;
|
||||
let mut days = Vec::new();
|
||||
while cursor <= today {
|
||||
@@ -998,6 +1002,9 @@ pub(super) async fn handle_users_me_usage_heatmap_get(
|
||||
(0, 0, 0.0, 0.0)
|
||||
};
|
||||
max_requests = max_requests.max(requests);
|
||||
if requests > 0 {
|
||||
active_days = active_days.saturating_add(1);
|
||||
}
|
||||
let mut day = json!({
|
||||
"date": date_str,
|
||||
"requests": requests,
|
||||
@@ -1017,12 +1024,66 @@ pub(super) async fn handle_users_me_usage_heatmap_get(
|
||||
"start_date": start_date.to_string(),
|
||||
"end_date": today.to_string(),
|
||||
"total_days": days.len(),
|
||||
"active_days": active_days,
|
||||
"max_requests": max_requests,
|
||||
"days": days,
|
||||
}))
|
||||
.into_response()
|
||||
}
|
||||
|
||||
async fn build_usage_heatmap_summaries(
|
||||
state: &AppState,
|
||||
created_from_unix_secs: u64,
|
||||
start_date: chrono::NaiveDate,
|
||||
today: chrono::NaiveDate,
|
||||
user_id: Option<&str>,
|
||||
) -> Result<Vec<StoredUsageDailySummary>, GatewayError> {
|
||||
let query = aether_data_contracts::repository::usage::UsageDailyHeatmapQuery {
|
||||
created_from_unix_secs,
|
||||
user_id: user_id.map(ToOwned::to_owned),
|
||||
admin_mode: user_id.is_none(),
|
||||
};
|
||||
let Some(pool) = state.postgres_pool() else {
|
||||
return state.summarize_usage_daily_heatmap(&query).await;
|
||||
};
|
||||
|
||||
let Some(cutoff_date) = read_stats_daily_cutoff_date(&pool).await? else {
|
||||
return state.summarize_usage_daily_heatmap(&query).await;
|
||||
};
|
||||
|
||||
let cutoff_day = cutoff_date.date_naive().min(today);
|
||||
let mut summaries =
|
||||
list_usage_heatmap_aggregate_rows(&pool, start_date, cutoff_day, user_id).await?;
|
||||
let raw_start_date = start_date.max(cutoff_day);
|
||||
if raw_start_date <= today {
|
||||
let raw_start_of_day = raw_start_date
|
||||
.and_hms_opt(0, 0, 0)
|
||||
.expect("heatmap day start should be valid");
|
||||
let raw_created_from_unix_secs = u64::try_from(
|
||||
chrono::DateTime::<chrono::Utc>::from_naive_utc_and_offset(
|
||||
raw_start_of_day,
|
||||
chrono::Utc,
|
||||
)
|
||||
.timestamp(),
|
||||
)
|
||||
.unwrap_or_default();
|
||||
summaries.extend(
|
||||
state
|
||||
.summarize_usage_daily_heatmap(
|
||||
&aether_data_contracts::repository::usage::UsageDailyHeatmapQuery {
|
||||
created_from_unix_secs: raw_created_from_unix_secs,
|
||||
user_id: user_id.map(ToOwned::to_owned),
|
||||
admin_mode: user_id.is_none(),
|
||||
},
|
||||
)
|
||||
.await?,
|
||||
);
|
||||
}
|
||||
|
||||
summaries.sort_by(|left, right| left.date.cmp(&right.date));
|
||||
Ok(summaries)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use std::collections::BTreeMap;
|
||||
|
||||
Reference in New Issue
Block a user