feat: customer detail page, size snapshots table, Spaces provisioning, Redis status cache
Some checks failed
Build & Release / build (push) Has been cancelled
Some checks failed
Build & Release / build (push) Has been cancelled
This commit is contained in:
87
src/services/sizeCollector.ts
Normal file
87
src/services/sizeCollector.ts
Normal file
@@ -0,0 +1,87 @@
|
||||
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();
|
||||
}
|
||||
Reference in New Issue
Block a user