import { db, jobs, invoices } from "@workspace/db"; import { sql } from "drizzle-orm"; import { latencyHistogram, type BucketStats } from "./histogram.js"; import { requestCounters, type RequestCountsSnapshot } from "./request-counters.js"; export interface JobStateCounts { awaiting_eval: number; awaiting_work: number; complete: number; rejected: number; failed: number; } export interface MetricsSnapshot { uptime_s: number; http: RequestCountsSnapshot; jobs: { total: number; by_state: JobStateCounts; }; invoices: { total: number; paid: number; conversion_rate: number | null; }; earnings: { total_sats: number; }; latency: { eval_phase: BucketStats | null; work_phase: BucketStats | null; routes: Record; }; } const START_TIME = Date.now(); export class MetricsService { async snapshot(): Promise { const [jobsByState, invoiceCounts, earningsRow] = await Promise.all([ db .select({ state: jobs.state, count: sql`cast(count(*) as int)`, }) .from(jobs) .groupBy(jobs.state), db .select({ total: sql`cast(count(*) as int)`, paid: sql`cast(sum(case when paid then 1 else 0 end) as int)`, }) .from(invoices), db .select({ total_sats: sql`cast(coalesce(sum(actual_amount_sats), 0) as int)`, }) .from(jobs), ]); // Group raw DB states into operational state keys const rawCounts: Record = {}; let jobsTotal = 0; for (const row of jobsByState) { const n = Number(row.count); rawCounts[row.state] = (rawCounts[row.state] ?? 0) + n; jobsTotal += n; } const byState: JobStateCounts = { awaiting_eval: (rawCounts["awaiting_eval_payment"] ?? 0) + (rawCounts["evaluating"] ?? 0), awaiting_work: (rawCounts["awaiting_work_payment"] ?? 0) + (rawCounts["executing"] ?? 0), complete: rawCounts["complete"] ?? 0, rejected: rawCounts["rejected"] ?? 0, failed: rawCounts["failed"] ?? 0, }; const invRow = invoiceCounts[0] ?? { total: 0, paid: 0 }; const invTotal = Number(invRow.total); const invPaid = Number(invRow.paid); const conversionRate = invTotal > 0 ? invPaid / invTotal : null; const totalSats = Number(earningsRow[0]?.total_sats ?? 0); const allRoutes = latencyHistogram.snapshot(); const evalPhase = allRoutes["eval_phase"] ?? null; const workPhase = allRoutes["work_phase"] ?? null; const routeLatency: Record = {}; for (const [key, stats] of Object.entries(allRoutes)) { if (key !== "eval_phase" && key !== "work_phase") { routeLatency[key] = stats; } } return { uptime_s: Math.floor((Date.now() - START_TIME) / 1000), http: requestCounters.snapshot(), jobs: { total: jobsTotal, by_state: byState, }, invoices: { total: invTotal, paid: invPaid, conversion_rate: conversionRate, }, earnings: { total_sats: totalSats, }, latency: { eval_phase: evalPhase, work_phase: workPhase, routes: routeLatency, }, }; } } export const metricsService = new MetricsService();