add webhook

This commit is contained in:
Alex Holliday
2026-02-16 20:55:00 +00:00
parent d2d75cd035
commit e18681606c
4 changed files with 132 additions and 21 deletions
@@ -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)));
@@ -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<boolean>;
sendMessage?: (notification: Notification, message: NotificationMessage) => Promise<boolean>;
sendTestAlert(notification: Notification): Promise<boolean>;
}
@@ -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<boolean> => {
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;
@@ -65,11 +65,23 @@ export class NotificationsService implements INotificationsService {
notification: Notification,
monitor: Monitor,
monitorStatusResponse: MonitorStatusResponse,
decision: MonitorActionDecision
decision: MonitorActionDecision,
notificationMessage?: any
): Promise<boolean> => {
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);
};