update providers

This commit is contained in:
Alex Holliday
2026-02-15 18:35:54 +00:00
parent 8020bfde21
commit be8580ee99
9 changed files with 140 additions and 35 deletions
@@ -1,6 +1,12 @@
import type { Monitor, Notification, Alert, MonitorStatusResponse } from "@/types/index.js";
import type { MonitorActionDecision } from "@/service/infrastructure/SuperSimpleQueue/SuperSimpleQueueHelper.js";
export interface INotificationProvider {
sendAlert: (notification: Notification, monitor: Monitor, monitorStatusResponse: MonitorStatusResponse) => Promise<boolean>;
sendAlert: (
notification: Notification,
monitor: Monitor,
monitorStatusResponse: MonitorStatusResponse,
decision: MonitorActionDecision
) => Promise<boolean>;
sendTestAlert(notification: Notification): Promise<boolean>;
}
@@ -1,6 +1,7 @@
const SERVICE_NAME = "DiscordProvider";
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, buildDiscordBody, getTestMessage } from "@/service/infrastructure/notificationProviders/utils.js";
import got from "got";
@@ -10,15 +11,20 @@ export class DiscordProvider implements INotificationProvider {
constructor(logger: any) {
this.logger = logger;
}
private getHardwareContent = (monitor: Monitor, monitorStatusResponse: MonitorStatusResponse) => {
private getHardwareContent = (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("HOST_PLACEHOLDER", monitor, monitorStatusResponse);
return discordPayload;
};
sendAlert = async (notification: Notification, monitor: Monitor, monitorStatusResponse: MonitorStatusResponse) => {
sendAlert = async (notification: Notification, monitor: Monitor, monitorStatusResponse: MonitorStatusResponse, decision: MonitorActionDecision) => {
let body;
if (monitor.type === "hardware") {
body = this.getHardwareContent(monitor, monitorStatusResponse);
body = this.getHardwareContent(monitor, monitorStatusResponse, decision);
} else {
body = buildDiscordBody(monitor, monitorStatusResponse);
}
@@ -1,6 +1,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";
export class EmailProvider implements INotificationProvider {
@@ -12,21 +13,42 @@ export class EmailProvider implements INotificationProvider {
this.logger = logger;
}
private buildHardwareEmail = (monitor: Monitor, monitorStatusResponse: MonitorStatusResponse) => {
private buildHardwareEmail = async (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("HOST_PLACEHOLDER", monitor, monitorStatusResponse);
const html = buildHardwareEmail(this.emailService, monitor, alertsToSend);
return html;
};
async sendAlert(notification: Notification, monitor: Monitor, monitorStatusResponse: MonitorStatusResponse): Promise<boolean> {
async sendAlert(
notification: Notification,
monitor: Monitor,
monitorStatusResponse: MonitorStatusResponse,
decision: MonitorActionDecision
): 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(":");
const subject = isGroupedNotification ? `Alert: ${monitor.name} are down` : `Monitor ${monitor.name} is down`;
// 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 = this.buildHardwareEmail(monitor, monitorStatusResponse);
html = await this.buildHardwareEmail(monitor, monitorStatusResponse, decision);
} else {
html = await buildEmail(this.emailService, monitor);
}
@@ -1,6 +1,7 @@
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 {
buildHardwareAlerts,
@@ -15,9 +16,19 @@ export class MatrixProvider implements INotificationProvider {
constructor(logger: any) {
this.logger = logger;
}
private getHardwareContent = (monitor: Monitor, monitorStatusResponse: MonitorStatusResponse) => {
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("HOST_PLACEHOLDER", monitor, monitorStatusResponse);
const body = buildHardwareWebhookBody(alertsToSend, monitor);
const body = buildHardwareWebhookBody(clientHost, alertsToSend, monitor);
return body;
};
@@ -26,12 +37,12 @@ export class MatrixProvider implements INotificationProvider {
return body;
};
sendAlert = async (notification: Notification, monitor: Monitor, monitorStatusResponse: MonitorStatusResponse) => {
sendAlert = async (notification: Notification, monitor: Monitor, monitorStatusResponse: MonitorStatusResponse, decision: MonitorActionDecision) => {
const { homeserverUrl, accessToken, roomId } = notification;
let content;
if (monitor.type === "hardware") {
content = this.getHardwareContent(monitor, monitorStatusResponse);
content = this.getHardwareContent("HOST_PLACEHOLDER", monitor, monitorStatusResponse, decision);
} else {
content = this.getContent(monitor, monitorStatusResponse);
}
@@ -1,6 +1,7 @@
const SERVICE_NAME = "PagerDutyProvider";
import got from "got";
import type { Monitor, Notification, MonitorStatusResponse } from "@/types/index.js";
import type { MonitorActionDecision } from "@/service/infrastructure/SuperSimpleQueue/SuperSimpleQueueHelper.js";
import { INotificationProvider } from "@/service/index.js";
import {
buildHardwareAlerts,
@@ -15,9 +16,19 @@ export class PagerDutyProvider implements INotificationProvider {
constructor(logger: any) {
this.logger = logger;
}
private getHardwareContent = (monitor: Monitor, monitorStatusResponse: MonitorStatusResponse) => {
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("HOST_PLACEHOLDER", monitor, monitorStatusResponse);
const body = buildHardwareWebhookBody(alertsToSend, monitor);
const body = buildHardwareWebhookBody(clientHost, alertsToSend, monitor);
return body;
};
@@ -26,10 +37,15 @@ export class PagerDutyProvider implements INotificationProvider {
return body;
};
async sendAlert(notification: Notification, monitor: Monitor, monitorStatusResponse: MonitorStatusResponse): Promise<boolean> {
async sendAlert(
notification: Notification,
monitor: Monitor,
monitorStatusResponse: MonitorStatusResponse,
decision: MonitorActionDecision
): Promise<boolean> {
let body;
if (monitor.type === "hardware") {
body = this.getHardwareContent(monitor, monitorStatusResponse);
body = this.getHardwareContent("HOST_PLACEHOLDER", monitor, monitorStatusResponse, decision);
} else {
body = this.getContent(monitor, monitorStatusResponse);
}
@@ -1,6 +1,7 @@
const SERVICE_NAME = "SlackProvider";
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,
buildHardwareWebhookBody,
@@ -15,9 +16,19 @@ export class SlackProvider implements INotificationProvider {
constructor(logger: any) {
this.logger = logger;
}
private getHardwareContent = (monitor: Monitor, monitorStatusResponse: MonitorStatusResponse) => {
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("HOST_PLACEHOLDER", monitor, monitorStatusResponse);
const body = buildHardwareWebhookBody(alertsToSend, monitor);
const body = buildHardwareWebhookBody(clientHost, alertsToSend, monitor);
return body;
};
@@ -26,10 +37,15 @@ export class SlackProvider implements INotificationProvider {
return body;
};
async sendAlert(notification: Notification, monitor: Monitor, monitorStatusResponse: MonitorStatusResponse): Promise<boolean> {
async sendAlert(
notification: Notification,
monitor: Monitor,
monitorStatusResponse: MonitorStatusResponse,
decision: MonitorActionDecision
): Promise<boolean> {
let body;
if (monitor.type === "hardware") {
body = this.getHardwareContent(monitor, monitorStatusResponse);
body = this.getHardwareContent("HOST_PLACEHOLDER", monitor, monitorStatusResponse, decision);
} else {
body = this.getContent(monitor, monitorStatusResponse);
}
@@ -123,8 +123,8 @@ export const buildHardwareNotificationMessage = (clientHost: string, alerts: any
return alertText.map((alert) => alert).join("\n");
};
export const buildHardwareWebhookBody = (alerts: string[], monitor: Monitor): string => {
const content = alerts.map((alert) => alert).join("\n");
export const buildHardwareWebhookBody = (clientHost: string, alerts: string[], monitor: Monitor): string => {
const content = buildHardwareNotificationMessage(clientHost, alerts, monitor);
return content;
};
@@ -1,6 +1,7 @@
const SERVICE_NAME = "WebhookProvider";
import type { Monitor, Alert, Notification, MonitorStatusResponse } from "@/types/index.js";
import { INotificationProvider } from "@/service/index.js";
import type { MonitorActionDecision } from "@/service/infrastructure/SuperSimpleQueue/SuperSimpleQueueHelper.js";
import {
buildHardwareAlerts,
buildHardwareWebhookBody,
@@ -15,9 +16,19 @@ export class WebhookProvider implements INotificationProvider {
constructor(logger: any) {
this.logger = logger;
}
private getHardwareContent = (monitor: Monitor, monitorStatusResponse: MonitorStatusResponse) => {
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("HOST_PLACEHOLDER", monitor, monitorStatusResponse);
const body = buildHardwareWebhookBody(alertsToSend, monitor);
const body = buildHardwareWebhookBody(clientHost, alertsToSend, monitor);
return body;
};
@@ -26,10 +37,10 @@ export class WebhookProvider implements INotificationProvider {
return body;
};
sendAlert = async (notification: Notification, monitor: Monitor, monitorStatusResponse: MonitorStatusResponse) => {
sendAlert = async (notification: Notification, monitor: Monitor, monitorStatusResponse: MonitorStatusResponse, decision: MonitorActionDecision) => {
let body;
if (monitor.type === "hardware") {
body = this.getHardwareContent(monitor, monitorStatusResponse);
body = this.getHardwareContent("HOST_PLACEHOLDER", monitor, monitorStatusResponse, decision);
} else {
body = this.getContent(monitor, monitorStatusResponse);
}
@@ -41,6 +41,7 @@ export class NotificationsService implements INotificationsService {
createdAt: number;
}
>;
private currentDecision?: MonitorActionDecision;
constructor(
notificationsRepository: INotificationsRepository,
@@ -83,17 +84,17 @@ export class NotificationsService implements INotificationsService {
private send = async (notification: Notification, monitor: Monitor, monitorStatusResponse: MonitorStatusResponse): Promise<boolean> => {
switch (notification.type) {
case "email":
return await this.emailProvider.sendAlert(notification, monitor, monitorStatusResponse);
return await this.emailProvider.sendAlert(notification, monitor, monitorStatusResponse, this.currentDecision!);
case "slack":
return await this.slackProvider.sendAlert(notification, monitor, monitorStatusResponse);
return await this.slackProvider.sendAlert(notification, monitor, monitorStatusResponse, this.currentDecision!);
case "discord":
return await this.discordProvider.sendAlert(notification, monitor, monitorStatusResponse);
return await this.discordProvider.sendAlert(notification, monitor, monitorStatusResponse, this.currentDecision!);
case "pager_duty":
return await this.pagerDutyProvider.sendAlert(notification, monitor, monitorStatusResponse);
return await this.pagerDutyProvider.sendAlert(notification, monitor, monitorStatusResponse, this.currentDecision!);
case "matrix":
return await this.matrixProvider.sendAlert(notification, monitor, monitorStatusResponse);
return await this.matrixProvider.sendAlert(notification, monitor, monitorStatusResponse, this.currentDecision!);
case "webhook":
return await this.webhookProvider.sendAlert(notification, monitor, monitorStatusResponse);
return await this.webhookProvider.sendAlert(notification, monitor, monitorStatusResponse, this.currentDecision!);
default:
return false;
}
@@ -157,7 +158,15 @@ export class NotificationsService implements INotificationsService {
this.pendingEmailGroups.delete(key);
try {
await this.flushEmailGroup(notification, group.monitors, group.statusResponses);
// For grouped notifications (always DOWN), create a decision
const groupedDecision: MonitorActionDecision = {
shouldCreateIncident: false,
shouldResolveIncident: false,
shouldSendNotification: true,
incidentReason: "status_down",
notificationReason: "status_change",
};
await this.flushEmailGroup(notification, group.monitors, group.statusResponses, groupedDecision);
} catch (error: any) {
this.logger.error({
message: error?.message,
@@ -196,7 +205,12 @@ export class NotificationsService implements INotificationsService {
* @param statusResponses Array of status responses (parallel to monitors)
* @returns true if email was sent successfully, false otherwise
*/
private flushEmailGroup = async (notification: Notification, monitors: Monitor[], statusResponses: MonitorStatusResponse[]): Promise<boolean> => {
private flushEmailGroup = async (
notification: Notification,
monitors: Monitor[],
statusResponses: MonitorStatusResponse[],
decision: MonitorActionDecision
): Promise<boolean> => {
if (!monitors.length || !statusResponses.length) {
return false;
}
@@ -221,7 +235,7 @@ export class NotificationsService implements INotificationsService {
};
// Reuse existing email provider to send grouped notification.
return await this.emailProvider.sendAlert(notification, syntheticMonitor, baseStatus);
return await this.emailProvider.sendAlert(notification, syntheticMonitor, baseStatus, decision);
};
handleNotifications = async (monitor: Monitor, monitorStatusResponse: MonitorStatusResponse, decision: MonitorActionDecision) => {
@@ -230,6 +244,9 @@ export class NotificationsService implements INotificationsService {
return false;
}
// Store decision for use in send method
this.currentDecision = decision;
// Send notifications based on decision
return await this.sendNotifications(monitor, monitorStatusResponse);
};