realtime-stats.ts73 lines · main
| 1 | import { env } from '../env.js'; |
| 2 | import { log } from '../lib/logger.js'; |
| 3 | |
| 4 | /** |
| 5 | * Live snapshot of the realtime service: per-project subscription counts, |
| 6 | * per-channel refcounts, the caps the service is enforcing right now. |
| 7 | * Authenticates via the shared secret — same gate as /v1/subscribe so |
| 8 | * an unauth scrape can't enumerate project ids through this surface. |
| 9 | * |
| 10 | * Reads are cheap (an O(subs.size) walk on the realtime side) so we |
| 11 | * fetch live every time instead of caching. At the year-one 10k |
| 12 | * concurrent-sub target a snapshot is single-digit-millisecond work. |
| 13 | */ |
| 14 | |
| 15 | export interface RealtimeStats { |
| 16 | readonly totalSubscriptions: number; |
| 17 | readonly totalChannels: number; |
| 18 | readonly limits: { |
| 19 | readonly perWs: number; |
| 20 | readonly perProject: number; |
| 21 | }; |
| 22 | readonly byProject: readonly { projectId: string; subscriptions: number }[]; |
| 23 | readonly byChannel: readonly { channel: string; subscriptions: number }[]; |
| 24 | } |
| 25 | |
| 26 | export async function fetchRealtimeStats(): Promise<RealtimeStats | null> { |
| 27 | if (!env.BRIVEN_REALTIME_URL || !env.BRIVEN_RUNTIME_SHARED_SECRET) return null; |
| 28 | try { |
| 29 | const res = await fetch(`${env.BRIVEN_REALTIME_URL}/v1/realtime/stats`, { |
| 30 | headers: { authorization: `Bearer ${env.BRIVEN_RUNTIME_SHARED_SECRET}` }, |
| 31 | }); |
| 32 | if (!res.ok) { |
| 33 | log.warn('realtime_stats_http', { status: res.status }); |
| 34 | return null; |
| 35 | } |
| 36 | return (await res.json()) as RealtimeStats; |
| 37 | } catch (err) { |
| 38 | log.warn('realtime_stats_failed', { |
| 39 | message: err instanceof Error ? err.message : String(err), |
| 40 | }); |
| 41 | return null; |
| 42 | } |
| 43 | } |
| 44 | |
| 45 | export interface ProjectRealtimeStats { |
| 46 | /** Live concurrent subscriptions for the project. Zero when no entry. */ |
| 47 | readonly subscriptions: number; |
| 48 | /** The hard cap the realtime service is enforcing right now. */ |
| 49 | readonly limit: number; |
| 50 | /** subscriptions / limit, 0-1. Convenience for severity coloring. */ |
| 51 | readonly fillRatio: number; |
| 52 | } |
| 53 | |
| 54 | /** |
| 55 | * Per-project realtime usage. Reuses fetchRealtimeStats() under the hood |
| 56 | * but scopes the result to a single project so the dashboard surface |
| 57 | * doesn't accidentally enumerate other projects. Returns null when the |
| 58 | * realtime service is unconfigured/unreachable; callers render a "—". |
| 59 | */ |
| 60 | export async function fetchProjectRealtimeStats( |
| 61 | projectId: string, |
| 62 | ): Promise<ProjectRealtimeStats | null> { |
| 63 | const stats = await fetchRealtimeStats(); |
| 64 | if (!stats) return null; |
| 65 | const hit = stats.byProject.find((p) => p.projectId === projectId); |
| 66 | const subscriptions = hit?.subscriptions ?? 0; |
| 67 | const limit = stats.limits.perProject; |
| 68 | return { |
| 69 | subscriptions, |
| 70 | limit, |
| 71 | fillRatio: limit > 0 ? subscriptions / limit : 0, |
| 72 | }; |
| 73 | } |