From f7f764fd68ea828e6ed74dc8b09c6aa8524ce536 Mon Sep 17 00:00:00 2001 From: Alex Holliday Date: Mon, 19 Jan 2026 18:58:17 +0000 Subject: [PATCH] providers --- .../notifications/INotificationsRepository.ts | 1 + .../MongoNotificationsRepository.ts | 6 +++ server/src/service/index.ts | 6 ++- .../SuperSimpleQueueHelper.ts | 2 +- .../INotificationProvider.ts | 3 +- .../notificationProviders/discord.ts | 10 +++++ .../notificationProviders/email.ts | 12 +++++ .../notificationProviders/slack.ts | 12 +++++ .../notificationProviders/utils.ts | 14 +++++- .../notificationProviders/webhook.ts | 44 ++++++++++++++++--- .../infrastructure/notificationsService.ts | 40 +++++++++++------ 11 files changed, 126 insertions(+), 24 deletions(-) create mode 100644 server/src/service/infrastructure/notificationProviders/discord.ts create mode 100644 server/src/service/infrastructure/notificationProviders/email.ts create mode 100644 server/src/service/infrastructure/notificationProviders/slack.ts diff --git a/server/src/repositories/notifications/INotificationsRepository.ts b/server/src/repositories/notifications/INotificationsRepository.ts index 0244972e4..8bb65769a 100644 --- a/server/src/repositories/notifications/INotificationsRepository.ts +++ b/server/src/repositories/notifications/INotificationsRepository.ts @@ -2,6 +2,7 @@ import type { Notification } from "@/types/index.js"; export interface INotificationsRepository { // create // fetch + findNotificationsByIds(ids: string[]): Promise; // update // delete } diff --git a/server/src/repositories/notifications/MongoNotificationsRepository.ts b/server/src/repositories/notifications/MongoNotificationsRepository.ts index 51839c125..01a514f1c 100644 --- a/server/src/repositories/notifications/MongoNotificationsRepository.ts +++ b/server/src/repositories/notifications/MongoNotificationsRepository.ts @@ -35,6 +35,12 @@ class MongoNotificationsRepository implements INotificationsRepository { updatedAt: toDateString(doc.updatedAt), }; }; + + findNotificationsByIds = async (ids: string[]) => { + const mongoIds = ids.map((id) => new mongoose.Types.ObjectId(id)); + const documents = await NotificationModel.find({ _id: { $in: mongoIds } }); + return this.mapDocuments(documents); + }; } export default MongoNotificationsRepository; diff --git a/server/src/service/index.ts b/server/src/service/index.ts index fbba7714d..f46e6d3d8 100644 --- a/server/src/service/index.ts +++ b/server/src/service/index.ts @@ -1,6 +1,10 @@ export * from "@/service/business/monitorService.js"; export * from "@/service/infrastructure/networkService.js"; -export * from "@/service/infrastructure/notificationProviders/webhook.js"; +export * from "@/service/infrastructure/notificationProviders/discord.js"; +export * from "@/service/infrastructure/notificationProviders/email.js"; export * from "@/service/infrastructure/notificationProviders/INotificationProvider.js"; +export * from "@/service/infrastructure/notificationProviders/slack.js"; +export * from "@/service/infrastructure/notificationProviders/webhook.js"; export * from "@/service/infrastructure/notificationsService.js"; export * from "@/service/infrastructure/statusService.js"; +export * from "@/service/infrastructure/notificationProviders/webhook.js"; diff --git a/server/src/service/infrastructure/SuperSimpleQueue/SuperSimpleQueueHelper.ts b/server/src/service/infrastructure/SuperSimpleQueue/SuperSimpleQueueHelper.ts index 41885a146..449386102 100644 --- a/server/src/service/infrastructure/SuperSimpleQueue/SuperSimpleQueueHelper.ts +++ b/server/src/service/infrastructure/SuperSimpleQueue/SuperSimpleQueueHelper.ts @@ -83,7 +83,7 @@ class SuperSimpleQueueHelper { // Step 4. Update monitor status const statusChangeResult = await this.statusService.updateMonitorStatus(status, check); - // Step 5 handle notifications + // Step 5 handle notifications (best effort, continue even in event of failure, don't wait) this.notificationsService.handleNotifications(monitor, status, statusChangeResult.prevStatus, statusChangeResult.statusChanged); // this.notificationService diff --git a/server/src/service/infrastructure/notificationProviders/INotificationProvider.ts b/server/src/service/infrastructure/notificationProviders/INotificationProvider.ts index 2743c40bb..2d363d6b3 100644 --- a/server/src/service/infrastructure/notificationProviders/INotificationProvider.ts +++ b/server/src/service/infrastructure/notificationProviders/INotificationProvider.ts @@ -1,7 +1,6 @@ import type { Monitor, Notification, Alert, MonitorStatusResponse } from "@/types/index.js"; export interface INotificationProvider { - buildAlert: (monitor: Monitor, monitorStatusResponse: MonitorStatusResponse) => Alert; - sendAlert: (alert: Alert, notification: Notification) => Promise; + sendAlert: (notification: Notification, monitor: Monitor, monitorStatusResponse: MonitorStatusResponse) => Promise; sendTestAlert(notification: Notification): Promise; } diff --git a/server/src/service/infrastructure/notificationProviders/discord.ts b/server/src/service/infrastructure/notificationProviders/discord.ts new file mode 100644 index 000000000..18b1522be --- /dev/null +++ b/server/src/service/infrastructure/notificationProviders/discord.ts @@ -0,0 +1,10 @@ +import type { Monitor, Notification, MonitorStatusResponse } from "@/types/index.js"; +import { INotificationProvider } from "@/service/index.js"; +export class DiscordProvider implements INotificationProvider { + sendAlert = async (notification: Notification, monitor: Monitor, monitorStatusResponse: MonitorStatusResponse) => { + return false; + }; + sendTestAlert = async (notification: Notification) => { + return false; + }; +} diff --git a/server/src/service/infrastructure/notificationProviders/email.ts b/server/src/service/infrastructure/notificationProviders/email.ts new file mode 100644 index 000000000..dda7338a0 --- /dev/null +++ b/server/src/service/infrastructure/notificationProviders/email.ts @@ -0,0 +1,12 @@ +import type { Monitor, Notification, MonitorStatusResponse } from "@/types/index.js"; +import { INotificationProvider } from "@/service/index.js"; + +export class EmailProvider implements INotificationProvider { + async sendAlert(notification: Notification, monitor: Monitor, monitorStatusResponse: MonitorStatusResponse): Promise { + return false; + } + + async sendTestAlert(notification: Notification): Promise { + return false; + } +} diff --git a/server/src/service/infrastructure/notificationProviders/slack.ts b/server/src/service/infrastructure/notificationProviders/slack.ts new file mode 100644 index 000000000..bd2836d2b --- /dev/null +++ b/server/src/service/infrastructure/notificationProviders/slack.ts @@ -0,0 +1,12 @@ +import type { Monitor, Notification, MonitorStatusResponse } from "@/types/index.js"; +import { INotificationProvider } from "@/service/index.js"; + +export class SlackProvider implements INotificationProvider { + async sendAlert(notification: Notification, monitor: Monitor, monitorStatusResponse: MonitorStatusResponse): Promise { + return false; + } + + async sendTestAlert(notification: Notification): Promise { + return false; + } +} diff --git a/server/src/service/infrastructure/notificationProviders/utils.ts b/server/src/service/infrastructure/notificationProviders/utils.ts index 7cba00a5d..9a10e668b 100644 --- a/server/src/service/infrastructure/notificationProviders/utils.ts +++ b/server/src/service/infrastructure/notificationProviders/utils.ts @@ -93,13 +93,19 @@ export const buildHardwareAlerts = ( return { alertsToSend, discordPayload }; }; -const buildHardwareNotificationMessage = (clientHost: string, alerts: any, monitor: Monitor) => { +export const buildHardwareNotificationMessage = (clientHost: string, alerts: any, monitor: Monitor) => { const alertsHeader = [`Monitor: ${monitor.name}`, `URL: ${monitor.url}`]; const alertFooter = [`Go to incident: ${clientHost}/infrastructure/${monitor.id}`]; const alertText = alerts.length > 0 ? [...alertsHeader, ...alerts, ...alertFooter] : []; return alertText.map((alert) => alert).join("\n"); }; +export const buildHardwareWebhookBody = (alerts: string[], monitor: Monitor): { text: string; name: string; url: string } => { + const content = alerts.map((alert) => alert).join("\n"); + const body = { text: content, name: monitor.name, url: monitor.url }; + return body; +}; + export const shouldSendHardwareAlert = (monitor: Monitor, networkResponse: MonitorStatusResponse): boolean => { const thresholds = monitor.thresholds || {}; const { usage_cpu: cpuThreshold = -1, usage_memory: memoryThreshold = -1, usage_disk: diskThreshold = -1 } = thresholds; @@ -125,3 +131,9 @@ export const shouldSendHardwareAlert = (monitor: Monitor, networkResponse: Monit return false; }; + +export const buildWebhookBody = (monitor: Monitor, monitorStatusResponse: MonitorStatusResponse) => { + const { status, code } = monitorStatusResponse; + const formattedTime = Date.now().toLocaleString(); + return `Monitor: ${monitor.name}\nTime: ${formattedTime}\nStatus: ${status ? "UP" : "DOWN"}\n Status Code: ${code}\n\u200B\n`; +}; diff --git a/server/src/service/infrastructure/notificationProviders/webhook.ts b/server/src/service/infrastructure/notificationProviders/webhook.ts index 4266bbf3b..cf56db89b 100644 --- a/server/src/service/infrastructure/notificationProviders/webhook.ts +++ b/server/src/service/infrastructure/notificationProviders/webhook.ts @@ -1,15 +1,47 @@ import type { Monitor, Alert, Notification, MonitorStatusResponse } from "@/types/index.js"; import { INotificationProvider } from "@/service/index.js"; +import { buildHardwareAlerts, buildHardwareWebhookBody, buildWebhookBody } from "@/service/infrastructure/notificationProviders/utils.js"; +import got from "got"; + export class WebhookProvider implements INotificationProvider { - buildAlert = (monitor: Monitor, monitorStatusResponse: MonitorStatusResponse) => { - return null; + private getHardwareContent = (monitor: Monitor, monitorStatusResponse: MonitorStatusResponse) => { + const { alertsToSend } = buildHardwareAlerts("HOST_PLACEHOLDER", monitor, monitorStatusResponse); + const body = buildHardwareWebhookBody(alertsToSend, monitor); + return body; }; - sendAlert = async (alert: Alert) => { - return false; + private getContent = (monitor: Monitor, monitorStatusResponse: MonitorStatusResponse) => { + const body = buildWebhookBody(monitor, monitorStatusResponse); + return body; }; - sendTestAlert(notification: Notification): Promise { + sendAlert = async (notification: Notification, monitor: Monitor, monitorStatusResponse: MonitorStatusResponse) => { + let body; + if (monitor.type === "hardware") { + body = this.getHardwareContent(monitor, monitorStatusResponse); + } else { + body = this.getContent(monitor, monitorStatusResponse); + } + + if (!notification.address) { + return false; + } + + try { + await got.post(notification.address, { + json: body, + headers: { + "Content-Type": "application/json", + }, + responseType: "json", + }); + return true; + } catch (error) { + return false; + } + }; + + sendTestAlert = async (notification: Notification) => { return false; - } + }; } diff --git a/server/src/service/infrastructure/notificationsService.ts b/server/src/service/infrastructure/notificationsService.ts index 2070b0c4f..e170dd3c4 100644 --- a/server/src/service/infrastructure/notificationsService.ts +++ b/server/src/service/infrastructure/notificationsService.ts @@ -1,7 +1,7 @@ import type { HardwareStatusPayload, Monitor, MonitorStatusResponse, Notification } from "@/types/index.js"; -import { NotificationModel } from "@/db/models/index.js"; -import { buildHardwareAlerts, shouldSendHardwareAlert } from "@/service/infrastructure/notificationProviders/utils.js"; +import { shouldSendHardwareAlert } from "@/service/infrastructure/notificationProviders/utils.js"; import { INotificationsRepository } from "@/repositories/index.js"; +import { WebhookProvider } from "@/service/index.js"; export interface INotificationsService { handleNotifications: ( monitor: Monitor, @@ -13,21 +13,36 @@ export interface INotificationsService { export class NotificationsService implements INotificationsService { private notificationsRepository: INotificationsRepository; - constructor(notificationsRepository: INotificationsRepository) { + private webhookProvider: WebhookProvider; + + constructor(notificationsRepository: INotificationsRepository, webhookProvider: WebhookProvider) { this.notificationsRepository = notificationsRepository; + this.webhookProvider = webhookProvider; } - private send = (notification: Notification, monitor: Monitor, monitorStatusResponse: MonitorStatusResponse) => { - console.log(notification); + private send = async (notification: Notification, monitor: Monitor, monitorStatusResponse: MonitorStatusResponse): Promise => { + switch (notification.type) { + case " email": { + } + case "webhook": { + return await this.webhookProvider.sendAlert(notification, monitor, monitorStatusResponse); + } + } + return false; }; private sendNotifications = async (monitor: Monitor, monitorStatusResponse: MonitorStatusResponse) => { - const notifications = await this.notificationsRepository.findByMonitorId(monitor.id); - console.log({ notifications }); - const tasks = notifications.map((notification) => { - this.send(notification, monitor, monitorStatusResponse); - }); - return true; + const notificationIds = monitor.notifications ?? []; + const notifications = await this.notificationsRepository.findNotificationsByIds(notificationIds); + const tasks = notifications.map((notification) => this.send(notification, monitor, monitorStatusResponse)); + const outcomes = await Promise.all(tasks); + const succeeded = outcomes.filter(Boolean).length; + const failed = outcomes.length - succeeded; + if (failed > 0) { + // logger.warn(`Notification send completed with ${succeeded} success, ${failed} failure(s)`); + } + // Return true if all notificaitons succeeded + return succeeded === notifications.length; }; handleNotifications = async ( @@ -51,12 +66,11 @@ export class NotificationsService implements INotificationsService { const metrics = payload?.data ?? null; if (metrics === null) return false; // No metrics, we're done - // We shoul dsend a notificaiton + // We should send a notificaiton const shouldSend = shouldSendHardwareAlert(monitor, monitorStatusResponse); if (shouldSend === false) return false; console.log(JSON.stringify(monitor, null, 2)); - console.log(JSON.stringify(monitorStatusResponse, null, 2)); // const { subject, html } = await this.notificationUtils.buildHardwareEmail(networkResponse, alerts); // const content = await this.notificationUtils.buildHardwareNotificationMessage(alerts, monitor); // const webhookBody = await this.notificationUtils.buildHardwareWebhookBody(alerts, monitor);