mirror of
https://github.com/fawney19/Aether.git
synced 2026-10-07 01:47:47 +08:00
Add dashboard and overview analytics, health monitoring, provider expense tracking, and announcement updates across the gateway and frontend. Keep schema migrations free of historical backfills while preserving automatic backfill execution. Bound migration deadlines, run schema preparation before Compose replacement, and anonymize deleted dashboard users. Include the current documentation cleanup and regression coverage.
243 lines
12 KiB
Rust
243 lines
12 KiB
Rust
use super::{
|
|
analytics::{dashboard_total_metrics_sql, read_analytics_metrics},
|
|
projection_reader::{read_dashboard_total_summary, read_projection_coverage},
|
|
SqlxUsageReadRepository,
|
|
};
|
|
use crate::error::SqlxResultExt;
|
|
use aether_data_contracts::{repository::usage::*, DataLayerError};
|
|
use chrono::{DateTime, Utc};
|
|
use sqlx::{Postgres, QueryBuilder, Row};
|
|
|
|
// This projection keeps the canonical billing token precedence while reading usage
|
|
// once. Joining the two public fact views reads and hashes the entire usage table
|
|
// twice. Extract both availability flags together so large JSON metadata is parsed
|
|
// once per row. Live database regressions compare these fields with the canonical view.
|
|
const DASHBOARD_TOTAL_FACTS_SQL: &str = r#"(
|
|
SELECT u.created_at, u.api_key_id, u.model, u.provider_id, u.api_format, u.endpoint_kind,
|
|
u.request_type, u.status, u.is_stream, u.has_format_conversion, u.failure_origin,
|
|
'request'::text AS record_kind,
|
|
COALESCE(s.billing_status, u.billing_status) AS settlement_status,
|
|
COALESCE(availability.usage_available, 'true'::jsonb) <> 'false'::jsonb AS usage_available,
|
|
COALESCE(availability.usage_pricing_available, 'true'::jsonb) <> 'false'::jsonb
|
|
AND (s.billing_total_cost_usd IS NOT NULL OR COALESCE(s.billing_status, u.billing_status) = 'settled') AS pricing_available,
|
|
CASE WHEN COALESCE(availability.usage_available, 'true'::jsonb) <> 'false'::jsonb THEN
|
|
GREATEST(
|
|
COALESCE(
|
|
CASE
|
|
WHEN s.billing_effective_input_tokens IS NOT NULL
|
|
THEN GREATEST(s.billing_effective_input_tokens, 0)
|
|
+ GREATEST(COALESCE(s.billing_output_tokens, u.output_tokens, 0), 0)
|
|
+ GREATEST(
|
|
COALESCE(
|
|
s.billing_cache_creation_tokens,
|
|
CASE
|
|
WHEN s.billing_cache_creation_5m_tokens IS NOT NULL
|
|
OR s.billing_cache_creation_1h_tokens IS NOT NULL
|
|
THEN COALESCE(s.billing_cache_creation_5m_tokens, 0)
|
|
+ COALESCE(s.billing_cache_creation_1h_tokens, 0)
|
|
END,
|
|
CASE
|
|
WHEN COALESCE(u.cache_creation_input_tokens, 0) = 0
|
|
AND (
|
|
COALESCE(u.cache_creation_input_tokens_5m, 0)
|
|
+ COALESCE(u.cache_creation_input_tokens_1h, 0)
|
|
) > 0
|
|
THEN COALESCE(u.cache_creation_input_tokens_5m, 0)
|
|
+ COALESCE(u.cache_creation_input_tokens_1h, 0)
|
|
ELSE COALESCE(u.cache_creation_input_tokens, 0)
|
|
END,
|
|
0
|
|
),
|
|
0
|
|
)
|
|
+ GREATEST(
|
|
COALESCE(
|
|
s.billing_cache_read_tokens,
|
|
u.cache_read_input_tokens,
|
|
0
|
|
),
|
|
0
|
|
)
|
|
WHEN s.billing_total_input_context IS NOT NULL
|
|
THEN GREATEST(s.billing_total_input_context, 0)
|
|
+ GREATEST(COALESCE(s.billing_output_tokens, u.output_tokens, 0), 0)
|
|
END,
|
|
NULLIF(GREATEST(COALESCE(u.total_tokens, 0), 0), 0),
|
|
CASE
|
|
WHEN split_part(lower(COALESCE(COALESCE(u.endpoint_api_format, u.api_format), '')), ':', 1)
|
|
IN ('openai', 'gemini', 'google')
|
|
THEN GREATEST(COALESCE(u.input_tokens, 0), 0)
|
|
+ GREATEST(COALESCE(u.output_tokens, 0), 0)
|
|
ELSE GREATEST(COALESCE(u.input_tokens, 0), 0)
|
|
+ GREATEST(COALESCE(u.output_tokens, 0), 0)
|
|
+ GREATEST(
|
|
CASE
|
|
WHEN COALESCE(u.cache_creation_input_tokens, 0) = 0
|
|
AND (
|
|
COALESCE(u.cache_creation_input_tokens_5m, 0)
|
|
+ COALESCE(u.cache_creation_input_tokens_1h, 0)
|
|
) > 0
|
|
THEN COALESCE(u.cache_creation_input_tokens_5m, 0)
|
|
+ COALESCE(u.cache_creation_input_tokens_1h, 0)
|
|
ELSE COALESCE(u.cache_creation_input_tokens, 0)
|
|
END,
|
|
0
|
|
)
|
|
+ GREATEST(COALESCE(u.cache_read_input_tokens, 0), 0)
|
|
END,
|
|
0
|
|
),
|
|
0
|
|
)::bigint END AS total_tokens,
|
|
CASE WHEN COALESCE(availability.usage_pricing_available, 'true'::jsonb) <> 'false'::jsonb
|
|
AND (s.billing_actual_total_cost_usd IS NOT NULL OR COALESCE(s.billing_status, u.billing_status) = 'settled')
|
|
THEN round(COALESCE(s.billing_actual_total_cost_usd::numeric, u.actual_total_cost_usd::numeric), 8) END AS billable_amount,
|
|
s.allocation_status
|
|
FROM public.usage u
|
|
LEFT JOIN public.usage_settlement_snapshots s USING (request_id)
|
|
CROSS JOIN LATERAL json_to_record(
|
|
CASE WHEN json_typeof(u.request_metadata)='object' THEN u.request_metadata ELSE '{}'::json END
|
|
) AS availability(usage_available jsonb, usage_pricing_available jsonb)
|
|
WHERE NOT EXISTS (SELECT 1 FROM public.usage_attribution_snapshots a
|
|
WHERE a.request_id=u.request_id AND a.record_kind='session')
|
|
) AS usage_analytics_facts_v1"#;
|
|
|
|
pub(super) fn push_dashboard_total_filter(
|
|
builder: &mut QueryBuilder<'_, Postgres>,
|
|
query: &UsageAnalyticsQuery,
|
|
) {
|
|
super::analytics::push_analytics_source_filter(builder, query, DASHBOARD_TOTAL_FACTS_SQL);
|
|
}
|
|
|
|
/// Lifetime cards only need additive totals and their coverage. Performance and
|
|
/// active-user metrics remain exact in the separate today snapshot.
|
|
pub(super) async fn read_dashboard_total_metrics(
|
|
tx: &mut sqlx::Transaction<'_, Postgres>,
|
|
query: &UsageAnalyticsQuery,
|
|
projection_reads: bool,
|
|
) -> Result<UsageAnalyticsMetrics, DataLayerError> {
|
|
// A materialized valid-bucket CTE prevents parallel aggregation of raw gaps.
|
|
// Upgraded installations can have years of raw history and no built buckets;
|
|
// use the parallel raw query directly until there is a bucket to combine.
|
|
let has_projection = if projection_reads {
|
|
sqlx::query_scalar::<_, bool>(r#"SELECT EXISTS (
|
|
SELECT 1 FROM stats_bucket_state WHERE projection_version='overview-v2'
|
|
AND granularity='hour' AND source_revision=built_revision AND coverage_status='complete'
|
|
AND NOT EXISTS (SELECT 1 FROM stats_overview_dirty_events e
|
|
WHERE e.projection_version=stats_bucket_state.projection_version
|
|
AND e.granularity=stats_bucket_state.granularity
|
|
AND e.bucket_start=stats_bucket_state.bucket_start)
|
|
AND bucket_start = date_trunc('hour', bucket_start AT TIME ZONE 'UTC') AT TIME ZONE 'UTC'
|
|
AND bucket_start >= $1 AND bucket_start + INTERVAL '1 hour' <= $2
|
|
AND bucket_start + INTERVAL '1 hour' <= NOW())"#)
|
|
.bind(DateTime::<Utc>::from_timestamp_millis(query.from_unix_ms as i64).expect("validated"))
|
|
.bind(DateTime::<Utc>::from_timestamp_millis(query.to_unix_ms as i64).expect("validated"))
|
|
.fetch_one(&mut **tx).await.map_postgres_err()?
|
|
} else {
|
|
false
|
|
};
|
|
if has_projection {
|
|
return read_dashboard_total_summary(tx, query).await;
|
|
}
|
|
let mut builder = QueryBuilder::<Postgres>::new("SELECT to_jsonb(m) AS metrics FROM (SELECT ");
|
|
builder.push(dashboard_total_metrics_sql());
|
|
push_dashboard_total_filter(&mut builder, query);
|
|
builder.push(") m");
|
|
let row = builder
|
|
.build()
|
|
.fetch_one(&mut **tx)
|
|
.await
|
|
.map_postgres_err()?;
|
|
serde_json::from_value(row.try_get("metrics").map_postgres_err()?)
|
|
.map_err(|error| DataLayerError::UnexpectedValue(error.to_string()))
|
|
}
|
|
|
|
impl SqlxUsageReadRepository {
|
|
pub async fn query_dashboard_analytics(
|
|
&self,
|
|
query: &UsageDashboardAnalyticsQuery,
|
|
) -> Result<StoredUsageDashboardAnalytics, DataLayerError> {
|
|
query.validate()?;
|
|
let mut tx = self.pool.begin().await.map_postgres_err()?;
|
|
sqlx::query("SET TRANSACTION ISOLATION LEVEL REPEATABLE READ, READ ONLY")
|
|
.execute(&mut *tx)
|
|
.await
|
|
.map_postgres_err()?;
|
|
sqlx::query("SET LOCAL statement_timeout='180s'")
|
|
.execute(&mut *tx)
|
|
.await
|
|
.map_postgres_err()?;
|
|
let state=sqlx::query(r#"SELECT date_trunc('milliseconds',NOW()) AS now,pg_current_snapshot()::text AS revision,
|
|
(SELECT min(created_at) FROM usage WHERE created_at<NOW()) AS raw_from,
|
|
(SELECT min(bucket_start) FROM (SELECT bucket_start FROM stats_bucket_state WHERE projection_version IN ('overview-v1','overview-v2') AND granularity='hour' UNION ALL SELECT bucket_start FROM stats_overview_dirty_events WHERE projection_version='overview-v2' AND granularity='hour') buckets WHERE bucket_start<NOW()) AS state_from,
|
|
(SELECT CASE WHEN pg_typeof(min(date)) IN ('bigint'::regtype,'integer'::regtype)
|
|
THEN to_timestamp(min(date)::text::double precision) ELSE min(date)::text::timestamptz END
|
|
FROM stats_daily WHERE total_requests>0) AS legacy_from,
|
|
(SELECT count(*) FROM users WHERE is_active AND NOT is_deleted) AS enabled_users"#)
|
|
.fetch_one(&mut *tx).await.map_postgres_err()?;
|
|
let now: DateTime<Utc> = state.try_get("now").map_postgres_err()?;
|
|
let raw_from: Option<DateTime<Utc>> = state.try_get("raw_from").map_postgres_err()?;
|
|
let state_from: Option<DateTime<Utc>> = state.try_get("state_from").map_postgres_err()?;
|
|
let legacy_from: Option<DateTime<Utc>> = state.try_get("legacy_from").map_postgres_err()?;
|
|
let total_from = raw_from.into_iter().chain(state_from).min();
|
|
let today_from = query.today_start(now)?;
|
|
let revision: String = state.try_get("revision").map_postgres_err()?;
|
|
let enabled_users = state
|
|
.try_get::<i64, _>("enabled_users")
|
|
.map_postgres_err()? as u64;
|
|
let mut snapshots = Vec::with_capacity(2);
|
|
for (from, lifetime) in [(today_from, false), (total_from.unwrap_or(now), true)] {
|
|
let range = UsageAnalyticsQuery {
|
|
from_unix_ms: from.timestamp_millis().max(0) as u64,
|
|
to_unix_ms: now.timestamp_millis().max(0) as u64,
|
|
timezone: query.timezone.clone(),
|
|
limit: 1,
|
|
..Default::default()
|
|
};
|
|
let mut summary = if lifetime {
|
|
read_dashboard_total_metrics(&mut tx, &range, self.overview_projection_reads)
|
|
.await?
|
|
} else {
|
|
read_analytics_metrics(&mut tx, &range, self.overview_projection_reads).await?
|
|
};
|
|
summary.enabled_users = enabled_users;
|
|
let unrecoverable=sqlx::query_scalar::<_,i64>("SELECT count(DISTINCT bucket_start) FROM (SELECT bucket_start FROM stats_bucket_state WHERE projection_version IN ('overview-v1','overview-v2') AND granularity='hour' AND coverage_status='unrecoverable' UNION SELECT bucket_start FROM stats_overview_dirty_events WHERE projection_version='overview-v2' AND granularity='hour' AND unrecoverable) pending WHERE bucket_start<$2 AND bucket_start+INTERVAL '1 hour'>$1")
|
|
.bind(from).bind(now).fetch_one(&mut *tx).await.map_postgres_err()? as u64;
|
|
if summary.request_count == 0 && unrecoverable == 0 {
|
|
summary.rated_amount = Some("0.00000000".into());
|
|
summary.billable_amount = Some("0.00000000".into());
|
|
}
|
|
let coverage =
|
|
read_projection_coverage(&mut tx, &range, self.overview_projection_reads).await?;
|
|
snapshots.push(StoredUsageAnalytics {
|
|
total: summary.request_count,
|
|
summary,
|
|
read_revision: revision.clone(),
|
|
generated_at: now.to_rfc3339(),
|
|
unrecoverable_bucket_count: unrecoverable,
|
|
coverage,
|
|
..Default::default()
|
|
});
|
|
}
|
|
let mut total = snapshots.pop().expect("total");
|
|
let today = snapshots.pop().expect("today");
|
|
let known_gap = total.unrecoverable_bucket_count > 0
|
|
|| legacy_from.is_some_and(|legacy| {
|
|
raw_from.is_none_or(|raw| legacy.date_naive() < raw.date_naive())
|
|
});
|
|
if known_gap && total.summary.request_count == 0 {
|
|
total.summary.rated_amount = None;
|
|
total.summary.billable_amount = None;
|
|
}
|
|
tx.commit().await.map_postgres_err()?;
|
|
Ok(StoredUsageDashboardAnalytics {
|
|
today,
|
|
total,
|
|
today_from: today_from.to_rfc3339(),
|
|
total_from: total_from.map(|value| value.to_rfc3339()),
|
|
to: now.to_rfc3339(),
|
|
history_complete: known_gap.then_some(false),
|
|
})
|
|
}
|
|
}
|