import postgres from "postgres"; import { db } from "../db/manager"; import { config } from "../lib/config"; import { getSecret } from "../lib/k8s"; import { getSpacesUsage } from "./spaces"; async function collectSizes() { const customers = await db`SELECT slug, spaces_key FROM customers WHERE status = 'provisioned'`; if (customers.length === 0) return; for (const customer of customers) { const { slug } = customer; try { // DB size let dbSizeBytes: number | null = null; try { const sql = postgres(config.doadminDbUrl.replace(/\/([^/?]+)(\?|$)/, `/${slug}$2`), { max: 1 }); try { const [row] = await sql<[{ bytes: string }]>`SELECT pg_database_size(${slug}::text)::text AS bytes`; dbSizeBytes = Number(row.bytes); } finally { await sql.end(); } } catch {} // Spaces size let spacesSizeBytes: number | null = null; let spacesObjectCount: number | null = null; if (customer.spaces_key) { try { const namespace = `customer-${slug}`; const secrets = await getSecret(namespace, "lunarfront-secrets"); const result = await getSpacesUsage( customer.spaces_key, secrets["spaces-secret"], config.spacesBucket, config.spacesRegion, `${slug}/`, ); spacesSizeBytes = result.sizeBytes; spacesObjectCount = result.objectCount; } catch {} } // Upsert today's snapshot (one row per day per customer) await db` INSERT INTO customer_size_snapshots (slug, recorded_at, db_size_bytes, spaces_size_bytes, spaces_object_count) VALUES (${slug}, CURRENT_DATE, ${dbSizeBytes}, ${spacesSizeBytes}, ${spacesObjectCount}) ON CONFLICT (slug, recorded_at) DO UPDATE SET db_size_bytes = EXCLUDED.db_size_bytes, spaces_size_bytes = EXCLUDED.spaces_size_bytes, spaces_object_count = EXCLUDED.spaces_object_count `; } catch (err) { console.error(`[sizeCollector] failed for ${slug}:`, err); } } } function msUntilNext12h(): number { const now = new Date(); const next = new Date(now); const h = now.getUTCHours(); // next run at 00:00 or 12:00 UTC, whichever is sooner if (h < 12) { next.setUTCHours(12, 0, 0, 0); } else { next.setUTCDate(next.getUTCDate() + 1); next.setUTCHours(0, 0, 0, 0); } return next.getTime() - now.getTime(); } export function startSizeCollector(log: { info: (msg: string) => void }) { // Run immediately on startup collectSizes().then(() => log.info("Initial size snapshot collected")).catch(() => {}); // Then schedule for 00:00 and 12:00 UTC function scheduleNext() { const delay = msUntilNext12h(); setTimeout(() => { collectSizes().then(() => log.info("Size snapshot collected")).catch(() => {}); scheduleNext(); }, delay); } scheduleNext(); }