mirror of
https://github.com/fawney19/Aether.git
synced 2026-09-02 01:10: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:
1203
crates/aether-data/src/backfill.rs
Normal file
1203
crates/aether-data/src/backfill.rs
Normal file
File diff suppressed because it is too large
Load Diff
@@ -1,4 +1,5 @@
|
||||
pub mod backends;
|
||||
pub mod backfill;
|
||||
mod config;
|
||||
mod error;
|
||||
pub mod migrate;
|
||||
|
||||
@@ -8,7 +8,7 @@ use tracing::{error, info, warn};
|
||||
|
||||
static MIGRATOR: Migrator = sqlx::migrate!("./migrations");
|
||||
static BASELINE_V2_SQL: &str = include_str!("../bootstrap/20260413020000_baseline_v2.sql");
|
||||
const BASELINE_V2_CUTOFF_VERSION: i64 = 20260418000000;
|
||||
const BASELINE_V2_CUTOFF_VERSION: i64 = 20260422110000;
|
||||
const MIGRATIONS_TABLE_EXISTS_SQL: &str =
|
||||
"SELECT to_regclass('public._sqlx_migrations') IS NOT NULL";
|
||||
const PUBLIC_BASE_TABLE_COUNT_SQL: &str = r#"
|
||||
@@ -637,6 +637,8 @@ mod tests {
|
||||
20260413030000,
|
||||
20260415000000,
|
||||
20260418000000,
|
||||
20260421000000,
|
||||
20260422110000,
|
||||
]
|
||||
);
|
||||
}
|
||||
@@ -658,6 +660,29 @@ mod tests {
|
||||
assert!(BASELINE_V2_SQL.contains("billing_snapshot_schema_version"));
|
||||
assert!(BASELINE_V2_SQL.contains("price_per_request"));
|
||||
assert!(BASELINE_V2_SQL.contains("candidate_index integer"));
|
||||
assert!(BASELINE_V2_SQL.contains("CREATE TABLE IF NOT EXISTS public.stats_user_summary"));
|
||||
assert!(
|
||||
BASELINE_V2_SQL.contains("CREATE TABLE IF NOT EXISTS public.stats_user_daily_model")
|
||||
);
|
||||
assert!(
|
||||
BASELINE_V2_SQL.contains("CREATE TABLE IF NOT EXISTS public.stats_hourly_user_model")
|
||||
);
|
||||
assert!(BASELINE_V2_SQL.contains("CREATE TABLE IF NOT EXISTS public.schema_backfills"));
|
||||
assert!(BASELINE_V2_SQL.contains("idx_schema_backfills_applied_at"));
|
||||
assert!(BASELINE_V2_SQL.contains("ALTER TABLE public.stats_hourly"));
|
||||
assert!(BASELINE_V2_SQL.contains("response_time_sum_ms double precision"));
|
||||
assert!(
|
||||
BASELINE_V2_SQL.contains("CREATE TABLE IF NOT EXISTS public.stats_user_daily_provider")
|
||||
);
|
||||
assert!(BASELINE_V2_SQL
|
||||
.contains("CREATE TABLE IF NOT EXISTS public.stats_user_daily_api_format"));
|
||||
assert!(BASELINE_V2_SQL
|
||||
.contains("CREATE TABLE IF NOT EXISTS public.stats_daily_cost_savings_model_provider"));
|
||||
assert!(BASELINE_V2_SQL.contains(
|
||||
"CREATE TABLE IF NOT EXISTS public.stats_user_daily_cost_savings_model_provider"
|
||||
));
|
||||
assert!(BASELINE_V2_SQL.contains("successful_response_time_sum_ms double precision"));
|
||||
assert!(BASELINE_V2_SQL.contains("cache_hit_total_requests bigint DEFAULT 0 NOT NULL"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -737,6 +762,8 @@ mod tests {
|
||||
20260413030000,
|
||||
20260415000000,
|
||||
20260418000000,
|
||||
20260421000000,
|
||||
20260422110000,
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
@@ -65,6 +65,7 @@ impl InMemoryAuthApiKeySnapshotRepository {
|
||||
.map(|value| value as i64),
|
||||
false,
|
||||
0,
|
||||
0,
|
||||
0.0,
|
||||
snapshot.api_key_is_standalone,
|
||||
)
|
||||
@@ -487,6 +488,7 @@ impl AuthApiKeyWriteRepository for InMemoryAuthApiKeySnapshotRepository {
|
||||
record.expires_at_unix_secs.map(|value| value as i64),
|
||||
record.auto_delete_on_expiry,
|
||||
record.total_requests as i64,
|
||||
record.total_tokens as i64,
|
||||
record.total_cost_usd,
|
||||
false,
|
||||
)?;
|
||||
@@ -607,6 +609,7 @@ impl AuthApiKeyWriteRepository for InMemoryAuthApiKeySnapshotRepository {
|
||||
record.expires_at_unix_secs.map(|value| value as i64),
|
||||
record.auto_delete_on_expiry,
|
||||
record.total_requests as i64,
|
||||
record.total_tokens as i64,
|
||||
record.total_cost_usd,
|
||||
true,
|
||||
)?;
|
||||
@@ -1018,6 +1021,7 @@ mod tests {
|
||||
Some(200),
|
||||
false,
|
||||
14,
|
||||
1_400,
|
||||
1.5,
|
||||
false,
|
||||
)
|
||||
@@ -1038,6 +1042,7 @@ mod tests {
|
||||
None,
|
||||
true,
|
||||
2,
|
||||
25,
|
||||
0.25,
|
||||
true,
|
||||
)
|
||||
|
||||
@@ -150,6 +150,7 @@ SELECT
|
||||
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(api_keys.total_tokens, 0)::BIGINT AS total_tokens,
|
||||
COALESCE(CAST(api_keys.total_cost_usd AS DOUBLE PRECISION), 0) AS total_cost_usd,
|
||||
api_keys.is_standalone
|
||||
FROM api_keys
|
||||
@@ -175,6 +176,7 @@ SELECT
|
||||
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(api_keys.total_tokens, 0)::BIGINT AS total_tokens,
|
||||
COALESCE(CAST(api_keys.total_cost_usd AS DOUBLE PRECISION), 0) AS total_cost_usd,
|
||||
api_keys.is_standalone
|
||||
FROM api_keys
|
||||
@@ -199,6 +201,7 @@ SELECT
|
||||
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(api_keys.total_tokens, 0)::BIGINT AS total_tokens,
|
||||
COALESCE(CAST(api_keys.total_cost_usd AS DOUBLE PRECISION), 0) AS total_cost_usd,
|
||||
api_keys.is_standalone
|
||||
FROM api_keys
|
||||
@@ -223,6 +226,7 @@ SELECT
|
||||
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(api_keys.total_tokens, 0)::BIGINT AS total_tokens,
|
||||
COALESCE(CAST(api_keys.total_cost_usd AS DOUBLE PRECISION), 0) AS total_cost_usd,
|
||||
api_keys.is_standalone
|
||||
FROM api_keys
|
||||
@@ -247,6 +251,7 @@ SELECT
|
||||
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(api_keys.total_tokens, 0)::BIGINT AS total_tokens,
|
||||
COALESCE(CAST(api_keys.total_cost_usd AS DOUBLE PRECISION), 0) AS total_cost_usd,
|
||||
api_keys.is_standalone
|
||||
FROM api_keys
|
||||
@@ -315,6 +320,7 @@ SELECT
|
||||
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(api_keys.total_tokens, 0)::BIGINT AS total_tokens,
|
||||
COALESCE(CAST(api_keys.total_cost_usd AS DOUBLE PRECISION), 0) AS total_cost_usd,
|
||||
api_keys.is_standalone
|
||||
FROM api_keys
|
||||
@@ -348,6 +354,7 @@ INSERT INTO api_keys (
|
||||
is_standalone,
|
||||
auto_delete_on_expiry,
|
||||
total_requests,
|
||||
total_tokens,
|
||||
total_cost_usd,
|
||||
created_at,
|
||||
updated_at
|
||||
@@ -371,6 +378,7 @@ VALUES (
|
||||
$14,
|
||||
$15,
|
||||
$16,
|
||||
$17,
|
||||
NOW(),
|
||||
NOW()
|
||||
)
|
||||
@@ -390,6 +398,7 @@ RETURNING
|
||||
CAST(EXTRACT(EPOCH FROM expires_at) AS BIGINT) AS expires_at_unix_secs,
|
||||
auto_delete_on_expiry,
|
||||
total_requests,
|
||||
COALESCE(total_tokens, 0)::BIGINT AS total_tokens,
|
||||
COALESCE(CAST(total_cost_usd AS DOUBLE PRECISION), 0) AS total_cost_usd,
|
||||
is_standalone
|
||||
"#;
|
||||
@@ -413,6 +422,7 @@ INSERT INTO api_keys (
|
||||
is_standalone,
|
||||
auto_delete_on_expiry,
|
||||
total_requests,
|
||||
total_tokens,
|
||||
total_cost_usd,
|
||||
created_at,
|
||||
updated_at
|
||||
@@ -436,6 +446,7 @@ VALUES (
|
||||
$14,
|
||||
$15,
|
||||
$16,
|
||||
$17,
|
||||
NOW(),
|
||||
NOW()
|
||||
)
|
||||
@@ -455,6 +466,7 @@ RETURNING
|
||||
CAST(EXTRACT(EPOCH FROM expires_at) AS BIGINT) AS expires_at_unix_secs,
|
||||
auto_delete_on_expiry,
|
||||
total_requests,
|
||||
COALESCE(total_tokens, 0)::BIGINT AS total_tokens,
|
||||
COALESCE(CAST(total_cost_usd AS DOUBLE PRECISION), 0) AS total_cost_usd,
|
||||
is_standalone
|
||||
"#;
|
||||
@@ -485,6 +497,7 @@ RETURNING
|
||||
CAST(EXTRACT(EPOCH FROM expires_at) AS BIGINT) AS expires_at_unix_secs,
|
||||
auto_delete_on_expiry,
|
||||
total_requests,
|
||||
COALESCE(total_tokens, 0)::BIGINT AS total_tokens,
|
||||
COALESCE(CAST(total_cost_usd AS DOUBLE PRECISION), 0) AS total_cost_usd,
|
||||
is_standalone
|
||||
"#;
|
||||
@@ -519,6 +532,7 @@ RETURNING
|
||||
CAST(EXTRACT(EPOCH FROM expires_at) AS BIGINT) AS expires_at_unix_secs,
|
||||
auto_delete_on_expiry,
|
||||
total_requests,
|
||||
COALESCE(total_tokens, 0)::BIGINT AS total_tokens,
|
||||
COALESCE(CAST(total_cost_usd AS DOUBLE PRECISION), 0) AS total_cost_usd,
|
||||
is_standalone
|
||||
"#;
|
||||
@@ -547,6 +561,7 @@ RETURNING
|
||||
CAST(EXTRACT(EPOCH FROM expires_at) AS BIGINT) AS expires_at_unix_secs,
|
||||
auto_delete_on_expiry,
|
||||
total_requests,
|
||||
COALESCE(total_tokens, 0)::BIGINT AS total_tokens,
|
||||
COALESCE(CAST(total_cost_usd AS DOUBLE PRECISION), 0) AS total_cost_usd,
|
||||
is_standalone
|
||||
"#;
|
||||
@@ -574,6 +589,7 @@ RETURNING
|
||||
CAST(EXTRACT(EPOCH FROM expires_at) AS BIGINT) AS expires_at_unix_secs,
|
||||
auto_delete_on_expiry,
|
||||
total_requests,
|
||||
COALESCE(total_tokens, 0)::BIGINT AS total_tokens,
|
||||
COALESCE(CAST(total_cost_usd AS DOUBLE PRECISION), 0) AS total_cost_usd,
|
||||
is_standalone
|
||||
"#;
|
||||
@@ -612,6 +628,7 @@ RETURNING
|
||||
CAST(EXTRACT(EPOCH FROM expires_at) AS BIGINT) AS expires_at_unix_secs,
|
||||
auto_delete_on_expiry,
|
||||
total_requests,
|
||||
COALESCE(total_tokens, 0)::BIGINT AS total_tokens,
|
||||
COALESCE(CAST(total_cost_usd AS DOUBLE PRECISION), 0) AS total_cost_usd,
|
||||
is_standalone
|
||||
"#;
|
||||
@@ -640,6 +657,7 @@ RETURNING
|
||||
CAST(EXTRACT(EPOCH FROM expires_at) AS BIGINT) AS expires_at_unix_secs,
|
||||
auto_delete_on_expiry,
|
||||
total_requests,
|
||||
COALESCE(total_tokens, 0)::BIGINT AS total_tokens,
|
||||
COALESCE(CAST(total_cost_usd AS DOUBLE PRECISION), 0) AS total_cost_usd,
|
||||
is_standalone
|
||||
"#;
|
||||
@@ -1049,6 +1067,7 @@ impl AuthApiKeyWriteRepository for SqlxAuthApiKeySnapshotReadRepository {
|
||||
.bind(expires_at)
|
||||
.bind(record.auto_delete_on_expiry)
|
||||
.bind(record.total_requests as i64)
|
||||
.bind(record.total_tokens as i64)
|
||||
.bind(record.total_cost_usd)
|
||||
.fetch_optional(&self.pool)
|
||||
.await
|
||||
@@ -1099,6 +1118,7 @@ impl AuthApiKeyWriteRepository for SqlxAuthApiKeySnapshotReadRepository {
|
||||
.bind(expires_at)
|
||||
.bind(record.auto_delete_on_expiry)
|
||||
.bind(record.total_requests as i64)
|
||||
.bind(record.total_tokens as i64)
|
||||
.bind(record.total_cost_usd)
|
||||
.fetch_optional(&self.pool)
|
||||
.await
|
||||
@@ -1373,6 +1393,7 @@ fn map_auth_api_key_export_row(
|
||||
row_get(row, "expires_at_unix_secs")?,
|
||||
row_get(row, "auto_delete_on_expiry")?,
|
||||
row_get::<i32>(row, "total_requests")?.into(),
|
||||
row_get::<i64>(row, "total_tokens")?,
|
||||
row_get(row, "total_cost_usd")?,
|
||||
row_get(row, "is_standalone")?,
|
||||
)
|
||||
|
||||
@@ -266,6 +266,7 @@ pub struct StoredAuthApiKeyExportRecord {
|
||||
pub expires_at_unix_secs: Option<u64>,
|
||||
pub auto_delete_on_expiry: bool,
|
||||
pub total_requests: u64,
|
||||
pub total_tokens: u64,
|
||||
pub total_cost_usd: f64,
|
||||
pub is_standalone: bool,
|
||||
}
|
||||
@@ -288,6 +289,7 @@ impl StoredAuthApiKeyExportRecord {
|
||||
expires_at_unix_secs: Option<i64>,
|
||||
auto_delete_on_expiry: bool,
|
||||
total_requests: i64,
|
||||
total_tokens: i64,
|
||||
total_cost_usd: f64,
|
||||
is_standalone: bool,
|
||||
) -> Result<Self, crate::DataLayerError> {
|
||||
@@ -333,6 +335,7 @@ impl StoredAuthApiKeyExportRecord {
|
||||
.transpose()?,
|
||||
auto_delete_on_expiry,
|
||||
total_requests: parse_u64_i64(total_requests, "api_keys.total_requests")?,
|
||||
total_tokens: parse_u64_i64(total_tokens, "api_keys.total_tokens")?,
|
||||
total_cost_usd,
|
||||
is_standalone,
|
||||
})
|
||||
@@ -369,6 +372,7 @@ pub struct CreateUserApiKeyRecord {
|
||||
pub expires_at_unix_secs: Option<u64>,
|
||||
pub auto_delete_on_expiry: bool,
|
||||
pub total_requests: u64,
|
||||
pub total_tokens: u64,
|
||||
pub total_cost_usd: f64,
|
||||
}
|
||||
|
||||
@@ -398,6 +402,7 @@ pub struct CreateStandaloneApiKeyRecord {
|
||||
pub expires_at_unix_secs: Option<u64>,
|
||||
pub auto_delete_on_expiry: bool,
|
||||
pub total_requests: u64,
|
||||
pub total_tokens: u64,
|
||||
pub total_cost_usd: f64,
|
||||
}
|
||||
|
||||
@@ -833,6 +838,7 @@ mod tests {
|
||||
None,
|
||||
false,
|
||||
-1,
|
||||
0,
|
||||
0.0,
|
||||
false,
|
||||
)
|
||||
@@ -857,6 +863,7 @@ mod tests {
|
||||
Some(200),
|
||||
false,
|
||||
12,
|
||||
480,
|
||||
1.25,
|
||||
false,
|
||||
)
|
||||
|
||||
@@ -1983,11 +1983,12 @@ impl UsageReadRepository for InMemoryUsageReadRepository {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
if query.admin_mode {
|
||||
if item.status == "pending" || item.status == "streaming" {
|
||||
continue;
|
||||
}
|
||||
} else if item.billing_status != "settled" || item.total_cost_usd <= 0.0 {
|
||||
if item.status == "pending" || item.status == "streaming" {
|
||||
continue;
|
||||
}
|
||||
if item.provider_name.eq_ignore_ascii_case("unknown")
|
||||
|| item.provider_name.eq_ignore_ascii_case("pending")
|
||||
{
|
||||
continue;
|
||||
}
|
||||
let ts = i64::try_from(item.created_at_unix_ms).unwrap_or_default();
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user