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:
fawney19
2026-04-22 17:29:39 +08:00
parent 063ef02306
commit a5e6bd3b62
45 changed files with 15323 additions and 1039 deletions

File diff suppressed because it is too large Load Diff

View File

@@ -1,4 +1,5 @@
pub mod backends;
pub mod backfill;
mod config;
mod error;
pub mod migrate;

View File

@@ -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,
]
);
}

View File

@@ -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,
)

View File

@@ -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")?,
)

View File

@@ -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,
)

View File

@@ -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