From e18681606c6ad7b1c0c13c5cdb997a39b709e320 Mon Sep 17 00:00:00 2001 From: Alex Holliday Date: Mon, 16 Feb 2026 20:55:00 +0000 Subject: [PATCH] add webhook --- .../notificationMessageBuilder.ts | 33 ++++--- .../INotificationProvider.ts | 2 + .../notificationProviders/webhook.ts | 93 +++++++++++++++++++ .../infrastructure/notificationsService.ts | 25 +++-- 4 files changed, 132 insertions(+), 21 deletions(-) diff --git a/server/src/service/infrastructure/notificationMessageBuilder.ts b/server/src/service/infrastructure/notificationMessageBuilder.ts index 6fd1b29c1..eff890638 100644 --- a/server/src/service/infrastructure/notificationMessageBuilder.ts +++ b/server/src/service/infrastructure/notificationMessageBuilder.ts @@ -208,11 +208,12 @@ export class NotificationMessageBuilder implements INotificationMessageBuilder { return breaches; } - // CPU threshold breach - if (monitor.cpuAlertThreshold && hardware.cpu?.usage_percent !== undefined) { - const cpuPercent = hardware.cpu.usage_percent; + // Note: usage_percent values in hardware payload are decimals (0-1) + if (monitor.cpuAlertThreshold !== undefined && monitor.cpuAlertThreshold !== null && hardware.cpu?.usage_percent !== undefined) { + const cpuUsageDecimal = hardware.cpu.usage_percent; + const cpuPercent = cpuUsageDecimal * 100; const threshold = monitor.cpuAlertThreshold; - if (cpuPercent >= threshold) { + if (cpuPercent > threshold) { breaches.push({ metric: "cpu", currentValue: cpuPercent, @@ -224,10 +225,11 @@ export class NotificationMessageBuilder implements INotificationMessageBuilder { } // Memory threshold breach - if (monitor.memoryAlertThreshold && hardware.memory?.usage_percent !== undefined) { - const memoryPercent = hardware.memory.usage_percent; + if (monitor.memoryAlertThreshold !== undefined && monitor.memoryAlertThreshold !== null && hardware.memory?.usage_percent !== undefined) { + const memoryUsageDecimal = hardware.memory.usage_percent; + const memoryPercent = memoryUsageDecimal * 100; const threshold = monitor.memoryAlertThreshold; - if (memoryPercent >= threshold) { + if (memoryPercent > threshold) { breaches.push({ metric: "memory", currentValue: memoryPercent, @@ -239,28 +241,29 @@ export class NotificationMessageBuilder implements INotificationMessageBuilder { } // Disk threshold breach - if (monitor.diskAlertThreshold && Array.isArray(hardware.disk)) { + if (monitor.diskAlertThreshold !== undefined && monitor.diskAlertThreshold !== null && Array.isArray(hardware.disk)) { // Find the highest disk usage - let maxDiskUsage = 0; + let maxDiskUsageDecimal = 0; for (const disk of hardware.disk) { - if (disk.usage_percent !== undefined && disk.usage_percent > maxDiskUsage) { - maxDiskUsage = disk.usage_percent; + if (disk.usage_percent !== undefined && disk.usage_percent > maxDiskUsageDecimal) { + maxDiskUsageDecimal = disk.usage_percent; } } + const maxDiskPercent = maxDiskUsageDecimal * 100; const threshold = monitor.diskAlertThreshold; - if (maxDiskUsage >= threshold) { + if (maxDiskPercent > threshold) { breaches.push({ metric: "disk", - currentValue: maxDiskUsage, + currentValue: maxDiskPercent, threshold, unit: "%", - formattedValue: `${maxDiskUsage.toFixed(1)}%`, + formattedValue: `${maxDiskPercent.toFixed(1)}%`, }); } } // Temperature threshold breach - if (monitor.tempAlertThreshold && hardware.cpu?.temperature) { + if (monitor.tempAlertThreshold !== undefined && monitor.tempAlertThreshold !== null && hardware.cpu?.temperature) { // Temperature is an array in cpu.temperature const temps = Array.isArray(hardware.cpu.temperature) ? hardware.cpu.temperature : [hardware.cpu.temperature]; const maxTemp = Math.max(...temps.filter((t: number) => !isNaN(t))); diff --git a/server/src/service/infrastructure/notificationProviders/INotificationProvider.ts b/server/src/service/infrastructure/notificationProviders/INotificationProvider.ts index 21a221c9c..73043fdb1 100644 --- a/server/src/service/infrastructure/notificationProviders/INotificationProvider.ts +++ b/server/src/service/infrastructure/notificationProviders/INotificationProvider.ts @@ -1,5 +1,6 @@ import type { Monitor, Notification, Alert, MonitorStatusResponse } from "@/types/index.js"; import type { MonitorActionDecision } from "@/service/infrastructure/SuperSimpleQueue/SuperSimpleQueueHelper.js"; +import type { NotificationMessage } from "@/types/notificationMessage.js"; export interface INotificationProvider { sendAlert: ( @@ -9,5 +10,6 @@ export interface INotificationProvider { decision: MonitorActionDecision, clientHost: string ) => Promise; + sendMessage?: (notification: Notification, message: NotificationMessage) => Promise; sendTestAlert(notification: Notification): Promise; } diff --git a/server/src/service/infrastructure/notificationProviders/webhook.ts b/server/src/service/infrastructure/notificationProviders/webhook.ts index 17d871950..c4b637bef 100644 --- a/server/src/service/infrastructure/notificationProviders/webhook.ts +++ b/server/src/service/infrastructure/notificationProviders/webhook.ts @@ -2,6 +2,7 @@ const SERVICE_NAME = "WebhookProvider"; import type { Monitor, Alert, Notification, MonitorStatusResponse } from "@/types/index.js"; import { INotificationProvider } from "@/service/index.js"; import type { MonitorActionDecision } from "@/service/infrastructure/SuperSimpleQueue/SuperSimpleQueueHelper.js"; +import type { NotificationMessage } from "@/types/notificationMessage.js"; import { buildHardwareAlerts, buildHardwareNotificationMessage, @@ -75,6 +76,98 @@ export class WebhookProvider implements INotificationProvider { } }; + /** + * New unified message format - builds webhook payload from NotificationMessage + */ + sendMessage = async (notification: Notification, message: NotificationMessage): Promise => { + if (!notification.address) { + return false; + } + + // Build webhook payload from unified message + const payload = this.buildWebhookPayload(message); + + try { + await got.post(notification.address, { + json: payload, + headers: { + "Content-Type": "application/json", + }, + }); + this.logger.info({ + message: "[NEW] Webhook notification sent via sendMessage", + service: SERVICE_NAME, + method: "sendMessage", + }); + return true; + } catch (error) { + const err = error as Error; + this.logger.warn({ + message: "[NEW] Webhook alert failed via sendMessage", + service: SERVICE_NAME, + method: "sendMessage", + stack: err?.stack, + }); + return false; + } + }; + + /** + * Build webhook payload from NotificationMessage + * Format: { text: string, severity: string, monitor: object, details: object } + */ + private buildWebhookPayload(message: NotificationMessage): object { + const lines: string[] = []; + + // Title and summary + lines.push(`**${message.content.title}**`); + lines.push(message.content.summary); + lines.push(""); + + // Monitor information + lines.push("**Monitor Details:**"); + lines.push(`- Name: ${message.monitor.name}`); + lines.push(`- URL: ${message.monitor.url}`); + lines.push(`- Type: ${message.monitor.type}`); + lines.push(`- Status: ${message.monitor.status}`); + lines.push(""); + + // Additional details + if (message.content.details && message.content.details.length > 0) { + lines.push("**Additional Information:**"); + message.content.details.forEach((detail) => lines.push(`- ${detail}`)); + lines.push(""); + } + + // Threshold breaches (for hardware monitors) + if (message.content.thresholds && message.content.thresholds.length > 0) { + lines.push("**Threshold Breaches:**"); + message.content.thresholds.forEach((breach) => { + lines.push(`- ${breach.metric.toUpperCase()}: ${breach.formattedValue} (threshold: ${breach.threshold}${breach.unit})`); + }); + lines.push(""); + } + + // Incident link + if (message.content.incident) { + lines.push(`[View Incident](${message.clientHost}/infrastructure/${message.monitor.id})`); + } + + // Return webhook payload with both text and structured data + return { + text: lines.join("\n"), + severity: message.severity, + type: message.type, + monitor: { + id: message.monitor.id, + name: message.monitor.name, + url: message.monitor.url, + status: message.monitor.status, + }, + timestamp: message.content.timestamp, + }; + } + sendTestAlert = async (notification: Notification) => { if (!notification.address) { return false; diff --git a/server/src/service/infrastructure/notificationsService.ts b/server/src/service/infrastructure/notificationsService.ts index 18a82c4a9..71d678bf8 100644 --- a/server/src/service/infrastructure/notificationsService.ts +++ b/server/src/service/infrastructure/notificationsService.ts @@ -65,11 +65,23 @@ export class NotificationsService implements INotificationsService { notification: Notification, monitor: Monitor, monitorStatusResponse: MonitorStatusResponse, - decision: MonitorActionDecision + decision: MonitorActionDecision, + notificationMessage?: any ): Promise => { const settings = this.settingsService.getSettings(); const clientHost = settings.clientHost || "Host not defined"; + // For webhook notifications, try new sendMessage method if available + if (notification.type === "webhook" && this.webhookProvider.sendMessage && notificationMessage) { + this.logger.info({ + message: "[NEW] Using sendMessage for webhook", + service: SERVICE_NAME, + method: "send", + }); + return await this.webhookProvider.sendMessage(notification, notificationMessage); + } + + // Fallback to existing sendAlert for all providers switch (notification.type) { case "email": return await this.emailProvider.sendAlert(notification, monitor, monitorStatusResponse, decision, clientHost); @@ -92,7 +104,12 @@ export class NotificationsService implements INotificationsService { const notificationIds = monitor.notifications ?? []; const notifications = await this.notificationsRepository.findNotificationsByIds(notificationIds); - const tasks = notifications.map((notification) => this.send(notification, monitor, monitorStatusResponse, decision)); + // Build notification message once for all notifications + const settings = this.settingsService.getSettings(); + const clientHost = settings.clientHost || "Host not defined"; + const notificationMessage = this.notificationMessageBuilder.buildMessage(monitor, monitorStatusResponse, decision, clientHost); + + const tasks = notifications.map((notification) => this.send(notification, monitor, monitorStatusResponse, decision, notificationMessage)); const outcomes = await Promise.all(tasks); const succeeded = outcomes.filter(Boolean).length; @@ -113,10 +130,6 @@ export class NotificationsService implements INotificationsService { return false; } - const settings = this.settingsService.getSettings(); - const clientHost = settings.clientHost || "Host not defined"; - const notificationMessage = this.notificationMessageBuilder.buildMessage(monitor, monitorStatusResponse, decision, clientHost); - // Send notifications based on decision return await this.sendNotifications(monitor, monitorStatusResponse, decision); };