mirror of
https://github.com/fawney19/Aether.git
synced 2026-09-02 09:20:22 +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:
@@ -104,6 +104,7 @@ fn sample_standalone_export_record(
|
||||
Some(4_102_444_800),
|
||||
false,
|
||||
7,
|
||||
0,
|
||||
1.25,
|
||||
true,
|
||||
)
|
||||
|
||||
@@ -414,6 +414,7 @@ fn sample_monitoring_export_api_key(
|
||||
None,
|
||||
false,
|
||||
0,
|
||||
0,
|
||||
0.0,
|
||||
false,
|
||||
)
|
||||
|
||||
@@ -635,6 +635,7 @@ async fn gateway_handles_admin_system_users_export_locally_with_trusted_admin_pr
|
||||
Some(1_900_000_000),
|
||||
false,
|
||||
42,
|
||||
420,
|
||||
12.34,
|
||||
false,
|
||||
)
|
||||
@@ -661,6 +662,7 @@ async fn gateway_handles_admin_system_users_export_locally_with_trusted_admin_pr
|
||||
None,
|
||||
false,
|
||||
7,
|
||||
84,
|
||||
3.21,
|
||||
true,
|
||||
)
|
||||
@@ -749,10 +751,15 @@ async fn gateway_handles_admin_system_users_export_locally_with_trusted_admin_pr
|
||||
payload["users"][0]["api_keys"][0]["is_standalone"],
|
||||
json!(false)
|
||||
);
|
||||
assert_eq!(
|
||||
payload["users"][0]["api_keys"][0]["total_tokens"],
|
||||
json!(420)
|
||||
);
|
||||
assert_eq!(
|
||||
payload["standalone_keys"][0]["key"],
|
||||
json!("ak-standalone-live-1")
|
||||
);
|
||||
assert_eq!(payload["standalone_keys"][0]["total_tokens"], json!(84));
|
||||
assert_eq!(
|
||||
payload["standalone_keys"][0]["wallet"]["unlimited"],
|
||||
json!(true)
|
||||
|
||||
@@ -607,6 +607,7 @@ async fn gateway_imports_admin_system_users_locally_and_persists_data() {
|
||||
"expires_at": "2099-01-01T00:00:00Z",
|
||||
"auto_delete_on_expiry": false,
|
||||
"total_requests": 12,
|
||||
"total_tokens": 3456,
|
||||
"total_cost_usd": 1.25
|
||||
}]
|
||||
}],
|
||||
@@ -622,6 +623,7 @@ async fn gateway_imports_admin_system_users_locally_and_persists_data() {
|
||||
"expires_at": "2099-02-01T00:00:00Z",
|
||||
"auto_delete_on_expiry": false,
|
||||
"total_requests": 3,
|
||||
"total_tokens": 789,
|
||||
"total_cost_usd": 0.75,
|
||||
"wallet": {
|
||||
"balance": 30.0,
|
||||
@@ -704,6 +706,7 @@ async fn gateway_imports_admin_system_users_locally_and_persists_data() {
|
||||
.expect("user api keys should load");
|
||||
assert_eq!(user_api_keys.len(), 1);
|
||||
assert_eq!(user_api_keys[0].name.as_deref(), Some("Alice CLI"));
|
||||
assert_eq!(user_api_keys[0].total_tokens, 3456);
|
||||
assert_eq!(
|
||||
user_api_keys[0].allowed_api_formats,
|
||||
Some(vec!["openai:chat".to_string()])
|
||||
@@ -729,6 +732,7 @@ async fn gateway_imports_admin_system_users_locally_and_persists_data() {
|
||||
standalone_keys[0].name.as_deref(),
|
||||
Some("Imported Standalone")
|
||||
);
|
||||
assert_eq!(standalone_keys[0].total_tokens, 789);
|
||||
assert_eq!(
|
||||
decrypt_python_fernet_ciphertext(
|
||||
DEVELOPMENT_ENCRYPTION_KEY,
|
||||
|
||||
@@ -674,6 +674,7 @@ async fn gateway_handles_admin_user_api_key_routes_locally_with_trusted_admin_pr
|
||||
Some(200),
|
||||
false,
|
||||
9,
|
||||
0,
|
||||
1.5,
|
||||
false,
|
||||
)
|
||||
@@ -835,6 +836,7 @@ async fn gateway_returns_conflict_for_admin_create_user_api_key_when_writer_unav
|
||||
Some(200),
|
||||
false,
|
||||
9,
|
||||
0,
|
||||
1.5,
|
||||
false,
|
||||
)
|
||||
@@ -963,6 +965,7 @@ async fn gateway_returns_conflict_for_admin_lock_user_api_key_when_writer_unavai
|
||||
Some(200),
|
||||
false,
|
||||
9,
|
||||
0,
|
||||
1.5,
|
||||
false,
|
||||
)
|
||||
@@ -1088,6 +1091,7 @@ async fn gateway_returns_conflict_for_admin_update_user_api_key_when_writer_unav
|
||||
Some(200),
|
||||
false,
|
||||
9,
|
||||
0,
|
||||
1.5,
|
||||
false,
|
||||
)
|
||||
@@ -1162,6 +1166,7 @@ async fn gateway_returns_conflict_for_admin_delete_user_api_key_when_writer_unav
|
||||
Some(200),
|
||||
false,
|
||||
9,
|
||||
0,
|
||||
1.5,
|
||||
false,
|
||||
)
|
||||
@@ -1238,6 +1243,7 @@ async fn gateway_lists_admin_user_api_keys_locally_with_trusted_admin_principal(
|
||||
Some(200),
|
||||
false,
|
||||
9,
|
||||
0,
|
||||
1.5,
|
||||
false,
|
||||
)
|
||||
@@ -1428,6 +1434,7 @@ async fn gateway_reveals_admin_user_full_key_locally_with_trusted_admin_principa
|
||||
Some(200),
|
||||
false,
|
||||
9,
|
||||
0,
|
||||
1.5,
|
||||
false,
|
||||
)
|
||||
|
||||
@@ -5833,6 +5833,7 @@ async fn gateway_handles_users_me_api_keys_locally_without_proxying_upstream() {
|
||||
Some(300),
|
||||
false,
|
||||
9,
|
||||
0,
|
||||
1.5,
|
||||
false,
|
||||
)
|
||||
@@ -6143,6 +6144,7 @@ async fn gateway_handles_users_me_api_key_writes_locally_without_proxying_upstre
|
||||
Some(300),
|
||||
false,
|
||||
3,
|
||||
0,
|
||||
0.5,
|
||||
false,
|
||||
)
|
||||
@@ -6411,6 +6413,7 @@ async fn gateway_returns_service_unavailable_for_users_me_api_key_writes_without
|
||||
Some(300),
|
||||
false,
|
||||
3,
|
||||
0,
|
||||
0.5,
|
||||
false,
|
||||
)
|
||||
|
||||
@@ -101,6 +101,7 @@ async fn gateway_handles_dashboard_stats_locally_without_proxying_upstream() {
|
||||
None,
|
||||
false,
|
||||
5,
|
||||
0,
|
||||
1.5,
|
||||
false,
|
||||
)
|
||||
@@ -121,6 +122,7 @@ async fn gateway_handles_dashboard_stats_locally_without_proxying_upstream() {
|
||||
None,
|
||||
false,
|
||||
1,
|
||||
0,
|
||||
0.5,
|
||||
false,
|
||||
)
|
||||
@@ -326,6 +328,7 @@ async fn gateway_handles_admin_dashboard_stats_locally_without_proxying_upstream
|
||||
None,
|
||||
false,
|
||||
5,
|
||||
0,
|
||||
1.5,
|
||||
false,
|
||||
)
|
||||
@@ -346,6 +349,7 @@ async fn gateway_handles_admin_dashboard_stats_locally_without_proxying_upstream
|
||||
None,
|
||||
false,
|
||||
1,
|
||||
0,
|
||||
0.5,
|
||||
false,
|
||||
)
|
||||
@@ -366,6 +370,7 @@ async fn gateway_handles_admin_dashboard_stats_locally_without_proxying_upstream
|
||||
None,
|
||||
false,
|
||||
3,
|
||||
0,
|
||||
0.75,
|
||||
true,
|
||||
)
|
||||
@@ -564,6 +569,118 @@ async fn gateway_handles_dashboard_daily_stats_locally_without_proxying_upstream
|
||||
upstream_handle.abort();
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn gateway_handles_user_dashboard_daily_stats_locally_without_proxying_upstream() {
|
||||
let now = stable_dashboard_now();
|
||||
let user = sample_auth_user(now);
|
||||
let access_token = build_test_auth_token(
|
||||
"access",
|
||||
serde_json::Map::from_iter([
|
||||
("user_id".to_string(), json!(user.id)),
|
||||
("role".to_string(), json!(user.role)),
|
||||
(
|
||||
"created_at".to_string(),
|
||||
json!(user.created_at.map(|value| value.to_rfc3339())),
|
||||
),
|
||||
(
|
||||
"session_id".to_string(),
|
||||
json!("session-dashboard-daily-stats-user"),
|
||||
),
|
||||
]),
|
||||
chrono::Utc::now() + chrono::Duration::hours(1),
|
||||
);
|
||||
let session = sample_auth_session(
|
||||
"user-auth-1",
|
||||
"session-dashboard-daily-stats-user",
|
||||
"device-dashboard-daily-stats-user",
|
||||
"refresh-dashboard-daily-stats-user",
|
||||
now,
|
||||
);
|
||||
let usage_repository = Arc::new(InMemoryUsageReadRepository::seed(vec![
|
||||
sample_user_usage_audit(
|
||||
"usage-dashboard-user-daily-1",
|
||||
"req-dashboard-user-daily-1",
|
||||
"user-auth-1",
|
||||
"gpt-5",
|
||||
"openai",
|
||||
"completed",
|
||||
now - chrono::Duration::hours(1),
|
||||
),
|
||||
sample_user_usage_audit(
|
||||
"usage-dashboard-user-daily-2",
|
||||
"req-dashboard-user-daily-2",
|
||||
"user-auth-1",
|
||||
"gpt-4.1",
|
||||
"openai",
|
||||
"completed",
|
||||
now - chrono::Duration::days(1) - chrono::Duration::hours(2),
|
||||
),
|
||||
sample_user_usage_audit(
|
||||
"usage-dashboard-user-daily-3",
|
||||
"req-dashboard-user-daily-3",
|
||||
"user-auth-2",
|
||||
"claude-3-7",
|
||||
"claude",
|
||||
"completed",
|
||||
now - chrono::Duration::hours(2),
|
||||
),
|
||||
]));
|
||||
|
||||
let (gateway_url, upstream_hits, gateway_handle, upstream_handle) =
|
||||
start_auth_gateway_with_builder(|| {
|
||||
let user_repository = Arc::new(InMemoryUserReadRepository::seed_auth_users(vec![
|
||||
user.clone()
|
||||
]));
|
||||
let wallet_repository =
|
||||
Arc::new(InMemoryWalletRepository::seed(vec![sample_auth_wallet(
|
||||
"user-auth-1",
|
||||
now,
|
||||
)]));
|
||||
let data_state = GatewayDataState::with_user_wallet_and_usage_for_tests(
|
||||
user_repository,
|
||||
wallet_repository,
|
||||
usage_repository,
|
||||
);
|
||||
AppState::new()
|
||||
.expect("gateway should build")
|
||||
.with_data_state_for_tests(data_state)
|
||||
.with_auth_sessions_for_tests([session])
|
||||
})
|
||||
.await;
|
||||
|
||||
let response = reqwest::Client::new()
|
||||
.get(format!("{gateway_url}/api/dashboard/daily-stats?days=2"))
|
||||
.header("authorization", format!("Bearer {access_token}"))
|
||||
.header("x-client-device-id", "device-dashboard-daily-stats-user")
|
||||
.header("user-agent", "AetherTest/1.0")
|
||||
.send()
|
||||
.await
|
||||
.expect("request should succeed");
|
||||
|
||||
assert_eq!(response.status(), StatusCode::OK);
|
||||
let payload: serde_json::Value = response.json().await.expect("json body should parse");
|
||||
let daily_stats = payload["daily_stats"]
|
||||
.as_array()
|
||||
.expect("daily stats should be array");
|
||||
assert_eq!(daily_stats.len(), 2);
|
||||
assert_eq!(daily_stats[0]["requests"], 1);
|
||||
assert_eq!(daily_stats[1]["requests"], 1);
|
||||
assert_eq!(
|
||||
daily_stats[1]["model_breakdown"].as_array().map(Vec::len),
|
||||
Some(1)
|
||||
);
|
||||
|
||||
let model_summary = payload["model_summary"]
|
||||
.as_array()
|
||||
.expect("model summary should exist");
|
||||
assert_eq!(model_summary.len(), 2);
|
||||
assert_eq!(payload.get("provider_summary"), None);
|
||||
assert_eq!(*upstream_hits.lock().expect("mutex should lock"), 0);
|
||||
|
||||
gateway_handle.abort();
|
||||
upstream_handle.abort();
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn gateway_handles_dashboard_recent_requests_locally_without_proxying_upstream() {
|
||||
let now = Utc::now();
|
||||
|
||||
Reference in New Issue
Block a user