Files
lunarfront-manager/src/services/sizeCollector.ts
Ryan Moon b11b51aa1e
Some checks failed
Build & Release / build (push) Has been cancelled
feat: customer detail page, size snapshots table, Spaces provisioning, Redis status cache
2026-04-03 20:07:19 -05:00

88 lines
2.9 KiB
TypeScript

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();
}