/** * GET /api/zabbix/alert-correlation * Cross-references Zabbix WAN problem events against Datto RMM ping/offline alerts * for the same company within a configurable time window. * * Query params: * days — look-back window in days (default 30) * windowMins — match window ± minutes around Zabbix event (default 120) * companyId — optional: filter to one Autotask company */ import { NextRequest, NextResponse } from 'next/server'; import { postgresClient } from '@/lib/services/postgres-client'; export async function GET(request: NextRequest) { const { searchParams } = request.nextUrl; const days = Number(searchParams.get('days') ?? 30); const windowMins = Number(searchParams.get('windowMins') ?? 120); const companyId = searchParams.get('companyId'); try { const since = new Date(Date.now() - days * 86400 * 1000); // ── 1. Zabbix events with matched RMM alert count ──────────────────────── const zabbixRows = await postgresClient.query<{ eventid: string; name: string; severity: number; clock: string; r_clock: string | null; duration_seconds: number | null; host_name: string; wan_ip: string; isp_name: string | null; autotask_company_id: number; autotask_company_name: string; rmm_site_uid: string | null; rmm_alert_count: string; rmm_alerts: string; // JSON array }>(` SELECT ze.eventid, ze.name, ze.severity, ze.clock, ze.r_clock, ze.duration_seconds, ze.host_name, ze.wan_ip, ze.isp_name, ze.autotask_company_id, ze.autotask_company_name, ze.rmm_site_uid, COUNT(ra.alert_uid)::text AS rmm_alert_count, json_agg(json_build_object( 'alert_uid', ra.alert_uid, 'site_name', ra.site_name, 'alert_class', ra.alert_context->>'@class', 'alert_message', ra.alert_message_en, 'timestamp', ra.timestamp, 'resolved', ra.resolved, 'resolved_on', ra.resolved_on, 'device_name', ra.device_name ) ORDER BY ra.timestamp) FILTER (WHERE ra.alert_uid IS NOT NULL) AS rmm_alerts FROM zabbix_events ze LEFT JOIN datto_rmm_alerts ra ON ra.timestamp BETWEEN ze.clock - ($1 * INTERVAL '1 minute') AND ze.clock + ($1 * INTERVAL '1 minute') AND EXISTS ( SELECT 1 FROM datto_rmm_sites ds JOIN rmm_site_mappings rsm ON rsm.rmm_site_uid = ds.uid WHERE ds.uid = ra.site_uid AND rsm.company_id = ze.autotask_company_id ) WHERE ze.clock >= $2 ${companyId ? 'AND ze.autotask_company_id = $3' : ''} GROUP BY ze.eventid, ze.name, ze.severity, ze.clock, ze.r_clock, ze.duration_seconds, ze.host_name, ze.wan_ip, ze.isp_name, ze.autotask_company_id, ze.autotask_company_name, ze.rmm_site_uid ORDER BY ze.clock DESC `, companyId ? [windowMins, since, Number(companyId)] : [windowMins, since]); // ── 2. RMM ping/offline alerts with NO matching Zabbix event ──────────── const rmmOnlyRows = await postgresClient.query<{ alert_uid: string; site_name: string; alert_class: string; alert_message: string | null; timestamp: string; resolved: boolean; resolved_on: string | null; device_name: string | null; autotask_company_id: number | null; autotask_company_name: string | null; }>(` SELECT ra.alert_uid, ra.site_name, ra.alert_context->>'@class' AS alert_class, ra.alert_message_en AS alert_message, ra.timestamp, ra.resolved, ra.resolved_on, ra.device_name, rsm.company_id AS autotask_company_id, c.company_name AS autotask_company_name FROM datto_rmm_alerts ra JOIN datto_rmm_sites ds ON ds.uid = ra.site_uid JOIN rmm_site_mappings rsm ON rsm.rmm_site_uid = ds.uid JOIN companies c ON c.id = rsm.company_id WHERE ra.timestamp >= $1 AND ra.alert_context->>'@class' IN ('ping_ctx', 'device_offline_ctx', 'network_ctx') AND NOT EXISTS ( SELECT 1 FROM zabbix_events ze WHERE ze.autotask_company_id = rsm.company_id AND ze.clock BETWEEN ra.timestamp - ($2 * INTERVAL '1 minute') AND ra.timestamp + ($2 * INTERVAL '1 minute') ) ${companyId ? 'AND rsm.company_id = $3' : ''} ORDER BY ra.timestamp DESC LIMIT 500 `, companyId ? [since, windowMins, Number(companyId)] : [since, windowMins]); // ── 3. Summary stats ───────────────────────────────────────────────────── const zabbixWithRmm = zabbixRows.rows.filter(r => Number(r.rmm_alert_count) > 0).length; const zabbixWithoutRmm = zabbixRows.rows.filter(r => Number(r.rmm_alert_count) === 0).length; const lastEventSync = await postgresClient.query<{ last_sync: string | null }>( 'SELECT MAX(last_synced_at) AS last_sync FROM zabbix_events' ); return NextResponse.json({ summary: { days, window_mins: windowMins, zabbix_events_total: zabbixRows.rows.length, zabbix_with_rmm_match: zabbixWithRmm, zabbix_without_rmm_match: zabbixWithoutRmm, rmm_only_alerts: rmmOnlyRows.rows.length, last_event_sync: lastEventSync.rows[0]?.last_sync ?? null, }, zabbix_events: zabbixRows.rows, rmm_only: rmmOnlyRows.rows, }); } catch (err) { const msg = err instanceof Error ? err.message : String(err); return NextResponse.json({ error: msg }, { status: 500 }); } }