refactor: modularizar WebhookServer y endpoints /metrics /health
Co-authored-by: aider (openrouter/openai/gpt-5) <aider@aider.chat>main
parent
ffad59f18f
commit
46bec524a2
@ -0,0 +1,90 @@
|
||||
import type { Database } from 'bun:sqlite';
|
||||
import { WebhookManager } from '../services/webhook-manager';
|
||||
import { GroupSyncService } from '../services/group-sync';
|
||||
import { ResponseQueue } from '../services/response-queue';
|
||||
import { RemindersService } from '../services/reminders';
|
||||
import { MaintenanceService } from '../services/maintenance';
|
||||
|
||||
export async function startServices(_db: Database): Promise<void> {
|
||||
await WebhookManager.registerWebhook();
|
||||
// Add small delay to allow webhook to propagate
|
||||
await new Promise(resolve => setTimeout(resolve, 1000));
|
||||
const isActive = await WebhookManager.verifyWebhook();
|
||||
if (!isActive) {
|
||||
console.error('❌ Webhook verification failed - retrying in 2 seconds...');
|
||||
await new Promise(resolve => setTimeout(resolve, 2000));
|
||||
const isActiveRetry = await WebhookManager.verifyWebhook();
|
||||
if (!isActiveRetry) {
|
||||
console.error('❌ Webhook verification failed after retry');
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
// Initialize groups - critical for operation
|
||||
await GroupSyncService.checkInitialGroups();
|
||||
|
||||
// Start groups scheduler (periodic sync of groups)
|
||||
try {
|
||||
GroupSyncService.startGroupsScheduler();
|
||||
console.log('✅ Group scheduler started');
|
||||
} catch (e) {
|
||||
console.error('⚠️ Failed to start Group scheduler:', e);
|
||||
}
|
||||
|
||||
// Initial members sync (non-blocking if fails)
|
||||
try {
|
||||
await GroupSyncService.syncMembersForActiveGroups();
|
||||
GroupSyncService.startMembersScheduler();
|
||||
console.log('✅ Group members scheduler started');
|
||||
} catch (e) {
|
||||
console.error('⚠️ Failed to run initial members sync or start scheduler:', e);
|
||||
}
|
||||
|
||||
// Start response queue worker (background)
|
||||
try {
|
||||
await ResponseQueue.process();
|
||||
console.log('✅ ResponseQueue worker started');
|
||||
// Start cleanup scheduler (daily retention)
|
||||
ResponseQueue.startCleanupScheduler();
|
||||
console.log('✅ ResponseQueue cleanup scheduler started');
|
||||
RemindersService.start();
|
||||
console.log('✅ RemindersService started');
|
||||
} catch (e) {
|
||||
console.error('❌ Failed to start ResponseQueue worker or cleanup scheduler:', e);
|
||||
}
|
||||
|
||||
// Mantenimiento (cleanup de miembros inactivos)
|
||||
try {
|
||||
MaintenanceService.start();
|
||||
console.log('✅ MaintenanceService started');
|
||||
// Ejecutar reconciliación de alias una vez al arranque (one-shot)
|
||||
try {
|
||||
await MaintenanceService.reconcileAliasUsersOnce();
|
||||
console.log('✅ MaintenanceService: reconciliación de alias ejecutada (one-shot)');
|
||||
} catch (e2) {
|
||||
console.error('⚠️ Failed to run alias reconciliation one-shot:', e2);
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('⚠️ Failed to start MaintenanceService:', e);
|
||||
}
|
||||
}
|
||||
|
||||
export function stopServices(): void {
|
||||
try {
|
||||
ResponseQueue.stopCleanupScheduler();
|
||||
} catch {}
|
||||
try {
|
||||
// No existe un "stop" público de workers; paramos el lazo
|
||||
(ResponseQueue as any).stop?.();
|
||||
} catch {}
|
||||
try {
|
||||
RemindersService.stop();
|
||||
} catch {}
|
||||
try {
|
||||
GroupSyncService.stopGroupsScheduler();
|
||||
GroupSyncService.stopMembersScheduler();
|
||||
} catch {}
|
||||
try {
|
||||
MaintenanceService.stop();
|
||||
} catch {}
|
||||
}
|
||||
@ -0,0 +1,46 @@
|
||||
import type { Database } from 'bun:sqlite';
|
||||
import { Metrics } from '../services/metrics';
|
||||
|
||||
export async function handleHealthRequest(url: URL, db: Database): Promise<Response> {
|
||||
// /health?full=1 devuelve JSON con detalles
|
||||
if (url.searchParams.get('full') === '1') {
|
||||
try {
|
||||
const rowG = db.prepare(`SELECT COUNT(*) AS c, MAX(last_verified) AS lv FROM groups WHERE active = 1`).get() as { c?: number; lv?: string | null } | undefined;
|
||||
const rowM = db.prepare(`SELECT COUNT(*) AS c FROM group_members WHERE is_active = 1`).get() as { c?: number } | undefined;
|
||||
const active_groups = Number(rowG?.c || 0);
|
||||
const active_members = Number(rowM?.c || 0);
|
||||
const lv = rowG?.lv ? String(rowG.lv) : null;
|
||||
let last_sync_at: string | null = lv;
|
||||
let snapshot_age_ms: number | null = null;
|
||||
if (lv) {
|
||||
const iso = lv.includes('T') ? lv : (lv.replace(' ', 'T') + 'Z');
|
||||
const ms = Date.parse(iso);
|
||||
if (Number.isFinite(ms)) {
|
||||
snapshot_age_ms = Date.now() - ms;
|
||||
}
|
||||
}
|
||||
const lastSyncMetric = Metrics.get('last_sync_ok');
|
||||
const maxAgeRaw = Number(process.env.MAX_MEMBERS_SNAPSHOT_AGE_MS);
|
||||
const maxAgeMs = Number.isFinite(maxAgeRaw) && maxAgeRaw > 0 ? maxAgeRaw : 24 * 60 * 60 * 1000;
|
||||
const snapshot_fresh = typeof snapshot_age_ms === 'number' ? (snapshot_age_ms <= maxAgeMs) : false;
|
||||
let last_sync_ok: number;
|
||||
if (typeof lastSyncMetric === 'number') {
|
||||
last_sync_ok = (lastSyncMetric === 1 && snapshot_fresh) ? 1 : 0;
|
||||
} else {
|
||||
// Si no hay métrica explícita, nos basamos exclusivamente en la frescura de la snapshot
|
||||
last_sync_ok = snapshot_fresh ? 1 : 0;
|
||||
}
|
||||
const payload = { status: 'ok', active_groups, active_members, last_sync_at, snapshot_age_ms, snapshot_fresh, last_sync_ok };
|
||||
return new Response(JSON.stringify(payload), {
|
||||
status: 200,
|
||||
headers: { 'Content-Type': 'application/json' }
|
||||
});
|
||||
} catch (e) {
|
||||
return new Response(JSON.stringify({ status: 'error' }), {
|
||||
status: 500,
|
||||
headers: { 'Content-Type': 'application/json' }
|
||||
});
|
||||
}
|
||||
}
|
||||
return new Response('OK', { status: 200 });
|
||||
}
|
||||
@ -0,0 +1,44 @@
|
||||
import type { Database } from 'bun:sqlite';
|
||||
import { Metrics } from '../services/metrics';
|
||||
import { GroupSyncService } from '../services/group-sync';
|
||||
|
||||
export async function handleMetricsRequest(request: Request, db: Database): Promise<Response> {
|
||||
if (request.method !== 'GET') {
|
||||
return new Response('🚫 Method not allowed', { status: 405 });
|
||||
}
|
||||
if (!Metrics.enabled()) {
|
||||
return new Response('Metrics disabled', { status: 404 });
|
||||
}
|
||||
|
||||
// Gauges de allowed_groups por estado (best-effort)
|
||||
try {
|
||||
const rows = db
|
||||
.prepare(`SELECT status, COUNT(*) AS c FROM allowed_groups GROUP BY status`)
|
||||
.all() as Array<{ status: string; c: number }>;
|
||||
let pending = 0, allowed = 0, blocked = 0;
|
||||
for (const r of rows) {
|
||||
const s = String(r?.status || '');
|
||||
const c = Number(r?.c || 0);
|
||||
if (s === 'pending') pending = c;
|
||||
else if (s === 'allowed') allowed = c;
|
||||
else if (s === 'blocked') blocked = c;
|
||||
}
|
||||
Metrics.set('allowed_groups_total_pending', pending);
|
||||
Metrics.set('allowed_groups_total_allowed', allowed);
|
||||
Metrics.set('allowed_groups_total_blocked', blocked);
|
||||
} catch {}
|
||||
|
||||
// Exponer métrica con el tiempo restante hasta el próximo group sync (o -1 si scheduler inactivo)
|
||||
try {
|
||||
const secs = GroupSyncService.getSecondsUntilNextGroupSync();
|
||||
const val = (secs == null || !Number.isFinite(secs)) ? -1 : secs;
|
||||
Metrics.set('group_sync_seconds_until_next', val);
|
||||
} catch {}
|
||||
|
||||
const format = (process.env.METRICS_FORMAT || 'prom').toLowerCase() === 'json' ? 'json' : 'prom';
|
||||
const body = Metrics.render(format as any);
|
||||
return new Response(body, {
|
||||
status: 200,
|
||||
headers: { 'Content-Type': format === 'json' ? 'application/json' : 'text/plain; version=0.0.4' }
|
||||
});
|
||||
}
|
||||
Loading…
Reference in New Issue