From a0ebf7d224bd06c2561a446c1f6e043293145104 Mon Sep 17 00:00:00 2001 From: Alex Holliday Date: Sun, 18 Jan 2026 06:02:07 +0000 Subject: [PATCH 01/16] initial commit --- server/src/db/models/Notification.js | 74 ------------------- server/src/db/models/Notification.ts | 50 +++++++++++++ server/src/db/models/User.ts | 4 +- server/src/db/models/index.ts | 3 + .../INotificationProvider.ts | 4 + .../infrastructure/notificationsService.ts | 4 + server/src/types/index.ts | 1 + server/src/types/notification.ts | 17 +++++ 8 files changed, 81 insertions(+), 76 deletions(-) delete mode 100755 server/src/db/models/Notification.js create mode 100755 server/src/db/models/Notification.ts create mode 100644 server/src/service/infrastructure/notificationProviders/INotificationProvider.ts create mode 100644 server/src/service/infrastructure/notificationsService.ts create mode 100644 server/src/types/notification.ts diff --git a/server/src/db/models/Notification.js b/server/src/db/models/Notification.js deleted file mode 100755 index 72c716a26..000000000 --- a/server/src/db/models/Notification.js +++ /dev/null @@ -1,74 +0,0 @@ -import mongoose from "mongoose"; - -const NotificationSchema = mongoose.Schema( - { - userId: { - type: mongoose.Schema.Types.ObjectId, - ref: "User", - immutable: true, - required: true, - }, - teamId: { - type: mongoose.Schema.Types.ObjectId, - ref: "Team", - immutable: true, - required: true, - }, - type: { - type: String, - enum: ["email", "slack", "discord", "webhook", "pager_duty", "matrix"], - }, - notificationName: { - type: String, - required: true, - }, - address: { - type: String, - }, - phone: { - type: String, - }, - // Matrix-specific fields - homeserverUrl: { - type: String, - }, - roomId: { - type: String, - }, - accessToken: { - type: String, - }, - }, - { - timestamps: true, - } -); - -NotificationSchema.pre("save", function (next) { - if (!this.cpuAlertThreshold || this.isModified("alertThreshold")) { - this.cpuAlertThreshold = this.alertThreshold; - } - if (!this.memoryAlertThreshold || this.isModified("alertThreshold")) { - this.memoryAlertThreshold = this.alertThreshold; - } - if (!this.diskAlertThreshold || this.isModified("alertThreshold")) { - this.diskAlertThreshold = this.alertThreshold; - } - if (!this.tempAlertThreshold || this.isModified("alertThreshold")) { - this.tempAlertThreshold = this.alertThreshold; - } - next(); -}); - -NotificationSchema.pre("findOneAndUpdate", function (next) { - const update = this.getUpdate(); - if (update.alertThreshold) { - update.cpuAlertThreshold = update.alertThreshold; - update.memoryAlertThreshold = update.alertThreshold; - update.diskAlertThreshold = update.alertThreshold; - update.tempAlertThreshold = update.alertThreshold; - } - next(); -}); - -export default mongoose.model("Notification", NotificationSchema); diff --git a/server/src/db/models/Notification.ts b/server/src/db/models/Notification.ts new file mode 100755 index 000000000..97e2d43fb --- /dev/null +++ b/server/src/db/models/Notification.ts @@ -0,0 +1,50 @@ +import { Schema, model, type Types } from "mongoose"; +import type { Notification, NotificationChannel } from "@/types/notification.js"; + +interface NotificationDocument extends Omit { + _id: Types.ObjectId; + userId: Types.ObjectId; + teamId: Types.ObjectId; + createdAt: Date; + updatedAt: Date; +} + +const NotificationSchema = new Schema( + { + userId: { + type: Schema.Types.ObjectId, + ref: "User", + immutable: true, + required: true, + }, + teamId: { + type: Schema.Types.ObjectId, + ref: "Team", + immutable: true, + required: true, + }, + type: { + type: String, + enum: ["email", "slack", "discord", "webhook", "pager_duty", "matrix"] as NotificationChannel[], + required: true, + }, + notificationName: { + type: String, + required: true, + }, + address: { type: String }, + phone: { type: String }, + homeserverUrl: { type: String }, + roomId: { type: String }, + accessToken: { type: String }, + }, + { + timestamps: true, + } +); + +const NotificationModel = model("Notification", NotificationSchema); + +export type { NotificationDocument }; +export { NotificationModel }; +export default NotificationModel; diff --git a/server/src/db/models/User.ts b/server/src/db/models/User.ts index e00250340..6aa3835a6 100644 --- a/server/src/db/models/User.ts +++ b/server/src/db/models/User.ts @@ -3,7 +3,7 @@ import bcrypt from "bcryptjs"; import type { User, UserProfileImage, UserRole } from "@/types/index.js"; import { MonitorModel } from "@/db/models/index.js"; import Team from "./Team.js"; -import Notification from "./Notification.js"; +import NotificationModel from "./Notification.js"; type UserDocumentBase = Omit & { teamId?: Types.ObjectId; @@ -76,7 +76,7 @@ UserSchema.pre("findOneAndDelete", async function (next) { await Team.deleteOne({ _id: userToDelete.teamId }); await MonitorModel.deleteMany({ userId: userToDelete._id }); await this.model.deleteMany({ teamId: userToDelete.teamId, _id: { $ne: userToDelete._id } }); - await Notification.deleteMany({ teamId: userToDelete.teamId }); + await NotificationModel.deleteMany({ teamId: userToDelete.teamId }); } next(); } catch (error) { diff --git a/server/src/db/models/index.ts b/server/src/db/models/index.ts index 59ed8125d..18a3a3b19 100644 --- a/server/src/db/models/index.ts +++ b/server/src/db/models/index.ts @@ -21,3 +21,6 @@ export { default as AppSettingsModel } from "@/db/models/AppSettings.js"; export * from "@/db/models/RecoveryToken.js"; export { default as RecoveryTokenModel } from "@/db/models/RecoveryToken.js"; + +export * from "@/db/models/Notification.js"; +export { default as NotificationModel } from "@/db/models/Notification.js"; diff --git a/server/src/service/infrastructure/notificationProviders/INotificationProvider.ts b/server/src/service/infrastructure/notificationProviders/INotificationProvider.ts new file mode 100644 index 000000000..c9a20ff5f --- /dev/null +++ b/server/src/service/infrastructure/notificationProviders/INotificationProvider.ts @@ -0,0 +1,4 @@ +import type { Monitor } from "@/types/index.js"; + +export interface IAlert {} +export interface IMessageProvider {} diff --git a/server/src/service/infrastructure/notificationsService.ts b/server/src/service/infrastructure/notificationsService.ts new file mode 100644 index 000000000..e9a6e8e2d --- /dev/null +++ b/server/src/service/infrastructure/notificationsService.ts @@ -0,0 +1,4 @@ +import type { Monitor } from "@/types/index.js"; +interface INotificationSerivce { + handleNotifications: (moniotr: Monitor) => Promise; +} diff --git a/server/src/types/index.ts b/server/src/types/index.ts index fcfb6fc37..9bffb3816 100644 --- a/server/src/types/index.ts +++ b/server/src/types/index.ts @@ -7,3 +7,4 @@ export * from "@/types/user.js"; export * from "@/types/invite.js"; export * from "@/types/recoveryToken.js"; export * from "@/types/settings.js"; +export * from "@/types/notification.js"; diff --git a/server/src/types/notification.ts b/server/src/types/notification.ts new file mode 100644 index 000000000..669c840ca --- /dev/null +++ b/server/src/types/notification.ts @@ -0,0 +1,17 @@ +export const NotificationChannels = ["email", "slack", "discord", "webhook", "pager_duty", "matrix"] as const; +export type NotificationChannel = (typeof NotificationChannels)[number]; + +export interface Notification { + id: string; + userId: string; + teamId: string; + type: NotificationChannel; + notificationName: string; + address?: string; + phone?: string; + homeserverUrl?: string; + roomId?: string; + accessToken?: string; + createdAt: string; + updatedAt: string; +} From cff5508614deb9a4270683092d85a8d4104a3dee Mon Sep 17 00:00:00 2001 From: Alex Holliday Date: Sun, 18 Jan 2026 16:34:20 +0000 Subject: [PATCH 02/16] notification provider --- server/src/config/services.ts | 4 +-- server/src/db/modules/notificationModule.js | 2 -- .../INotificationProvider.ts | 9 ++++-- server/src/types/alert.ts | 29 +++++++++++++++++++ server/src/types/index.ts | 1 + 5 files changed, 38 insertions(+), 7 deletions(-) create mode 100644 server/src/types/alert.ts diff --git a/server/src/config/services.ts b/server/src/config/services.ts index bbb42984e..49e6f3d5e 100644 --- a/server/src/config/services.ts +++ b/server/src/config/services.ts @@ -50,7 +50,7 @@ import StatusPage from "../db/models/StatusPage.js"; import Team from "../db/models/Team.js"; import MaintenanceWindow from "../db/models/MaintenanceWindow.js"; import MonitorStats from "../db/models/MonitorStats.js"; -import Notification from "../db/models/Notification.js"; +import NotificationModel from "../db/models/Notification.js"; import RecoveryToken from "../db/models/RecoveryToken.js"; import AppSettings from "../db/models/AppSettings.js"; import Incident from "../db/models/Incident.js"; @@ -141,7 +141,7 @@ export const initializeServices = async ({ const statusPageModule = new StatusPageModule({ StatusPage, NormalizeData, stringService, AppSettings }); const userModule = new UserModule({ User, Team, GenerateAvatarImage, ParseBoolean, stringService }); const maintenanceWindowModule = new MaintenanceWindowModule({ MaintenanceWindow }); - const notificationModule = new NotificationModule({ Notification, Monitor }); + const notificationModule = new NotificationModule({ Notification: NotificationModel, Monitor }); const recoveryModule = new RecoveryModule({ User, RecoveryToken, crypto, stringService }); const settingsModule = new SettingsModule({ AppSettings }); const incidentModule = new IncidentModule({ logger, Incident, Monitor, User }); diff --git a/server/src/db/modules/notificationModule.js b/server/src/db/modules/notificationModule.js index 3c6edb54a..f45900184 100755 --- a/server/src/db/modules/notificationModule.js +++ b/server/src/db/modules/notificationModule.js @@ -1,5 +1,3 @@ -// import Notification from "../../models/Notification.js"; -// import Monitor from "../../models/Monitor.js"; const SERVICE_NAME = "notificationModule"; class NotificationModule { diff --git a/server/src/service/infrastructure/notificationProviders/INotificationProvider.ts b/server/src/service/infrastructure/notificationProviders/INotificationProvider.ts index c9a20ff5f..12b28d907 100644 --- a/server/src/service/infrastructure/notificationProviders/INotificationProvider.ts +++ b/server/src/service/infrastructure/notificationProviders/INotificationProvider.ts @@ -1,4 +1,7 @@ -import type { Monitor } from "@/types/index.js"; +import type { Monitor, Notification, Alert } from "@/types/index.js"; -export interface IAlert {} -export interface IMessageProvider {} +export interface IMessageProvider { + buildAlert: (monitor: Monitor) => Alert; + sendAlert: (alert: Alert, notification: Notification) => Promise; + sendTestAlert(notification: Notification): Promise; +} diff --git a/server/src/types/alert.ts b/server/src/types/alert.ts new file mode 100644 index 000000000..4725ebee8 --- /dev/null +++ b/server/src/types/alert.ts @@ -0,0 +1,29 @@ +import { NotificationChannel } from "@/types/index.js"; +export interface AlertWebhookPayload { + body: Record; +} + +export interface AlertPagerDutyPayload { + routingKey: string; + monitorUrl?: string; +} + +export interface AlertMatrixPayload { + friendlyName?: string; + homeserverUrl: string; + accessToken: string; + roomId: string; + monitorName: string; +} + +export interface Alert { + channel: NotificationChannel; + address?: string; + subject: string; + message: string; + html?: string; + discordContent?: Record | null; + webhook?: AlertWebhookPayload | null; + pagerDuty?: AlertPagerDutyPayload | null; + matrix?: AlertMatrixPayload | null; +} diff --git a/server/src/types/index.ts b/server/src/types/index.ts index 9bffb3816..fda48f0c6 100644 --- a/server/src/types/index.ts +++ b/server/src/types/index.ts @@ -8,3 +8,4 @@ export * from "@/types/invite.js"; export * from "@/types/recoveryToken.js"; export * from "@/types/settings.js"; export * from "@/types/notification.js"; +export * from "@/types/alert.js"; From 391f42ee53dd62e2a093122236df320dc8cf2151 Mon Sep 17 00:00:00 2001 From: Alex Holliday Date: Sun, 18 Jan 2026 16:59:38 +0000 Subject: [PATCH 03/16] add webhook provider and utils --- server/src/service/index.ts | 2 + .../INotificationProvider.ts | 6 +- .../notificationProviders/utils.ts | 91 +++++++++++++++++++ .../notificationProviders/webhook.ts | 15 +++ 4 files changed, 111 insertions(+), 3 deletions(-) create mode 100644 server/src/service/infrastructure/notificationProviders/utils.ts create mode 100644 server/src/service/infrastructure/notificationProviders/webhook.ts diff --git a/server/src/service/index.ts b/server/src/service/index.ts index a094f8b01..4dba8f6f4 100644 --- a/server/src/service/index.ts +++ b/server/src/service/index.ts @@ -1,2 +1,4 @@ export * from "@/service/business/monitorService.js"; export * from "@/service/infrastructure/networkService.js"; +export * from "@/service/infrastructure/notificationProviders/webhook.js"; +export * from "@/service/infrastructure/notificationProviders/INotificationProvider.js"; diff --git a/server/src/service/infrastructure/notificationProviders/INotificationProvider.ts b/server/src/service/infrastructure/notificationProviders/INotificationProvider.ts index 12b28d907..2743c40bb 100644 --- a/server/src/service/infrastructure/notificationProviders/INotificationProvider.ts +++ b/server/src/service/infrastructure/notificationProviders/INotificationProvider.ts @@ -1,7 +1,7 @@ -import type { Monitor, Notification, Alert } from "@/types/index.js"; +import type { Monitor, Notification, Alert, MonitorStatusResponse } from "@/types/index.js"; -export interface IMessageProvider { - buildAlert: (monitor: Monitor) => Alert; +export interface INotificationProvider { + buildAlert: (monitor: Monitor, monitorStatusResponse: MonitorStatusResponse) => Alert; sendAlert: (alert: Alert, notification: Notification) => Promise; sendTestAlert(notification: Notification): Promise; } diff --git a/server/src/service/infrastructure/notificationProviders/utils.ts b/server/src/service/infrastructure/notificationProviders/utils.ts new file mode 100644 index 000000000..9a1b2e7f8 --- /dev/null +++ b/server/src/service/infrastructure/notificationProviders/utils.ts @@ -0,0 +1,91 @@ +import { Monitor, HardwareStatusPayload, MonitorStatusResponse } from "@/types/index.js"; + +const buildHardwareAlerts = async (clientHost: string, monitor: Monitor, networkResponse: MonitorStatusResponse) => { + const thresholds = monitor.thresholds || {}; + const { usage_cpu: cpuThreshold = -1, usage_memory: memoryThreshold = -1, usage_disk: diskThreshold = -1 } = thresholds; + + const payload = networkResponse?.payload as HardwareStatusPayload; + const metrics = payload.data || {}; + const { cpu: { usage_percent: cpuUsage = -1 } = {}, memory: { usage_percent: memoryUsage = -1 } = {}, disk = [] } = metrics; + + const alerts = { + cpu: cpuThreshold !== -1 && cpuUsage > cpuThreshold ? true : false, + memory: memoryThreshold !== -1 && memoryUsage > memoryThreshold ? true : false, + disk: disk?.some((d) => diskThreshold !== -1 && typeof d?.usage_percent === "number" && d?.usage_percent > diskThreshold) ?? false, + }; + + const alertsToSend = []; + const discordEmbeds = []; + const monitorInfoFields = [ + { name: "Monitor", value: monitor.name, inline: true }, + { name: "URL", value: monitor.url, inline: false }, + ]; + const goToIncidentField = { name: `Go to incident`, value: `${clientHost}/infrastructure/${monitor.id}` }; + const formatDiscordAlert = { + cpu: () => ({ + title: "CPU alert", + description: `Your current CPU usage (${(cpuUsage * 100).toFixed(0)}%) is above your threshold (${(cpuThreshold * 100).toFixed(0)}%)`, + color: 15548997, + fields: [...monitorInfoFields, goToIncidentField], + footer: { text: "Checkmate" }, + }), + + memory: () => ({ + title: "Memory alert", + description: `Your current memory usage (${(memoryUsage * 100).toFixed(0)}%) is above your threshold (${(memoryThreshold * 100).toFixed(0)}%)`, + color: 15548997, + fields: [...monitorInfoFields, goToIncidentField], + footer: { text: "Checkmate" }, + }), + + disk: () => ({ + title: "Disk alert", + description: `Your current disk usage is above your threshold (${(diskThreshold * 100).toFixed(0)}%)`, + color: 15548997, + footer: { text: "Checkmate" }, + fields: [ + ...monitorInfoFields, + ...(Array.isArray(disk) ? disk : []).map((d: any, idx: number) => ({ + name: `Disk ${idx}`, + value: `${(d?.usage_percent * 100).toFixed(0)}%`, + inline: true, + })), + goToIncidentField, + ], + }), + }; + const alertTypes = ["cpu", "memory", "disk"]; + for (const type of alertTypes) { + // Iterate over each alert type to see if any need to be decremented + if (alerts[type] === true) { + monitor[`${type}AlertThreshold`]--; // Decrement threshold if an alert is triggered + + if (monitor[`${type}AlertThreshold`] <= 0) { + // If threshold drops below 0, reset and send notification + monitor[`${type}AlertThreshold`] = monitor.alertThreshold; + + const formatAlert = { + cpu: () => `Your current CPU usage (${(cpuUsage * 100).toFixed(0)}%) is above your threshold (${(cpuThreshold * 100).toFixed(0)}%)`, + memory: () => + `Your current memory usage (${(memoryUsage * 100).toFixed(0)}%) is above your threshold (${(memoryThreshold * 100).toFixed(0)}%)`, + disk: () => + `Your current disk usage: ${disk + .map((d, idx) => `(Disk${idx}: ${(d.usage_percent * 100).toFixed(0)}%)`) + .join(", ")} is above your threshold (${(diskThreshold * 100).toFixed(0)}%)`, + }; + alertsToSend.push(formatAlert[type]()); + discordEmbeds.push(formatDiscordAlert[type]()); + } + } + } + const discordPayload = discordEmbeds.length ? { embeds: discordEmbeds } : null; + return [alertsToSend, discordPayload]; +}; + +const buildHardwareNotificationMessage = (alerts: any, monitor: Monitor) => { + const { clientHost } = this.settingsService.getSettings(); + 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"); +}; diff --git a/server/src/service/infrastructure/notificationProviders/webhook.ts b/server/src/service/infrastructure/notificationProviders/webhook.ts new file mode 100644 index 000000000..4266bbf3b --- /dev/null +++ b/server/src/service/infrastructure/notificationProviders/webhook.ts @@ -0,0 +1,15 @@ +import type { Monitor, Alert, Notification, MonitorStatusResponse } from "@/types/index.js"; +import { INotificationProvider } from "@/service/index.js"; +export class WebhookProvider implements INotificationProvider { + buildAlert = (monitor: Monitor, monitorStatusResponse: MonitorStatusResponse) => { + return null; + }; + + sendAlert = async (alert: Alert) => { + return false; + }; + + sendTestAlert(notification: Notification): Promise { + return false; + } +} From 4e9839e2e5a3bd27968660c660f497d2e69193ea Mon Sep 17 00:00:00 2001 From: Alex Holliday Date: Sun, 18 Jan 2026 22:52:59 +0000 Subject: [PATCH 04/16] notificationsService --- server/src/config/services.ts | 9 +++- server/src/service/index.ts | 1 + .../SuperSimpleQueueHelper.ts | 43 +++++++++++-------- .../service/infrastructure/bufferService.ts | 16 +------ .../infrastructure/notificationsService.ts | 13 ++++-- .../service/infrastructure/statusService.ts | 16 +------ 6 files changed, 46 insertions(+), 52 deletions(-) diff --git a/server/src/config/services.ts b/server/src/config/services.ts index 49e6f3d5e..92acd8868 100644 --- a/server/src/config/services.ts +++ b/server/src/config/services.ts @@ -8,6 +8,7 @@ import BufferService from "../service/infrastructure/bufferService.js"; import StatusService from "../service/infrastructure/statusService.js"; import NotificationUtils from "../service/infrastructure/notificationUtils.js"; import NotificationService from "../service/infrastructure/notificationService.js"; +import { NotificationsService } from "@/service/index.js"; import ErrorService from "../service/infrastructure/errorService.js"; import SuperSimpleQueueHelper from "../service/infrastructure/SuperSimpleQueue/SuperSimpleQueueHelper.js"; import SuperSimpleQueue from "../service/infrastructure/SuperSimpleQueue/SuperSimpleQueue.js"; @@ -206,9 +207,9 @@ export const initializeServices = async ({ checksRepository, }); - const bufferService = new BufferService({ db, logger, envSettings, incidentService, checkService }); + const bufferService = new BufferService({ logger, incidentService, checkService }); - const statusService = new StatusService({ db, logger, buffer: bufferService, incidentService, monitorsRepository }); + const statusService = new StatusService({ db, logger, buffer: bufferService, monitorsRepository }); const notificationUtils = new NotificationUtils({ stringService, @@ -225,12 +226,15 @@ export const initializeServices = async ({ notificationUtils, }); + const notificationsService = new NotificationsService(); + const superSimpleQueueHelper = new SuperSimpleQueueHelper({ db, logger, networkService, statusService, notificationService, + notificationsService, checkService, buffer: bufferService, }); @@ -317,6 +321,7 @@ export const initializeServices = async ({ invitesRepository, recoveryTokensRepository, settingsRepository, + notificationsService, }; Object.values(services).forEach((service) => { diff --git a/server/src/service/index.ts b/server/src/service/index.ts index 4dba8f6f4..08c02f397 100644 --- a/server/src/service/index.ts +++ b/server/src/service/index.ts @@ -2,3 +2,4 @@ export * from "@/service/business/monitorService.js"; export * from "@/service/infrastructure/networkService.js"; export * from "@/service/infrastructure/notificationProviders/webhook.js"; export * from "@/service/infrastructure/notificationProviders/INotificationProvider.js"; +export * from "@/service/infrastructure/notificationsService.js"; diff --git a/server/src/service/infrastructure/SuperSimpleQueue/SuperSimpleQueueHelper.ts b/server/src/service/infrastructure/SuperSimpleQueue/SuperSimpleQueueHelper.ts index 891635d56..c2a9e1267 100644 --- a/server/src/service/infrastructure/SuperSimpleQueue/SuperSimpleQueueHelper.ts +++ b/server/src/service/infrastructure/SuperSimpleQueue/SuperSimpleQueueHelper.ts @@ -1,7 +1,7 @@ const SERVICE_NAME = "JobQueueHelper"; import type { Monitor } from "@/types/monitor.js"; import { AppError } from "@/utils/AppError.js"; -import { INetworkService } from "@/service/index.js"; +import { INetworkService, INotificationsService } from "@/service/index.js"; class SuperSimpleQueueHelper { static SERVICE_NAME = SERVICE_NAME; @@ -10,6 +10,7 @@ class SuperSimpleQueueHelper { private networkService: INetworkService; private statusService: any; private notificationService: any; + private notificationsService: INotificationsService; private checkService: any; private buffer: any; @@ -19,6 +20,7 @@ class SuperSimpleQueueHelper { networkService, statusService, notificationService, + notificationsService, checkService, buffer, }: { @@ -27,6 +29,7 @@ class SuperSimpleQueueHelper { networkService: INetworkService; statusService: any; notificationService: any; + notificationsService: INotificationsService; checkService: any; buffer: any; }) { @@ -37,6 +40,7 @@ class SuperSimpleQueueHelper { this.notificationService = notificationService; this.checkService = checkService; this.buffer = buffer; + this.notificationsService = notificationsService; } get serviceName() { @@ -78,22 +82,27 @@ class SuperSimpleQueueHelper { // Step 4. Update monitor status const statusChangeResult = await this.statusService.updateMonitorStatus(status, check); - this.notificationService - .handleNotifications({ - ...status, - monitor: statusChangeResult.monitor, - prevStatus: statusChangeResult.prevStatus, - statusChanged: statusChangeResult.statusChanged, - }) - .catch((error: any) => { - this.logger.error({ - message: error.message, - service: SERVICE_NAME, - method: "getMonitorJob", - details: `Error sending notifications for job ${monitor.id}: ${error.message}`, - stack: error.stack, - }); - }); + // Step 5 handle incidents and notifications + if (statusChangeResult.statusChanged === true) { + this.notificationsService.handleNotifications(monitor, status); + } + + // this.notificationService + // .handleNotifications({ + // ...status, + // monitor: statusChangeResult.monitor, + // prevStatus: statusChangeResult.prevStatus, + // statusChanged: statusChangeResult.statusChanged, + // }) + // .catch((error: any) => { + // this.logger.error({ + // message: error.message, + // service: SERVICE_NAME, + // method: "getMonitorJob", + // details: `Error sending notifications for job ${monitor.id}: ${error.message}`, + // stack: error.stack, + // }); + // }); } catch (error: any) { this.logger.warn({ message: error.message, diff --git a/server/src/service/infrastructure/bufferService.ts b/server/src/service/infrastructure/bufferService.ts index 5a809eea5..0363aa362 100755 --- a/server/src/service/infrastructure/bufferService.ts +++ b/server/src/service/infrastructure/bufferService.ts @@ -6,7 +6,6 @@ const SERVICE_NAME = "BufferService"; class BufferService { static SERVICE_NAME = SERVICE_NAME; private BUFFER_TIMEOUT: number; - private db: any; private logger: any; private incidentService: any; private SERVICE_NAME: string; @@ -15,21 +14,8 @@ class BufferService { private bufferTimer: NodeJS.Timeout | null = null; private checksService: any; - constructor({ - db, - logger, - envSettings, - incidentService, - checkService, - }: { - db: any; - logger: any; - envSettings: any; - incidentService: any; - checkService: any; - }) { + constructor({ logger, incidentService, checkService }: { logger: any; incidentService: any; checkService: any }) { this.BUFFER_TIMEOUT = config.NODE_ENV === "development" ? 10 : 1000 * 60 * 1; // 1 minute - this.db = db; this.logger = logger; this.incidentService = incidentService; this.checksService = checkService; diff --git a/server/src/service/infrastructure/notificationsService.ts b/server/src/service/infrastructure/notificationsService.ts index e9a6e8e2d..f9fdbfea8 100644 --- a/server/src/service/infrastructure/notificationsService.ts +++ b/server/src/service/infrastructure/notificationsService.ts @@ -1,4 +1,11 @@ -import type { Monitor } from "@/types/index.js"; -interface INotificationSerivce { - handleNotifications: (moniotr: Monitor) => Promise; +import type { Monitor, MonitorStatusResponse } from "@/types/index.js"; +export interface INotificationsService { + handleNotifications: (monitor: Monitor, monitorStatusResponse: MonitorStatusResponse) => Promise; +} + +export class NotificationsService implements INotificationsService { + handleNotifications = async (monitor: Monitor, monitorStatusResponse: MonitorStatusResponse) => { + console.log(JSON.stringify(monitor, null, 2)); + console.log(JSON.stringify(monitorStatusResponse, null, 2)); + }; } diff --git a/server/src/service/infrastructure/statusService.ts b/server/src/service/infrastructure/statusService.ts index 7d34017a5..c9aab706b 100755 --- a/server/src/service/infrastructure/statusService.ts +++ b/server/src/service/infrastructure/statusService.ts @@ -17,26 +17,12 @@ class StatusService { private db: any; private logger: any; private buffer: any; - private incidentService: any; private monitorsRepository: IMonitorsRepository; - constructor({ - db, - logger, - buffer, - incidentService, - monitorsRepository, - }: { - db: any; - logger: any; - buffer: any; - incidentService: any; - monitorsRepository: IMonitorsRepository; - }) { + constructor({ db, logger, buffer, monitorsRepository }: { db: any; logger: any; buffer: any; monitorsRepository: IMonitorsRepository }) { this.db = db; this.logger = logger; this.buffer = buffer; - this.incidentService = incidentService; this.monitorsRepository = monitorsRepository; } From a86588f080b03ce88664b86517e1dc9d380dc7d0 Mon Sep 17 00:00:00 2001 From: Alex Holliday Date: Sun, 18 Jan 2026 23:38:40 +0000 Subject: [PATCH 05/16] refactoring notifications --- server/src/config/services.ts | 3 +- server/src/service/index.ts | 1 + .../SuperSimpleQueueHelper.ts | 17 ++++--- .../infrastructure/notificationsService.ts | 48 +++++++++++++++++-- .../service/infrastructure/statusService.ts | 31 +++++++----- server/src/types/network.ts | 2 +- 6 files changed, 74 insertions(+), 28 deletions(-) diff --git a/server/src/config/services.ts b/server/src/config/services.ts index 92acd8868..ed1224ec7 100644 --- a/server/src/config/services.ts +++ b/server/src/config/services.ts @@ -5,10 +5,9 @@ import MongoDB from "../db/MongoDB.js"; import NetworkService from "../service/infrastructure/networkService.js"; import EmailService from "../service/infrastructure/emailService.js"; import BufferService from "../service/infrastructure/bufferService.js"; -import StatusService from "../service/infrastructure/statusService.js"; import NotificationUtils from "../service/infrastructure/notificationUtils.js"; import NotificationService from "../service/infrastructure/notificationService.js"; -import { NotificationsService } from "@/service/index.js"; +import { NotificationsService, StatusService } from "@/service/index.js"; import ErrorService from "../service/infrastructure/errorService.js"; import SuperSimpleQueueHelper from "../service/infrastructure/SuperSimpleQueue/SuperSimpleQueueHelper.js"; import SuperSimpleQueue from "../service/infrastructure/SuperSimpleQueue/SuperSimpleQueue.js"; diff --git a/server/src/service/index.ts b/server/src/service/index.ts index 08c02f397..fbba7714d 100644 --- a/server/src/service/index.ts +++ b/server/src/service/index.ts @@ -3,3 +3,4 @@ export * from "@/service/infrastructure/networkService.js"; export * from "@/service/infrastructure/notificationProviders/webhook.js"; export * from "@/service/infrastructure/notificationProviders/INotificationProvider.js"; export * from "@/service/infrastructure/notificationsService.js"; +export * from "@/service/infrastructure/statusService.js"; diff --git a/server/src/service/infrastructure/SuperSimpleQueue/SuperSimpleQueueHelper.ts b/server/src/service/infrastructure/SuperSimpleQueue/SuperSimpleQueueHelper.ts index c2a9e1267..21ee6e388 100644 --- a/server/src/service/infrastructure/SuperSimpleQueue/SuperSimpleQueueHelper.ts +++ b/server/src/service/infrastructure/SuperSimpleQueue/SuperSimpleQueueHelper.ts @@ -1,18 +1,19 @@ const SERVICE_NAME = "JobQueueHelper"; import type { Monitor } from "@/types/monitor.js"; import { AppError } from "@/utils/AppError.js"; -import { INetworkService, INotificationsService } from "@/service/index.js"; +import { INetworkService, INotificationsService, IStatusService } from "@/service/index.js"; class SuperSimpleQueueHelper { static SERVICE_NAME = SERVICE_NAME; private db: any; private logger: any; private networkService: INetworkService; - private statusService: any; + private statusService: IStatusService; private notificationService: any; private notificationsService: INotificationsService; private checkService: any; private buffer: any; + private incidentService: any; constructor({ db, @@ -23,15 +24,17 @@ class SuperSimpleQueueHelper { notificationsService, checkService, buffer, + incidentService, }: { db: any; logger: any; networkService: INetworkService; - statusService: any; + statusService: IStatusService; notificationService: any; notificationsService: INotificationsService; checkService: any; buffer: any; + incidentService: any; }) { this.db = db; this.logger = logger; @@ -41,6 +44,7 @@ class SuperSimpleQueueHelper { this.checkService = checkService; this.buffer = buffer; this.notificationsService = notificationsService; + this.incidentService = incidentService; } get serviceName() { @@ -82,10 +86,9 @@ class SuperSimpleQueueHelper { // Step 4. Update monitor status const statusChangeResult = await this.statusService.updateMonitorStatus(status, check); - // Step 5 handle incidents and notifications - if (statusChangeResult.statusChanged === true) { - this.notificationsService.handleNotifications(monitor, status); - } + // Step 5 handle notifications + + this.notificationsService.handleNotifications(monitor, status, statusChangeResult.prevStatus, statusChangeResult.statusChanged); // this.notificationService // .handleNotifications({ diff --git a/server/src/service/infrastructure/notificationsService.ts b/server/src/service/infrastructure/notificationsService.ts index f9fdbfea8..3dc5fe65e 100644 --- a/server/src/service/infrastructure/notificationsService.ts +++ b/server/src/service/infrastructure/notificationsService.ts @@ -1,11 +1,49 @@ -import type { Monitor, MonitorStatusResponse } from "@/types/index.js"; +import type { HardwareStatusPayload, Monitor, MonitorStatusResponse } from "@/types/index.js"; export interface INotificationsService { - handleNotifications: (monitor: Monitor, monitorStatusResponse: MonitorStatusResponse) => Promise; + handleNotifications: ( + monitor: Monitor, + monitorStatusResponse: MonitorStatusResponse, + prevStatus: boolean | undefined, + statusChanged: boolean + ) => Promise; } export class NotificationsService implements INotificationsService { - handleNotifications = async (monitor: Monitor, monitorStatusResponse: MonitorStatusResponse) => { - console.log(JSON.stringify(monitor, null, 2)); - console.log(JSON.stringify(monitorStatusResponse, null, 2)); + handleNotifications = async ( + monitor: Monitor, + monitorStatusResponse: MonitorStatusResponse, + prevStatus: boolean | undefined, + statusChanged: boolean + ) => { + const { type } = monitor; + const payload = monitorStatusResponse.payload as HardwareStatusPayload; + // If this is a non-hardeware type monitor and status did not change, we're done + if (type !== "hardware" && statusChanged === false) return false; + // if prevStatus is undefined, monitor is resuming, we're done + if (type !== "hardware" && prevStatus === undefined) return false; + + // Deal with hardware thresholds + if (type === "hardware") { + const thresholds = monitor.thresholds; + + if (thresholds === undefined) return false; // No thresholds set, we're done + const metrics = payload?.data ?? null; + if (metrics === null) return false; // No metrics, we're done + + // We shoul dsend a notificaiton + + console.log(JSON.stringify(monitor, null, 2)); + console.log(JSON.stringify(monitorStatusResponse, null, 2)); + // const [alerts, discordContent] = await this.notificationUtils.buildHardwareAlerts(networkResponse); + // if (alerts.length === 0) return false; + + // 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); + // const success = await this.notifyAll({ notificationIDs, subject, html, content, discordContent, webhookBody }); + return true; + } + + return false; }; } diff --git a/server/src/service/infrastructure/statusService.ts b/server/src/service/infrastructure/statusService.ts index c9aab706b..3d9591c02 100755 --- a/server/src/service/infrastructure/statusService.ts +++ b/server/src/service/infrastructure/statusService.ts @@ -12,7 +12,17 @@ import type { } from "@/types/index.js"; const SERVICE_NAME = "StatusService"; -class StatusService { +export interface IStatusService { + updateRunningStats({ monitor, networkResponse }: { monitor: Monitor; networkResponse: any }): Promise; + getStatusString(status: boolean | undefined): string; + handleIncidentForCheck(check: any, monitor: Monitor, action: any, errorContext?: string): Promise; + updateMonitorStatus( + statusResponse: MonitorStatusResponse, + check: Check + ): Promise; +} + +export class StatusService implements IStatusService { static SERVICE_NAME = SERVICE_NAME; private db: any; private logger: any; @@ -108,11 +118,10 @@ class StatusService { return "unknown"; }; - handleIncidentForCheck = async (check: any, monitor: Monitor, action: any, errorContext = "incident handling") => { + handleIncidentForCheck = async (check: Check, monitor: Monitor, action: any, errorContext = "incident handling") => { try { - let savedCheck = check; - - if (!check._id) { + let savedCheck; + if (!check.id) { try { const checkModel = new CheckModel(check); savedCheck = await checkModel.save(); @@ -130,7 +139,7 @@ class StatusService { } } - if (savedCheck && savedCheck._id) { + if (savedCheck && savedCheck.id) { try { this.buffer.addIncidentToBuffer({ monitor, check: savedCheck, action }); } catch (incidentError: any) { @@ -167,13 +176,10 @@ class StatusService { this.updateRunningStats({ monitor, networkResponse: statusResponse }); // If the status window size has changed, empty - while (monitor.statusWindow.length > monitor.statusWindowSize) { - monitor.statusWindow.shift(); - } - // Update status sliding window + monitor.statusWindow = monitor.statusWindow || []; monitor.statusWindow.push(status); - if (monitor.statusWindow.length > monitor.statusWindowSize) { + while (monitor.statusWindow.length > monitor.statusWindowSize) { monitor.statusWindow.shift(); } @@ -181,9 +187,9 @@ class StatusService { monitor.status = status; } + const prevStatus = monitor.status; let newStatus = monitor.status; let statusChanged = false; - const prevStatus = monitor.status; // Return early if not enough data points if (monitor.statusWindow.length < monitor.statusWindowSize) { @@ -316,4 +322,3 @@ class StatusService { } }; } -export default StatusService; diff --git a/server/src/types/network.ts b/server/src/types/network.ts index 10f89842f..6bed40316 100644 --- a/server/src/types/network.ts +++ b/server/src/types/network.ts @@ -13,7 +13,7 @@ import type { MonitorType, } from "@/types/index.js"; -export interface MonitorStatusResponse { +export interface MonitorStatusResponse { monitorId: string; teamId: string; type: MonitorType; From c2487f013437b1c6e85adedf2d029c40eb3fb78f Mon Sep 17 00:00:00 2001 From: Alex Holliday Date: Sun, 18 Jan 2026 23:54:42 +0000 Subject: [PATCH 06/16] alerts --- .../infrastructure/SuperSimpleQueue/SuperSimpleQueueHelper.ts | 3 --- .../src/service/infrastructure/notificationProviders/utils.ts | 2 +- 2 files changed, 1 insertion(+), 4 deletions(-) diff --git a/server/src/service/infrastructure/SuperSimpleQueue/SuperSimpleQueueHelper.ts b/server/src/service/infrastructure/SuperSimpleQueue/SuperSimpleQueueHelper.ts index 21ee6e388..d1315e151 100644 --- a/server/src/service/infrastructure/SuperSimpleQueue/SuperSimpleQueueHelper.ts +++ b/server/src/service/infrastructure/SuperSimpleQueue/SuperSimpleQueueHelper.ts @@ -24,7 +24,6 @@ class SuperSimpleQueueHelper { notificationsService, checkService, buffer, - incidentService, }: { db: any; logger: any; @@ -34,7 +33,6 @@ class SuperSimpleQueueHelper { notificationsService: INotificationsService; checkService: any; buffer: any; - incidentService: any; }) { this.db = db; this.logger = logger; @@ -44,7 +42,6 @@ class SuperSimpleQueueHelper { this.checkService = checkService; this.buffer = buffer; this.notificationsService = notificationsService; - this.incidentService = incidentService; } get serviceName() { diff --git a/server/src/service/infrastructure/notificationProviders/utils.ts b/server/src/service/infrastructure/notificationProviders/utils.ts index 9a1b2e7f8..ad8dc14ae 100644 --- a/server/src/service/infrastructure/notificationProviders/utils.ts +++ b/server/src/service/infrastructure/notificationProviders/utils.ts @@ -8,7 +8,7 @@ const buildHardwareAlerts = async (clientHost: string, monitor: Monitor, network const metrics = payload.data || {}; const { cpu: { usage_percent: cpuUsage = -1 } = {}, memory: { usage_percent: memoryUsage = -1 } = {}, disk = [] } = metrics; - const alerts = { + const alerts: Record = { cpu: cpuThreshold !== -1 && cpuUsage > cpuThreshold ? true : false, memory: memoryThreshold !== -1 && memoryUsage > memoryThreshold ? true : false, disk: disk?.some((d) => diskThreshold !== -1 && typeof d?.usage_percent === "number" && d?.usage_percent > diskThreshold) ?? false, From 7d6bc4b2dc80a25ef31840c9595974a9f1345029 Mon Sep 17 00:00:00 2001 From: Alex Holliday Date: Mon, 19 Jan 2026 00:48:20 +0000 Subject: [PATCH 07/16] notificaitons provider --- server/src/config/services.ts | 7 ++- server/src/repositories/index.ts | 3 + .../notifications/INotificationsRepository.ts | 7 +++ .../MongoNotificationsRepository.ts | 40 +++++++++++++ .../src/service/business/incidentService.js | 12 ++-- .../SuperSimpleQueueHelper.ts | 1 - .../notificationProviders/utils.ts | 56 +++++++++++++++---- .../infrastructure/notificationsService.ts | 33 +++++++++-- 8 files changed, 134 insertions(+), 25 deletions(-) create mode 100644 server/src/repositories/notifications/INotificationsRepository.ts create mode 100644 server/src/repositories/notifications/MongoNotificationsRepository.ts diff --git a/server/src/config/services.ts b/server/src/config/services.ts index ed1224ec7..e25ee1706 100644 --- a/server/src/config/services.ts +++ b/server/src/config/services.ts @@ -75,6 +75,7 @@ import { MongoInvitesRepository, MongoRecoveryTokensRepository, MongoSettingsRepository, + MongoNotificationsRepository, IMonitorsRepository, IChecksRepository, IMonitorStatsRepository, @@ -83,6 +84,7 @@ import { IInvitesRepository, IRecoveryTokensRepository, ISettingsRepository, + INotificationsRepository, } from "@/repositories/index.js"; export type InitializedSerivces = { @@ -116,6 +118,7 @@ export type InitializedSerivces = { invitesRepository: IInvitesRepository; recoveryTokensRepository: IRecoveryTokensRepository; settingsRepository: ISettingsRepository; + notificationsRepository: INotificationsRepository; }; export const initializeServices = async ({ @@ -171,7 +174,7 @@ export const initializeServices = async ({ const invitesRepository = new MongoInvitesRepository(); const recoveryTokensRepository = new MongoRecoveryTokensRepository(); const settingsRepository = new MongoSettingsRepository(); - + const notificationsRepository = new MongoNotificationsRepository(); const networkService = new NetworkService({ axios, got, @@ -225,7 +228,7 @@ export const initializeServices = async ({ notificationUtils, }); - const notificationsService = new NotificationsService(); + const notificationsService = new NotificationsService(notificationsRepository); const superSimpleQueueHelper = new SuperSimpleQueueHelper({ db, diff --git a/server/src/repositories/index.ts b/server/src/repositories/index.ts index 09a686595..1a3199081 100644 --- a/server/src/repositories/index.ts +++ b/server/src/repositories/index.ts @@ -21,3 +21,6 @@ export { default as MongoRecoveryTokensRepository } from "@/repositories/recover export * from "@/repositories/settings/ISettingsRepository.js"; export { default as MongoSettingsRepository } from "@/repositories/settings/MongoSettingsRepository.js"; + +export * from "@/repositories/notifications/INotificationsRepository.js"; +export { default as MongoNotificationsRepository } from "@/repositories/notifications/MongoNotificationsRepository.js"; diff --git a/server/src/repositories/notifications/INotificationsRepository.ts b/server/src/repositories/notifications/INotificationsRepository.ts new file mode 100644 index 000000000..0244972e4 --- /dev/null +++ b/server/src/repositories/notifications/INotificationsRepository.ts @@ -0,0 +1,7 @@ +import type { Notification } from "@/types/index.js"; +export interface INotificationsRepository { + // create + // fetch + // update + // delete +} diff --git a/server/src/repositories/notifications/MongoNotificationsRepository.ts b/server/src/repositories/notifications/MongoNotificationsRepository.ts new file mode 100644 index 000000000..51839c125 --- /dev/null +++ b/server/src/repositories/notifications/MongoNotificationsRepository.ts @@ -0,0 +1,40 @@ +import mongoose from "mongoose"; +import { NotificationModel, type NotificationDocument } from "@/db/models/index.js"; +import { INotificationsRepository } from "@/repositories/index.js"; +import type { Notification } from "@/types/index.js"; + +class MongoNotificationsRepository implements INotificationsRepository { + private mapDocuments = (documents: NotificationDocument[]): Notification[] => { + if (!documents?.length) { + return []; + } + return documents.map((doc) => this.toEntity(doc)); + }; + + private toEntity = (doc: NotificationDocument): Notification => { + const toStringId = (value: mongoose.Types.ObjectId | string): string => { + return value instanceof mongoose.Types.ObjectId ? value.toString() : value; + }; + + const toDateString = (value: Date | string): string => { + return value instanceof Date ? value.toISOString() : value; + }; + + return { + id: toStringId(doc._id), + userId: toStringId(doc.userId), + teamId: toStringId(doc.teamId), + type: doc.type, + notificationName: doc.notificationName, + address: doc.address ?? undefined, + phone: doc.phone ?? undefined, + homeserverUrl: doc.homeserverUrl ?? undefined, + roomId: doc.roomId ?? undefined, + accessToken: doc.accessToken ?? undefined, + createdAt: toDateString(doc.createdAt), + updatedAt: toDateString(doc.updatedAt), + }; + }; +} + +export default MongoNotificationsRepository; diff --git a/server/src/service/business/incidentService.js b/server/src/service/business/incidentService.js index 71e60d279..db89caa68 100644 --- a/server/src/service/business/incidentService.js +++ b/server/src/service/business/incidentService.js @@ -305,7 +305,7 @@ class IncidentService { const groupedByMonitor = {}; for (const item of createItems) { - if (!item.monitor || !item.monitor._id || !item.check || !item.check._id) { + if (!item.monitor || !item.monitor.id || !item.check || !item.check.id) { this.logger.warn({ service: this.SERVICE_NAME, method: "processIncidentsFromBuffer", @@ -315,7 +315,7 @@ class IncidentService { continue; } - const monitorId = item.monitor._id.toString(); + const monitorId = item.monitor.id.toString(); if (!groupedByMonitor[monitorId]) { groupedByMonitor[monitorId] = []; } @@ -341,20 +341,20 @@ class IncidentService { for (const item of items) { checksToAddToIncidents.push({ incidentId, - checkId: item.check._id.toString(), + checkId: item.check.id.toString(), }); } } else { const firstItem = items[0]; const incidentData = { - monitorId: firstItem.monitor._id, + monitorId: firstItem.monitor.id, teamId: firstItem.monitor.teamId, type: firstItem.monitor.type, startTime: new Date(), status: true, message: firstItem.check.message || null, statusCode: firstItem.check.statusCode || null, - checks: [firstItem.check._id], + checks: [firstItem.check.id], }; newIncidentsToCreate.push({ @@ -380,7 +380,7 @@ class IncidentService { for (const remainingItem of item.remainingChecks) { checksToAddToIncidents.push({ incidentId, - checkId: remainingItem.check._id.toString(), + checkId: remainingItem.check.id, }); } } diff --git a/server/src/service/infrastructure/SuperSimpleQueue/SuperSimpleQueueHelper.ts b/server/src/service/infrastructure/SuperSimpleQueue/SuperSimpleQueueHelper.ts index d1315e151..41885a146 100644 --- a/server/src/service/infrastructure/SuperSimpleQueue/SuperSimpleQueueHelper.ts +++ b/server/src/service/infrastructure/SuperSimpleQueue/SuperSimpleQueueHelper.ts @@ -84,7 +84,6 @@ class SuperSimpleQueueHelper { const statusChangeResult = await this.statusService.updateMonitorStatus(status, check); // Step 5 handle notifications - this.notificationsService.handleNotifications(monitor, status, statusChangeResult.prevStatus, statusChangeResult.statusChanged); // this.notificationService diff --git a/server/src/service/infrastructure/notificationProviders/utils.ts b/server/src/service/infrastructure/notificationProviders/utils.ts index ad8dc14ae..7cba00a5d 100644 --- a/server/src/service/infrastructure/notificationProviders/utils.ts +++ b/server/src/service/infrastructure/notificationProviders/utils.ts @@ -1,6 +1,10 @@ import { Monitor, HardwareStatusPayload, MonitorStatusResponse } from "@/types/index.js"; -const buildHardwareAlerts = async (clientHost: string, monitor: Monitor, networkResponse: MonitorStatusResponse) => { +export const buildHardwareAlerts = ( + clientHost: string, + monitor: Monitor, + networkResponse: MonitorStatusResponse +): { alertsToSend: string[]; discordPayload: any } => { const thresholds = monitor.thresholds || {}; const { usage_cpu: cpuThreshold = -1, usage_memory: memoryThreshold = -1, usage_disk: diskThreshold = -1 } = thresholds; @@ -54,15 +58,22 @@ const buildHardwareAlerts = async (clientHost: string, monitor: Monitor, network ], }), }; - const alertTypes = ["cpu", "memory", "disk"]; + const alertTypes = ["cpu", "memory", "disk"] as const; + const alertThresholdKeyMap: Record<(typeof alertTypes)[number], "cpuAlertThreshold" | "memoryAlertThreshold" | "diskAlertThreshold"> = { + cpu: "cpuAlertThreshold", + memory: "memoryAlertThreshold", + disk: "diskAlertThreshold", + }; for (const type of alertTypes) { + const thresholdKey = alertThresholdKeyMap[type]; // Iterate over each alert type to see if any need to be decremented if (alerts[type] === true) { - monitor[`${type}AlertThreshold`]--; // Decrement threshold if an alert is triggered + const nextValue = ((monitor[thresholdKey] ?? monitor.alertThreshold) as number) - 1; + monitor[thresholdKey] = nextValue; // Decrement threshold if an alert is triggered - if (monitor[`${type}AlertThreshold`] <= 0) { + if (monitor[thresholdKey] <= 0) { // If threshold drops below 0, reset and send notification - monitor[`${type}AlertThreshold`] = monitor.alertThreshold; + monitor[thresholdKey] = monitor.alertThreshold; const formatAlert = { cpu: () => `Your current CPU usage (${(cpuUsage * 100).toFixed(0)}%) is above your threshold (${(cpuThreshold * 100).toFixed(0)}%)`, @@ -70,7 +81,7 @@ const buildHardwareAlerts = async (clientHost: string, monitor: Monitor, network `Your current memory usage (${(memoryUsage * 100).toFixed(0)}%) is above your threshold (${(memoryThreshold * 100).toFixed(0)}%)`, disk: () => `Your current disk usage: ${disk - .map((d, idx) => `(Disk${idx}: ${(d.usage_percent * 100).toFixed(0)}%)`) + .map((d, idx) => `(Disk${idx}: ${(d?.usage_percent ?? 0 * 100).toFixed(0)}%)`) .join(", ")} is above your threshold (${(diskThreshold * 100).toFixed(0)}%)`, }; alertsToSend.push(formatAlert[type]()); @@ -79,13 +90,38 @@ const buildHardwareAlerts = async (clientHost: string, monitor: Monitor, network } } const discordPayload = discordEmbeds.length ? { embeds: discordEmbeds } : null; - return [alertsToSend, discordPayload]; + return { alertsToSend, discordPayload }; }; -const buildHardwareNotificationMessage = (alerts: any, monitor: Monitor) => { - const { clientHost } = this.settingsService.getSettings(); +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 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 shouldSendHardwareAlert = (monitor: Monitor, networkResponse: MonitorStatusResponse): boolean => { + const thresholds = monitor.thresholds || {}; + const { usage_cpu: cpuThreshold = -1, usage_memory: memoryThreshold = -1, usage_disk: diskThreshold = -1 } = thresholds; + + const payload = networkResponse?.payload as HardwareStatusPayload; + const metrics = payload.data || {}; + const { cpu: { usage_percent: cpuUsage = -1 } = {}, memory: { usage_percent: memoryUsage = -1 } = {}, disk = [] } = metrics; + + const cpuBreach = cpuThreshold !== -1 && cpuUsage > cpuThreshold; + if (cpuBreach && ((monitor.cpuAlertThreshold ?? monitor.alertThreshold) as number) - 1 <= 0) { + return true; + } + + const memoryBreach = memoryThreshold !== -1 && memoryUsage > memoryThreshold; + if (memoryBreach && ((monitor.memoryAlertThreshold ?? monitor.alertThreshold) as number) - 1 <= 0) { + return true; + } + + const diskBreach = disk?.some((d) => diskThreshold !== -1 && typeof d?.usage_percent === "number" && d?.usage_percent > diskThreshold); + if (diskBreach && ((monitor.diskAlertThreshold ?? monitor.alertThreshold) as number) - 1 <= 0) { + return true; + } + + return false; +}; diff --git a/server/src/service/infrastructure/notificationsService.ts b/server/src/service/infrastructure/notificationsService.ts index 3dc5fe65e..2070b0c4f 100644 --- a/server/src/service/infrastructure/notificationsService.ts +++ b/server/src/service/infrastructure/notificationsService.ts @@ -1,4 +1,7 @@ -import type { HardwareStatusPayload, Monitor, MonitorStatusResponse } from "@/types/index.js"; +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 { INotificationsRepository } from "@/repositories/index.js"; export interface INotificationsService { handleNotifications: ( monitor: Monitor, @@ -9,6 +12,24 @@ export interface INotificationsService { } export class NotificationsService implements INotificationsService { + private notificationsRepository: INotificationsRepository; + constructor(notificationsRepository: INotificationsRepository) { + this.notificationsRepository = notificationsRepository; + } + + private send = (notification: Notification, monitor: Monitor, monitorStatusResponse: MonitorStatusResponse) => { + console.log(notification); + }; + + 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; + }; + handleNotifications = async ( monitor: Monitor, monitorStatusResponse: MonitorStatusResponse, @@ -32,18 +53,18 @@ export class NotificationsService implements INotificationsService { // We shoul dsend 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 [alerts, discordContent] = await this.notificationUtils.buildHardwareAlerts(networkResponse); - // if (alerts.length === 0) return false; - // 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); // const success = await this.notifyAll({ notificationIDs, subject, html, content, discordContent, webhookBody }); - return true; + return await this.sendNotifications(monitor, monitorStatusResponse); } - return false; + // We should send a notification for non-hardware monitor status change + return await this.sendNotifications(monitor, monitorStatusResponse); }; } From f7f764fd68ea828e6ed74dc8b09c6aa8524ce536 Mon Sep 17 00:00:00 2001 From: Alex Holliday Date: Mon, 19 Jan 2026 18:58:17 +0000 Subject: [PATCH 08/16] 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); From fd33b903edee65b807452c0dc1eef43256d6ee74 Mon Sep 17 00:00:00 2001 From: Alex Holliday Date: Mon, 19 Jan 2026 19:30:46 +0000 Subject: [PATCH 09/16] email provider --- server/src/config/services.ts | 15 ++++++-- .../notificationProviders/email.ts | 28 ++++++++++++++- .../notificationProviders/utils.ts | 32 +++++++++++++++-- .../infrastructure/notificationsService.ts | 34 ++++++++++++++----- 4 files changed, 96 insertions(+), 13 deletions(-) diff --git a/server/src/config/services.ts b/server/src/config/services.ts index e25ee1706..3a740549a 100644 --- a/server/src/config/services.ts +++ b/server/src/config/services.ts @@ -7,7 +7,7 @@ import EmailService from "../service/infrastructure/emailService.js"; import BufferService from "../service/infrastructure/bufferService.js"; import NotificationUtils from "../service/infrastructure/notificationUtils.js"; import NotificationService from "../service/infrastructure/notificationService.js"; -import { NotificationsService, StatusService } from "@/service/index.js"; +import { NotificationsService, StatusService, WebhookProvider, SlackProvider, EmailProvider, DiscordProvider } from "@/service/index.js"; import ErrorService from "../service/infrastructure/errorService.js"; import SuperSimpleQueueHelper from "../service/infrastructure/SuperSimpleQueue/SuperSimpleQueueHelper.js"; import SuperSimpleQueue from "../service/infrastructure/SuperSimpleQueue/SuperSimpleQueue.js"; @@ -228,7 +228,17 @@ export const initializeServices = async ({ notificationUtils, }); - const notificationsService = new NotificationsService(notificationsRepository); + const webhookProvider = new WebhookProvider(); + const slackProvider = new SlackProvider(); + const emailProvider = new EmailProvider(emailService); + const discordProvider = new DiscordProvider(); + + const notificationsService = new NotificationsService(notificationsRepository, { + webhookProvider, + slackProvider, + emailProvider, + discordProvider, + }); const superSimpleQueueHelper = new SuperSimpleQueueHelper({ db, @@ -323,6 +333,7 @@ export const initializeServices = async ({ invitesRepository, recoveryTokensRepository, settingsRepository, + notificationsRepository, notificationsService, }; diff --git a/server/src/service/infrastructure/notificationProviders/email.ts b/server/src/service/infrastructure/notificationProviders/email.ts index dda7338a0..e60775982 100644 --- a/server/src/service/infrastructure/notificationProviders/email.ts +++ b/server/src/service/infrastructure/notificationProviders/email.ts @@ -1,9 +1,35 @@ import type { Monitor, Notification, MonitorStatusResponse } from "@/types/index.js"; import { INotificationProvider } from "@/service/index.js"; +import { buildHardwareAlerts, buildHardwareEmail, buildEmail } from "@/service/infrastructure/notificationProviders/utils.js"; export class EmailProvider implements INotificationProvider { + private emailService: any; + constructor(emailService: any) { + this.emailService = emailService; + } + + private buildHardwareEmail = (monitor: Monitor, monitorStatusResponse: MonitorStatusResponse) => { + const { alertsToSend } = buildHardwareAlerts("HOST_PLACEHOLDER", monitor, monitorStatusResponse); + const html = buildHardwareEmail(this.emailService, monitor, alertsToSend); + return html; + }; + async sendAlert(notification: Notification, monitor: Monitor, monitorStatusResponse: MonitorStatusResponse): Promise { - return false; + const subject = `Monitor ${monitor.name} infrastructure alerts`; + let html; + if (monitor.type === "hardware") { + html = this.buildHardwareEmail(monitor, monitorStatusResponse); + } else { + html = await buildEmail(this.emailService, monitor); + } + + if (!notification.address) { + return false; + } + + const messageId = await this.emailService.sendEmail(notification.address, subject, html); + if (!messageId) return false; + return true; } async sendTestAlert(notification: Notification): Promise { diff --git a/server/src/service/infrastructure/notificationProviders/utils.ts b/server/src/service/infrastructure/notificationProviders/utils.ts index 9a10e668b..0d25543ea 100644 --- a/server/src/service/infrastructure/notificationProviders/utils.ts +++ b/server/src/service/infrastructure/notificationProviders/utils.ts @@ -134,6 +134,34 @@ export const shouldSendHardwareAlert = (monitor: Monitor, networkResponse: Monit 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`; + const now = new Date(); + const localTimeZone = Intl.DateTimeFormat().resolvedOptions().timeZone; + const localTime = new Intl.DateTimeFormat(undefined, { + year: "numeric", + month: "2-digit", + day: "2-digit", + hour: "2-digit", + minute: "2-digit", + second: "2-digit", + hour12: false, + timeZone: localTimeZone, + timeZoneName: "short", + }).format(now); + const utcTime = now.toUTCString(); + return `Monitor: ${monitor.name}\nLocal Time (${localTimeZone}): ${localTime}\nUTC Time: ${utcTime}\nStatus: ${status ? "UP" : "DOWN"}\nStatus Code: ${code}\n\u200B\n`; +}; + +export const buildHardwareEmail = async (emailService: any, monitor: Monitor, alerts: string[]) => { + const template = "hardwareIncidentTemplate"; + const context = { monitor: monitor.name, url: monitor.url, alerts }; + const html = await emailService.buildEmail(template, context); + return html; +}; + +export const buildEmail = async (emailService: any, monitor: Monitor): Promise => { + const template = monitor.status === false ? "serverIsUpTemplate" : "serverIsDownTemplate"; + const context = { monitor: monitor.name, url: monitor.url }; + const html = await emailService.buildEmail(template, context); + console.log({ html: typeof html }); + return html; }; diff --git a/server/src/service/infrastructure/notificationsService.ts b/server/src/service/infrastructure/notificationsService.ts index e170dd3c4..8b44d4f9c 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 { shouldSendHardwareAlert } from "@/service/infrastructure/notificationProviders/utils.js"; import { INotificationsRepository } from "@/repositories/index.js"; -import { WebhookProvider } from "@/service/index.js"; +import { DiscordProvider, EmailProvider, SlackProvider, WebhookProvider } from "@/service/index.js"; export interface INotificationsService { handleNotifications: ( monitor: Monitor, @@ -14,21 +14,39 @@ export interface INotificationsService { export class NotificationsService implements INotificationsService { private notificationsRepository: INotificationsRepository; private webhookProvider: WebhookProvider; + private emailProvider: EmailProvider; + private slackProvider: SlackProvider; + private discordProvider: DiscordProvider; - constructor(notificationsRepository: INotificationsRepository, webhookProvider: WebhookProvider) { + constructor( + notificationsRepository: INotificationsRepository, + providers: { + webhookProvider: WebhookProvider; + emailProvider: EmailProvider; + slackProvider: SlackProvider; + discordProvider: DiscordProvider; + } + ) { this.notificationsRepository = notificationsRepository; - this.webhookProvider = webhookProvider; + this.webhookProvider = providers.webhookProvider; + this.emailProvider = providers.emailProvider; + this.slackProvider = providers.slackProvider; + this.discordProvider = providers.discordProvider; } private send = async (notification: Notification, monitor: Monitor, monitorStatusResponse: MonitorStatusResponse): Promise => { switch (notification.type) { - case " email": { - } - case "webhook": { + case "email": + return await this.emailProvider.sendAlert(notification, monitor, monitorStatusResponse); + case "slack": + return await this.slackProvider.sendAlert(notification, monitor, monitorStatusResponse); + case "discord": + return await this.discordProvider.sendAlert(notification, monitor, monitorStatusResponse); + case "webhook": return await this.webhookProvider.sendAlert(notification, monitor, monitorStatusResponse); - } + default: + return false; } - return false; }; private sendNotifications = async (monitor: Monitor, monitorStatusResponse: MonitorStatusResponse) => { From be82a7ee2dd5c2708022ab857a572f06c33144eb Mon Sep 17 00:00:00 2001 From: Alex Holliday Date: Mon, 19 Jan 2026 19:58:15 +0000 Subject: [PATCH 10/16] discord --- .../notificationProviders/discord.ts | 34 +++++++++++- .../notificationProviders/utils.ts | 55 ++++++++++++++----- .../notificationProviders/webhook.ts | 3 +- 3 files changed, 76 insertions(+), 16 deletions(-) diff --git a/server/src/service/infrastructure/notificationProviders/discord.ts b/server/src/service/infrastructure/notificationProviders/discord.ts index 18b1522be..fcb126455 100644 --- a/server/src/service/infrastructure/notificationProviders/discord.ts +++ b/server/src/service/infrastructure/notificationProviders/discord.ts @@ -1,8 +1,40 @@ import type { Monitor, Notification, MonitorStatusResponse } from "@/types/index.js"; import { INotificationProvider } from "@/service/index.js"; +import { buildHardwareAlerts, buildDiscordBody } from "@/service/infrastructure/notificationProviders/utils.js"; +import got, { HTTPError } from "got"; + export class DiscordProvider implements INotificationProvider { + private getHardwareContent = (monitor: Monitor, monitorStatusResponse: MonitorStatusResponse) => { + const { discordPayload } = buildHardwareAlerts("HOST_PLACEHOLDER", monitor, monitorStatusResponse); + return discordPayload; + }; + sendAlert = async (notification: Notification, monitor: Monitor, monitorStatusResponse: MonitorStatusResponse) => { - return false; + let body; + if (monitor.type === "hardware") { + body = this.getHardwareContent(monitor, monitorStatusResponse); + } else { + body = buildDiscordBody(monitor, monitorStatusResponse); + } + + if (!notification.address) { + return false; + } + + console.log({ body }); + + try { + await got.post(notification.address, { + json: body, + headers: { + "Content-Type": "application/json", + }, + responseType: "json", + }); + return true; + } catch (error: any) { + return false; + } }; sendTestAlert = async (notification: Notification) => { return false; diff --git a/server/src/service/infrastructure/notificationProviders/utils.ts b/server/src/service/infrastructure/notificationProviders/utils.ts index 0d25543ea..3c6737263 100644 --- a/server/src/service/infrastructure/notificationProviders/utils.ts +++ b/server/src/service/infrastructure/notificationProviders/utils.ts @@ -1,5 +1,23 @@ import { Monitor, HardwareStatusPayload, MonitorStatusResponse } from "@/types/index.js"; +const getTime = (): { localTimeZone: string; localTime: string; utcTime: string } => { + const now = new Date(); + const localTimeZone = Intl.DateTimeFormat().resolvedOptions().timeZone; + const localTime = new Intl.DateTimeFormat(undefined, { + year: "numeric", + month: "2-digit", + day: "2-digit", + hour: "2-digit", + minute: "2-digit", + second: "2-digit", + hour12: false, + timeZone: localTimeZone, + timeZoneName: "short", + }).format(now); + const utcTime = now.toUTCString(); + return { localTimeZone, localTime, utcTime }; +}; + export const buildHardwareAlerts = ( clientHost: string, monitor: Monitor, @@ -134,20 +152,7 @@ export const shouldSendHardwareAlert = (monitor: Monitor, networkResponse: Monit export const buildWebhookBody = (monitor: Monitor, monitorStatusResponse: MonitorStatusResponse) => { const { status, code } = monitorStatusResponse; - const now = new Date(); - const localTimeZone = Intl.DateTimeFormat().resolvedOptions().timeZone; - const localTime = new Intl.DateTimeFormat(undefined, { - year: "numeric", - month: "2-digit", - day: "2-digit", - hour: "2-digit", - minute: "2-digit", - second: "2-digit", - hour12: false, - timeZone: localTimeZone, - timeZoneName: "short", - }).format(now); - const utcTime = now.toUTCString(); + const { localTimeZone, localTime, utcTime } = getTime(); return `Monitor: ${monitor.name}\nLocal Time (${localTimeZone}): ${localTime}\nUTC Time: ${utcTime}\nStatus: ${status ? "UP" : "DOWN"}\nStatus Code: ${code}\n\u200B\n`; }; @@ -165,3 +170,25 @@ export const buildEmail = async (emailService: any, monitor: Monitor): Promise { + const { localTime, utcTime } = getTime(); + let body = { + embeds: [ + { + title: `Monitor ${monitor.name}`, + color: monitor.status ? 5763719 : 15548997, + + fields: [ + { name: "Monitor", value: monitor.name, inline: true }, + { name: "Status", value: monitor.status ? "Up" : "Down", inline: true }, + { name: "Status Code", value: String(monitorStatusResponse.code), inline: true }, + { name: "Time", value: `${localTime} (Local) / ${utcTime} (UTC)`, inline: true }, + { name: "URL", value: monitor.url, inline: false }, + ], + footer: { text: "Checkmate" }, + }, + ], + }; + return body; +}; diff --git a/server/src/service/infrastructure/notificationProviders/webhook.ts b/server/src/service/infrastructure/notificationProviders/webhook.ts index cf56db89b..d5ae3bcd3 100644 --- a/server/src/service/infrastructure/notificationProviders/webhook.ts +++ b/server/src/service/infrastructure/notificationProviders/webhook.ts @@ -15,7 +15,8 @@ export class WebhookProvider implements INotificationProvider { return body; }; - sendAlert = async (notification: Notification, monitor: Monitor, monitorStatusResponse: MonitorStatusResponse) => { + sendAlert = async (notification: Notification, monitor: Monitor, monitorStatusResponse: MonitorStatusResponse) => { + console.log("WebhookProvider.sendAlert called"); let body; if (monitor.type === "hardware") { body = this.getHardwareContent(monitor, monitorStatusResponse); From 9006707e57ee97902e21857ee96876ff675877b8 Mon Sep 17 00:00:00 2001 From: Alex Holliday Date: Mon, 19 Jan 2026 20:03:20 +0000 Subject: [PATCH 11/16] discord --- .../infrastructure/notificationProviders/discord.ts | 2 -- .../infrastructure/notificationProviders/utils.ts | 12 ++++++------ 2 files changed, 6 insertions(+), 8 deletions(-) diff --git a/server/src/service/infrastructure/notificationProviders/discord.ts b/server/src/service/infrastructure/notificationProviders/discord.ts index fcb126455..8ac042a92 100644 --- a/server/src/service/infrastructure/notificationProviders/discord.ts +++ b/server/src/service/infrastructure/notificationProviders/discord.ts @@ -21,8 +21,6 @@ export class DiscordProvider implements INotificationProvider { return false; } - console.log({ body }); - try { await got.post(notification.address, { json: body, diff --git a/server/src/service/infrastructure/notificationProviders/utils.ts b/server/src/service/infrastructure/notificationProviders/utils.ts index 3c6737263..fff682246 100644 --- a/server/src/service/infrastructure/notificationProviders/utils.ts +++ b/server/src/service/infrastructure/notificationProviders/utils.ts @@ -176,14 +176,14 @@ export const buildDiscordBody = (monitor: Monitor, monitorStatusResponse: Monito let body = { embeds: [ { - title: `Monitor ${monitor.name}`, - color: monitor.status ? 5763719 : 15548997, + title: `Monitor name: ${monitor.name}`, + color: monitorStatusResponse.status ? 5763719 : 15548997, fields: [ - { name: "Monitor", value: monitor.name, inline: true }, - { name: "Status", value: monitor.status ? "Up" : "Down", inline: true }, - { name: "Status Code", value: String(monitorStatusResponse.code), inline: true }, - { name: "Time", value: `${localTime} (Local) / ${utcTime} (UTC)`, inline: true }, + { name: "Monitor", value: monitor.name, inline: false }, + { name: "Status", value: monitorStatusResponse.status ? "Up" : "Down", inline: false }, + { name: "Status Code", value: String(monitorStatusResponse.code), inline: false }, + { name: "Time", value: `${localTime} (Local) / ${utcTime} (UTC)`, inline: false }, { name: "URL", value: monitor.url, inline: false }, ], footer: { text: "Checkmate" }, From 1d36f40caf983a8c7470982ed54a689791812f61 Mon Sep 17 00:00:00 2001 From: Alex Holliday Date: Mon, 19 Jan 2026 20:12:54 +0000 Subject: [PATCH 12/16] slack --- .../notificationProviders/discord.ts | 2 +- .../notificationProviders/slack.ts | 37 ++++++++++++++++++- .../notificationProviders/utils.ts | 6 +-- .../notificationProviders/webhook.ts | 1 - 4 files changed, 39 insertions(+), 7 deletions(-) diff --git a/server/src/service/infrastructure/notificationProviders/discord.ts b/server/src/service/infrastructure/notificationProviders/discord.ts index 8ac042a92..5aa8dd1d4 100644 --- a/server/src/service/infrastructure/notificationProviders/discord.ts +++ b/server/src/service/infrastructure/notificationProviders/discord.ts @@ -1,7 +1,7 @@ import type { Monitor, Notification, MonitorStatusResponse } from "@/types/index.js"; import { INotificationProvider } from "@/service/index.js"; import { buildHardwareAlerts, buildDiscordBody } from "@/service/infrastructure/notificationProviders/utils.js"; -import got, { HTTPError } from "got"; +import got from "got"; export class DiscordProvider implements INotificationProvider { private getHardwareContent = (monitor: Monitor, monitorStatusResponse: MonitorStatusResponse) => { diff --git a/server/src/service/infrastructure/notificationProviders/slack.ts b/server/src/service/infrastructure/notificationProviders/slack.ts index bd2836d2b..4f4c9df18 100644 --- a/server/src/service/infrastructure/notificationProviders/slack.ts +++ b/server/src/service/infrastructure/notificationProviders/slack.ts @@ -1,9 +1,44 @@ import type { Monitor, 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 SlackProvider implements INotificationProvider { + private getHardwareContent = (monitor: Monitor, monitorStatusResponse: MonitorStatusResponse) => { + const { alertsToSend } = buildHardwareAlerts("HOST_PLACEHOLDER", monitor, monitorStatusResponse); + const body = buildHardwareWebhookBody(alertsToSend, monitor); + return body; + }; + + private getContent = (monitor: Monitor, monitorStatusResponse: MonitorStatusResponse) => { + const body = buildWebhookBody(monitor, monitorStatusResponse); + return body; + }; + async sendAlert(notification: Notification, monitor: Monitor, monitorStatusResponse: MonitorStatusResponse): Promise { - return false; + 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: { text: body }, + headers: { + "Content-Type": "application/json", + }, + responseType: "json", + }); + return true; + } catch (error) { + return false; + } } async sendTestAlert(notification: Notification): Promise { diff --git a/server/src/service/infrastructure/notificationProviders/utils.ts b/server/src/service/infrastructure/notificationProviders/utils.ts index fff682246..7d7d25f49 100644 --- a/server/src/service/infrastructure/notificationProviders/utils.ts +++ b/server/src/service/infrastructure/notificationProviders/utils.ts @@ -118,10 +118,9 @@ export const buildHardwareNotificationMessage = (clientHost: string, alerts: any return alertText.map((alert) => alert).join("\n"); }; -export const buildHardwareWebhookBody = (alerts: string[], monitor: Monitor): { text: string; name: string; url: string } => { +export const buildHardwareWebhookBody = (alerts: string[], monitor: Monitor): string => { const content = alerts.map((alert) => alert).join("\n"); - const body = { text: content, name: monitor.name, url: monitor.url }; - return body; + return content; }; export const shouldSendHardwareAlert = (monitor: Monitor, networkResponse: MonitorStatusResponse): boolean => { @@ -167,7 +166,6 @@ export const buildEmail = async (emailService: any, monitor: Monitor): Promise { - console.log("WebhookProvider.sendAlert called"); let body; if (monitor.type === "hardware") { body = this.getHardwareContent(monitor, monitorStatusResponse); From 7c9410f9d73453ce48923b64f3c9643d3699288c Mon Sep 17 00:00:00 2001 From: Alex Holliday Date: Mon, 19 Jan 2026 21:39:02 +0000 Subject: [PATCH 13/16] test alerts --- server/src/config/controllers.ts | 2 +- server/src/config/services.ts | 40 +++++---- .../src/controllers/notificationController.ts | 15 ++-- server/src/service/index.ts | 3 + .../SuperSimpleQueueHelper.ts | 33 +++---- .../notificationProviders/email.ts | 8 +- .../notificationProviders/matrix.js | 34 ------- .../notificationProviders/matrix.ts | 56 ++++++++++++ .../notificationProviders/pagerduty.ts | 50 +++++++++++ .../notificationProviders/slack.ts | 24 ++++- .../notificationProviders/utils.ts | 10 +++ .../infrastructure/notificationsService.ts | 90 ++++++++++++++----- 12 files changed, 258 insertions(+), 107 deletions(-) delete mode 100644 server/src/service/infrastructure/notificationProviders/matrix.js create mode 100644 server/src/service/infrastructure/notificationProviders/matrix.ts create mode 100644 server/src/service/infrastructure/notificationProviders/pagerduty.ts diff --git a/server/src/config/controllers.ts b/server/src/config/controllers.ts index 44b051580..5ddbf2a06 100644 --- a/server/src/config/controllers.ts +++ b/server/src/config/controllers.ts @@ -26,7 +26,7 @@ export const initializeControllers = (services: InitializedSerivces) => { controllers.queueController = new QueueController(services.jobQueue); controllers.logController = new LogController(services.logger); controllers.statusPageController = new StatusPageController(services.db); - controllers.notificationController = new NotificationController(services.notificationService, services.db, services.monitorsRepository); + controllers.notificationController = new NotificationController(services.notificationsService, services.db, services.monitorsRepository); controllers.diagnosticController = new DiagnosticController(services.diagnosticService); controllers.incidentController = new IncidentController(services.incidentService); diff --git a/server/src/config/services.ts b/server/src/config/services.ts index 3a740549a..035a0782f 100644 --- a/server/src/config/services.ts +++ b/server/src/config/services.ts @@ -6,8 +6,17 @@ import NetworkService from "../service/infrastructure/networkService.js"; import EmailService from "../service/infrastructure/emailService.js"; import BufferService from "../service/infrastructure/bufferService.js"; import NotificationUtils from "../service/infrastructure/notificationUtils.js"; -import NotificationService from "../service/infrastructure/notificationService.js"; -import { NotificationsService, StatusService, WebhookProvider, SlackProvider, EmailProvider, DiscordProvider } from "@/service/index.js"; +import { + NotificationsService, + StatusService, + WebhookProvider, + SlackProvider, + EmailProvider, + DiscordProvider, + PagerDutyProvider, + MatrixProvider, + INotificationsService, +} from "@/service/index.js"; import ErrorService from "../service/infrastructure/errorService.js"; import SuperSimpleQueueHelper from "../service/infrastructure/SuperSimpleQueue/SuperSimpleQueueHelper.js"; import SuperSimpleQueue from "../service/infrastructure/SuperSimpleQueue/SuperSimpleQueue.js"; @@ -97,7 +106,6 @@ export type InitializedSerivces = { emailService: any; bufferService: any; statusService: any; - notificationService: any; jobQueue: any; userService: any; checkService: any; @@ -108,6 +116,7 @@ export type InitializedSerivces = { incidentService: any; errorService: any; logger: any; + notificationsService: INotificationsService; // Repositories monitorsRepository: IMonitorsRepository; @@ -219,33 +228,29 @@ export const initializeServices = async ({ settingsService, }); - const notificationService = new NotificationService({ - emailService, - db, - logger, - networkService, - stringService, - notificationUtils, - }); - const webhookProvider = new WebhookProvider(); const slackProvider = new SlackProvider(); const emailProvider = new EmailProvider(emailService); const discordProvider = new DiscordProvider(); + const pagerDutyProvider = new PagerDutyProvider(); + const matrixProvider = new MatrixProvider(); - const notificationsService = new NotificationsService(notificationsRepository, { + const notificationsService = new NotificationsService( + notificationsRepository, webhookProvider, - slackProvider, emailProvider, + slackProvider, discordProvider, - }); + pagerDutyProvider, + matrixProvider, + logger + ); const superSimpleQueueHelper = new SuperSimpleQueueHelper({ db, logger, networkService, statusService, - notificationService, notificationsService, checkService, buffer: bufferService, @@ -312,7 +317,6 @@ export const initializeServices = async ({ emailService, bufferService, statusService, - notificationService, jobQueue: superSimpleQueue, userService, checkService, @@ -323,6 +327,7 @@ export const initializeServices = async ({ incidentService, errorService, logger, + notificationsService, // Repositories monitorsRepository, @@ -334,7 +339,6 @@ export const initializeServices = async ({ recoveryTokensRepository, settingsRepository, notificationsRepository, - notificationsService, }; Object.values(services).forEach((service) => { diff --git a/server/src/controllers/notificationController.ts b/server/src/controllers/notificationController.ts index 6f4a7f164..1569f0986 100644 --- a/server/src/controllers/notificationController.ts +++ b/server/src/controllers/notificationController.ts @@ -1,18 +1,20 @@ import { Request, Response, NextFunction } from "express"; +import { Notification } from "@/types/index.js"; import { createNotificationBodyValidation } from "@/validation/joi.js"; import { AppError } from "@/utils/AppError.js"; import { IMonitorsRepository } from "@/repositories/index.js"; +import { INotificationsService } from "@/service/index.js"; const SERVICE_NAME = "NotificationController"; class NotificationController { static SERVICE_NAME = SERVICE_NAME; private db: any; - private notificationService: any; + private notificationsService: INotificationsService; private monitorsRepository: IMonitorsRepository; - constructor(notificationService: any, db: any, monitorsRepository: IMonitorsRepository) { - this.notificationService = notificationService; + constructor(notificationsService: INotificationsService, db: any, monitorsRepository: IMonitorsRepository) { + this.notificationsService = notificationsService; this.db = db; this.monitorsRepository = monitorsRepository; } @@ -23,9 +25,8 @@ class NotificationController { testNotification = async (req: Request, res: Response, next: NextFunction) => { try { - const notification = req.body; - - const success = await this.notificationService.sendTestNotification(notification); + const notification: Notification = req.body; + const success = await this.notificationsService.sendTestNotification(notification); if (!success) { throw new AppError({ message: "Sending notification failed", status: 500 }); @@ -177,7 +178,7 @@ class NotificationController { throw new AppError({ message: "No notifications", status: 400 }); } - const result = await this.notificationService.testAllNotifications(notifications); + const result = await this.notificationsService.testAllNotifications(notifications); if (!result) { throw new AppError({ message: "Failed to send all notifications", status: 500 }); diff --git a/server/src/service/index.ts b/server/src/service/index.ts index f46e6d3d8..7f7d0f14a 100644 --- a/server/src/service/index.ts +++ b/server/src/service/index.ts @@ -3,8 +3,11 @@ export * from "@/service/infrastructure/networkService.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/pagerduty.js"; +export * from "@/service/infrastructure/notificationProviders/matrix.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"; +export * from "@/service/infrastructure/notificationProviders/matrix.js"; diff --git a/server/src/service/infrastructure/SuperSimpleQueue/SuperSimpleQueueHelper.ts b/server/src/service/infrastructure/SuperSimpleQueue/SuperSimpleQueueHelper.ts index 449386102..ebb0949b0 100644 --- a/server/src/service/infrastructure/SuperSimpleQueue/SuperSimpleQueueHelper.ts +++ b/server/src/service/infrastructure/SuperSimpleQueue/SuperSimpleQueueHelper.ts @@ -9,11 +9,9 @@ class SuperSimpleQueueHelper { private logger: any; private networkService: INetworkService; private statusService: IStatusService; - private notificationService: any; private notificationsService: INotificationsService; private checkService: any; private buffer: any; - private incidentService: any; constructor({ db, @@ -29,7 +27,6 @@ class SuperSimpleQueueHelper { logger: any; networkService: INetworkService; statusService: IStatusService; - notificationService: any; notificationsService: INotificationsService; checkService: any; buffer: any; @@ -38,7 +35,6 @@ class SuperSimpleQueueHelper { this.logger = logger; this.networkService = networkService; this.statusService = statusService; - this.notificationService = notificationService; this.checkService = checkService; this.buffer = buffer; this.notificationsService = notificationsService; @@ -84,24 +80,17 @@ class SuperSimpleQueueHelper { const statusChangeResult = await this.statusService.updateMonitorStatus(status, check); // 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 - // .handleNotifications({ - // ...status, - // monitor: statusChangeResult.monitor, - // prevStatus: statusChangeResult.prevStatus, - // statusChanged: statusChangeResult.statusChanged, - // }) - // .catch((error: any) => { - // this.logger.error({ - // message: error.message, - // service: SERVICE_NAME, - // method: "getMonitorJob", - // details: `Error sending notifications for job ${monitor.id}: ${error.message}`, - // stack: error.stack, - // }); - // }); + this.notificationsService + .handleNotifications(monitor, status, statusChangeResult.prevStatus, statusChangeResult.statusChanged) + .catch((error: any) => { + this.logger.error({ + message: error.message, + service: SERVICE_NAME, + method: "getMonitorJob", + details: `Error sending notifications for job ${monitor.id}: ${error.message}`, + stack: error.stack, + }); + }); } catch (error: any) { this.logger.warn({ message: error.message, diff --git a/server/src/service/infrastructure/notificationProviders/email.ts b/server/src/service/infrastructure/notificationProviders/email.ts index e60775982..0a405a078 100644 --- a/server/src/service/infrastructure/notificationProviders/email.ts +++ b/server/src/service/infrastructure/notificationProviders/email.ts @@ -1,6 +1,6 @@ import type { Monitor, Notification, MonitorStatusResponse } from "@/types/index.js"; import { INotificationProvider } from "@/service/index.js"; -import { buildHardwareAlerts, buildHardwareEmail, buildEmail } from "@/service/infrastructure/notificationProviders/utils.js"; +import { buildHardwareAlerts, buildHardwareEmail, buildEmail, buildTestEmail } from "@/service/infrastructure/notificationProviders/utils.js"; export class EmailProvider implements INotificationProvider { private emailService: any; @@ -33,6 +33,10 @@ export class EmailProvider implements INotificationProvider { } async sendTestAlert(notification: Notification): Promise { - return false; + const subject = "Test notification"; + const html = await buildTestEmail(this.emailService); + const messageId = await this.emailService.sendEmail(notification.address, subject, html); + if (!messageId) return false; + return true; } } diff --git a/server/src/service/infrastructure/notificationProviders/matrix.js b/server/src/service/infrastructure/notificationProviders/matrix.js deleted file mode 100644 index a8b33d4b8..000000000 --- a/server/src/service/infrastructure/notificationProviders/matrix.js +++ /dev/null @@ -1,34 +0,0 @@ -const SERVICE_NAME = "Matrix"; - -class Matrix { - static SERVICE_NAME = SERVICE_NAME; - - constructor({ networkService, logger }) { - this.networkService = networkService; - this.logger = logger; - } - - get serviceName() { - return Matrix.SERVICE_NAME; - } - - async send({ friendlyName, homeserverUrl, accessToken, roomId, message, monitorName }) { - const title = `Checkmate status for ${monitorName}`; - const formattedMessage = `## ${title}\n${message}`; - try { - await this.networkService.requestMatrix({ - homeserverUrl, - accessToken, - roomId, - message: formattedMessage, - }); - this.logger.info(`Successfully sent Matrix notification for ${friendlyName}`); - return true; - } catch (error) { - this.logger.error(`Failed to send Matrix notification for ${friendlyName}: ${error.message}`); - return false; - } - } -} - -export default Matrix; diff --git a/server/src/service/infrastructure/notificationProviders/matrix.ts b/server/src/service/infrastructure/notificationProviders/matrix.ts new file mode 100644 index 000000000..8985ac10b --- /dev/null +++ b/server/src/service/infrastructure/notificationProviders/matrix.ts @@ -0,0 +1,56 @@ +import got from "got"; +import type { INotificationProvider } from "@/service/index.js"; +import type { Notification, Monitor, MonitorStatusResponse } from "@/types/index.js"; +import { buildHardwareAlerts, buildHardwareWebhookBody, buildWebhookBody } from "@/service/infrastructure/notificationProviders/utils.js"; + +export class MatrixProvider implements INotificationProvider { + private getHardwareContent = (monitor: Monitor, monitorStatusResponse: MonitorStatusResponse) => { + const { alertsToSend } = buildHardwareAlerts("HOST_PLACEHOLDER", monitor, monitorStatusResponse); + const body = buildHardwareWebhookBody(alertsToSend, monitor); + return body; + }; + + private getContent = (monitor: Monitor, monitorStatusResponse: MonitorStatusResponse) => { + const body = buildWebhookBody(monitor, monitorStatusResponse); + return body; + }; + + sendAlert = async (notification: Notification, monitor: Monitor, monitorStatusResponse: MonitorStatusResponse) => { + const { homeserverUrl, accessToken, roomId } = notification; + + let content; + if (monitor.type === "hardware") { + content = this.getHardwareContent(monitor, monitorStatusResponse); + } else { + content = this.getContent(monitor, monitorStatusResponse); + } + + const title = `Checkmate status for ${monitor.name}`; + + const formattedMessage = `## ${title}\n${content}`; + + const url = `${homeserverUrl}/_matrix/client/v3/rooms/${roomId}/send/m.room.message?access_token=${accessToken}`; + const body = { + msgtype: "m.text", + body: formattedMessage, + format: "org.matrix.custom.html", + formatted_body: formattedMessage, + }; + try { + await got.post(url, { + 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/notificationProviders/pagerduty.ts b/server/src/service/infrastructure/notificationProviders/pagerduty.ts new file mode 100644 index 000000000..d0309550b --- /dev/null +++ b/server/src/service/infrastructure/notificationProviders/pagerduty.ts @@ -0,0 +1,50 @@ +import got from "got"; +import type { Monitor, Notification, MonitorStatusResponse } from "@/types/index.js"; +import { INotificationProvider } from "@/service/index.js"; +import { buildHardwareAlerts, buildHardwareWebhookBody, buildWebhookBody } from "@/service/infrastructure/notificationProviders/utils.js"; + +export class PagerDutyProvider implements INotificationProvider { + private getHardwareContent = (monitor: Monitor, monitorStatusResponse: MonitorStatusResponse) => { + const { alertsToSend } = buildHardwareAlerts("HOST_PLACEHOLDER", monitor, monitorStatusResponse); + const body = buildHardwareWebhookBody(alertsToSend, monitor); + return body; + }; + + private getContent = (monitor: Monitor, monitorStatusResponse: MonitorStatusResponse) => { + const body = buildWebhookBody(monitor, monitorStatusResponse); + return body; + }; + + async sendAlert(notification: Notification, monitor: Monitor, monitorStatusResponse: MonitorStatusResponse): Promise { + let body; + if (monitor.type === "hardware") { + body = this.getHardwareContent(monitor, monitorStatusResponse); + } else { + body = this.getContent(monitor, monitorStatusResponse); + } + + try { + await got.post("https://events.pagerduty.com/v2/enqueue", { + json: { + routing_key: notification.address, + event_action: "trigger", + payload: { + summary: body, + severity: "critical", + source: monitor.url, + timestamp: new Date().toISOString(), + }, + }, + responseType: "json", + }); + + return true; + } catch (error) { + 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 index 4f4c9df18..c5745ce05 100644 --- a/server/src/service/infrastructure/notificationProviders/slack.ts +++ b/server/src/service/infrastructure/notificationProviders/slack.ts @@ -1,6 +1,11 @@ import type { Monitor, Notification, MonitorStatusResponse } from "@/types/index.js"; import { INotificationProvider } from "@/service/index.js"; -import { buildHardwareAlerts, buildHardwareWebhookBody, buildWebhookBody } from "@/service/infrastructure/notificationProviders/utils.js"; +import { + buildHardwareAlerts, + buildHardwareWebhookBody, + buildWebhookBody, + getTestMessage, +} from "@/service/infrastructure/notificationProviders/utils.js"; import got from "got"; export class SlackProvider implements INotificationProvider { @@ -42,6 +47,21 @@ export class SlackProvider implements INotificationProvider { } async sendTestAlert(notification: Notification): Promise { - return false; + if (!notification.address) { + return false; + } + + try { + await got.post(notification.address, { + json: { text: getTestMessage() }, + headers: { + "Content-Type": "application/json", + }, + responseType: "json", + }); + return true; + } catch (error) { + return false; + } } } diff --git a/server/src/service/infrastructure/notificationProviders/utils.ts b/server/src/service/infrastructure/notificationProviders/utils.ts index 7d7d25f49..a7e954e55 100644 --- a/server/src/service/infrastructure/notificationProviders/utils.ts +++ b/server/src/service/infrastructure/notificationProviders/utils.ts @@ -190,3 +190,13 @@ export const buildDiscordBody = (monitor: Monitor, monitorStatusResponse: Monito }; return body; }; + +export const buildTestEmail = async (emailService: any) => { + const context = { testName: "Monitoring System" }; + const html = await emailService.buildEmail("testEmailTemplate", context); + return html; +}; + +export const getTestMessage = () => { + return "This is a test notification from Checkmate"; +}; diff --git a/server/src/service/infrastructure/notificationsService.ts b/server/src/service/infrastructure/notificationsService.ts index 8b44d4f9c..7ca52945e 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 { shouldSendHardwareAlert } from "@/service/infrastructure/notificationProviders/utils.js"; import { INotificationsRepository } from "@/repositories/index.js"; -import { DiscordProvider, EmailProvider, SlackProvider, WebhookProvider } from "@/service/index.js"; +import { INotificationProvider } from "./notificationProviders/INotificationProvider.js"; export interface INotificationsService { handleNotifications: ( monitor: Monitor, @@ -9,29 +9,42 @@ export interface INotificationsService { prevStatus: boolean | undefined, statusChanged: boolean ) => Promise; + sendTestNotification: (notification: Notification) => Promise; + testAllNotifications: (notificationIds: string[]) => Promise; } +const SERVICE_NAME = "NotificationsService"; + export class NotificationsService implements INotificationsService { + static SERVICE_NAME = SERVICE_NAME; + private notificationsRepository: INotificationsRepository; - private webhookProvider: WebhookProvider; - private emailProvider: EmailProvider; - private slackProvider: SlackProvider; - private discordProvider: DiscordProvider; + private webhookProvider: INotificationProvider; + private emailProvider: INotificationProvider; + private slackProvider: INotificationProvider; + private discordProvider: INotificationProvider; + private pagerDutyProvider: INotificationProvider; + private matrixProvider: INotificationProvider; + private logger: any; constructor( notificationsRepository: INotificationsRepository, - providers: { - webhookProvider: WebhookProvider; - emailProvider: EmailProvider; - slackProvider: SlackProvider; - discordProvider: DiscordProvider; - } + webhookProvider: INotificationProvider, + emailProvider: INotificationProvider, + slackProvider: INotificationProvider, + discordProvider: INotificationProvider, + pagerDutyProvider: INotificationProvider, + matrixProvider: INotificationProvider, + logger: any ) { this.notificationsRepository = notificationsRepository; - this.webhookProvider = providers.webhookProvider; - this.emailProvider = providers.emailProvider; - this.slackProvider = providers.slackProvider; - this.discordProvider = providers.discordProvider; + this.webhookProvider = webhookProvider; + this.emailProvider = emailProvider; + this.slackProvider = slackProvider; + this.discordProvider = discordProvider; + this.pagerDutyProvider = pagerDutyProvider; + this.matrixProvider = matrixProvider; + this.logger = logger; } private send = async (notification: Notification, monitor: Monitor, monitorStatusResponse: MonitorStatusResponse): Promise => { @@ -42,6 +55,10 @@ export class NotificationsService implements INotificationsService { return await this.slackProvider.sendAlert(notification, monitor, monitorStatusResponse); case "discord": return await this.discordProvider.sendAlert(notification, monitor, monitorStatusResponse); + case "pager_duty": + return await this.pagerDutyProvider.sendAlert(notification, monitor, monitorStatusResponse); + case "matrix": + return await this.matrixProvider.sendAlert(notification, monitor, monitorStatusResponse); case "webhook": return await this.webhookProvider.sendAlert(notification, monitor, monitorStatusResponse); default: @@ -57,7 +74,11 @@ export class NotificationsService implements INotificationsService { 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)`); + this.logger.warn({ + message: `Notification send completed with ${succeeded} success, ${failed} failure(s)`, + service: SERVICE_NAME, + method: "getMonitorJob", + }); } // Return true if all notificaitons succeeded return succeeded === notifications.length; @@ -88,15 +109,42 @@ export class NotificationsService implements INotificationsService { const shouldSend = shouldSendHardwareAlert(monitor, monitorStatusResponse); if (shouldSend === false) return false; - console.log(JSON.stringify(monitor, 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); - // const success = await this.notifyAll({ notificationIDs, subject, html, content, discordContent, webhookBody }); + return await this.sendNotifications(monitor, monitorStatusResponse); } // We should send a notification for non-hardware monitor status change return await this.sendNotifications(monitor, monitorStatusResponse); }; + + sendTestNotification = async (notification: Notification) => { + switch (notification.type) { + case "email": + return await this.emailProvider.sendTestAlert(notification); + case "slack": + return await this.slackProvider.sendTestAlert(notification); + case "discord": + return await this.discordProvider.sendTestAlert(notification); + case "pager_duty": + return await this.pagerDutyProvider.sendTestAlert(notification); + case "matrix": + return await this.matrixProvider.sendTestAlert(notification); + case "webhook": + return await this.webhookProvider.sendTestAlert(notification); + default: + return false; + } + }; + + testAllNotifications = async (notificationIds: string[]) => { + const notifications = await this.notificationsRepository.findNotificationsByIds(notificationIds); + const tasks = notifications.map((notification) => this.sendTestNotification(notification)); + const outcomes = await Promise.all(tasks); + const succeeded = outcomes.filter(Boolean).length; + const failed = outcomes.length - succeeded; + if (failed > 0) { + return false; + } + return true; + }; } From bebb5d6942a0e3ad85d0d4cf1afb735eb4c57ffe Mon Sep 17 00:00:00 2001 From: Alex Holliday Date: Mon, 19 Jan 2026 21:58:48 +0000 Subject: [PATCH 14/16] inject logger --- server/src/config/services.ts | 12 +++---- .../SuperSimpleQueueHelper.ts | 1 - .../notificationProviders/discord.ts | 23 +++++++++++-- .../notificationProviders/email.ts | 4 ++- .../notificationProviders/matrix.ts | 34 +++++++++++++++++-- .../notificationProviders/pagerduty.ts | 31 +++++++++++++++-- .../notificationProviders/slack.ts | 5 +++ .../notificationProviders/webhook.ts | 32 ++++++++++++++--- .../infrastructure/notificationsService.ts | 1 + 9 files changed, 125 insertions(+), 18 deletions(-) diff --git a/server/src/config/services.ts b/server/src/config/services.ts index 035a0782f..953fc5a1a 100644 --- a/server/src/config/services.ts +++ b/server/src/config/services.ts @@ -228,12 +228,12 @@ export const initializeServices = async ({ settingsService, }); - const webhookProvider = new WebhookProvider(); - const slackProvider = new SlackProvider(); - const emailProvider = new EmailProvider(emailService); - const discordProvider = new DiscordProvider(); - const pagerDutyProvider = new PagerDutyProvider(); - const matrixProvider = new MatrixProvider(); + const webhookProvider = new WebhookProvider(logger); + const slackProvider = new SlackProvider(logger); + const emailProvider = new EmailProvider(emailService, logger); + const discordProvider = new DiscordProvider(logger); + const pagerDutyProvider = new PagerDutyProvider(logger); + const matrixProvider = new MatrixProvider(logger); const notificationsService = new NotificationsService( notificationsRepository, diff --git a/server/src/service/infrastructure/SuperSimpleQueue/SuperSimpleQueueHelper.ts b/server/src/service/infrastructure/SuperSimpleQueue/SuperSimpleQueueHelper.ts index ebb0949b0..bbd3a11f0 100644 --- a/server/src/service/infrastructure/SuperSimpleQueue/SuperSimpleQueueHelper.ts +++ b/server/src/service/infrastructure/SuperSimpleQueue/SuperSimpleQueueHelper.ts @@ -18,7 +18,6 @@ class SuperSimpleQueueHelper { logger, networkService, statusService, - notificationService, notificationsService, checkService, buffer, diff --git a/server/src/service/infrastructure/notificationProviders/discord.ts b/server/src/service/infrastructure/notificationProviders/discord.ts index 5aa8dd1d4..83d8e4ec2 100644 --- a/server/src/service/infrastructure/notificationProviders/discord.ts +++ b/server/src/service/infrastructure/notificationProviders/discord.ts @@ -1,9 +1,10 @@ import type { Monitor, Notification, MonitorStatusResponse } from "@/types/index.js"; import { INotificationProvider } from "@/service/index.js"; -import { buildHardwareAlerts, buildDiscordBody } from "@/service/infrastructure/notificationProviders/utils.js"; +import { buildHardwareAlerts, buildDiscordBody, getTestMessage } from "@/service/infrastructure/notificationProviders/utils.js"; import got from "got"; export class DiscordProvider implements INotificationProvider { + constructor(private logger: any) {} private getHardwareContent = (monitor: Monitor, monitorStatusResponse: MonitorStatusResponse) => { const { discordPayload } = buildHardwareAlerts("HOST_PLACEHOLDER", monitor, monitorStatusResponse); return discordPayload; @@ -22,6 +23,7 @@ export class DiscordProvider implements INotificationProvider { } try { + this.logger?.debug?.("Sending Discord alert", { address: notification.address }); await got.post(notification.address, { json: body, headers: { @@ -31,10 +33,27 @@ export class DiscordProvider implements INotificationProvider { }); return true; } catch (error: any) { + this.logger?.error?.("Discord alert failed", { error }); return false; } }; sendTestAlert = async (notification: Notification) => { - return false; + if (!notification.address) { + return false; + } + try { + this.logger?.debug?.("Sending Discord test alert", { address: notification.address }); + await got.post(notification.address, { + json: { content: getTestMessage() }, + headers: { + "Content-Type": "application/json", + }, + responseType: "json", + }); + return true; + } catch (error) { + this.logger?.error?.("Discord test alert failed", { error }); + return false; + } }; } diff --git a/server/src/service/infrastructure/notificationProviders/email.ts b/server/src/service/infrastructure/notificationProviders/email.ts index 0a405a078..344285e9c 100644 --- a/server/src/service/infrastructure/notificationProviders/email.ts +++ b/server/src/service/infrastructure/notificationProviders/email.ts @@ -4,7 +4,7 @@ import { buildHardwareAlerts, buildHardwareEmail, buildEmail, buildTestEmail } f export class EmailProvider implements INotificationProvider { private emailService: any; - constructor(emailService: any) { + constructor(emailService: any, private logger: any) { this.emailService = emailService; } @@ -27,6 +27,7 @@ export class EmailProvider implements INotificationProvider { return false; } + this.logger?.debug?.("Sending Email alert", { address: notification.address }); const messageId = await this.emailService.sendEmail(notification.address, subject, html); if (!messageId) return false; return true; @@ -35,6 +36,7 @@ export class EmailProvider implements INotificationProvider { async sendTestAlert(notification: Notification): Promise { const subject = "Test notification"; const html = await buildTestEmail(this.emailService); + this.logger?.debug?.("Sending Email test alert", { address: notification.address }); const messageId = await this.emailService.sendEmail(notification.address, subject, html); if (!messageId) return false; return true; diff --git a/server/src/service/infrastructure/notificationProviders/matrix.ts b/server/src/service/infrastructure/notificationProviders/matrix.ts index 8985ac10b..4592500a5 100644 --- a/server/src/service/infrastructure/notificationProviders/matrix.ts +++ b/server/src/service/infrastructure/notificationProviders/matrix.ts @@ -1,9 +1,15 @@ import got from "got"; import type { INotificationProvider } from "@/service/index.js"; import type { Notification, Monitor, MonitorStatusResponse } from "@/types/index.js"; -import { buildHardwareAlerts, buildHardwareWebhookBody, buildWebhookBody } from "@/service/infrastructure/notificationProviders/utils.js"; +import { + buildHardwareAlerts, + buildHardwareWebhookBody, + buildWebhookBody, + getTestMessage, +} from "@/service/infrastructure/notificationProviders/utils.js"; export class MatrixProvider implements INotificationProvider { + constructor(private logger: any) {} private getHardwareContent = (monitor: Monitor, monitorStatusResponse: MonitorStatusResponse) => { const { alertsToSend } = buildHardwareAlerts("HOST_PLACEHOLDER", monitor, monitorStatusResponse); const body = buildHardwareWebhookBody(alertsToSend, monitor); @@ -37,6 +43,7 @@ export class MatrixProvider implements INotificationProvider { formatted_body: formattedMessage, }; try { + this.logger?.debug?.("Sending Matrix alert", { room: roomId }); await got.post(url, { json: body, headers: { @@ -46,11 +53,34 @@ export class MatrixProvider implements INotificationProvider { }); return true; } catch (error) { + this.logger?.error?.("Matrix alert failed", { error }); return false; } }; sendTestAlert = async (notification: Notification) => { - return false; + const { homeserverUrl, accessToken, roomId } = notification; + if (!homeserverUrl || !accessToken || !roomId) { + return false; + } + const url = `${homeserverUrl}/_matrix/client/v3/rooms/${roomId}/send/m.room.message?access_token=${accessToken}`; + const body = { + msgtype: "m.text", + body: getTestMessage(), + }; + try { + this.logger?.debug?.("Sending Matrix test alert", { room: roomId }); + await got.post(url, { + json: body, + headers: { + "Content-Type": "application/json", + }, + responseType: "json", + }); + return true; + } catch (error) { + this.logger?.error?.("Matrix test alert failed", { error }); + return false; + } }; } diff --git a/server/src/service/infrastructure/notificationProviders/pagerduty.ts b/server/src/service/infrastructure/notificationProviders/pagerduty.ts index d0309550b..2d85cd4d6 100644 --- a/server/src/service/infrastructure/notificationProviders/pagerduty.ts +++ b/server/src/service/infrastructure/notificationProviders/pagerduty.ts @@ -1,9 +1,15 @@ import got from "got"; import type { Monitor, Notification, MonitorStatusResponse } from "@/types/index.js"; import { INotificationProvider } from "@/service/index.js"; -import { buildHardwareAlerts, buildHardwareWebhookBody, buildWebhookBody } from "@/service/infrastructure/notificationProviders/utils.js"; +import { + buildHardwareAlerts, + buildHardwareWebhookBody, + buildWebhookBody, + getTestMessage, +} from "@/service/infrastructure/notificationProviders/utils.js"; export class PagerDutyProvider implements INotificationProvider { + constructor(private logger: any) {} private getHardwareContent = (monitor: Monitor, monitorStatusResponse: MonitorStatusResponse) => { const { alertsToSend } = buildHardwareAlerts("HOST_PLACEHOLDER", monitor, monitorStatusResponse); const body = buildHardwareWebhookBody(alertsToSend, monitor); @@ -24,6 +30,7 @@ export class PagerDutyProvider implements INotificationProvider { } try { + this.logger?.debug?.("Sending PagerDuty alert", { routingKey: notification.address }); await got.post("https://events.pagerduty.com/v2/enqueue", { json: { routing_key: notification.address, @@ -40,11 +47,31 @@ export class PagerDutyProvider implements INotificationProvider { return true; } catch (error) { + this.logger?.error?.("PagerDuty alert failed", { error }); return false; } } async sendTestAlert(notification: Notification): Promise { - return false; + try { + this.logger?.debug?.("Sending PagerDuty test alert", { routingKey: notification.address }); + await got.post("https://events.pagerduty.com/v2/enqueue", { + json: { + routing_key: notification.address, + event_action: "trigger", + payload: { + summary: getTestMessage(), + severity: "info", + source: "checkmate", + timestamp: new Date().toISOString(), + }, + }, + responseType: "json", + }); + return true; + } catch (error) { + this.logger?.error?.("PagerDuty test alert failed", { error }); + return false; + } } } diff --git a/server/src/service/infrastructure/notificationProviders/slack.ts b/server/src/service/infrastructure/notificationProviders/slack.ts index c5745ce05..947048e4c 100644 --- a/server/src/service/infrastructure/notificationProviders/slack.ts +++ b/server/src/service/infrastructure/notificationProviders/slack.ts @@ -9,6 +9,7 @@ import { import got from "got"; export class SlackProvider implements INotificationProvider { + constructor(private logger: any) {} private getHardwareContent = (monitor: Monitor, monitorStatusResponse: MonitorStatusResponse) => { const { alertsToSend } = buildHardwareAlerts("HOST_PLACEHOLDER", monitor, monitorStatusResponse); const body = buildHardwareWebhookBody(alertsToSend, monitor); @@ -33,6 +34,7 @@ export class SlackProvider implements INotificationProvider { } try { + this.logger?.debug?.("Sending Slack alert", { address: notification.address }); await got.post(notification.address, { json: { text: body }, headers: { @@ -42,6 +44,7 @@ export class SlackProvider implements INotificationProvider { }); return true; } catch (error) { + this.logger?.error?.("Slack alert failed", { error }); return false; } } @@ -52,6 +55,7 @@ export class SlackProvider implements INotificationProvider { } try { + this.logger?.debug?.("Sending Slack test alert", { address: notification.address }); await got.post(notification.address, { json: { text: getTestMessage() }, headers: { @@ -61,6 +65,7 @@ export class SlackProvider implements INotificationProvider { }); return true; } catch (error) { + this.logger?.error?.("Slack test alert failed", { error }); return false; } } diff --git a/server/src/service/infrastructure/notificationProviders/webhook.ts b/server/src/service/infrastructure/notificationProviders/webhook.ts index 4d102c2b7..3976fb8e5 100644 --- a/server/src/service/infrastructure/notificationProviders/webhook.ts +++ b/server/src/service/infrastructure/notificationProviders/webhook.ts @@ -1,16 +1,22 @@ 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 { + buildHardwareAlerts, + buildHardwareWebhookBody, + buildWebhookBody, + getTestMessage, +} from "@/service/infrastructure/notificationProviders/utils.js"; import got from "got"; export class WebhookProvider implements INotificationProvider { + constructor(private logger: any) {} private getHardwareContent = (monitor: Monitor, monitorStatusResponse: MonitorStatusResponse) => { const { alertsToSend } = buildHardwareAlerts("HOST_PLACEHOLDER", monitor, monitorStatusResponse); const body = buildHardwareWebhookBody(alertsToSend, monitor); return body; }; - private getContent = (monitor: Monitor, monitorStatusResponse: MonitorStatusResponse) => { + private getContent = (monitor: Monitor, monitorStatusResponse: MonitorStatusResponse): string => { const body = buildWebhookBody(monitor, monitorStatusResponse); return body; }; @@ -28,8 +34,9 @@ export class WebhookProvider implements INotificationProvider { } try { + this.logger?.debug?.("Sending webhook alert", { address: notification.address }); await got.post(notification.address, { - json: body, + json: { text: body }, headers: { "Content-Type": "application/json", }, @@ -37,11 +44,28 @@ export class WebhookProvider implements INotificationProvider { }); return true; } catch (error) { + this.logger?.error?.("Webhook alert failed", { error }); return false; } }; sendTestAlert = async (notification: Notification) => { - return false; + if (!notification.address) { + return false; + } + try { + this.logger?.debug?.("Sending webhook test alert", { address: notification.address }); + await got.post(notification.address, { + json: { text: getTestMessage() }, + headers: { + "Content-Type": "application/json", + }, + responseType: "json", + }); + return true; + } catch (error) { + this.logger?.error?.("Webhook test alert failed", { error }); + return false; + } }; } diff --git a/server/src/service/infrastructure/notificationsService.ts b/server/src/service/infrastructure/notificationsService.ts index 7ca52945e..ad27fe99a 100644 --- a/server/src/service/infrastructure/notificationsService.ts +++ b/server/src/service/infrastructure/notificationsService.ts @@ -140,6 +140,7 @@ export class NotificationsService implements INotificationsService { const notifications = await this.notificationsRepository.findNotificationsByIds(notificationIds); const tasks = notifications.map((notification) => this.sendTestNotification(notification)); const outcomes = await Promise.all(tasks); + console.log(outcomes); const succeeded = outcomes.filter(Boolean).length; const failed = outcomes.length - succeeded; if (failed > 0) { From 5f17520ba0e7cbda174f8bfa3ed595effec64c24 Mon Sep 17 00:00:00 2001 From: Alex Holliday Date: Mon, 19 Jan 2026 22:16:33 +0000 Subject: [PATCH 15/16] logging --- .../notificationProviders/discord.ts | 29 ++++++++++++++----- .../notificationProviders/email.ts | 27 +++++++++++++---- .../notificationProviders/matrix.ts | 27 ++++++++++++----- .../notificationProviders/pagerduty.ts | 26 +++++++++++++---- .../notificationProviders/slack.ts | 29 ++++++++++++++----- .../notificationProviders/webhook.ts | 27 ++++++++++++----- .../infrastructure/notificationsService.ts | 1 - 7 files changed, 124 insertions(+), 42 deletions(-) diff --git a/server/src/service/infrastructure/notificationProviders/discord.ts b/server/src/service/infrastructure/notificationProviders/discord.ts index 83d8e4ec2..ad1f2f28a 100644 --- a/server/src/service/infrastructure/notificationProviders/discord.ts +++ b/server/src/service/infrastructure/notificationProviders/discord.ts @@ -1,10 +1,15 @@ +const SERVICE_NAME = "DiscordProvider"; import type { Monitor, Notification, MonitorStatusResponse } from "@/types/index.js"; import { INotificationProvider } from "@/service/index.js"; import { buildHardwareAlerts, buildDiscordBody, getTestMessage } from "@/service/infrastructure/notificationProviders/utils.js"; import got from "got"; export class DiscordProvider implements INotificationProvider { - constructor(private logger: any) {} + private logger: any; + + constructor(logger: any) { + this.logger = logger; + } private getHardwareContent = (monitor: Monitor, monitorStatusResponse: MonitorStatusResponse) => { const { discordPayload } = buildHardwareAlerts("HOST_PLACEHOLDER", monitor, monitorStatusResponse); return discordPayload; @@ -23,17 +28,21 @@ export class DiscordProvider implements INotificationProvider { } try { - this.logger?.debug?.("Sending Discord alert", { address: notification.address }); await got.post(notification.address, { json: body, headers: { "Content-Type": "application/json", }, - responseType: "json", }); return true; - } catch (error: any) { - this.logger?.error?.("Discord alert failed", { error }); + } catch (error) { + const err = error as Error; + this.logger.warn({ + message: "Discord alert failed", + service: SERVICE_NAME, + method: "sendAlert", + stack: err?.stack, + }); return false; } }; @@ -42,17 +51,21 @@ export class DiscordProvider implements INotificationProvider { return false; } try { - this.logger?.debug?.("Sending Discord test alert", { address: notification.address }); await got.post(notification.address, { json: { content: getTestMessage() }, headers: { "Content-Type": "application/json", }, - responseType: "json", }); return true; } catch (error) { - this.logger?.error?.("Discord test alert failed", { error }); + const err = error as Error; + this.logger.warn({ + message: "Discord test alert failed", + service: SERVICE_NAME, + method: "sendTestAlert", + stack: err?.stack, + }); return false; } }; diff --git a/server/src/service/infrastructure/notificationProviders/email.ts b/server/src/service/infrastructure/notificationProviders/email.ts index 344285e9c..16e48f5c1 100644 --- a/server/src/service/infrastructure/notificationProviders/email.ts +++ b/server/src/service/infrastructure/notificationProviders/email.ts @@ -1,11 +1,15 @@ +const SERVICE_NAME = "EmailProvider"; import type { Monitor, Notification, MonitorStatusResponse } from "@/types/index.js"; import { INotificationProvider } from "@/service/index.js"; import { buildHardwareAlerts, buildHardwareEmail, buildEmail, buildTestEmail } from "@/service/infrastructure/notificationProviders/utils.js"; export class EmailProvider implements INotificationProvider { private emailService: any; - constructor(emailService: any, private logger: any) { + private logger: any; + + constructor(emailService: any, logger: any) { this.emailService = emailService; + this.logger = logger; } private buildHardwareEmail = (monitor: Monitor, monitorStatusResponse: MonitorStatusResponse) => { @@ -27,18 +31,31 @@ export class EmailProvider implements INotificationProvider { return false; } - this.logger?.debug?.("Sending Email alert", { address: notification.address }); const messageId = await this.emailService.sendEmail(notification.address, subject, html); - if (!messageId) return false; + if (!messageId) { + this.logger.warn({ + message: "Email alert failed", + service: SERVICE_NAME, + method: "sendAlert", + }); + return false; + } return true; } async sendTestAlert(notification: Notification): Promise { const subject = "Test notification"; const html = await buildTestEmail(this.emailService); - this.logger?.debug?.("Sending Email test alert", { address: notification.address }); + const messageId = await this.emailService.sendEmail(notification.address, subject, html); - if (!messageId) return false; + if (!messageId) { + this.logger.warn({ + message: "Email test alert failed", + service: SERVICE_NAME, + method: "sendTestAlert", + }); + return false; + } return true; } } diff --git a/server/src/service/infrastructure/notificationProviders/matrix.ts b/server/src/service/infrastructure/notificationProviders/matrix.ts index 4592500a5..8914dbf80 100644 --- a/server/src/service/infrastructure/notificationProviders/matrix.ts +++ b/server/src/service/infrastructure/notificationProviders/matrix.ts @@ -1,3 +1,4 @@ +const SERVICE_NAME = "MatrixProvider"; import got from "got"; import type { INotificationProvider } from "@/service/index.js"; import type { Notification, Monitor, MonitorStatusResponse } from "@/types/index.js"; @@ -9,7 +10,11 @@ import { } from "@/service/infrastructure/notificationProviders/utils.js"; export class MatrixProvider implements INotificationProvider { - constructor(private logger: any) {} + private logger: any; + + constructor(logger: any) { + this.logger = logger; + } private getHardwareContent = (monitor: Monitor, monitorStatusResponse: MonitorStatusResponse) => { const { alertsToSend } = buildHardwareAlerts("HOST_PLACEHOLDER", monitor, monitorStatusResponse); const body = buildHardwareWebhookBody(alertsToSend, monitor); @@ -43,17 +48,21 @@ export class MatrixProvider implements INotificationProvider { formatted_body: formattedMessage, }; try { - this.logger?.debug?.("Sending Matrix alert", { room: roomId }); await got.post(url, { json: body, headers: { "Content-Type": "application/json", }, - responseType: "json", }); return true; } catch (error) { - this.logger?.error?.("Matrix alert failed", { error }); + const err = error as Error; + this.logger.warn({ + message: "Matrix alert failed", + service: SERVICE_NAME, + method: "sendAlert", + stack: err?.stack, + }); return false; } }; @@ -69,17 +78,21 @@ export class MatrixProvider implements INotificationProvider { body: getTestMessage(), }; try { - this.logger?.debug?.("Sending Matrix test alert", { room: roomId }); await got.post(url, { json: body, headers: { "Content-Type": "application/json", }, - responseType: "json", }); return true; } catch (error) { - this.logger?.error?.("Matrix test alert failed", { error }); + const err = error as Error; + this.logger.warn({ + message: "Matrix test alert failed", + service: SERVICE_NAME, + method: "sendTestAlert", + stack: err?.stack, + }); return false; } }; diff --git a/server/src/service/infrastructure/notificationProviders/pagerduty.ts b/server/src/service/infrastructure/notificationProviders/pagerduty.ts index 2d85cd4d6..f65e50c68 100644 --- a/server/src/service/infrastructure/notificationProviders/pagerduty.ts +++ b/server/src/service/infrastructure/notificationProviders/pagerduty.ts @@ -1,3 +1,4 @@ +const SERVICE_NAME = "PagerDutyProvider"; import got from "got"; import type { Monitor, Notification, MonitorStatusResponse } from "@/types/index.js"; import { INotificationProvider } from "@/service/index.js"; @@ -9,7 +10,11 @@ import { } from "@/service/infrastructure/notificationProviders/utils.js"; export class PagerDutyProvider implements INotificationProvider { - constructor(private logger: any) {} + private logger: any; + + constructor(logger: any) { + this.logger = logger; + } private getHardwareContent = (monitor: Monitor, monitorStatusResponse: MonitorStatusResponse) => { const { alertsToSend } = buildHardwareAlerts("HOST_PLACEHOLDER", monitor, monitorStatusResponse); const body = buildHardwareWebhookBody(alertsToSend, monitor); @@ -30,7 +35,6 @@ export class PagerDutyProvider implements INotificationProvider { } try { - this.logger?.debug?.("Sending PagerDuty alert", { routingKey: notification.address }); await got.post("https://events.pagerduty.com/v2/enqueue", { json: { routing_key: notification.address, @@ -42,19 +46,23 @@ export class PagerDutyProvider implements INotificationProvider { timestamp: new Date().toISOString(), }, }, - responseType: "json", }); return true; } catch (error) { - this.logger?.error?.("PagerDuty alert failed", { error }); + const err = error as Error; + this.logger.warn({ + message: "PagerDuty alert failed", + service: SERVICE_NAME, + method: "sendAlert", + stack: err?.stack, + }); return false; } } async sendTestAlert(notification: Notification): Promise { try { - this.logger?.debug?.("Sending PagerDuty test alert", { routingKey: notification.address }); await got.post("https://events.pagerduty.com/v2/enqueue", { json: { routing_key: notification.address, @@ -70,7 +78,13 @@ export class PagerDutyProvider implements INotificationProvider { }); return true; } catch (error) { - this.logger?.error?.("PagerDuty test alert failed", { error }); + const err = error as Error; + this.logger.warn({ + message: "PagerDuty test alert failed", + service: SERVICE_NAME, + method: "sendTestAlert", + stack: err?.stack, + }); return false; } } diff --git a/server/src/service/infrastructure/notificationProviders/slack.ts b/server/src/service/infrastructure/notificationProviders/slack.ts index 947048e4c..2aeb442eb 100644 --- a/server/src/service/infrastructure/notificationProviders/slack.ts +++ b/server/src/service/infrastructure/notificationProviders/slack.ts @@ -1,3 +1,4 @@ +const SERVICE_NAME = "SlackProvider"; import type { Monitor, Notification, MonitorStatusResponse } from "@/types/index.js"; import { INotificationProvider } from "@/service/index.js"; import { @@ -6,10 +7,14 @@ import { buildWebhookBody, getTestMessage, } from "@/service/infrastructure/notificationProviders/utils.js"; -import got from "got"; +import got, { HTTPError } from "got"; export class SlackProvider implements INotificationProvider { - constructor(private logger: any) {} + private logger: any; + + constructor(logger: any) { + this.logger = logger; + } private getHardwareContent = (monitor: Monitor, monitorStatusResponse: MonitorStatusResponse) => { const { alertsToSend } = buildHardwareAlerts("HOST_PLACEHOLDER", monitor, monitorStatusResponse); const body = buildHardwareWebhookBody(alertsToSend, monitor); @@ -34,17 +39,21 @@ export class SlackProvider implements INotificationProvider { } try { - this.logger?.debug?.("Sending Slack alert", { address: notification.address }); await got.post(notification.address, { json: { text: body }, headers: { "Content-Type": "application/json", }, - responseType: "json", }); return true; } catch (error) { - this.logger?.error?.("Slack alert failed", { error }); + const err = error as Error; + this.logger.warn({ + message: "Slack alert failed", + service: SERVICE_NAME, + method: "sendAlert", + stack: err?.stack, + }); return false; } } @@ -55,17 +64,21 @@ export class SlackProvider implements INotificationProvider { } try { - this.logger?.debug?.("Sending Slack test alert", { address: notification.address }); await got.post(notification.address, { json: { text: getTestMessage() }, headers: { "Content-Type": "application/json", }, - responseType: "json", }); return true; } catch (error) { - this.logger?.error?.("Slack test alert failed", { error }); + const err = error as HTTPError; + this.logger.warn({ + message: "Slack test alert failed", + service: SERVICE_NAME, + method: "sendTestAlert", + stack: err?.stack, + }); return false; } } diff --git a/server/src/service/infrastructure/notificationProviders/webhook.ts b/server/src/service/infrastructure/notificationProviders/webhook.ts index 3976fb8e5..9e743ae53 100644 --- a/server/src/service/infrastructure/notificationProviders/webhook.ts +++ b/server/src/service/infrastructure/notificationProviders/webhook.ts @@ -1,3 +1,4 @@ +const SERVICE_NAME = "WebhookProvider"; import type { Monitor, Alert, Notification, MonitorStatusResponse } from "@/types/index.js"; import { INotificationProvider } from "@/service/index.js"; import { @@ -9,7 +10,11 @@ import { import got from "got"; export class WebhookProvider implements INotificationProvider { - constructor(private logger: any) {} + private logger: any; + + constructor(logger: any) { + this.logger = logger; + } private getHardwareContent = (monitor: Monitor, monitorStatusResponse: MonitorStatusResponse) => { const { alertsToSend } = buildHardwareAlerts("HOST_PLACEHOLDER", monitor, monitorStatusResponse); const body = buildHardwareWebhookBody(alertsToSend, monitor); @@ -34,17 +39,21 @@ export class WebhookProvider implements INotificationProvider { } try { - this.logger?.debug?.("Sending webhook alert", { address: notification.address }); await got.post(notification.address, { json: { text: body }, headers: { "Content-Type": "application/json", }, - responseType: "json", }); return true; } catch (error) { - this.logger?.error?.("Webhook alert failed", { error }); + const err = error as Error; + this.logger.warn({ + message: "Webhook alert failed", + service: SERVICE_NAME, + method: "sendAlert", + stack: err?.stack, + }); return false; } }; @@ -54,17 +63,21 @@ export class WebhookProvider implements INotificationProvider { return false; } try { - this.logger?.debug?.("Sending webhook test alert", { address: notification.address }); await got.post(notification.address, { json: { text: getTestMessage() }, headers: { "Content-Type": "application/json", }, - responseType: "json", }); return true; } catch (error) { - this.logger?.error?.("Webhook test alert failed", { error }); + const err = error as Error; + this.logger.warn({ + message: "Webhook test alert failed", + service: SERVICE_NAME, + method: "sendTestAlert", + stack: err?.stack, + }); return false; } }; diff --git a/server/src/service/infrastructure/notificationsService.ts b/server/src/service/infrastructure/notificationsService.ts index ad27fe99a..7ca52945e 100644 --- a/server/src/service/infrastructure/notificationsService.ts +++ b/server/src/service/infrastructure/notificationsService.ts @@ -140,7 +140,6 @@ export class NotificationsService implements INotificationsService { const notifications = await this.notificationsRepository.findNotificationsByIds(notificationIds); const tasks = notifications.map((notification) => this.sendTestNotification(notification)); const outcomes = await Promise.all(tasks); - console.log(outcomes); const succeeded = outcomes.filter(Boolean).length; const failed = outcomes.length - succeeded; if (failed > 0) { From dde54a4e4540a9b057780779c617322cba3fee10 Mon Sep 17 00:00:00 2001 From: Alex Holliday Date: Mon, 19 Jan 2026 22:18:17 +0000 Subject: [PATCH 16/16] remove old service --- server/src/config/services.ts | 7 - .../infrastructure/notificationService.js | 128 ----------- .../infrastructure/notificationUtils.js | 205 ------------------ 3 files changed, 340 deletions(-) delete mode 100644 server/src/service/infrastructure/notificationService.js delete mode 100644 server/src/service/infrastructure/notificationUtils.js diff --git a/server/src/config/services.ts b/server/src/config/services.ts index 953fc5a1a..9ae5ecb12 100644 --- a/server/src/config/services.ts +++ b/server/src/config/services.ts @@ -5,7 +5,6 @@ import MongoDB from "../db/MongoDB.js"; import NetworkService from "../service/infrastructure/networkService.js"; import EmailService from "../service/infrastructure/emailService.js"; import BufferService from "../service/infrastructure/bufferService.js"; -import NotificationUtils from "../service/infrastructure/notificationUtils.js"; import { NotificationsService, StatusService, @@ -222,12 +221,6 @@ export const initializeServices = async ({ const statusService = new StatusService({ db, logger, buffer: bufferService, monitorsRepository }); - const notificationUtils = new NotificationUtils({ - stringService, - emailService, - settingsService, - }); - const webhookProvider = new WebhookProvider(logger); const slackProvider = new SlackProvider(logger); const emailProvider = new EmailProvider(emailService, logger); diff --git a/server/src/service/infrastructure/notificationService.js b/server/src/service/infrastructure/notificationService.js deleted file mode 100644 index 8c6fb063f..000000000 --- a/server/src/service/infrastructure/notificationService.js +++ /dev/null @@ -1,128 +0,0 @@ -const SERVICE_NAME = "NotificationService"; -import Matrix from "./notificationProviders/matrix.js"; - -class NotificationService { - static SERVICE_NAME = SERVICE_NAME; - - constructor({ emailService, db, logger, networkService, stringService, notificationUtils }) { - this.emailService = emailService; - this.db = db; - this.logger = logger; - this.networkService = networkService; - this.stringService = stringService; - this.notificationUtils = notificationUtils; - } - - get serviceName() { - return NotificationService.SERVICE_NAME; - } - - sendNotification = async ({ notification, subject, content, html, discordContent = null, webhookBody = null }) => { - const { type, address } = notification; - - if (type === "email") { - const messageId = await this.emailService.sendEmail(address, subject, html); - if (!messageId) return false; - return true; - } - - // Create a body for webhooks - let body = { text: content }; - if (type === "discord") { - body = !discordContent ? { content } : discordContent; - } - if (type === "webhook") { - body = !webhookBody ? { content } : webhookBody; - } - if (type === "slack" || type === "discord" || type === "webhook") { - const response = await this.networkService.requestWebhook(type, address, body); - return response.status; - } - if (type === "pager_duty") { - const response = await this.networkService.requestPagerDuty({ - message: content, - monitorUrl: subject, - routingKey: address, - }); - - return response; - } - if (type === "matrix") { - const { friendlyName, homeserverUrl, accessToken, roomId } = notification; - const monitorName = subject; - const message = content; - const matrix = new Matrix({ networkService: this.networkService, logger: this.logger }); - const success = await matrix.send({ friendlyName, homeserverUrl, accessToken, roomId, message, monitorName }); - return success; - } - }; - - async handleNotifications(networkResponse) { - const { monitor, statusChanged, prevStatus } = networkResponse; - const { type } = monitor; - if (type !== "hardware" && statusChanged === false) return false; - // if prevStatus is undefined, monitor is resuming, we're done - if (type !== "hardware" && prevStatus === undefined) return false; - - const notificationIDs = networkResponse.monitor?.notifications ?? []; - if (notificationIDs.length === 0) return false; - if (networkResponse.monitor.type === "hardware") { - const thresholds = networkResponse?.monitor?.thresholds; - - if (thresholds === undefined) return false; // No thresholds set, we're done - const metrics = networkResponse?.payload?.data ?? null; - if (metrics === null) return false; // No metrics, we're done - - const [alerts, discordContent] = await this.notificationUtils.buildHardwareAlerts(networkResponse); - if (alerts.length === 0) return false; - - 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); - const success = await this.notifyAll({ notificationIDs, subject, html, content, discordContent, webhookBody }); - return success; - } - - // Status monitors - const { subject, html } = await this.notificationUtils.buildStatusEmail(networkResponse); - const [content, discordContent] = await this.notificationUtils.buildWebhookMessage(networkResponse); - const success = this.notifyAll({ notificationIDs, subject, html, content, discordContent }); - return success; - } - - async notifyAll({ notificationIDs, subject, html, content, discordContent = null, webhookBody = null }) { - const notifications = await this.db.notificationModule.getNotificationsByIds(notificationIDs); - // Map each notification to a test promise - const promises = notifications.map(async (notification) => { - try { - await this.sendNotification({ notification, subject, content, html, discordContent, webhookBody }); - return true; - } catch (err) { - return false; - } - }); - - const results = await Promise.all(promises); - return results.every((r) => r === true); - } - - async getTestNotification() { - const html = await this.notificationUtils.buildTestEmail(); - const content = "This is a test notification"; - const subject = "Test Notification"; - return { subject, html, content }; - } - - async testAllNotifications(notificationIDs) { - const { subject, html, content } = await this.getTestNotification(); - return this.notifyAll({ notificationIDs, subject, html, content }); - } - - async sendTestNotification(notification) { - const { subject, html, content } = await this.getTestNotification(); - const success = await this.sendNotification({ notification, subject, content, html }); - return success; - } -} - -export default NotificationService; diff --git a/server/src/service/infrastructure/notificationUtils.js b/server/src/service/infrastructure/notificationUtils.js deleted file mode 100644 index 106c8f349..000000000 --- a/server/src/service/infrastructure/notificationUtils.js +++ /dev/null @@ -1,205 +0,0 @@ -const SERVICE_NAME = "NotificationUtils"; - -class NotificationUtils { - static SERVICE_NAME = SERVICE_NAME; - - constructor({ stringService, emailService, settingsService }) { - this.stringService = stringService; - this.emailService = emailService; - this.settingsService = settingsService; - } - - get serviceName() { - return NotificationUtils.SERVICE_NAME; - } - - buildTestEmail = async () => { - const context = { testName: "Monitoring System" }; - const html = await this.emailService.buildEmail("testEmailTemplate", context); - return html; - }; - - buildStatusEmail = async (networkResponse) => { - const { monitor, status, prevStatus } = networkResponse; - const template = prevStatus === false ? "serverIsUpTemplate" : "serverIsDownTemplate"; - const context = { monitor: monitor.name, url: monitor.url }; - const subject = `Monitor ${monitor.name} is ${status === true ? "up" : "down"}`; - const html = await this.emailService.buildEmail(template, context); - return { subject, html }; - }; - - buildWebhookMessage = (networkResponse) => { - const { monitor, status, code, timestamp } = networkResponse; - // Format timestamp using the local system timezone - const formatTime = (timestamp) => { - const date = new Date(timestamp); - - // Get timezone abbreviation and format the date - const timeZoneAbbr = date.toLocaleTimeString("en-US", { timeZoneName: "short" }).split(" ").pop(); - - // Format the date with readable format - return ( - date - .toLocaleString("en-US", { - year: "numeric", - month: "2-digit", - day: "2-digit", - hour: "2-digit", - minute: "2-digit", - second: "2-digit", - hour12: false, - }) - .replace(/(\d+)\/(\d+)\/(\d+),\s/, "$3-$1-$2 ") + - " " + - timeZoneAbbr - ); - }; - - // Get formatted time - const formattedTime = timestamp ? formatTime(timestamp) : formatTime(new Date().getTime()); - - // Create different messages based on status with extra spacing - let messageText; - if (status === true) { - messageText = this.stringService.monitorUpAlert - .replace("{monitorName}", monitor.name) - .replace("{time}", formattedTime) - .replace("{code}", code || "Unknown"); - } else { - messageText = this.stringService.monitorDownAlert - .replace("{monitorName}", monitor.name) - .replace("{time}", formattedTime) - .replace("{code}", code || "Unknown"); - } - const dn = this.stringService.discordNotification; - let discordMessageText = { - embeds: [ - { - title: status - ? dn.uptimeAlert.replace("{monitorName}", monitor?.name ?? dn.unknown) - : dn.downtimeAlert.replace("{monitorName}", monitor?.name ?? dn.unknown), - color: status ? 5763719 : 15548997, - - fields: [ - { name: dn.monitor, value: monitor?.name ?? dn.unknown, inline: true }, - { name: dn.status, value: status ? dn.up : dn.down, inline: true }, - { name: dn.statusCode, value: String(code ?? dn.unknown), inline: true }, - { name: dn.time, value: formattedTime, inline: true }, - { name: dn.url, value: monitor?.url ?? dn.unknown, inline: false }, - ], - footer: { text: dn.checkmate }, - }, - ], - }; - return [messageText, discordMessageText]; - }; - - buildHardwareAlerts = async (networkResponse) => { - const monitor = networkResponse?.monitor; - const thresholds = networkResponse?.monitor?.thresholds; - const { usage_cpu: cpuThreshold = -1, usage_memory: memoryThreshold = -1, usage_disk: diskThreshold = -1 } = thresholds; - - const metrics = networkResponse?.payload?.data; - const { cpu: { usage_percent: cpuUsage = -1 } = {}, memory: { usage_percent: memoryUsage = -1 } = {}, disk = [] } = metrics; - - const { clientHost } = this.settingsService.getSettings(); - - const alerts = { - cpu: cpuThreshold !== -1 && cpuUsage > cpuThreshold ? true : false, - memory: memoryThreshold !== -1 && memoryUsage > memoryThreshold ? true : false, - disk: disk?.some((d) => diskThreshold !== -1 && typeof d?.usage_percent === "number" && d?.usage_percent > diskThreshold) ?? false, - }; - - const alertsToSend = []; - const discordEmbeds = []; - const monitorInfoFields = [ - { name: "Monitor", value: monitor.name, inline: true }, - { name: "URL", value: monitor.url, inline: false }, - ]; - const goToIncidentField = { name: `Go to incident`, value: `${clientHost}/infrastructure/${monitor._id}` }; - const formatDiscordAlert = { - cpu: () => ({ - title: "CPU alert", - description: `Your current CPU usage (${(cpuUsage * 100).toFixed(0)}%) is above your threshold (${(cpuThreshold * 100).toFixed(0)}%)`, - color: 15548997, - fields: [...monitorInfoFields, goToIncidentField], - footer: { text: "Checkmate" }, - }), - - memory: () => ({ - title: "Memory alert", - description: `Your current memory usage (${(memoryUsage * 100).toFixed(0)}%) is above your threshold (${(memoryThreshold * 100).toFixed(0)}%)`, - color: 15548997, - fields: [...monitorInfoFields, goToIncidentField], - footer: { text: "Checkmate" }, - }), - - disk: () => ({ - title: "Disk alert", - description: `Your current disk usage is above your threshold (${(diskThreshold * 100).toFixed(0)}%)`, - color: 15548997, - footer: { text: "Checkmate" }, - fields: [ - ...monitorInfoFields, - ...(Array.isArray(disk) ? disk : []).map((d, idx) => ({ - name: `Disk ${idx}`, - value: `${(d?.usage_percent * 100).toFixed(0)}%`, - inline: true, - })), - goToIncidentField, - ], - }), - }; - const alertTypes = ["cpu", "memory", "disk"]; - for (const type of alertTypes) { - // Iterate over each alert type to see if any need to be decremented - if (alerts[type] === true) { - monitor[`${type}AlertThreshold`]--; // Decrement threshold if an alert is triggered - - if (monitor[`${type}AlertThreshold`] <= 0) { - // If threshold drops below 0, reset and send notification - monitor[`${type}AlertThreshold`] = monitor.alertThreshold; - - const formatAlert = { - cpu: () => `Your current CPU usage (${(cpuUsage * 100).toFixed(0)}%) is above your threshold (${(cpuThreshold * 100).toFixed(0)}%)`, - memory: () => - `Your current memory usage (${(memoryUsage * 100).toFixed(0)}%) is above your threshold (${(memoryThreshold * 100).toFixed(0)}%)`, - disk: () => - `Your current disk usage: ${disk - .map((d, idx) => `(Disk${idx}: ${(d.usage_percent * 100).toFixed(0)}%)`) - .join(", ")} is above your threshold (${(diskThreshold * 100).toFixed(0)}%)`, - }; - alertsToSend.push(formatAlert[type]()); - discordEmbeds.push(formatDiscordAlert[type]()); - } - } - } - await monitor.save(); - const discordPayload = discordEmbeds.length ? { embeds: discordEmbeds } : null; - return [alertsToSend, discordPayload]; - }; - - buildHardwareEmail = async (networkResponse, alerts) => { - const { monitor } = networkResponse; - const template = "hardwareIncidentTemplate"; - const context = { monitor: monitor.name, url: monitor.url, alerts }; - const subject = `Monitor ${monitor.name} infrastructure alerts`; - const html = await this.emailService.buildEmail(template, context); - return { subject, html }; - }; - - buildHardwareNotificationMessage = (alerts, monitor) => { - const { clientHost } = this.settingsService.getSettings(); - 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"); - }; - buildHardwareWebhookBody = (alerts, monitor) => { - const content = alerts.map((alert) => alert).join("\n"); - const body = { text: content, name: monitor.name, url: monitor.url }; - return body; - }; -} - -export default NotificationUtils;