initial commit

This commit is contained in:
Alex Holliday committed 2026-02-16 20:18:25 +00:00
1 parent a65515fc08
commit d2d75cd035
4 files changed
+350 -3

No files matched your search

+7 -1
View File
@@ -23,6 +23,7 @@ import MaintenanceWindowService from "../service/business/maintenanceWindowServi
import { MonitorService } from "@/service/index.js";
import { StatusPageService, IStatusPageService } from "../service/business/statusPageService.js";
import IncidentService from "../service/business/incidentService.js";
import { NotificationMessageBuilder, INotificationMessageBuilder } from "../service/infrastructure/notificationMessageBuilder.js";
import axios from "axios";
import got from "got";
import ping from "ping";
@@ -88,6 +89,7 @@ export type InitializedServices = {
logger: any;
notificationsService: INotificationsService;
statusPageService: IStatusPageService;
notificationMessageBuilder: INotificationMessageBuilder;
// Repositories
monitorsRepository: IMonitorsRepository;
@@ -173,6 +175,8 @@ export const initializeServices = async ({
const pagerDutyProvider = new PagerDutyProvider(logger);
const matrixProvider = new MatrixProvider(logger);
const notificationMessageBuilder = new NotificationMessageBuilder();
const notificationsService = new NotificationsService(
notificationsRepository,
monitorsRepository,
@@ -183,7 +187,8 @@ export const initializeServices = async ({
pagerDutyProvider,
matrixProvider,
settingsService,
logger
logger,
notificationMessageBuilder
);
const superSimpleQueueHelper = new SuperSimpleQueueHelper({
@@ -262,6 +267,7 @@ export const initializeServices = async ({
logger,
notificationsService,
statusPageService,
notificationMessageBuilder,
// Repositories
monitorsRepository,
@@ -0,0 +1,281 @@
import type { Monitor, MonitorStatusResponse } from "@/types/index.js";
import type { MonitorActionDecision } from "@/service/infrastructure/SuperSimpleQueue/SuperSimpleQueueHelper.js";
import type {
NotificationMessage,
NotificationType,
NotificationSeverity,
ThresholdBreach,
NotificationContent,
} from "@/types/notificationMessage.js";
export interface INotificationMessageBuilder {
buildMessage(
monitor: Monitor,
monitorStatusResponse: MonitorStatusResponse,
decision: MonitorActionDecision,
clientHost: string
): NotificationMessage;
}
const SERVICE_NAME = "NotificationMessageBuilder";
export class NotificationMessageBuilder implements INotificationMessageBuilder {
static SERVICE_NAME = SERVICE_NAME;
buildMessage(
monitor: Monitor,
monitorStatusResponse: MonitorStatusResponse,
decision: MonitorActionDecision,
clientHost: string
): NotificationMessage {
const type = this.determineNotificationType(decision, monitor);
const severity = this.determineSeverity(type, monitor.status);
const content = this.buildContent(type, monitor, monitorStatusResponse, decision, clientHost);
return {
type,
severity,
monitor: {
id: monitor.id,
name: monitor.name,
url: monitor.url,
type: monitor.type,
status: monitor.status,
},
content,
clientHost,
metadata: {
teamId: monitor.teamId,
notificationReason: decision.notificationReason || "status_change",
},
};
}
private determineNotificationType(decision: MonitorActionDecision, monitor: Monitor): NotificationType {
// Down status has highest priority (critical)
if (monitor.status === "down") {
return "monitor_down";
}
// Threshold breach (only if not down)
if (decision.notificationReason === "threshold_breach") {
return "threshold_breach";
}
// Recovery from threshold breach
if (decision.notificationReason === "status_change" && monitor.status === "up") {
return "threshold_resolved";
}
// Standard recovery (up)
if (monitor.status === "up") {
return "monitor_up";
}
// Default to monitor_up for any other case
return "monitor_up";
}
private determineSeverity(type: NotificationType, monitorStatus: string): NotificationSeverity {
switch (type) {
case "monitor_down":
return "critical";
case "threshold_breach":
return "warning";
case "monitor_up":
case "threshold_resolved":
return "success";
case "test":
return "info";
default:
return "info";
}
}
private buildContent(
type: NotificationType,
monitor: Monitor,
monitorStatusResponse: MonitorStatusResponse,
decision: MonitorActionDecision,
clientHost: string
): NotificationContent {
switch (type) {
case "monitor_down":
return this.buildMonitorDownContent(monitor, monitorStatusResponse, clientHost);
case "monitor_up":
return this.buildMonitorUpContent(monitor, clientHost);
case "threshold_breach":
return this.buildThresholdBreachContent(monitor, monitorStatusResponse, decision, clientHost);
case "threshold_resolved":
return this.buildThresholdResolvedContent(monitor, clientHost);
default:
return this.buildDefaultContent(monitor, clientHost);
}
}
private buildMonitorDownContent(monitor: Monitor, monitorStatusResponse: MonitorStatusResponse, clientHost: string): NotificationContent {
const title = `Monitor Down: ${monitor.name}`;
const summary = `Monitor "${monitor.name}" is currently down and unreachable.`;
const details = [`URL: ${monitor.url}`, `Status: Down`, `Type: ${monitor.type}`];
// Add response code if available
if (monitorStatusResponse.code) {
details.push(`Response Code: ${monitorStatusResponse.code}`);
}
// Add error message if available
if (monitorStatusResponse.message) {
details.push(`Error: ${monitorStatusResponse.message}`);
}
return {
title,
summary,
details,
timestamp: new Date(),
};
}
private buildMonitorUpContent(monitor: Monitor, clientHost: string): NotificationContent {
const title = `Monitor Recovered: ${monitor.name}`;
const summary = `Monitor "${monitor.name}" is back up and operational.`;
const details = [`URL: ${monitor.url}`, `Status: Up`, `Type: ${monitor.type}`];
return {
title,
summary,
details,
timestamp: new Date(),
};
}
private buildThresholdBreachContent(
monitor: Monitor,
monitorStatusResponse: MonitorStatusResponse,
decision: MonitorActionDecision,
clientHost: string
): NotificationContent {
const title = `Threshold Breach: ${monitor.name}`;
const summary = `Hardware monitor "${monitor.name}" has breached one or more thresholds.`;
const details = [`URL: ${monitor.url}`, `Status: Breached`, `Type: ${monitor.type}`];
const thresholds = this.extractThresholdBreaches(monitor, monitorStatusResponse);
return {
title,
summary,
details,
thresholds,
timestamp: new Date(),
};
}
private buildThresholdResolvedContent(monitor: Monitor, clientHost: string): NotificationContent {
const title = `Thresholds Resolved: ${monitor.name}`;
const summary = `Hardware monitor "${monitor.name}" thresholds have returned to normal.`;
const details = [`URL: ${monitor.url}`, `Status: Up`, `Type: ${monitor.type}`];
return {
title,
summary,
details,
timestamp: new Date(),
};
}
private buildDefaultContent(monitor: Monitor, clientHost: string): NotificationContent {
return {
title: `Monitor: ${monitor.name}`,
summary: `Status update for monitor "${monitor.name}".`,
details: [`URL: ${monitor.url}`, `Status: ${monitor.status}`, `Type: ${monitor.type}`],
timestamp: new Date(),
};
}
private extractThresholdBreaches(monitor: Monitor, monitorStatusResponse: MonitorStatusResponse): ThresholdBreach[] {
const breaches: ThresholdBreach[] = [];
// Check if this is a hardware monitor with threshold data
if (monitor.type !== "hardware" || !monitorStatusResponse.payload) {
return breaches;
}
// Cast to HardwareStatusPayload type
const payload = monitorStatusResponse.payload as { data?: any };
const hardware = payload.data;
if (!hardware) {
return breaches;
}
// CPU threshold breach
if (monitor.cpuAlertThreshold && hardware.cpu?.usage_percent !== undefined) {
const cpuPercent = hardware.cpu.usage_percent;
const threshold = monitor.cpuAlertThreshold;
if (cpuPercent >= threshold) {
breaches.push({
metric: "cpu",
currentValue: cpuPercent,
threshold,
unit: "%",
formattedValue: `${cpuPercent.toFixed(1)}%`,
});
}
}
// Memory threshold breach
if (monitor.memoryAlertThreshold && hardware.memory?.usage_percent !== undefined) {
const memoryPercent = hardware.memory.usage_percent;
const threshold = monitor.memoryAlertThreshold;
if (memoryPercent >= threshold) {
breaches.push({
metric: "memory",
currentValue: memoryPercent,
threshold,
unit: "%",
formattedValue: `${memoryPercent.toFixed(1)}%`,
});
}
}
// Disk threshold breach
if (monitor.diskAlertThreshold && Array.isArray(hardware.disk)) {
// Find the highest disk usage
let maxDiskUsage = 0;
for (const disk of hardware.disk) {
if (disk.usage_percent !== undefined && disk.usage_percent > maxDiskUsage) {
maxDiskUsage = disk.usage_percent;
}
}
const threshold = monitor.diskAlertThreshold;
if (maxDiskUsage >= threshold) {
breaches.push({
metric: "disk",
currentValue: maxDiskUsage,
threshold,
unit: "%",
formattedValue: `${maxDiskUsage.toFixed(1)}%`,
});
}
}
// Temperature threshold breach
if (monitor.tempAlertThreshold && hardware.cpu?.temperature) {
// Temperature is an array in cpu.temperature
const temps = Array.isArray(hardware.cpu.temperature) ? hardware.cpu.temperature : [hardware.cpu.temperature];
const maxTemp = Math.max(...temps.filter((t: number) => !isNaN(t)));
const threshold = monitor.tempAlertThreshold;
if (maxTemp >= threshold) {
breaches.push({
metric: "temp",
currentValue: maxTemp,
threshold,
unit: "°C",
formattedValue: `${maxTemp.toFixed(1)}°C`,
});
}
}
return breaches;
}
}
@@ -4,6 +4,7 @@ import { INotificationProvider } from "./notificationProviders/INotificationProv
import type { MonitorActionDecision } from "@/service/infrastructure/SuperSimpleQueue/SuperSimpleQueueHelper.js";
import type { ISettingsService } from "@/service/system/settingsService.js";
import { ILogger } from "@/utils/logger.js";
import type { INotificationMessageBuilder } from "@/service/infrastructure/notificationMessageBuilder.js";
export interface INotificationsService {
createNotification: (notificationData: Partial<Notification>) => Promise<Notification>;
@@ -32,6 +33,7 @@ export class NotificationsService implements INotificationsService {
private matrixProvider: INotificationProvider;
private logger: ILogger;
private settingsService: ISettingsService;
private notificationMessageBuilder: INotificationMessageBuilder;
constructor(
notificationsRepository: INotificationsRepository,
@@ -43,7 +45,8 @@ export class NotificationsService implements INotificationsService {
pagerDutyProvider: INotificationProvider,
matrixProvider: INotificationProvider,
settingsService: ISettingsService,
logger: ILogger
logger: ILogger,
notificationMessageBuilder: INotificationMessageBuilder
) {
this.notificationsRepository = notificationsRepository;
this.monitorsRepository = monitorsRepository;
@@ -55,6 +58,7 @@ export class NotificationsService implements INotificationsService {
this.matrixProvider = matrixProvider;
this.settingsService = settingsService;
this.logger = logger;
this.notificationMessageBuilder = notificationMessageBuilder;
}
private send = async (
@@ -105,11 +109,14 @@ export class NotificationsService implements INotificationsService {
};
handleNotifications = async (monitor: Monitor, monitorStatusResponse: MonitorStatusResponse, decision: MonitorActionDecision) => {
// Early return if no notification should be sent
if (!decision.shouldSendNotification) {
return false;
}
const settings = this.settingsService.getSettings();
const clientHost = settings.clientHost || "Host not defined";
const notificationMessage = this.notificationMessageBuilder.buildMessage(monitor, monitorStatusResponse, decision, clientHost);
// Send notifications based on decision
return await this.sendNotifications(monitor, monitorStatusResponse, decision);
};
+53
View File
@@ -0,0 +1,53 @@
/**
* Unified notification message types for cross-provider consistency
* Part of notification system unification effort
*/
export type NotificationType = "monitor_down" | "monitor_up" | "threshold_breach" | "threshold_resolved" | "test";
export type NotificationSeverity = "critical" | "warning" | "info" | "success";
export interface MonitorInfo {
id: string;
name: string;
url: string;
type: string;
status: string;
}
export interface ThresholdBreach {
metric: "cpu" | "memory" | "disk" | "temp";
currentValue: number;
threshold: number;
unit: string;
formattedValue: string; // e.g., "85%" or "72°C"
}
export interface IncidentInfo {
id: string;
url: string;
createdAt: Date;
resolvedAt?: Date;
duration?: string;
}
export interface NotificationContent {
title: string;
summary: string;
details?: string[];
thresholds?: ThresholdBreach[];
incident?: IncidentInfo;
timestamp: Date;
}
export interface NotificationMessage {
type: NotificationType;
severity: NotificationSeverity;
monitor: MonitorInfo;
content: NotificationContent;
clientHost: string;
metadata: {
teamId: string;
notificationReason: string;
};
}