cleanup
This commit is contained in:
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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
Reference in new issue
Block a user