This commit is contained in:
Alex Holliday committed 2026-02-17 17:28:43 +00:00
1 parent fbb0e940c6
commit 232be7a405
13 files changed
+38 -855

No files matched your search

@@ -60,10 +60,7 @@ class EmailService {
welcomeEmailTemplate: this.loadTemplate("welcomeEmail"),
employeeActivationTemplate: this.loadTemplate("employeeActivation"),
noIncidentsThisWeekTemplate: this.loadTemplate("noIncidentsThisWeek"),
serverIsDownTemplate: this.loadTemplate("serverIsDown"),
serverIsUpTemplate: this.loadTemplate("serverIsUp"),
passwordResetTemplate: this.loadTemplate("passwordReset"),
hardwareIncidentTemplate: this.loadTemplate("hardwareIncident"),
testEmailTemplate: this.loadTemplate("testEmailTemplate"),
unifiedNotificationTemplate: this.loadTemplate("unifiedNotification"),
};
@@ -1,15 +1,7 @@
import type { Monitor, Notification, Alert, MonitorStatusResponse } from "@/types/index.js";
import type { MonitorActionDecision } from "@/service/infrastructure/SuperSimpleQueue/SuperSimpleQueueHelper.js";
import type { Notification } from "@/types/index.js";
import type { NotificationMessage } from "@/types/notificationMessage.js";
export interface INotificationProvider {
sendAlert: (
notification: Notification,
monitor: Monitor,
monitorStatusResponse: MonitorStatusResponse,
decision: MonitorActionDecision,
clientHost: string
) => Promise<boolean>;
sendMessage?: (notification: Notification, message: NotificationMessage) => Promise<boolean>;
sendMessage: (notification: Notification, message: NotificationMessage) => Promise<boolean>;
sendTestAlert(notification: Notification): Promise<boolean>;
}
@@ -1,8 +1,7 @@
const SERVICE_NAME = "DiscordProvider";
import type { Monitor, Notification, MonitorStatusResponse } from "@/types/index.js";
import type { Notification } from "@/types/index.js";
import { INotificationProvider } from "@/service/index.js";
import type { MonitorActionDecision } from "@/service/infrastructure/SuperSimpleQueue/SuperSimpleQueueHelper.js";
import { buildHardwareAlerts, buildDiscordBody, getTestMessage } from "@/service/infrastructure/notificationProviders/utils.js";
import { getTestMessage } from "@/service/infrastructure/notificationProviders/utils.js";
import type { NotificationMessage } from "@/types/notificationMessage.js";
import got from "got";
@@ -12,58 +11,7 @@ export class DiscordProvider implements INotificationProvider {
constructor(logger: any) {
this.logger = logger;
}
private getHardwareContent = (
clientHost: string,
monitor: Monitor,
monitorStatusResponse: MonitorStatusResponse,
decision: MonitorActionDecision
) => {
// For status changes (recovery), use standard format
if (decision.notificationReason === "status_change") {
return buildDiscordBody(monitor, monitorStatusResponse);
}
// For threshold breaches, use hardware alert format
const { discordPayload } = buildHardwareAlerts(clientHost, monitor, monitorStatusResponse);
return discordPayload;
};
sendAlert = async (
notification: Notification,
monitor: Monitor,
monitorStatusResponse: MonitorStatusResponse,
decision: MonitorActionDecision,
clientHost: string
) => {
let body;
if (monitor.type === "hardware") {
body = this.getHardwareContent(clientHost, monitor, monitorStatusResponse, decision);
} else {
body = buildDiscordBody(monitor, monitorStatusResponse);
}
if (!notification.address) {
return false;
}
try {
await got.post(notification.address, {
json: body,
headers: {
"Content-Type": "application/json",
},
});
return true;
} catch (error) {
const err = error as Error;
this.logger.warn({
message: "Discord alert failed",
service: SERVICE_NAME,
method: "sendAlert",
stack: err?.stack,
});
return false;
}
};
sendTestAlert = async (notification: Notification) => {
if (!notification.address) {
return false;
@@ -110,11 +58,6 @@ export class DiscordProvider implements INotificationProvider {
"Content-Type": "application/json",
},
});
this.logger.info({
message: "[NEW] Discord notification sent via sendMessage",
service: SERVICE_NAME,
method: "sendMessage",
});
return true;
} catch (error) {
const err = error as Error;
@@ -2,7 +2,7 @@ const SERVICE_NAME = "EmailProvider";
import type { Monitor, Notification, MonitorStatusResponse } from "@/types/index.js";
import { INotificationProvider } from "@/service/index.js";
import type { MonitorActionDecision } from "@/service/infrastructure/SuperSimpleQueue/SuperSimpleQueueHelper.js";
import { buildHardwareAlerts, buildHardwareEmail, buildEmail, buildTestEmail } from "@/service/infrastructure/notificationProviders/utils.js";
import { buildTestEmail } from "@/service/infrastructure/notificationProviders/utils.js";
import type { NotificationMessage } from "@/types/notificationMessage.js";
export class EmailProvider implements INotificationProvider {
@@ -14,68 +14,6 @@ export class EmailProvider implements INotificationProvider {
this.logger = logger;
}
private buildHardwareEmail = async (
clientHost: string,
monitor: Monitor,
monitorStatusResponse: MonitorStatusResponse,
decision: MonitorActionDecision
) => {
// For status changes (recovery), use standard email format
if (decision.notificationReason === "status_change") {
return await buildEmail(this.emailService, monitor);
}
// For threshold breaches, use hardware alert format
const { alertsToSend } = buildHardwareAlerts(clientHost, monitor, monitorStatusResponse);
const html = buildHardwareEmail(this.emailService, monitor, alertsToSend);
return html;
};
async sendAlert(
notification: Notification,
monitor: Monitor,
monitorStatusResponse: MonitorStatusResponse,
decision: MonitorActionDecision,
clientHost: string
): Promise<boolean> {
// For grouped notifications (identified by ":" in name), customize subject to indicate multiple services.
// Example: "2 services: Service A, Service B" becomes "Alert: 2 services are down"
const isGroupedNotification = monitor.name.includes(":");
// Build subject based on notification reason and monitor status
let subject: string;
if (isGroupedNotification) {
subject = `Alert: ${monitor.name} are down`;
} else if (decision.notificationReason === "threshold_breach") {
subject = `Monitor ${monitor.name} threshold breached`;
} else if (monitor.status === "up") {
subject = `Monitor ${monitor.name} is back up`;
} else {
subject = `Monitor ${monitor.name} is down`;
}
let html;
if (monitor.type === "hardware") {
html = await this.buildHardwareEmail(clientHost, monitor, monitorStatusResponse, decision);
} else {
html = await buildEmail(this.emailService, monitor);
}
if (!notification.address) {
return false;
}
const messageId = await this.emailService.sendEmail(notification.address, subject, html);
if (!messageId) {
this.logger.warn({
message: "Email alert failed",
service: SERVICE_NAME,
method: "sendAlert",
});
return false;
}
return true;
}
async sendTestAlert(notification: Notification): Promise<boolean> {
const subject = "Test notification";
const html = await buildTestEmail(this.emailService);
@@ -103,7 +41,7 @@ export class EmailProvider implements INotificationProvider {
const messageId = await this.emailService.sendEmail(notification.address, subject, html);
if (!messageId) {
this.logger.warn({
message: "Email notification failed via sendMessage",
message: "Email notification failed",
service: SERVICE_NAME,
method: "sendMessage",
});
@@ -148,7 +86,6 @@ export class EmailProvider implements INotificationProvider {
details: { context },
});
return await this.emailService.buildEmail("unifiedNotificationTemplate", context);
const html = await this.emailService.buildEmail("unifiedNotificationTemplate", context);
this.logger.info({
@@ -1,15 +1,9 @@
const SERVICE_NAME = "MatrixProvider";
import got from "got";
import type { INotificationProvider } from "@/service/index.js";
import type { MonitorActionDecision } from "@/service/infrastructure/SuperSimpleQueue/SuperSimpleQueueHelper.js";
import type { Notification, Monitor, MonitorStatusResponse } from "@/types/index.js";
import type { Notification } from "@/types/index.js";
import type { NotificationMessage } from "@/types/notificationMessage.js";
import {
buildHardwareAlerts,
buildHardwareNotificationMessage,
buildWebhookBody,
getTestMessage,
} from "@/service/infrastructure/notificationProviders/utils.js";
import { getTestMessage } from "@/service/infrastructure/notificationProviders/utils.js";
export class MatrixProvider implements INotificationProvider {
private logger: any;
@@ -17,73 +11,6 @@ export class MatrixProvider implements INotificationProvider {
constructor(logger: any) {
this.logger = logger;
}
private getHardwareContent = (
clientHost: string,
monitor: Monitor,
monitorStatusResponse: MonitorStatusResponse,
decision: MonitorActionDecision
) => {
// For status changes (recovery), use standard format
if (decision.notificationReason === "status_change") {
return buildWebhookBody(monitor, monitorStatusResponse);
}
// For threshold breaches, use hardware alert format
const { alertsToSend } = buildHardwareAlerts(clientHost, monitor, monitorStatusResponse);
const body = buildHardwareNotificationMessage(clientHost, 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,
decision: MonitorActionDecision,
clientHost: string
) => {
const { homeserverUrl, accessToken, roomId } = notification;
let content;
if (monitor.type === "hardware") {
content = this.getHardwareContent(clientHost, monitor, monitorStatusResponse, decision);
} 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",
},
});
return true;
} catch (error) {
const err = error as Error;
this.logger.warn({
message: "Matrix alert failed",
service: SERVICE_NAME,
method: "sendAlert",
stack: err?.stack,
});
return false;
}
};
sendTestAlert = async (notification: Notification) => {
const { homeserverUrl, accessToken, roomId } = notification;
@@ -142,16 +69,11 @@ export class MatrixProvider implements INotificationProvider {
"Content-Type": "application/json",
},
});
this.logger.info({
message: "[NEW] Matrix notification sent via sendMessage",
service: SERVICE_NAME,
method: "sendMessage",
});
return true;
} catch (error) {
const err = error as Error;
this.logger.warn({
message: "[NEW] Matrix alert failed via sendMessage",
message: "Matrix notification failed",
service: SERVICE_NAME,
method: "sendMessage",
stack: err?.stack,
@@ -4,12 +4,7 @@ import type { Monitor, Notification, MonitorStatusResponse } from "@/types/index
import type { MonitorActionDecision } from "@/service/infrastructure/SuperSimpleQueue/SuperSimpleQueueHelper.js";
import { INotificationProvider } from "@/service/index.js";
import type { NotificationMessage } from "@/types/notificationMessage.js";
import {
buildHardwareAlerts,
buildHardwareNotificationMessage,
buildWebhookBody,
getTestMessage,
} from "@/service/infrastructure/notificationProviders/utils.js";
import { getTestMessage } from "@/service/infrastructure/notificationProviders/utils.js";
export class PagerDutyProvider implements INotificationProvider {
private logger: any;
@@ -17,67 +12,6 @@ export class PagerDutyProvider implements INotificationProvider {
constructor(logger: any) {
this.logger = logger;
}
private getHardwareContent = (
clientHost: string,
monitor: Monitor,
monitorStatusResponse: MonitorStatusResponse,
decision: MonitorActionDecision
) => {
// For status changes (recovery), use standard format
if (decision.notificationReason === "status_change") {
return buildWebhookBody(monitor, monitorStatusResponse);
}
// For threshold breaches, use hardware alert format
const { alertsToSend } = buildHardwareAlerts(clientHost, monitor, monitorStatusResponse);
const body = buildHardwareNotificationMessage(clientHost, 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,
decision: MonitorActionDecision,
clientHost: string
): Promise<boolean> {
let body;
if (monitor.type === "hardware") {
body = this.getHardwareContent(clientHost, monitor, monitorStatusResponse, decision);
} 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(),
},
},
});
return true;
} catch (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<boolean> {
try {
@@ -3,12 +3,7 @@ import type { Monitor, Notification, MonitorStatusResponse } from "@/types/index
import { INotificationProvider } from "@/service/index.js";
import type { MonitorActionDecision } from "@/service/infrastructure/SuperSimpleQueue/SuperSimpleQueueHelper.js";
import type { NotificationMessage } from "@/types/notificationMessage.js";
import {
buildHardwareAlerts,
buildHardwareNotificationMessage,
buildWebhookBody,
getTestMessage,
} from "@/service/infrastructure/notificationProviders/utils.js";
import { getTestMessage } from "@/service/infrastructure/notificationProviders/utils.js";
import got, { HTTPError } from "got";
export class SlackProvider implements INotificationProvider {
@@ -17,64 +12,6 @@ export class SlackProvider implements INotificationProvider {
constructor(logger: any) {
this.logger = logger;
}
private getHardwareContent = (
clientHost: string,
monitor: Monitor,
monitorStatusResponse: MonitorStatusResponse,
decision: MonitorActionDecision
) => {
// For status changes (recovery), use standard format
if (decision.notificationReason === "status_change") {
return buildWebhookBody(monitor, monitorStatusResponse);
}
// For threshold breaches, use hardware alert format
const { alertsToSend } = buildHardwareAlerts(clientHost, monitor, monitorStatusResponse);
const body = buildHardwareNotificationMessage(clientHost, 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,
decision: MonitorActionDecision,
clientHost: string
): Promise<boolean> {
let body;
if (monitor.type === "hardware") {
body = this.getHardwareContent(clientHost, monitor, monitorStatusResponse, decision);
} 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",
},
});
return true;
} catch (error) {
const err = error as Error;
this.logger.warn({
message: "Slack alert failed",
service: SERVICE_NAME,
method: "sendAlert",
stack: err?.stack,
});
return false;
}
}
async sendTestAlert(notification: Notification): Promise<boolean> {
if (!notification.address) {
@@ -1,170 +1,5 @@
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,
networkResponse: MonitorStatusResponse
): { alertsToSend: string[]; discordPayload: any } => {
// Thresholds are stored as percentages (0-100), convert to decimal (0-1) for comparison
const cpuThreshold = monitor.cpuAlertThreshold !== undefined ? monitor.cpuAlertThreshold / 100 : -1;
const memoryThreshold = monitor.memoryAlertThreshold !== undefined ? monitor.memoryAlertThreshold / 100 : -1;
const diskThreshold = monitor.diskAlertThreshold !== undefined ? monitor.diskAlertThreshold / 100 : -1;
const tempThreshold = monitor.tempAlertThreshold !== undefined ? monitor.tempAlertThreshold : -1;
const payload = networkResponse?.payload as HardwareStatusPayload;
const metrics = payload.data || {};
const { cpu = {}, memory = {}, disk = [] } = metrics;
const cpuUsage = cpu.usage_percent ?? -1;
const memoryUsage = memory.usage_percent ?? -1;
// Get max temperature from CPU temperature sensors array
const temps = cpu.temperature ?? [];
const maxTemp = temps.length > 0 ? Math.max(...temps) : -1;
const alerts: Record<string, boolean> = {
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,
temp: tempThreshold !== -1 && maxTemp > tempThreshold ? true : 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,
],
}),
temp: () => ({
title: "Temperature alert",
description: `Your current temperature (${maxTemp.toFixed(0)}°C) is above your threshold (${tempThreshold.toFixed(0)}°C)`,
color: 15548997,
fields: [...monitorInfoFields, goToIncidentField],
footer: { text: "Checkmate" },
}),
};
const alertTypes = ["cpu", "memory", "disk", "temp"] as const;
for (const type of alertTypes) {
if (alerts[type] === true) {
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 ?? 0 * 100).toFixed(0)}%)`).join(", ")} is above your threshold (${(diskThreshold * 100).toFixed(0)}%)`,
temp: () => `Your current temperature (${maxTemp.toFixed(0)}°C) is above your threshold (${tempThreshold.toFixed(0)}°C)`,
};
alertsToSend.push(formatAlert[type]());
discordEmbeds.push(formatDiscordAlert[type]());
}
}
const discordPayload = discordEmbeds.length ? { embeds: discordEmbeds } : null;
return { alertsToSend, discordPayload };
};
export const buildHardwareNotificationMessage = (clientHost: string, alerts: string[], monitor: Monitor): string => {
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.join("\n");
};
export const buildWebhookBody = (monitor: Monitor, monitorStatusResponse: MonitorStatusResponse) => {
const { status, code } = monitorStatusResponse;
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`;
};
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<string> => {
const template = monitor.status === "up" ? "serverIsUpTemplate" : "serverIsDownTemplate";
const context = { monitor: monitor.name, url: monitor.url };
const html = await emailService.buildEmail(template, context);
return html;
};
export const buildDiscordBody = (monitor: Monitor, monitorStatusResponse: MonitorStatusResponse) => {
const { localTime, utcTime } = getTime();
let body = {
embeds: [
{
title: `Monitor name: ${monitor.name}`,
color: monitorStatusResponse.status ? 5763719 : 15548997,
fields: [
{ 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" },
},
],
};
return body;
};
// Test notification helpers - used by all providers for test alerts
export const buildTestEmail = async (emailService: any) => {
const context = { testName: "Monitoring System" };
const html = await emailService.buildEmail("testEmailTemplate", context);
@@ -3,12 +3,7 @@ import type { Monitor, Alert, Notification, MonitorStatusResponse } from "@/type
import { INotificationProvider } from "@/service/index.js";
import type { MonitorActionDecision } from "@/service/infrastructure/SuperSimpleQueue/SuperSimpleQueueHelper.js";
import type { NotificationMessage } from "@/types/notificationMessage.js";
import {
buildHardwareAlerts,
buildHardwareNotificationMessage,
buildWebhookBody,
getTestMessage,
} from "@/service/infrastructure/notificationProviders/utils.js";
import { getTestMessage } from "@/service/infrastructure/notificationProviders/utils.js";
import got from "got";
export class WebhookProvider implements INotificationProvider {
@@ -17,68 +12,7 @@ export class WebhookProvider implements INotificationProvider {
constructor(logger: any) {
this.logger = logger;
}
private getHardwareContent = (
clientHost: string,
monitor: Monitor,
monitorStatusResponse: MonitorStatusResponse,
decision: MonitorActionDecision
) => {
// For status changes (recovery), use standard format
if (decision.notificationReason === "status_change") {
return buildWebhookBody(monitor, monitorStatusResponse);
}
// For threshold breaches, use hardware alert format
const { alertsToSend } = buildHardwareAlerts(clientHost, monitor, monitorStatusResponse);
const body = buildHardwareNotificationMessage(clientHost, alertsToSend, monitor);
return body;
};
private getContent = (monitor: Monitor, monitorStatusResponse: MonitorStatusResponse): string => {
const body = buildWebhookBody(monitor, monitorStatusResponse);
return body;
};
sendAlert = async (
notification: Notification,
monitor: Monitor,
monitorStatusResponse: MonitorStatusResponse,
decision: MonitorActionDecision,
clientHost: string
) => {
let body;
if (monitor.type === "hardware") {
body = this.getHardwareContent(clientHost, monitor, monitorStatusResponse, decision);
} 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",
},
});
return true;
} catch (error) {
const err = error as Error;
this.logger.warn({
message: "Webhook alert failed",
service: SERVICE_NAME,
method: "sendAlert",
stack: err?.stack,
});
return false;
}
};
/**
* New unified message format - builds webhook payload from NotificationMessage
*/
sendMessage = async (notification: Notification, message: NotificationMessage): Promise<boolean> => {
if (!notification.address) {
return false;
@@ -95,7 +29,7 @@ export class WebhookProvider implements INotificationProvider {
},
});
this.logger.info({
message: "[NEW] Webhook notification sent via sendMessage",
message: "Webhook notification sent",
service: SERVICE_NAME,
method: "sendMessage",
});
@@ -103,7 +37,7 @@ export class WebhookProvider implements INotificationProvider {
} catch (error) {
const err = error as Error;
this.logger.warn({
message: "[NEW] Webhook alert failed via sendMessage",
message: "Webhook alert failed",
service: SERVICE_NAME,
method: "sendMessage",
stack: err?.stack,
@@ -1,4 +1,5 @@
import type { HardwareStatusPayload, Monitor, MonitorStatusResponse, Notification, MonitorStatus } from "@/types/index.js";
import type { NotificationMessage } from "@/types/notificationMessage.js";
import { IMonitorsRepository, INotificationsRepository } from "@/repositories/index.js";
import { INotificationProvider } from "./notificationProviders/INotificationProvider.js";
import type { MonitorActionDecision } from "@/service/infrastructure/SuperSimpleQueue/SuperSimpleQueueHelper.js";
@@ -66,81 +67,37 @@ export class NotificationsService implements INotificationsService {
monitor: Monitor,
monitorStatusResponse: MonitorStatusResponse,
decision: MonitorActionDecision,
notificationMessage?: any
notificationMessage: NotificationMessage | undefined
): Promise<boolean> => {
const settings = this.settingsService.getSettings();
const clientHost = settings.clientHost || "Host not defined";
// Route to new sendMessage method if provider supports it
if (notification.type === "webhook" && this.webhookProvider.sendMessage && notificationMessage) {
this.logger.info({
message: "[NEW] Using sendMessage for webhook",
if (!notificationMessage) {
this.logger.warn({
message: "Notification message not provided",
service: SERVICE_NAME,
method: "send",
});
return await this.webhookProvider.sendMessage(notification, notificationMessage);
return false;
}
if (notification.type === "slack" && this.slackProvider.sendMessage && notificationMessage) {
this.logger.info({
message: "[NEW] Using sendMessage for slack",
service: SERVICE_NAME,
method: "send",
});
return await this.slackProvider.sendMessage(notification, notificationMessage);
}
if (notification.type === "matrix" && this.matrixProvider.sendMessage && notificationMessage) {
this.logger.info({
message: "[NEW] Using sendMessage for matrix",
service: SERVICE_NAME,
method: "send",
});
return await this.matrixProvider.sendMessage(notification, notificationMessage);
}
if (notification.type === "pager_duty" && this.pagerDutyProvider.sendMessage && notificationMessage) {
this.logger.info({
message: "[NEW] Using sendMessage for pagerduty",
service: SERVICE_NAME,
method: "send",
});
return await this.pagerDutyProvider.sendMessage(notification, notificationMessage);
}
if (notification.type === "discord" && this.discordProvider.sendMessage && notificationMessage) {
this.logger.info({
message: "[NEW] Using sendMessage for discord",
service: SERVICE_NAME,
method: "send",
});
return await this.discordProvider.sendMessage(notification, notificationMessage);
}
if (notification.type === "email" && this.emailProvider.sendMessage && notificationMessage) {
this.logger.info({
message: "[NEW] Using sendMessage for email",
service: SERVICE_NAME,
method: "send",
});
return await this.emailProvider.sendMessage(notification, notificationMessage);
}
// Fallback to existing sendAlert for all providers
// Route to provider based on notification type
switch (notification.type) {
case "email":
return await this.emailProvider.sendAlert(notification, monitor, monitorStatusResponse, decision, clientHost);
case "slack":
return await this.slackProvider.sendAlert(notification, monitor, monitorStatusResponse, decision, clientHost);
case "discord":
return await this.discordProvider.sendAlert(notification, monitor, monitorStatusResponse, decision, clientHost);
case "pager_duty":
return await this.pagerDutyProvider.sendAlert(notification, monitor, monitorStatusResponse, decision, clientHost);
case "matrix":
return await this.matrixProvider.sendAlert(notification, monitor, monitorStatusResponse, decision, clientHost);
case "webhook":
return await this.webhookProvider.sendAlert(notification, monitor, monitorStatusResponse, decision, clientHost);
return await this.webhookProvider.sendMessage!(notification, notificationMessage);
case "slack":
return await this.slackProvider.sendMessage!(notification, notificationMessage);
case "matrix":
return await this.matrixProvider.sendMessage!(notification, notificationMessage);
case "pager_duty":
return await this.pagerDutyProvider.sendMessage!(notification, notificationMessage);
case "discord":
return await this.discordProvider.sendMessage!(notification, notificationMessage);
case "email":
return await this.emailProvider.sendMessage!(notification, notificationMessage);
default:
this.logger.warn({
message: `Unknown notification type: ${notification.type}`,
service: SERVICE_NAME,
method: "send",
});
return false;
}
};
@@ -1,69 +0,0 @@
<mjml>
<mj-head>
<mj-font
name="Roboto"
href="https://fonts.googleapis.com/css?family=Roboto:300,500"
></mj-font>
<mj-attributes>
<mj-all font-family="Roboto, Helvetica, sans-serif"></mj-all>
<mj-text
font-weight="300"
font-size="16px"
color="#616161"
line-height="24px"
></mj-text>
<mj-section padding="0px"></mj-section>
</mj-attributes>
</mj-head>
<mj-body>
<mj-section padding="20px 0">
<mj-column width="100%">
<mj-text
align="left"
font-size="10px"
>
Message from BlueWave Infrastructure Monitoring
</mj-text>
</mj-column>
<mj-column
width="45%"
padding-top="20px"
>
<mj-text
align="center"
font-weight="500"
padding="0px"
font-size="18px"
color="red"
>
Infrastructure Alerts
</mj-text>
<mj-divider
border-width="2px"
border-color="#616161"
></mj-divider>
</mj-column>
</mj-section>
<mj-section>
<mj-column width="100%">
<mj-text>
<p>Hello {{ name }}!</p>
<p>{{ monitor }} at {{ url }} has the following infrastructure alerts:</p>
{{#each alerts}}
<p>• {{ this }}</p>
{{/each}}
</mj-text>
</mj-column>
<mj-column width="100%">
<mj-divider
border-width="1px"
border-color="#E0E0E0"
></mj-divider>
<mj-button background-color="#1570EF"> View Infrastructure Details </mj-button>
<mj-text font-size="12px">
<p>This email was sent by BlueWave Infrastructure Monitoring.</p>
</mj-text>
</mj-column>
</mj-section>
</mj-body>
</mjml>
-68
View File
@@ -1,68 +0,0 @@
<mjml>
<mj-head>
<mj-font
name="Roboto"
href="https://fonts.googleapis.com/css?family=Roboto:300,500"
></mj-font>
<mj-attributes>
<mj-all font-family="Roboto, Helvetica, sans-serif"></mj-all>
<mj-text
font-weight="300"
font-size="16px"
color="#616161"
line-height="24px"
></mj-text>
<mj-section padding="0px"></mj-section>
</mj-attributes>
</mj-head>
<mj-body>
<mj-section padding="20px 0">
<mj-column width="100%">
<mj-text
align="left"
font-size="10px"
>
Message from Checkmate Service
</mj-text>
</mj-column>
<mj-column
width="45%"
padding-top="20px"
>
<mj-text
align="center"
font-weight="500"
padding="0px"
font-size="18px"
color="red"
>
{{ monitor }} is down
</mj-text>
<mj-divider
border-width="2px"
border-color="#616161"
></mj-divider>
</mj-column>
</mj-section>
<mj-section>
<mj-column width="100%">
<mj-text>
<p>Hello {{ name }}!</p>
<p>We detected an incident on one of your monitors. Your service is currently down. We'll send a message to you once it is up again.</p>
<p><b>Monitor name:</b> {{ monitor }}</p>
<p><b>URL:</b> {{ url }}</p>
</mj-text>
</mj-column>
<mj-column width="100%">
<mj-divider
border-width="1px"
border-color="#E0E0E0"
></mj-divider>
<mj-button background-color="#1570EF"> View incident details </mj-button>
<mj-text font-size="12px">
<p>This email was sent by Checkmate.</p>
</mj-text>
</mj-column>
</mj-section>
</mj-body>
</mjml>
-68
View File
@@ -1,68 +0,0 @@
<mjml>
<mj-head>
<mj-font
name="Roboto"
href="https://fonts.googleapis.com/css?family=Roboto:300,500"
></mj-font>
<mj-attributes>
<mj-all font-family="Roboto, Helvetica, sans-serif"></mj-all>
<mj-text
font-weight="300"
font-size="16px"
color="#616161"
line-height="24px"
></mj-text>
<mj-section padding="0px"></mj-section>
</mj-attributes>
</mj-head>
<mj-body>
<mj-section padding="20px 0">
<mj-column width="100%">
<mj-text
align="left"
font-size="10px"
>
Message from Checkmate Service
</mj-text>
</mj-column>
<mj-column
width="45%"
padding-top="20px"
>
<mj-text
align="center"
font-weight="500"
padding="0px"
font-size="18px"
color="green"
>
{{ monitor }} is up
</mj-text>
<mj-divider
border-width="2px"
border-color="#616161"
></mj-divider>
</mj-column>
</mj-section>
<mj-section>
<mj-column width="100%">
<mj-text>
<p>Hello {{ name }}!</p>
<p>Your latest incident is resolved and your monitored service is up again.</p>
<p><b>Monitor name:</b> {{ monitor }}</p>
<p><b>URL:</b> {{ url }}</p>
</mj-text>
</mj-column>
<mj-column width="100%">
<mj-divider
border-width="1px"
border-color="#E0E0E0"
></mj-divider>
<mj-button background-color="#1570EF"> View incident details </mj-button>
<mj-text font-size="12px">
<p>This email was sent by Checkmate.</p>
</mj-text>
</mj-column>
</mj-section>
</mj-body>
</mjml>