Merge pull request #3152 from bluewave-labs/feat/notifications-refactor
feat/notifications refactor
This commit is contained in:
32 files changed
+1106
-541
No files matched your search
@@ -26,7 +26,7 @@ export const initializeControllers = (services: InitializedSerivces) => {
|
||||
controllers.queueController = new QueueController(services.jobQueue);
|
||||
controllers.logController = new LogController(services.logger);
|
||||
controllers.statusPageController = new StatusPageController(services.db);
|
||||
controllers.notificationController = new NotificationController(services.notificationService, services.db, services.monitorsRepository);
|
||||
controllers.notificationController = new NotificationController(services.notificationsService, services.db, services.monitorsRepository);
|
||||
controllers.diagnosticController = new DiagnosticController(services.diagnosticService);
|
||||
|
||||
controllers.incidentController = new IncidentController(services.incidentService);
|
||||
|
||||
@@ -5,9 +5,17 @@ import MongoDB from "../db/MongoDB.js";
|
||||
import NetworkService from "../service/infrastructure/networkService.js";
|
||||
import EmailService from "../service/infrastructure/emailService.js";
|
||||
import BufferService from "../service/infrastructure/bufferService.js";
|
||||
import StatusService from "../service/infrastructure/statusService.js";
|
||||
import NotificationUtils from "../service/infrastructure/notificationUtils.js";
|
||||
import NotificationService from "../service/infrastructure/notificationService.js";
|
||||
import {
|
||||
NotificationsService,
|
||||
StatusService,
|
||||
WebhookProvider,
|
||||
SlackProvider,
|
||||
EmailProvider,
|
||||
DiscordProvider,
|
||||
PagerDutyProvider,
|
||||
MatrixProvider,
|
||||
INotificationsService,
|
||||
} from "@/service/index.js";
|
||||
import ErrorService from "../service/infrastructure/errorService.js";
|
||||
import SuperSimpleQueueHelper from "../service/infrastructure/SuperSimpleQueue/SuperSimpleQueueHelper.js";
|
||||
import SuperSimpleQueue from "../service/infrastructure/SuperSimpleQueue/SuperSimpleQueue.js";
|
||||
@@ -50,7 +58,7 @@ import StatusPage from "../db/models/StatusPage.js";
|
||||
import Team from "../db/models/Team.js";
|
||||
import MaintenanceWindow from "../db/models/MaintenanceWindow.js";
|
||||
import MonitorStats from "../db/models/MonitorStats.js";
|
||||
import Notification from "../db/models/Notification.js";
|
||||
import NotificationModel from "../db/models/Notification.js";
|
||||
import RecoveryToken from "../db/models/RecoveryToken.js";
|
||||
import AppSettings from "../db/models/AppSettings.js";
|
||||
import Incident from "../db/models/Incident.js";
|
||||
@@ -75,6 +83,7 @@ import {
|
||||
MongoInvitesRepository,
|
||||
MongoRecoveryTokensRepository,
|
||||
MongoSettingsRepository,
|
||||
MongoNotificationsRepository,
|
||||
IMonitorsRepository,
|
||||
IChecksRepository,
|
||||
IMonitorStatsRepository,
|
||||
@@ -83,6 +92,7 @@ import {
|
||||
IInvitesRepository,
|
||||
IRecoveryTokensRepository,
|
||||
ISettingsRepository,
|
||||
INotificationsRepository,
|
||||
} from "@/repositories/index.js";
|
||||
|
||||
export type InitializedSerivces = {
|
||||
@@ -95,7 +105,6 @@ export type InitializedSerivces = {
|
||||
emailService: any;
|
||||
bufferService: any;
|
||||
statusService: any;
|
||||
notificationService: any;
|
||||
jobQueue: any;
|
||||
userService: any;
|
||||
checkService: any;
|
||||
@@ -106,6 +115,7 @@ export type InitializedSerivces = {
|
||||
incidentService: any;
|
||||
errorService: any;
|
||||
logger: any;
|
||||
notificationsService: INotificationsService;
|
||||
|
||||
// Repositories
|
||||
monitorsRepository: IMonitorsRepository;
|
||||
@@ -116,6 +126,7 @@ export type InitializedSerivces = {
|
||||
invitesRepository: IInvitesRepository;
|
||||
recoveryTokensRepository: IRecoveryTokensRepository;
|
||||
settingsRepository: ISettingsRepository;
|
||||
notificationsRepository: INotificationsRepository;
|
||||
};
|
||||
|
||||
export const initializeServices = async ({
|
||||
@@ -141,7 +152,7 @@ export const initializeServices = async ({
|
||||
const statusPageModule = new StatusPageModule({ StatusPage, NormalizeData, stringService, AppSettings });
|
||||
const userModule = new UserModule({ User, Team, GenerateAvatarImage, ParseBoolean, stringService });
|
||||
const maintenanceWindowModule = new MaintenanceWindowModule({ MaintenanceWindow });
|
||||
const notificationModule = new NotificationModule({ Notification, Monitor });
|
||||
const notificationModule = new NotificationModule({ Notification: NotificationModel, Monitor });
|
||||
const recoveryModule = new RecoveryModule({ User, RecoveryToken, crypto, stringService });
|
||||
const settingsModule = new SettingsModule({ AppSettings });
|
||||
const incidentModule = new IncidentModule({ logger, Incident, Monitor, User });
|
||||
@@ -171,7 +182,7 @@ export const initializeServices = async ({
|
||||
const invitesRepository = new MongoInvitesRepository();
|
||||
const recoveryTokensRepository = new MongoRecoveryTokensRepository();
|
||||
const settingsRepository = new MongoSettingsRepository();
|
||||
|
||||
const notificationsRepository = new MongoNotificationsRepository();
|
||||
const networkService = new NetworkService({
|
||||
axios,
|
||||
got,
|
||||
@@ -206,31 +217,34 @@ export const initializeServices = async ({
|
||||
checksRepository,
|
||||
});
|
||||
|
||||
const bufferService = new BufferService({ db, logger, envSettings, incidentService, checkService });
|
||||
const bufferService = new BufferService({ logger, incidentService, checkService });
|
||||
|
||||
const statusService = new StatusService({ db, logger, buffer: bufferService, incidentService, monitorsRepository });
|
||||
const statusService = new StatusService({ db, logger, buffer: bufferService, monitorsRepository });
|
||||
|
||||
const notificationUtils = new NotificationUtils({
|
||||
stringService,
|
||||
emailService,
|
||||
settingsService,
|
||||
});
|
||||
const webhookProvider = new WebhookProvider(logger);
|
||||
const slackProvider = new SlackProvider(logger);
|
||||
const emailProvider = new EmailProvider(emailService, logger);
|
||||
const discordProvider = new DiscordProvider(logger);
|
||||
const pagerDutyProvider = new PagerDutyProvider(logger);
|
||||
const matrixProvider = new MatrixProvider(logger);
|
||||
|
||||
const notificationService = new NotificationService({
|
||||
emailService,
|
||||
db,
|
||||
logger,
|
||||
networkService,
|
||||
stringService,
|
||||
notificationUtils,
|
||||
});
|
||||
const notificationsService = new NotificationsService(
|
||||
notificationsRepository,
|
||||
webhookProvider,
|
||||
emailProvider,
|
||||
slackProvider,
|
||||
discordProvider,
|
||||
pagerDutyProvider,
|
||||
matrixProvider,
|
||||
logger
|
||||
);
|
||||
|
||||
const superSimpleQueueHelper = new SuperSimpleQueueHelper({
|
||||
db,
|
||||
logger,
|
||||
networkService,
|
||||
statusService,
|
||||
notificationService,
|
||||
notificationsService,
|
||||
checkService,
|
||||
buffer: bufferService,
|
||||
});
|
||||
@@ -296,7 +310,6 @@ export const initializeServices = async ({
|
||||
emailService,
|
||||
bufferService,
|
||||
statusService,
|
||||
notificationService,
|
||||
jobQueue: superSimpleQueue,
|
||||
userService,
|
||||
checkService,
|
||||
@@ -307,6 +320,7 @@ export const initializeServices = async ({
|
||||
incidentService,
|
||||
errorService,
|
||||
logger,
|
||||
notificationsService,
|
||||
|
||||
// Repositories
|
||||
monitorsRepository,
|
||||
@@ -317,6 +331,7 @@ export const initializeServices = async ({
|
||||
invitesRepository,
|
||||
recoveryTokensRepository,
|
||||
settingsRepository,
|
||||
notificationsRepository,
|
||||
};
|
||||
|
||||
Object.values(services).forEach((service) => {
|
||||
|
||||
@@ -1,18 +1,20 @@
|
||||
import { Request, Response, NextFunction } from "express";
|
||||
|
||||
import { Notification } from "@/types/index.js";
|
||||
import { createNotificationBodyValidation } from "@/validation/joi.js";
|
||||
import { AppError } from "@/utils/AppError.js";
|
||||
import { IMonitorsRepository } from "@/repositories/index.js";
|
||||
import { INotificationsService } from "@/service/index.js";
|
||||
|
||||
const SERVICE_NAME = "NotificationController";
|
||||
|
||||
class NotificationController {
|
||||
static SERVICE_NAME = SERVICE_NAME;
|
||||
private db: any;
|
||||
private notificationService: any;
|
||||
private notificationsService: INotificationsService;
|
||||
private monitorsRepository: IMonitorsRepository;
|
||||
constructor(notificationService: any, db: any, monitorsRepository: IMonitorsRepository) {
|
||||
this.notificationService = notificationService;
|
||||
constructor(notificationsService: INotificationsService, db: any, monitorsRepository: IMonitorsRepository) {
|
||||
this.notificationsService = notificationsService;
|
||||
this.db = db;
|
||||
this.monitorsRepository = monitorsRepository;
|
||||
}
|
||||
@@ -23,9 +25,8 @@ class NotificationController {
|
||||
|
||||
testNotification = async (req: Request, res: Response, next: NextFunction) => {
|
||||
try {
|
||||
const notification = req.body;
|
||||
|
||||
const success = await this.notificationService.sendTestNotification(notification);
|
||||
const notification: Notification = req.body;
|
||||
const success = await this.notificationsService.sendTestNotification(notification);
|
||||
|
||||
if (!success) {
|
||||
throw new AppError({ message: "Sending notification failed", status: 500 });
|
||||
@@ -177,7 +178,7 @@ class NotificationController {
|
||||
throw new AppError({ message: "No notifications", status: 400 });
|
||||
}
|
||||
|
||||
const result = await this.notificationService.testAllNotifications(notifications);
|
||||
const result = await this.notificationsService.testAllNotifications(notifications);
|
||||
|
||||
if (!result) {
|
||||
throw new AppError({ message: "Failed to send all notifications", status: 500 });
|
||||
|
||||
@@ -1,74 +0,0 @@
|
||||
import mongoose from "mongoose";
|
||||
|
||||
const NotificationSchema = mongoose.Schema(
|
||||
{
|
||||
userId: {
|
||||
type: mongoose.Schema.Types.ObjectId,
|
||||
ref: "User",
|
||||
immutable: true,
|
||||
required: true,
|
||||
},
|
||||
teamId: {
|
||||
type: mongoose.Schema.Types.ObjectId,
|
||||
ref: "Team",
|
||||
immutable: true,
|
||||
required: true,
|
||||
},
|
||||
type: {
|
||||
type: String,
|
||||
enum: ["email", "slack", "discord", "webhook", "pager_duty", "matrix"],
|
||||
},
|
||||
notificationName: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
address: {
|
||||
type: String,
|
||||
},
|
||||
phone: {
|
||||
type: String,
|
||||
},
|
||||
// Matrix-specific fields
|
||||
homeserverUrl: {
|
||||
type: String,
|
||||
},
|
||||
roomId: {
|
||||
type: String,
|
||||
},
|
||||
accessToken: {
|
||||
type: String,
|
||||
},
|
||||
},
|
||||
{
|
||||
timestamps: true,
|
||||
}
|
||||
);
|
||||
|
||||
NotificationSchema.pre("save", function (next) {
|
||||
if (!this.cpuAlertThreshold || this.isModified("alertThreshold")) {
|
||||
this.cpuAlertThreshold = this.alertThreshold;
|
||||
}
|
||||
if (!this.memoryAlertThreshold || this.isModified("alertThreshold")) {
|
||||
this.memoryAlertThreshold = this.alertThreshold;
|
||||
}
|
||||
if (!this.diskAlertThreshold || this.isModified("alertThreshold")) {
|
||||
this.diskAlertThreshold = this.alertThreshold;
|
||||
}
|
||||
if (!this.tempAlertThreshold || this.isModified("alertThreshold")) {
|
||||
this.tempAlertThreshold = this.alertThreshold;
|
||||
}
|
||||
next();
|
||||
});
|
||||
|
||||
NotificationSchema.pre("findOneAndUpdate", function (next) {
|
||||
const update = this.getUpdate();
|
||||
if (update.alertThreshold) {
|
||||
update.cpuAlertThreshold = update.alertThreshold;
|
||||
update.memoryAlertThreshold = update.alertThreshold;
|
||||
update.diskAlertThreshold = update.alertThreshold;
|
||||
update.tempAlertThreshold = update.alertThreshold;
|
||||
}
|
||||
next();
|
||||
});
|
||||
|
||||
export default mongoose.model("Notification", NotificationSchema);
|
||||
Executable
+50
@@ -0,0 +1,50 @@
|
||||
import { Schema, model, type Types } from "mongoose";
|
||||
import type { Notification, NotificationChannel } from "@/types/notification.js";
|
||||
|
||||
interface NotificationDocument extends Omit<Notification, "id" | "userId" | "teamId" | "createdAt" | "updatedAt"> {
|
||||
_id: Types.ObjectId;
|
||||
userId: Types.ObjectId;
|
||||
teamId: Types.ObjectId;
|
||||
createdAt: Date;
|
||||
updatedAt: Date;
|
||||
}
|
||||
|
||||
const NotificationSchema = new Schema<NotificationDocument>(
|
||||
{
|
||||
userId: {
|
||||
type: Schema.Types.ObjectId,
|
||||
ref: "User",
|
||||
immutable: true,
|
||||
required: true,
|
||||
},
|
||||
teamId: {
|
||||
type: Schema.Types.ObjectId,
|
||||
ref: "Team",
|
||||
immutable: true,
|
||||
required: true,
|
||||
},
|
||||
type: {
|
||||
type: String,
|
||||
enum: ["email", "slack", "discord", "webhook", "pager_duty", "matrix"] as NotificationChannel[],
|
||||
required: true,
|
||||
},
|
||||
notificationName: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
address: { type: String },
|
||||
phone: { type: String },
|
||||
homeserverUrl: { type: String },
|
||||
roomId: { type: String },
|
||||
accessToken: { type: String },
|
||||
},
|
||||
{
|
||||
timestamps: true,
|
||||
}
|
||||
);
|
||||
|
||||
const NotificationModel = model<NotificationDocument>("Notification", NotificationSchema);
|
||||
|
||||
export type { NotificationDocument };
|
||||
export { NotificationModel };
|
||||
export default NotificationModel;
|
||||
@@ -3,7 +3,7 @@ import bcrypt from "bcryptjs";
|
||||
import type { User, UserProfileImage, UserRole } from "@/types/index.js";
|
||||
import { MonitorModel } from "@/db/models/index.js";
|
||||
import Team from "./Team.js";
|
||||
import Notification from "./Notification.js";
|
||||
import NotificationModel from "./Notification.js";
|
||||
|
||||
type UserDocumentBase = Omit<User, "id" | "teamId" | "createdAt" | "updatedAt"> & {
|
||||
teamId?: Types.ObjectId;
|
||||
@@ -76,7 +76,7 @@ UserSchema.pre("findOneAndDelete", async function (next) {
|
||||
await Team.deleteOne({ _id: userToDelete.teamId });
|
||||
await MonitorModel.deleteMany({ userId: userToDelete._id });
|
||||
await this.model.deleteMany({ teamId: userToDelete.teamId, _id: { $ne: userToDelete._id } });
|
||||
await Notification.deleteMany({ teamId: userToDelete.teamId });
|
||||
await NotificationModel.deleteMany({ teamId: userToDelete.teamId });
|
||||
}
|
||||
next();
|
||||
} catch (error) {
|
||||
|
||||
@@ -21,3 +21,6 @@ export { default as AppSettingsModel } from "@/db/models/AppSettings.js";
|
||||
|
||||
export * from "@/db/models/RecoveryToken.js";
|
||||
export { default as RecoveryTokenModel } from "@/db/models/RecoveryToken.js";
|
||||
|
||||
export * from "@/db/models/Notification.js";
|
||||
export { default as NotificationModel } from "@/db/models/Notification.js";
|
||||
@@ -1,5 +1,3 @@
|
||||
// import Notification from "../../models/Notification.js";
|
||||
// import Monitor from "../../models/Monitor.js";
|
||||
const SERVICE_NAME = "notificationModule";
|
||||
|
||||
class NotificationModule {
|
||||
|
||||
@@ -21,3 +21,6 @@ export { default as MongoRecoveryTokensRepository } from "@/repositories/recover
|
||||
|
||||
export * from "@/repositories/settings/ISettingsRepository.js";
|
||||
export { default as MongoSettingsRepository } from "@/repositories/settings/MongoSettingsRepository.js";
|
||||
|
||||
export * from "@/repositories/notifications/INotificationsRepository.js";
|
||||
export { default as MongoNotificationsRepository } from "@/repositories/notifications/MongoNotificationsRepository.js";
|
||||
@@ -0,0 +1,8 @@
|
||||
import type { Notification } from "@/types/index.js";
|
||||
export interface INotificationsRepository {
|
||||
// create
|
||||
// fetch
|
||||
findNotificationsByIds(ids: string[]): Promise<Notification[]>;
|
||||
// update
|
||||
// delete
|
||||
}
|
||||
@@ -0,0 +1,46 @@
|
||||
import mongoose from "mongoose";
|
||||
import { NotificationModel, type NotificationDocument } from "@/db/models/index.js";
|
||||
import { INotificationsRepository } from "@/repositories/index.js";
|
||||
import type { Notification } from "@/types/index.js";
|
||||
|
||||
class MongoNotificationsRepository implements INotificationsRepository {
|
||||
private mapDocuments = (documents: NotificationDocument[]): Notification[] => {
|
||||
if (!documents?.length) {
|
||||
return [];
|
||||
}
|
||||
return documents.map((doc) => this.toEntity(doc));
|
||||
};
|
||||
|
||||
private toEntity = (doc: NotificationDocument): Notification => {
|
||||
const toStringId = (value: mongoose.Types.ObjectId | string): string => {
|
||||
return value instanceof mongoose.Types.ObjectId ? value.toString() : value;
|
||||
};
|
||||
|
||||
const toDateString = (value: Date | string): string => {
|
||||
return value instanceof Date ? value.toISOString() : value;
|
||||
};
|
||||
|
||||
return {
|
||||
id: toStringId(doc._id),
|
||||
userId: toStringId(doc.userId),
|
||||
teamId: toStringId(doc.teamId),
|
||||
type: doc.type,
|
||||
notificationName: doc.notificationName,
|
||||
address: doc.address ?? undefined,
|
||||
phone: doc.phone ?? undefined,
|
||||
homeserverUrl: doc.homeserverUrl ?? undefined,
|
||||
roomId: doc.roomId ?? undefined,
|
||||
accessToken: doc.accessToken ?? undefined,
|
||||
createdAt: toDateString(doc.createdAt),
|
||||
updatedAt: toDateString(doc.updatedAt),
|
||||
};
|
||||
};
|
||||
|
||||
findNotificationsByIds = async (ids: string[]) => {
|
||||
const mongoIds = ids.map((id) => new mongoose.Types.ObjectId(id));
|
||||
const documents = await NotificationModel.find({ _id: { $in: mongoIds } });
|
||||
return this.mapDocuments(documents);
|
||||
};
|
||||
}
|
||||
|
||||
export default MongoNotificationsRepository;
|
||||
@@ -305,7 +305,7 @@ class IncidentService {
|
||||
|
||||
const groupedByMonitor = {};
|
||||
for (const item of createItems) {
|
||||
if (!item.monitor || !item.monitor._id || !item.check || !item.check._id) {
|
||||
if (!item.monitor || !item.monitor.id || !item.check || !item.check.id) {
|
||||
this.logger.warn({
|
||||
service: this.SERVICE_NAME,
|
||||
method: "processIncidentsFromBuffer",
|
||||
@@ -315,7 +315,7 @@ class IncidentService {
|
||||
continue;
|
||||
}
|
||||
|
||||
const monitorId = item.monitor._id.toString();
|
||||
const monitorId = item.monitor.id.toString();
|
||||
if (!groupedByMonitor[monitorId]) {
|
||||
groupedByMonitor[monitorId] = [];
|
||||
}
|
||||
@@ -341,20 +341,20 @@ class IncidentService {
|
||||
for (const item of items) {
|
||||
checksToAddToIncidents.push({
|
||||
incidentId,
|
||||
checkId: item.check._id.toString(),
|
||||
checkId: item.check.id.toString(),
|
||||
});
|
||||
}
|
||||
} else {
|
||||
const firstItem = items[0];
|
||||
const incidentData = {
|
||||
monitorId: firstItem.monitor._id,
|
||||
monitorId: firstItem.monitor.id,
|
||||
teamId: firstItem.monitor.teamId,
|
||||
type: firstItem.monitor.type,
|
||||
startTime: new Date(),
|
||||
status: true,
|
||||
message: firstItem.check.message || null,
|
||||
statusCode: firstItem.check.statusCode || null,
|
||||
checks: [firstItem.check._id],
|
||||
checks: [firstItem.check.id],
|
||||
};
|
||||
|
||||
newIncidentsToCreate.push({
|
||||
@@ -380,7 +380,7 @@ class IncidentService {
|
||||
for (const remainingItem of item.remainingChecks) {
|
||||
checksToAddToIncidents.push({
|
||||
incidentId,
|
||||
checkId: remainingItem.check._id.toString(),
|
||||
checkId: remainingItem.check.id,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,2 +1,13 @@
|
||||
export * from "@/service/business/monitorService.js";
|
||||
export * from "@/service/infrastructure/networkService.js";
|
||||
export * from "@/service/infrastructure/notificationProviders/discord.js";
|
||||
export * from "@/service/infrastructure/notificationProviders/email.js";
|
||||
export * from "@/service/infrastructure/notificationProviders/INotificationProvider.js";
|
||||
export * from "@/service/infrastructure/notificationProviders/pagerduty.js";
|
||||
export * from "@/service/infrastructure/notificationProviders/matrix.js";
|
||||
export * from "@/service/infrastructure/notificationProviders/slack.js";
|
||||
export * from "@/service/infrastructure/notificationProviders/webhook.js";
|
||||
export * from "@/service/infrastructure/notificationsService.js";
|
||||
export * from "@/service/infrastructure/statusService.js";
|
||||
export * from "@/service/infrastructure/notificationProviders/webhook.js";
|
||||
export * from "@/service/infrastructure/notificationProviders/matrix.js";
|
||||
@@ -1,15 +1,15 @@
|
||||
const SERVICE_NAME = "JobQueueHelper";
|
||||
import type { Monitor } from "@/types/monitor.js";
|
||||
import { AppError } from "@/utils/AppError.js";
|
||||
import { INetworkService } from "@/service/index.js";
|
||||
import { INetworkService, INotificationsService, IStatusService } from "@/service/index.js";
|
||||
class SuperSimpleQueueHelper {
|
||||
static SERVICE_NAME = SERVICE_NAME;
|
||||
|
||||
private db: any;
|
||||
private logger: any;
|
||||
private networkService: INetworkService;
|
||||
private statusService: any;
|
||||
private notificationService: any;
|
||||
private statusService: IStatusService;
|
||||
private notificationsService: INotificationsService;
|
||||
private checkService: any;
|
||||
private buffer: any;
|
||||
|
||||
@@ -18,15 +18,15 @@ class SuperSimpleQueueHelper {
|
||||
logger,
|
||||
networkService,
|
||||
statusService,
|
||||
notificationService,
|
||||
notificationsService,
|
||||
checkService,
|
||||
buffer,
|
||||
}: {
|
||||
db: any;
|
||||
logger: any;
|
||||
networkService: INetworkService;
|
||||
statusService: any;
|
||||
notificationService: any;
|
||||
statusService: IStatusService;
|
||||
notificationsService: INotificationsService;
|
||||
checkService: any;
|
||||
buffer: any;
|
||||
}) {
|
||||
@@ -34,9 +34,9 @@ class SuperSimpleQueueHelper {
|
||||
this.logger = logger;
|
||||
this.networkService = networkService;
|
||||
this.statusService = statusService;
|
||||
this.notificationService = notificationService;
|
||||
this.checkService = checkService;
|
||||
this.buffer = buffer;
|
||||
this.notificationsService = notificationsService;
|
||||
}
|
||||
|
||||
get serviceName() {
|
||||
@@ -78,13 +78,9 @@ class SuperSimpleQueueHelper {
|
||||
// Step 4. Update monitor status
|
||||
const statusChangeResult = await this.statusService.updateMonitorStatus(status, check);
|
||||
|
||||
this.notificationService
|
||||
.handleNotifications({
|
||||
...status,
|
||||
monitor: statusChangeResult.monitor,
|
||||
prevStatus: statusChangeResult.prevStatus,
|
||||
statusChanged: statusChangeResult.statusChanged,
|
||||
})
|
||||
// Step 5 handle notifications (best effort, continue even in event of failure, don't wait)
|
||||
this.notificationsService
|
||||
.handleNotifications(monitor, status, statusChangeResult.prevStatus, statusChangeResult.statusChanged)
|
||||
.catch((error: any) => {
|
||||
this.logger.error({
|
||||
message: error.message,
|
||||
|
||||
@@ -6,7 +6,6 @@ const SERVICE_NAME = "BufferService";
|
||||
class BufferService {
|
||||
static SERVICE_NAME = SERVICE_NAME;
|
||||
private BUFFER_TIMEOUT: number;
|
||||
private db: any;
|
||||
private logger: any;
|
||||
private incidentService: any;
|
||||
private SERVICE_NAME: string;
|
||||
@@ -15,21 +14,8 @@ class BufferService {
|
||||
private bufferTimer: NodeJS.Timeout | null = null;
|
||||
private checksService: any;
|
||||
|
||||
constructor({
|
||||
db,
|
||||
logger,
|
||||
envSettings,
|
||||
incidentService,
|
||||
checkService,
|
||||
}: {
|
||||
db: any;
|
||||
logger: any;
|
||||
envSettings: any;
|
||||
incidentService: any;
|
||||
checkService: any;
|
||||
}) {
|
||||
constructor({ logger, incidentService, checkService }: { logger: any; incidentService: any; checkService: any }) {
|
||||
this.BUFFER_TIMEOUT = config.NODE_ENV === "development" ? 10 : 1000 * 60 * 1; // 1 minute
|
||||
this.db = db;
|
||||
this.logger = logger;
|
||||
this.incidentService = incidentService;
|
||||
this.checksService = checkService;
|
||||
|
||||
@@ -0,0 +1,6 @@
|
||||
import type { Monitor, Notification, Alert, MonitorStatusResponse } from "@/types/index.js";
|
||||
|
||||
export interface INotificationProvider {
|
||||
sendAlert: (notification: Notification, monitor: Monitor, monitorStatusResponse: MonitorStatusResponse) => Promise<boolean>;
|
||||
sendTestAlert(notification: Notification): Promise<boolean>;
|
||||
}
|
||||
@@ -0,0 +1,72 @@
|
||||
const SERVICE_NAME = "DiscordProvider";
|
||||
import type { Monitor, Notification, MonitorStatusResponse } from "@/types/index.js";
|
||||
import { INotificationProvider } from "@/service/index.js";
|
||||
import { buildHardwareAlerts, buildDiscordBody, getTestMessage } from "@/service/infrastructure/notificationProviders/utils.js";
|
||||
import got from "got";
|
||||
|
||||
export class DiscordProvider implements INotificationProvider {
|
||||
private logger: any;
|
||||
|
||||
constructor(logger: any) {
|
||||
this.logger = logger;
|
||||
}
|
||||
private getHardwareContent = (monitor: Monitor, monitorStatusResponse: MonitorStatusResponse) => {
|
||||
const { discordPayload } = buildHardwareAlerts("HOST_PLACEHOLDER", monitor, monitorStatusResponse);
|
||||
return discordPayload;
|
||||
};
|
||||
|
||||
sendAlert = async (notification: Notification, monitor: Monitor, monitorStatusResponse: MonitorStatusResponse) => {
|
||||
let body;
|
||||
if (monitor.type === "hardware") {
|
||||
body = this.getHardwareContent(monitor, monitorStatusResponse);
|
||||
} 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;
|
||||
}
|
||||
try {
|
||||
await got.post(notification.address, {
|
||||
json: { content: getTestMessage() },
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
});
|
||||
return true;
|
||||
} catch (error) {
|
||||
const err = error as Error;
|
||||
this.logger.warn({
|
||||
message: "Discord test alert failed",
|
||||
service: SERVICE_NAME,
|
||||
method: "sendTestAlert",
|
||||
stack: err?.stack,
|
||||
});
|
||||
return false;
|
||||
}
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,61 @@
|
||||
const SERVICE_NAME = "EmailProvider";
|
||||
import type { Monitor, Notification, MonitorStatusResponse } from "@/types/index.js";
|
||||
import { INotificationProvider } from "@/service/index.js";
|
||||
import { buildHardwareAlerts, buildHardwareEmail, buildEmail, buildTestEmail } from "@/service/infrastructure/notificationProviders/utils.js";
|
||||
|
||||
export class EmailProvider implements INotificationProvider {
|
||||
private emailService: any;
|
||||
private logger: any;
|
||||
|
||||
constructor(emailService: any, logger: any) {
|
||||
this.emailService = emailService;
|
||||
this.logger = logger;
|
||||
}
|
||||
|
||||
private buildHardwareEmail = (monitor: Monitor, monitorStatusResponse: MonitorStatusResponse) => {
|
||||
const { alertsToSend } = buildHardwareAlerts("HOST_PLACEHOLDER", monitor, monitorStatusResponse);
|
||||
const html = buildHardwareEmail(this.emailService, monitor, alertsToSend);
|
||||
return html;
|
||||
};
|
||||
|
||||
async sendAlert(notification: Notification, monitor: Monitor, monitorStatusResponse: MonitorStatusResponse): Promise<boolean> {
|
||||
const subject = `Monitor ${monitor.name} infrastructure alerts`;
|
||||
let html;
|
||||
if (monitor.type === "hardware") {
|
||||
html = this.buildHardwareEmail(monitor, monitorStatusResponse);
|
||||
} else {
|
||||
html = await buildEmail(this.emailService, monitor);
|
||||
}
|
||||
|
||||
if (!notification.address) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const messageId = await this.emailService.sendEmail(notification.address, subject, html);
|
||||
if (!messageId) {
|
||||
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);
|
||||
|
||||
const messageId = await this.emailService.sendEmail(notification.address, subject, html);
|
||||
if (!messageId) {
|
||||
this.logger.warn({
|
||||
message: "Email test alert failed",
|
||||
service: SERVICE_NAME,
|
||||
method: "sendTestAlert",
|
||||
});
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
}
|
||||
@@ -1,34 +0,0 @@
|
||||
const SERVICE_NAME = "Matrix";
|
||||
|
||||
class Matrix {
|
||||
static SERVICE_NAME = SERVICE_NAME;
|
||||
|
||||
constructor({ networkService, logger }) {
|
||||
this.networkService = networkService;
|
||||
this.logger = logger;
|
||||
}
|
||||
|
||||
get serviceName() {
|
||||
return Matrix.SERVICE_NAME;
|
||||
}
|
||||
|
||||
async send({ friendlyName, homeserverUrl, accessToken, roomId, message, monitorName }) {
|
||||
const title = `Checkmate status for ${monitorName}`;
|
||||
const formattedMessage = `## ${title}\n${message}`;
|
||||
try {
|
||||
await this.networkService.requestMatrix({
|
||||
homeserverUrl,
|
||||
accessToken,
|
||||
roomId,
|
||||
message: formattedMessage,
|
||||
});
|
||||
this.logger.info(`Successfully sent Matrix notification for ${friendlyName}`);
|
||||
return true;
|
||||
} catch (error) {
|
||||
this.logger.error(`Failed to send Matrix notification for ${friendlyName}: ${error.message}`);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export default Matrix;
|
||||
@@ -0,0 +1,99 @@
|
||||
const SERVICE_NAME = "MatrixProvider";
|
||||
import got from "got";
|
||||
import type { INotificationProvider } from "@/service/index.js";
|
||||
import type { Notification, Monitor, MonitorStatusResponse } from "@/types/index.js";
|
||||
import {
|
||||
buildHardwareAlerts,
|
||||
buildHardwareWebhookBody,
|
||||
buildWebhookBody,
|
||||
getTestMessage,
|
||||
} from "@/service/infrastructure/notificationProviders/utils.js";
|
||||
|
||||
export class MatrixProvider implements INotificationProvider {
|
||||
private logger: any;
|
||||
|
||||
constructor(logger: any) {
|
||||
this.logger = logger;
|
||||
}
|
||||
private getHardwareContent = (monitor: Monitor, monitorStatusResponse: MonitorStatusResponse) => {
|
||||
const { alertsToSend } = buildHardwareAlerts("HOST_PLACEHOLDER", monitor, monitorStatusResponse);
|
||||
const body = buildHardwareWebhookBody(alertsToSend, monitor);
|
||||
return body;
|
||||
};
|
||||
|
||||
private getContent = (monitor: Monitor, monitorStatusResponse: MonitorStatusResponse) => {
|
||||
const body = buildWebhookBody(monitor, monitorStatusResponse);
|
||||
return body;
|
||||
};
|
||||
|
||||
sendAlert = async (notification: Notification, monitor: Monitor, monitorStatusResponse: MonitorStatusResponse) => {
|
||||
const { homeserverUrl, accessToken, roomId } = notification;
|
||||
|
||||
let content;
|
||||
if (monitor.type === "hardware") {
|
||||
content = this.getHardwareContent(monitor, monitorStatusResponse);
|
||||
} else {
|
||||
content = this.getContent(monitor, monitorStatusResponse);
|
||||
}
|
||||
|
||||
const title = `Checkmate status for ${monitor.name}`;
|
||||
|
||||
const formattedMessage = `## ${title}\n${content}`;
|
||||
|
||||
const url = `${homeserverUrl}/_matrix/client/v3/rooms/${roomId}/send/m.room.message?access_token=${accessToken}`;
|
||||
const body = {
|
||||
msgtype: "m.text",
|
||||
body: formattedMessage,
|
||||
format: "org.matrix.custom.html",
|
||||
formatted_body: formattedMessage,
|
||||
};
|
||||
try {
|
||||
await got.post(url, {
|
||||
json: body,
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
});
|
||||
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;
|
||||
if (!homeserverUrl || !accessToken || !roomId) {
|
||||
return false;
|
||||
}
|
||||
const url = `${homeserverUrl}/_matrix/client/v3/rooms/${roomId}/send/m.room.message?access_token=${accessToken}`;
|
||||
const body = {
|
||||
msgtype: "m.text",
|
||||
body: getTestMessage(),
|
||||
};
|
||||
try {
|
||||
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 test alert failed",
|
||||
service: SERVICE_NAME,
|
||||
method: "sendTestAlert",
|
||||
stack: err?.stack,
|
||||
});
|
||||
return false;
|
||||
}
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,91 @@
|
||||
const SERVICE_NAME = "PagerDutyProvider";
|
||||
import got from "got";
|
||||
import type { Monitor, Notification, MonitorStatusResponse } from "@/types/index.js";
|
||||
import { INotificationProvider } from "@/service/index.js";
|
||||
import {
|
||||
buildHardwareAlerts,
|
||||
buildHardwareWebhookBody,
|
||||
buildWebhookBody,
|
||||
getTestMessage,
|
||||
} from "@/service/infrastructure/notificationProviders/utils.js";
|
||||
|
||||
export class PagerDutyProvider implements INotificationProvider {
|
||||
private logger: any;
|
||||
|
||||
constructor(logger: any) {
|
||||
this.logger = logger;
|
||||
}
|
||||
private getHardwareContent = (monitor: Monitor, monitorStatusResponse: MonitorStatusResponse) => {
|
||||
const { alertsToSend } = buildHardwareAlerts("HOST_PLACEHOLDER", monitor, monitorStatusResponse);
|
||||
const body = buildHardwareWebhookBody(alertsToSend, monitor);
|
||||
return body;
|
||||
};
|
||||
|
||||
private getContent = (monitor: Monitor, monitorStatusResponse: MonitorStatusResponse) => {
|
||||
const body = buildWebhookBody(monitor, monitorStatusResponse);
|
||||
return body;
|
||||
};
|
||||
|
||||
async sendAlert(notification: Notification, monitor: Monitor, monitorStatusResponse: MonitorStatusResponse): Promise<boolean> {
|
||||
let body;
|
||||
if (monitor.type === "hardware") {
|
||||
body = this.getHardwareContent(monitor, monitorStatusResponse);
|
||||
} else {
|
||||
body = this.getContent(monitor, monitorStatusResponse);
|
||||
}
|
||||
|
||||
try {
|
||||
await got.post("https://events.pagerduty.com/v2/enqueue", {
|
||||
json: {
|
||||
routing_key: notification.address,
|
||||
event_action: "trigger",
|
||||
payload: {
|
||||
summary: body,
|
||||
severity: "critical",
|
||||
source: monitor.url,
|
||||
timestamp: new Date().toISOString(),
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
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 {
|
||||
await got.post("https://events.pagerduty.com/v2/enqueue", {
|
||||
json: {
|
||||
routing_key: notification.address,
|
||||
event_action: "trigger",
|
||||
payload: {
|
||||
summary: getTestMessage(),
|
||||
severity: "info",
|
||||
source: "checkmate",
|
||||
timestamp: new Date().toISOString(),
|
||||
},
|
||||
},
|
||||
responseType: "json",
|
||||
});
|
||||
return true;
|
||||
} catch (error) {
|
||||
const err = error as Error;
|
||||
this.logger.warn({
|
||||
message: "PagerDuty test alert failed",
|
||||
service: SERVICE_NAME,
|
||||
method: "sendTestAlert",
|
||||
stack: err?.stack,
|
||||
});
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,85 @@
|
||||
const SERVICE_NAME = "SlackProvider";
|
||||
import type { Monitor, Notification, MonitorStatusResponse } from "@/types/index.js";
|
||||
import { INotificationProvider } from "@/service/index.js";
|
||||
import {
|
||||
buildHardwareAlerts,
|
||||
buildHardwareWebhookBody,
|
||||
buildWebhookBody,
|
||||
getTestMessage,
|
||||
} from "@/service/infrastructure/notificationProviders/utils.js";
|
||||
import got, { HTTPError } from "got";
|
||||
|
||||
export class SlackProvider implements INotificationProvider {
|
||||
private logger: any;
|
||||
|
||||
constructor(logger: any) {
|
||||
this.logger = logger;
|
||||
}
|
||||
private getHardwareContent = (monitor: Monitor, monitorStatusResponse: MonitorStatusResponse) => {
|
||||
const { alertsToSend } = buildHardwareAlerts("HOST_PLACEHOLDER", monitor, monitorStatusResponse);
|
||||
const body = buildHardwareWebhookBody(alertsToSend, monitor);
|
||||
return body;
|
||||
};
|
||||
|
||||
private getContent = (monitor: Monitor, monitorStatusResponse: MonitorStatusResponse) => {
|
||||
const body = buildWebhookBody(monitor, monitorStatusResponse);
|
||||
return body;
|
||||
};
|
||||
|
||||
async sendAlert(notification: Notification, monitor: Monitor, monitorStatusResponse: MonitorStatusResponse): Promise<boolean> {
|
||||
let body;
|
||||
if (monitor.type === "hardware") {
|
||||
body = this.getHardwareContent(monitor, monitorStatusResponse);
|
||||
} else {
|
||||
body = this.getContent(monitor, monitorStatusResponse);
|
||||
}
|
||||
|
||||
if (!notification.address) {
|
||||
return false;
|
||||
}
|
||||
|
||||
try {
|
||||
await got.post(notification.address, {
|
||||
json: { text: body },
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
});
|
||||
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) {
|
||||
return false;
|
||||
}
|
||||
|
||||
try {
|
||||
await got.post(notification.address, {
|
||||
json: { text: getTestMessage() },
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
});
|
||||
return true;
|
||||
} catch (error) {
|
||||
const err = error as HTTPError;
|
||||
this.logger.warn({
|
||||
message: "Slack test alert failed",
|
||||
service: SERVICE_NAME,
|
||||
method: "sendTestAlert",
|
||||
stack: err?.stack,
|
||||
});
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,202 @@
|
||||
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 } => {
|
||||
const thresholds = monitor.thresholds || {};
|
||||
const { usage_cpu: cpuThreshold = -1, usage_memory: memoryThreshold = -1, usage_disk: diskThreshold = -1 } = thresholds;
|
||||
|
||||
const payload = networkResponse?.payload as HardwareStatusPayload;
|
||||
const metrics = payload.data || {};
|
||||
const { cpu: { usage_percent: cpuUsage = -1 } = {}, memory: { usage_percent: memoryUsage = -1 } = {}, disk = [] } = metrics;
|
||||
|
||||
const alerts: 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,
|
||||
};
|
||||
|
||||
const alertsToSend = [];
|
||||
const discordEmbeds = [];
|
||||
const monitorInfoFields = [
|
||||
{ name: "Monitor", value: monitor.name, inline: true },
|
||||
{ name: "URL", value: monitor.url, inline: false },
|
||||
];
|
||||
const goToIncidentField = { name: `Go to incident`, value: `${clientHost}/infrastructure/${monitor.id}` };
|
||||
const formatDiscordAlert = {
|
||||
cpu: () => ({
|
||||
title: "CPU alert",
|
||||
description: `Your current CPU usage (${(cpuUsage * 100).toFixed(0)}%) is above your threshold (${(cpuThreshold * 100).toFixed(0)}%)`,
|
||||
color: 15548997,
|
||||
fields: [...monitorInfoFields, goToIncidentField],
|
||||
footer: { text: "Checkmate" },
|
||||
}),
|
||||
|
||||
memory: () => ({
|
||||
title: "Memory alert",
|
||||
description: `Your current memory usage (${(memoryUsage * 100).toFixed(0)}%) is above your threshold (${(memoryThreshold * 100).toFixed(0)}%)`,
|
||||
color: 15548997,
|
||||
fields: [...monitorInfoFields, goToIncidentField],
|
||||
footer: { text: "Checkmate" },
|
||||
}),
|
||||
|
||||
disk: () => ({
|
||||
title: "Disk alert",
|
||||
description: `Your current disk usage is above your threshold (${(diskThreshold * 100).toFixed(0)}%)`,
|
||||
color: 15548997,
|
||||
footer: { text: "Checkmate" },
|
||||
fields: [
|
||||
...monitorInfoFields,
|
||||
...(Array.isArray(disk) ? disk : []).map((d: any, idx: number) => ({
|
||||
name: `Disk ${idx}`,
|
||||
value: `${(d?.usage_percent * 100).toFixed(0)}%`,
|
||||
inline: true,
|
||||
})),
|
||||
goToIncidentField,
|
||||
],
|
||||
}),
|
||||
};
|
||||
const alertTypes = ["cpu", "memory", "disk"] as const;
|
||||
const alertThresholdKeyMap: Record<(typeof alertTypes)[number], "cpuAlertThreshold" | "memoryAlertThreshold" | "diskAlertThreshold"> = {
|
||||
cpu: "cpuAlertThreshold",
|
||||
memory: "memoryAlertThreshold",
|
||||
disk: "diskAlertThreshold",
|
||||
};
|
||||
for (const type of alertTypes) {
|
||||
const thresholdKey = alertThresholdKeyMap[type];
|
||||
// Iterate over each alert type to see if any need to be decremented
|
||||
if (alerts[type] === true) {
|
||||
const nextValue = ((monitor[thresholdKey] ?? monitor.alertThreshold) as number) - 1;
|
||||
monitor[thresholdKey] = nextValue; // Decrement threshold if an alert is triggered
|
||||
|
||||
if (monitor[thresholdKey] <= 0) {
|
||||
// If threshold drops below 0, reset and send notification
|
||||
monitor[thresholdKey] = monitor.alertThreshold;
|
||||
|
||||
const formatAlert = {
|
||||
cpu: () => `Your current CPU usage (${(cpuUsage * 100).toFixed(0)}%) is above your threshold (${(cpuThreshold * 100).toFixed(0)}%)`,
|
||||
memory: () =>
|
||||
`Your current memory usage (${(memoryUsage * 100).toFixed(0)}%) is above your threshold (${(memoryThreshold * 100).toFixed(0)}%)`,
|
||||
disk: () =>
|
||||
`Your current disk usage: ${disk
|
||||
.map((d, idx) => `(Disk${idx}: ${(d?.usage_percent ?? 0 * 100).toFixed(0)}%)`)
|
||||
.join(", ")} is above your threshold (${(diskThreshold * 100).toFixed(0)}%)`,
|
||||
};
|
||||
alertsToSend.push(formatAlert[type]());
|
||||
discordEmbeds.push(formatDiscordAlert[type]());
|
||||
}
|
||||
}
|
||||
}
|
||||
const discordPayload = discordEmbeds.length ? { embeds: discordEmbeds } : null;
|
||||
return { alertsToSend, discordPayload };
|
||||
};
|
||||
|
||||
export const buildHardwareNotificationMessage = (clientHost: string, alerts: any, monitor: Monitor) => {
|
||||
const alertsHeader = [`Monitor: ${monitor.name}`, `URL: ${monitor.url}`];
|
||||
const alertFooter = [`Go to incident: ${clientHost}/infrastructure/${monitor.id}`];
|
||||
const alertText = alerts.length > 0 ? [...alertsHeader, ...alerts, ...alertFooter] : [];
|
||||
return alertText.map((alert) => alert).join("\n");
|
||||
};
|
||||
|
||||
export const buildHardwareWebhookBody = (alerts: string[], monitor: Monitor): string => {
|
||||
const content = alerts.map((alert) => alert).join("\n");
|
||||
return content;
|
||||
};
|
||||
|
||||
export const shouldSendHardwareAlert = (monitor: Monitor, networkResponse: MonitorStatusResponse): boolean => {
|
||||
const thresholds = monitor.thresholds || {};
|
||||
const { usage_cpu: cpuThreshold = -1, usage_memory: memoryThreshold = -1, usage_disk: diskThreshold = -1 } = thresholds;
|
||||
|
||||
const payload = networkResponse?.payload as HardwareStatusPayload;
|
||||
const metrics = payload.data || {};
|
||||
const { cpu: { usage_percent: cpuUsage = -1 } = {}, memory: { usage_percent: memoryUsage = -1 } = {}, disk = [] } = metrics;
|
||||
|
||||
const cpuBreach = cpuThreshold !== -1 && cpuUsage > cpuThreshold;
|
||||
if (cpuBreach && ((monitor.cpuAlertThreshold ?? monitor.alertThreshold) as number) - 1 <= 0) {
|
||||
return true;
|
||||
}
|
||||
|
||||
const memoryBreach = memoryThreshold !== -1 && memoryUsage > memoryThreshold;
|
||||
if (memoryBreach && ((monitor.memoryAlertThreshold ?? monitor.alertThreshold) as number) - 1 <= 0) {
|
||||
return true;
|
||||
}
|
||||
|
||||
const diskBreach = disk?.some((d) => diskThreshold !== -1 && typeof d?.usage_percent === "number" && d?.usage_percent > diskThreshold);
|
||||
if (diskBreach && ((monitor.diskAlertThreshold ?? monitor.alertThreshold) as number) - 1 <= 0) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
};
|
||||
|
||||
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 === false ? "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;
|
||||
};
|
||||
|
||||
export const buildTestEmail = async (emailService: any) => {
|
||||
const context = { testName: "Monitoring System" };
|
||||
const html = await emailService.buildEmail("testEmailTemplate", context);
|
||||
return html;
|
||||
};
|
||||
|
||||
export const getTestMessage = () => {
|
||||
return "This is a test notification from Checkmate";
|
||||
};
|
||||
@@ -0,0 +1,84 @@
|
||||
const SERVICE_NAME = "WebhookProvider";
|
||||
import type { Monitor, Alert, Notification, MonitorStatusResponse } from "@/types/index.js";
|
||||
import { INotificationProvider } from "@/service/index.js";
|
||||
import {
|
||||
buildHardwareAlerts,
|
||||
buildHardwareWebhookBody,
|
||||
buildWebhookBody,
|
||||
getTestMessage,
|
||||
} from "@/service/infrastructure/notificationProviders/utils.js";
|
||||
import got from "got";
|
||||
|
||||
export class WebhookProvider implements INotificationProvider {
|
||||
private logger: any;
|
||||
|
||||
constructor(logger: any) {
|
||||
this.logger = logger;
|
||||
}
|
||||
private getHardwareContent = (monitor: Monitor, monitorStatusResponse: MonitorStatusResponse) => {
|
||||
const { alertsToSend } = buildHardwareAlerts("HOST_PLACEHOLDER", monitor, monitorStatusResponse);
|
||||
const body = buildHardwareWebhookBody(alertsToSend, monitor);
|
||||
return body;
|
||||
};
|
||||
|
||||
private getContent = (monitor: Monitor, monitorStatusResponse: MonitorStatusResponse): string => {
|
||||
const body = buildWebhookBody(monitor, monitorStatusResponse);
|
||||
return body;
|
||||
};
|
||||
|
||||
sendAlert = async (notification: Notification, monitor: Monitor, monitorStatusResponse: MonitorStatusResponse) => {
|
||||
let body;
|
||||
if (monitor.type === "hardware") {
|
||||
body = this.getHardwareContent(monitor, monitorStatusResponse);
|
||||
} else {
|
||||
body = this.getContent(monitor, monitorStatusResponse);
|
||||
}
|
||||
|
||||
if (!notification.address) {
|
||||
return false;
|
||||
}
|
||||
|
||||
try {
|
||||
await got.post(notification.address, {
|
||||
json: { 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;
|
||||
}
|
||||
};
|
||||
|
||||
sendTestAlert = async (notification: Notification) => {
|
||||
if (!notification.address) {
|
||||
return false;
|
||||
}
|
||||
try {
|
||||
await got.post(notification.address, {
|
||||
json: { text: getTestMessage() },
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
});
|
||||
return true;
|
||||
} catch (error) {
|
||||
const err = error as Error;
|
||||
this.logger.warn({
|
||||
message: "Webhook test alert failed",
|
||||
service: SERVICE_NAME,
|
||||
method: "sendTestAlert",
|
||||
stack: err?.stack,
|
||||
});
|
||||
return false;
|
||||
}
|
||||
};
|
||||
}
|
||||
@@ -1,128 +0,0 @@
|
||||
const SERVICE_NAME = "NotificationService";
|
||||
import Matrix from "./notificationProviders/matrix.js";
|
||||
|
||||
class NotificationService {
|
||||
static SERVICE_NAME = SERVICE_NAME;
|
||||
|
||||
constructor({ emailService, db, logger, networkService, stringService, notificationUtils }) {
|
||||
this.emailService = emailService;
|
||||
this.db = db;
|
||||
this.logger = logger;
|
||||
this.networkService = networkService;
|
||||
this.stringService = stringService;
|
||||
this.notificationUtils = notificationUtils;
|
||||
}
|
||||
|
||||
get serviceName() {
|
||||
return NotificationService.SERVICE_NAME;
|
||||
}
|
||||
|
||||
sendNotification = async ({ notification, subject, content, html, discordContent = null, webhookBody = null }) => {
|
||||
const { type, address } = notification;
|
||||
|
||||
if (type === "email") {
|
||||
const messageId = await this.emailService.sendEmail(address, subject, html);
|
||||
if (!messageId) return false;
|
||||
return true;
|
||||
}
|
||||
|
||||
// Create a body for webhooks
|
||||
let body = { text: content };
|
||||
if (type === "discord") {
|
||||
body = !discordContent ? { content } : discordContent;
|
||||
}
|
||||
if (type === "webhook") {
|
||||
body = !webhookBody ? { content } : webhookBody;
|
||||
}
|
||||
if (type === "slack" || type === "discord" || type === "webhook") {
|
||||
const response = await this.networkService.requestWebhook(type, address, body);
|
||||
return response.status;
|
||||
}
|
||||
if (type === "pager_duty") {
|
||||
const response = await this.networkService.requestPagerDuty({
|
||||
message: content,
|
||||
monitorUrl: subject,
|
||||
routingKey: address,
|
||||
});
|
||||
|
||||
return response;
|
||||
}
|
||||
if (type === "matrix") {
|
||||
const { friendlyName, homeserverUrl, accessToken, roomId } = notification;
|
||||
const monitorName = subject;
|
||||
const message = content;
|
||||
const matrix = new Matrix({ networkService: this.networkService, logger: this.logger });
|
||||
const success = await matrix.send({ friendlyName, homeserverUrl, accessToken, roomId, message, monitorName });
|
||||
return success;
|
||||
}
|
||||
};
|
||||
|
||||
async handleNotifications(networkResponse) {
|
||||
const { monitor, statusChanged, prevStatus } = networkResponse;
|
||||
const { type } = monitor;
|
||||
if (type !== "hardware" && statusChanged === false) return false;
|
||||
// if prevStatus is undefined, monitor is resuming, we're done
|
||||
if (type !== "hardware" && prevStatus === undefined) return false;
|
||||
|
||||
const notificationIDs = networkResponse.monitor?.notifications ?? [];
|
||||
if (notificationIDs.length === 0) return false;
|
||||
if (networkResponse.monitor.type === "hardware") {
|
||||
const thresholds = networkResponse?.monitor?.thresholds;
|
||||
|
||||
if (thresholds === undefined) return false; // No thresholds set, we're done
|
||||
const metrics = networkResponse?.payload?.data ?? null;
|
||||
if (metrics === null) return false; // No metrics, we're done
|
||||
|
||||
const [alerts, discordContent] = await this.notificationUtils.buildHardwareAlerts(networkResponse);
|
||||
if (alerts.length === 0) return false;
|
||||
|
||||
const { subject, html } = await this.notificationUtils.buildHardwareEmail(networkResponse, alerts);
|
||||
const content = await this.notificationUtils.buildHardwareNotificationMessage(alerts, monitor);
|
||||
const webhookBody = await this.notificationUtils.buildHardwareWebhookBody(alerts, monitor);
|
||||
const success = await this.notifyAll({ notificationIDs, subject, html, content, discordContent, webhookBody });
|
||||
return success;
|
||||
}
|
||||
|
||||
// Status monitors
|
||||
const { subject, html } = await this.notificationUtils.buildStatusEmail(networkResponse);
|
||||
const [content, discordContent] = await this.notificationUtils.buildWebhookMessage(networkResponse);
|
||||
const success = this.notifyAll({ notificationIDs, subject, html, content, discordContent });
|
||||
return success;
|
||||
}
|
||||
|
||||
async notifyAll({ notificationIDs, subject, html, content, discordContent = null, webhookBody = null }) {
|
||||
const notifications = await this.db.notificationModule.getNotificationsByIds(notificationIDs);
|
||||
// Map each notification to a test promise
|
||||
const promises = notifications.map(async (notification) => {
|
||||
try {
|
||||
await this.sendNotification({ notification, subject, content, html, discordContent, webhookBody });
|
||||
return true;
|
||||
} catch (err) {
|
||||
return false;
|
||||
}
|
||||
});
|
||||
|
||||
const results = await Promise.all(promises);
|
||||
return results.every((r) => r === true);
|
||||
}
|
||||
|
||||
async getTestNotification() {
|
||||
const html = await this.notificationUtils.buildTestEmail();
|
||||
const content = "This is a test notification";
|
||||
const subject = "Test Notification";
|
||||
return { subject, html, content };
|
||||
}
|
||||
|
||||
async testAllNotifications(notificationIDs) {
|
||||
const { subject, html, content } = await this.getTestNotification();
|
||||
return this.notifyAll({ notificationIDs, subject, html, content });
|
||||
}
|
||||
|
||||
async sendTestNotification(notification) {
|
||||
const { subject, html, content } = await this.getTestNotification();
|
||||
const success = await this.sendNotification({ notification, subject, content, html });
|
||||
return success;
|
||||
}
|
||||
}
|
||||
|
||||
export default NotificationService;
|
||||
@@ -1,205 +0,0 @@
|
||||
const SERVICE_NAME = "NotificationUtils";
|
||||
|
||||
class NotificationUtils {
|
||||
static SERVICE_NAME = SERVICE_NAME;
|
||||
|
||||
constructor({ stringService, emailService, settingsService }) {
|
||||
this.stringService = stringService;
|
||||
this.emailService = emailService;
|
||||
this.settingsService = settingsService;
|
||||
}
|
||||
|
||||
get serviceName() {
|
||||
return NotificationUtils.SERVICE_NAME;
|
||||
}
|
||||
|
||||
buildTestEmail = async () => {
|
||||
const context = { testName: "Monitoring System" };
|
||||
const html = await this.emailService.buildEmail("testEmailTemplate", context);
|
||||
return html;
|
||||
};
|
||||
|
||||
buildStatusEmail = async (networkResponse) => {
|
||||
const { monitor, status, prevStatus } = networkResponse;
|
||||
const template = prevStatus === false ? "serverIsUpTemplate" : "serverIsDownTemplate";
|
||||
const context = { monitor: monitor.name, url: monitor.url };
|
||||
const subject = `Monitor ${monitor.name} is ${status === true ? "up" : "down"}`;
|
||||
const html = await this.emailService.buildEmail(template, context);
|
||||
return { subject, html };
|
||||
};
|
||||
|
||||
buildWebhookMessage = (networkResponse) => {
|
||||
const { monitor, status, code, timestamp } = networkResponse;
|
||||
// Format timestamp using the local system timezone
|
||||
const formatTime = (timestamp) => {
|
||||
const date = new Date(timestamp);
|
||||
|
||||
// Get timezone abbreviation and format the date
|
||||
const timeZoneAbbr = date.toLocaleTimeString("en-US", { timeZoneName: "short" }).split(" ").pop();
|
||||
|
||||
// Format the date with readable format
|
||||
return (
|
||||
date
|
||||
.toLocaleString("en-US", {
|
||||
year: "numeric",
|
||||
month: "2-digit",
|
||||
day: "2-digit",
|
||||
hour: "2-digit",
|
||||
minute: "2-digit",
|
||||
second: "2-digit",
|
||||
hour12: false,
|
||||
})
|
||||
.replace(/(\d+)\/(\d+)\/(\d+),\s/, "$3-$1-$2 ") +
|
||||
" " +
|
||||
timeZoneAbbr
|
||||
);
|
||||
};
|
||||
|
||||
// Get formatted time
|
||||
const formattedTime = timestamp ? formatTime(timestamp) : formatTime(new Date().getTime());
|
||||
|
||||
// Create different messages based on status with extra spacing
|
||||
let messageText;
|
||||
if (status === true) {
|
||||
messageText = this.stringService.monitorUpAlert
|
||||
.replace("{monitorName}", monitor.name)
|
||||
.replace("{time}", formattedTime)
|
||||
.replace("{code}", code || "Unknown");
|
||||
} else {
|
||||
messageText = this.stringService.monitorDownAlert
|
||||
.replace("{monitorName}", monitor.name)
|
||||
.replace("{time}", formattedTime)
|
||||
.replace("{code}", code || "Unknown");
|
||||
}
|
||||
const dn = this.stringService.discordNotification;
|
||||
let discordMessageText = {
|
||||
embeds: [
|
||||
{
|
||||
title: status
|
||||
? dn.uptimeAlert.replace("{monitorName}", monitor?.name ?? dn.unknown)
|
||||
: dn.downtimeAlert.replace("{monitorName}", monitor?.name ?? dn.unknown),
|
||||
color: status ? 5763719 : 15548997,
|
||||
|
||||
fields: [
|
||||
{ name: dn.monitor, value: monitor?.name ?? dn.unknown, inline: true },
|
||||
{ name: dn.status, value: status ? dn.up : dn.down, inline: true },
|
||||
{ name: dn.statusCode, value: String(code ?? dn.unknown), inline: true },
|
||||
{ name: dn.time, value: formattedTime, inline: true },
|
||||
{ name: dn.url, value: monitor?.url ?? dn.unknown, inline: false },
|
||||
],
|
||||
footer: { text: dn.checkmate },
|
||||
},
|
||||
],
|
||||
};
|
||||
return [messageText, discordMessageText];
|
||||
};
|
||||
|
||||
buildHardwareAlerts = async (networkResponse) => {
|
||||
const monitor = networkResponse?.monitor;
|
||||
const thresholds = networkResponse?.monitor?.thresholds;
|
||||
const { usage_cpu: cpuThreshold = -1, usage_memory: memoryThreshold = -1, usage_disk: diskThreshold = -1 } = thresholds;
|
||||
|
||||
const metrics = networkResponse?.payload?.data;
|
||||
const { cpu: { usage_percent: cpuUsage = -1 } = {}, memory: { usage_percent: memoryUsage = -1 } = {}, disk = [] } = metrics;
|
||||
|
||||
const { clientHost } = this.settingsService.getSettings();
|
||||
|
||||
const alerts = {
|
||||
cpu: cpuThreshold !== -1 && cpuUsage > cpuThreshold ? true : false,
|
||||
memory: memoryThreshold !== -1 && memoryUsage > memoryThreshold ? true : false,
|
||||
disk: disk?.some((d) => diskThreshold !== -1 && typeof d?.usage_percent === "number" && d?.usage_percent > diskThreshold) ?? false,
|
||||
};
|
||||
|
||||
const alertsToSend = [];
|
||||
const discordEmbeds = [];
|
||||
const monitorInfoFields = [
|
||||
{ name: "Monitor", value: monitor.name, inline: true },
|
||||
{ name: "URL", value: monitor.url, inline: false },
|
||||
];
|
||||
const goToIncidentField = { name: `Go to incident`, value: `${clientHost}/infrastructure/${monitor._id}` };
|
||||
const formatDiscordAlert = {
|
||||
cpu: () => ({
|
||||
title: "CPU alert",
|
||||
description: `Your current CPU usage (${(cpuUsage * 100).toFixed(0)}%) is above your threshold (${(cpuThreshold * 100).toFixed(0)}%)`,
|
||||
color: 15548997,
|
||||
fields: [...monitorInfoFields, goToIncidentField],
|
||||
footer: { text: "Checkmate" },
|
||||
}),
|
||||
|
||||
memory: () => ({
|
||||
title: "Memory alert",
|
||||
description: `Your current memory usage (${(memoryUsage * 100).toFixed(0)}%) is above your threshold (${(memoryThreshold * 100).toFixed(0)}%)`,
|
||||
color: 15548997,
|
||||
fields: [...monitorInfoFields, goToIncidentField],
|
||||
footer: { text: "Checkmate" },
|
||||
}),
|
||||
|
||||
disk: () => ({
|
||||
title: "Disk alert",
|
||||
description: `Your current disk usage is above your threshold (${(diskThreshold * 100).toFixed(0)}%)`,
|
||||
color: 15548997,
|
||||
footer: { text: "Checkmate" },
|
||||
fields: [
|
||||
...monitorInfoFields,
|
||||
...(Array.isArray(disk) ? disk : []).map((d, idx) => ({
|
||||
name: `Disk ${idx}`,
|
||||
value: `${(d?.usage_percent * 100).toFixed(0)}%`,
|
||||
inline: true,
|
||||
})),
|
||||
goToIncidentField,
|
||||
],
|
||||
}),
|
||||
};
|
||||
const alertTypes = ["cpu", "memory", "disk"];
|
||||
for (const type of alertTypes) {
|
||||
// Iterate over each alert type to see if any need to be decremented
|
||||
if (alerts[type] === true) {
|
||||
monitor[`${type}AlertThreshold`]--; // Decrement threshold if an alert is triggered
|
||||
|
||||
if (monitor[`${type}AlertThreshold`] <= 0) {
|
||||
// If threshold drops below 0, reset and send notification
|
||||
monitor[`${type}AlertThreshold`] = monitor.alertThreshold;
|
||||
|
||||
const formatAlert = {
|
||||
cpu: () => `Your current CPU usage (${(cpuUsage * 100).toFixed(0)}%) is above your threshold (${(cpuThreshold * 100).toFixed(0)}%)`,
|
||||
memory: () =>
|
||||
`Your current memory usage (${(memoryUsage * 100).toFixed(0)}%) is above your threshold (${(memoryThreshold * 100).toFixed(0)}%)`,
|
||||
disk: () =>
|
||||
`Your current disk usage: ${disk
|
||||
.map((d, idx) => `(Disk${idx}: ${(d.usage_percent * 100).toFixed(0)}%)`)
|
||||
.join(", ")} is above your threshold (${(diskThreshold * 100).toFixed(0)}%)`,
|
||||
};
|
||||
alertsToSend.push(formatAlert[type]());
|
||||
discordEmbeds.push(formatDiscordAlert[type]());
|
||||
}
|
||||
}
|
||||
}
|
||||
await monitor.save();
|
||||
const discordPayload = discordEmbeds.length ? { embeds: discordEmbeds } : null;
|
||||
return [alertsToSend, discordPayload];
|
||||
};
|
||||
|
||||
buildHardwareEmail = async (networkResponse, alerts) => {
|
||||
const { monitor } = networkResponse;
|
||||
const template = "hardwareIncidentTemplate";
|
||||
const context = { monitor: monitor.name, url: monitor.url, alerts };
|
||||
const subject = `Monitor ${monitor.name} infrastructure alerts`;
|
||||
const html = await this.emailService.buildEmail(template, context);
|
||||
return { subject, html };
|
||||
};
|
||||
|
||||
buildHardwareNotificationMessage = (alerts, monitor) => {
|
||||
const { clientHost } = this.settingsService.getSettings();
|
||||
const alertsHeader = [`Monitor: ${monitor.name}`, `URL: ${monitor.url}`];
|
||||
const alertFooter = [`Go to incident: ${clientHost}/infrastructure/${monitor._id}`];
|
||||
const alertText = alerts.length > 0 ? [...alertsHeader, ...alerts, ...alertFooter] : [];
|
||||
return alertText.map((alert) => alert).join("\n");
|
||||
};
|
||||
buildHardwareWebhookBody = (alerts, monitor) => {
|
||||
const content = alerts.map((alert) => alert).join("\n");
|
||||
const body = { text: content, name: monitor.name, url: monitor.url };
|
||||
return body;
|
||||
};
|
||||
}
|
||||
|
||||
export default NotificationUtils;
|
||||
@@ -0,0 +1,150 @@
|
||||
import type { HardwareStatusPayload, Monitor, MonitorStatusResponse, Notification } from "@/types/index.js";
|
||||
import { shouldSendHardwareAlert } from "@/service/infrastructure/notificationProviders/utils.js";
|
||||
import { INotificationsRepository } from "@/repositories/index.js";
|
||||
import { INotificationProvider } from "./notificationProviders/INotificationProvider.js";
|
||||
export interface INotificationsService {
|
||||
handleNotifications: (
|
||||
monitor: Monitor,
|
||||
monitorStatusResponse: MonitorStatusResponse,
|
||||
prevStatus: boolean | undefined,
|
||||
statusChanged: boolean
|
||||
) => Promise<boolean>;
|
||||
sendTestNotification: (notification: Notification) => Promise<boolean>;
|
||||
testAllNotifications: (notificationIds: string[]) => Promise<boolean>;
|
||||
}
|
||||
|
||||
const SERVICE_NAME = "NotificationsService";
|
||||
|
||||
export class NotificationsService implements INotificationsService {
|
||||
static SERVICE_NAME = SERVICE_NAME;
|
||||
|
||||
private notificationsRepository: INotificationsRepository;
|
||||
private webhookProvider: INotificationProvider;
|
||||
private emailProvider: INotificationProvider;
|
||||
private slackProvider: INotificationProvider;
|
||||
private discordProvider: INotificationProvider;
|
||||
private pagerDutyProvider: INotificationProvider;
|
||||
private matrixProvider: INotificationProvider;
|
||||
private logger: any;
|
||||
|
||||
constructor(
|
||||
notificationsRepository: INotificationsRepository,
|
||||
webhookProvider: INotificationProvider,
|
||||
emailProvider: INotificationProvider,
|
||||
slackProvider: INotificationProvider,
|
||||
discordProvider: INotificationProvider,
|
||||
pagerDutyProvider: INotificationProvider,
|
||||
matrixProvider: INotificationProvider,
|
||||
logger: any
|
||||
) {
|
||||
this.notificationsRepository = notificationsRepository;
|
||||
this.webhookProvider = webhookProvider;
|
||||
this.emailProvider = emailProvider;
|
||||
this.slackProvider = slackProvider;
|
||||
this.discordProvider = discordProvider;
|
||||
this.pagerDutyProvider = pagerDutyProvider;
|
||||
this.matrixProvider = matrixProvider;
|
||||
this.logger = logger;
|
||||
}
|
||||
|
||||
private send = async (notification: Notification, monitor: Monitor, monitorStatusResponse: MonitorStatusResponse): Promise<boolean> => {
|
||||
switch (notification.type) {
|
||||
case "email":
|
||||
return await this.emailProvider.sendAlert(notification, monitor, monitorStatusResponse);
|
||||
case "slack":
|
||||
return await this.slackProvider.sendAlert(notification, monitor, monitorStatusResponse);
|
||||
case "discord":
|
||||
return await this.discordProvider.sendAlert(notification, monitor, monitorStatusResponse);
|
||||
case "pager_duty":
|
||||
return await this.pagerDutyProvider.sendAlert(notification, monitor, monitorStatusResponse);
|
||||
case "matrix":
|
||||
return await this.matrixProvider.sendAlert(notification, monitor, monitorStatusResponse);
|
||||
case "webhook":
|
||||
return await this.webhookProvider.sendAlert(notification, monitor, monitorStatusResponse);
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
||||
private sendNotifications = async (monitor: Monitor, monitorStatusResponse: MonitorStatusResponse) => {
|
||||
const notificationIds = monitor.notifications ?? [];
|
||||
const notifications = await this.notificationsRepository.findNotificationsByIds(notificationIds);
|
||||
const tasks = notifications.map((notification) => this.send(notification, monitor, monitorStatusResponse));
|
||||
const outcomes = await Promise.all(tasks);
|
||||
const succeeded = outcomes.filter(Boolean).length;
|
||||
const failed = outcomes.length - succeeded;
|
||||
if (failed > 0) {
|
||||
this.logger.warn({
|
||||
message: `Notification send completed with ${succeeded} success, ${failed} failure(s)`,
|
||||
service: SERVICE_NAME,
|
||||
method: "getMonitorJob",
|
||||
});
|
||||
}
|
||||
// Return true if all notificaitons succeeded
|
||||
return succeeded === notifications.length;
|
||||
};
|
||||
|
||||
handleNotifications = async (
|
||||
monitor: Monitor,
|
||||
monitorStatusResponse: MonitorStatusResponse,
|
||||
prevStatus: boolean | undefined,
|
||||
statusChanged: boolean
|
||||
) => {
|
||||
const { type } = monitor;
|
||||
const payload = monitorStatusResponse.payload as HardwareStatusPayload;
|
||||
// If this is a non-hardeware type monitor and status did not change, we're done
|
||||
if (type !== "hardware" && statusChanged === false) return false;
|
||||
// if prevStatus is undefined, monitor is resuming, we're done
|
||||
if (type !== "hardware" && prevStatus === undefined) return false;
|
||||
|
||||
// Deal with hardware thresholds
|
||||
if (type === "hardware") {
|
||||
const thresholds = monitor.thresholds;
|
||||
|
||||
if (thresholds === undefined) return false; // No thresholds set, we're done
|
||||
const metrics = payload?.data ?? null;
|
||||
if (metrics === null) return false; // No metrics, we're done
|
||||
|
||||
// We should send a notificaiton
|
||||
|
||||
const shouldSend = shouldSendHardwareAlert(monitor, monitorStatusResponse);
|
||||
if (shouldSend === false) return false;
|
||||
|
||||
return await this.sendNotifications(monitor, monitorStatusResponse);
|
||||
}
|
||||
|
||||
// We should send a notification for non-hardware monitor status change
|
||||
return await this.sendNotifications(monitor, monitorStatusResponse);
|
||||
};
|
||||
|
||||
sendTestNotification = async (notification: Notification) => {
|
||||
switch (notification.type) {
|
||||
case "email":
|
||||
return await this.emailProvider.sendTestAlert(notification);
|
||||
case "slack":
|
||||
return await this.slackProvider.sendTestAlert(notification);
|
||||
case "discord":
|
||||
return await this.discordProvider.sendTestAlert(notification);
|
||||
case "pager_duty":
|
||||
return await this.pagerDutyProvider.sendTestAlert(notification);
|
||||
case "matrix":
|
||||
return await this.matrixProvider.sendTestAlert(notification);
|
||||
case "webhook":
|
||||
return await this.webhookProvider.sendTestAlert(notification);
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
||||
testAllNotifications = async (notificationIds: string[]) => {
|
||||
const notifications = await this.notificationsRepository.findNotificationsByIds(notificationIds);
|
||||
const tasks = notifications.map((notification) => this.sendTestNotification(notification));
|
||||
const outcomes = await Promise.all(tasks);
|
||||
const succeeded = outcomes.filter(Boolean).length;
|
||||
const failed = outcomes.length - succeeded;
|
||||
if (failed > 0) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
};
|
||||
}
|
||||
@@ -12,31 +12,27 @@ import type {
|
||||
} from "@/types/index.js";
|
||||
const SERVICE_NAME = "StatusService";
|
||||
|
||||
class StatusService {
|
||||
export interface IStatusService {
|
||||
updateRunningStats({ monitor, networkResponse }: { monitor: Monitor; networkResponse: any }): Promise<boolean>;
|
||||
getStatusString(status: boolean | undefined): string;
|
||||
handleIncidentForCheck(check: any, monitor: Monitor, action: any, errorContext?: string): Promise<void>;
|
||||
updateMonitorStatus(
|
||||
statusResponse: MonitorStatusResponse<PageSpeedStatusPayload | HardwareStatusPayload | undefined>,
|
||||
check: Check
|
||||
): Promise<StatusChangeResult>;
|
||||
}
|
||||
|
||||
export class StatusService implements IStatusService {
|
||||
static SERVICE_NAME = SERVICE_NAME;
|
||||
private db: any;
|
||||
private logger: any;
|
||||
private buffer: any;
|
||||
private incidentService: any;
|
||||
private monitorsRepository: IMonitorsRepository;
|
||||
|
||||
constructor({
|
||||
db,
|
||||
logger,
|
||||
buffer,
|
||||
incidentService,
|
||||
monitorsRepository,
|
||||
}: {
|
||||
db: any;
|
||||
logger: any;
|
||||
buffer: any;
|
||||
incidentService: any;
|
||||
monitorsRepository: IMonitorsRepository;
|
||||
}) {
|
||||
constructor({ db, logger, buffer, monitorsRepository }: { db: any; logger: any; buffer: any; monitorsRepository: IMonitorsRepository }) {
|
||||
this.db = db;
|
||||
this.logger = logger;
|
||||
this.buffer = buffer;
|
||||
this.incidentService = incidentService;
|
||||
this.monitorsRepository = monitorsRepository;
|
||||
}
|
||||
|
||||
@@ -122,11 +118,10 @@ class StatusService {
|
||||
return "unknown";
|
||||
};
|
||||
|
||||
handleIncidentForCheck = async (check: any, monitor: Monitor, action: any, errorContext = "incident handling") => {
|
||||
handleIncidentForCheck = async (check: Check, monitor: Monitor, action: any, errorContext = "incident handling") => {
|
||||
try {
|
||||
let savedCheck = check;
|
||||
|
||||
if (!check._id) {
|
||||
let savedCheck;
|
||||
if (!check.id) {
|
||||
try {
|
||||
const checkModel = new CheckModel(check);
|
||||
savedCheck = await checkModel.save();
|
||||
@@ -144,7 +139,7 @@ class StatusService {
|
||||
}
|
||||
}
|
||||
|
||||
if (savedCheck && savedCheck._id) {
|
||||
if (savedCheck && savedCheck.id) {
|
||||
try {
|
||||
this.buffer.addIncidentToBuffer({ monitor, check: savedCheck, action });
|
||||
} catch (incidentError: any) {
|
||||
@@ -181,13 +176,10 @@ class StatusService {
|
||||
this.updateRunningStats({ monitor, networkResponse: statusResponse });
|
||||
|
||||
// If the status window size has changed, empty
|
||||
while (monitor.statusWindow.length > monitor.statusWindowSize) {
|
||||
monitor.statusWindow.shift();
|
||||
}
|
||||
|
||||
// Update status sliding window
|
||||
monitor.statusWindow = monitor.statusWindow || [];
|
||||
monitor.statusWindow.push(status);
|
||||
if (monitor.statusWindow.length > monitor.statusWindowSize) {
|
||||
while (monitor.statusWindow.length > monitor.statusWindowSize) {
|
||||
monitor.statusWindow.shift();
|
||||
}
|
||||
|
||||
@@ -195,9 +187,9 @@ class StatusService {
|
||||
monitor.status = status;
|
||||
}
|
||||
|
||||
const prevStatus = monitor.status;
|
||||
let newStatus = monitor.status;
|
||||
let statusChanged = false;
|
||||
const prevStatus = monitor.status;
|
||||
|
||||
// Return early if not enough data points
|
||||
if (monitor.statusWindow.length < monitor.statusWindowSize) {
|
||||
@@ -330,4 +322,3 @@ class StatusService {
|
||||
}
|
||||
};
|
||||
}
|
||||
export default StatusService;
|
||||
@@ -0,0 +1,29 @@
|
||||
import { NotificationChannel } from "@/types/index.js";
|
||||
export interface AlertWebhookPayload {
|
||||
body: Record<string, unknown>;
|
||||
}
|
||||
|
||||
export interface AlertPagerDutyPayload {
|
||||
routingKey: string;
|
||||
monitorUrl?: string;
|
||||
}
|
||||
|
||||
export interface AlertMatrixPayload {
|
||||
friendlyName?: string;
|
||||
homeserverUrl: string;
|
||||
accessToken: string;
|
||||
roomId: string;
|
||||
monitorName: string;
|
||||
}
|
||||
|
||||
export interface Alert {
|
||||
channel: NotificationChannel;
|
||||
address?: string;
|
||||
subject: string;
|
||||
message: string;
|
||||
html?: string;
|
||||
discordContent?: Record<string, unknown> | null;
|
||||
webhook?: AlertWebhookPayload | null;
|
||||
pagerDuty?: AlertPagerDutyPayload | null;
|
||||
matrix?: AlertMatrixPayload | null;
|
||||
}
|
||||
@@ -7,3 +7,5 @@ export * from "@/types/user.js";
|
||||
export * from "@/types/invite.js";
|
||||
export * from "@/types/recoveryToken.js";
|
||||
export * from "@/types/settings.js";
|
||||
export * from "@/types/notification.js";
|
||||
export * from "@/types/alert.js";
|
||||
@@ -13,7 +13,7 @@ import type {
|
||||
MonitorType,
|
||||
} from "@/types/index.js";
|
||||
|
||||
export interface MonitorStatusResponse<T = unknown> {
|
||||
export interface MonitorStatusResponse<T = any> {
|
||||
monitorId: string;
|
||||
teamId: string;
|
||||
type: MonitorType;
|
||||
|
||||
@@ -0,0 +1,17 @@
|
||||
export const NotificationChannels = ["email", "slack", "discord", "webhook", "pager_duty", "matrix"] as const;
|
||||
export type NotificationChannel = (typeof NotificationChannels)[number];
|
||||
|
||||
export interface Notification {
|
||||
id: string;
|
||||
userId: string;
|
||||
teamId: string;
|
||||
type: NotificationChannel;
|
||||
notificationName: string;
|
||||
address?: string;
|
||||
phone?: string;
|
||||
homeserverUrl?: string;
|
||||
roomId?: string;
|
||||
accessToken?: string;
|
||||
createdAt: string;
|
||||
updatedAt: string;
|
||||
}
|
||||
Reference in new issue
Block a user