Merge branch 'bluewave-labs:develop' into network-page-BE-part-1

This commit is contained in:
ThetaOI authored and GitHub committed 2025-06-21 00:09:51 +05:30
commit 24cb4c70a7
16 files changed
+391 -985

No files matched your search

-3
View File
@@ -98,12 +98,9 @@ const useGetNotificationById = (id, setNotification) => {
const notification = response?.data?.data ?? null;
const notificationData = {
userId: notification?.userId,
teamId: notification?.teamId,
address: notification?.address,
notificationName: notification?.notificationName,
type: NOTIFICATION_TYPES.find((type) => type.value === notification?.type)?._id,
config: notification?.config,
};
setNotification(notificationData);
+32 -279
View File
@@ -17,15 +17,17 @@ import {
useEditNotification,
useTestNotification,
} from "../../../Hooks/useNotifications";
import {
notificationEmailValidation,
notificationWebhookValidation,
notificationPagerDutyValidation,
} from "../../../Validation/validation";
import { notificationValidation } from "../../../Validation/validation";
import { createToast } from "../../../Utils/toastUtils";
import { useTranslation } from "react-i18next";
import { useParams } from "react-router-dom";
import { NOTIFICATION_TYPES } from "../utils";
import {
NOTIFICATION_TYPES,
TITLE_MAP,
DESCRIPTION_MAP,
LABEL_MAP,
PLACEHOLDER_MAP,
} from "../utils";
// Setup
@@ -49,11 +51,6 @@ const CreateNotifications = () => {
notificationName: "",
address: "",
type: NOTIFICATION_TYPES[0]._id,
config: {
webhookUrl: "",
platform: "",
routingKey: "",
},
});
const [errors, setErrors] = useState({});
const { t } = useTranslation();
@@ -71,42 +68,17 @@ const CreateNotifications = () => {
type: NOTIFICATION_TYPES.find((type) => type._id === notification.type).value,
};
if (form.type === "slack" || form.type === "discord") {
form.type = "webhook";
}
let error = null;
if (form.type === "email") {
error = notificationEmailValidation.validate(
{ notificationName: form.notificationName, address: form.address },
{ abortEarly: false }
).error;
} else if (form.type === "webhook") {
form.config = {
platform: form.config.platform,
webhookUrl: form.config.webhookUrl,
};
error = notificationWebhookValidation.validate(
{ notificationName: form.notificationName, config: form.config },
{ abortEarly: false }
).error;
} else if (form.type === "pager_duty") {
form.config = {
platform: form.config.platform,
routingKey: form.config.routingKey,
};
error = notificationPagerDutyValidation.validate(
{ notificationName: form.notificationName, config: form.config },
{ abortEarly: false }
).error;
}
error = notificationValidation.validate(form, { abortEarly: false }).error;
if (error) {
const newErrors = {};
error.details.forEach((err) => {
newErrors[err.path[0]] = err.message;
});
console.log(JSON.stringify(newErrors));
console.log(JSON.stringify(form, null, 2));
createToast({ body: "Please check the form for errors." });
setErrors(newErrors);
return;
@@ -124,63 +96,10 @@ const CreateNotifications = () => {
const newNotification = { ...notification, [name]: value };
// Handle config/platform initialization if type is webhook
const type = NOTIFICATION_TYPES.find(
(type) => type._id === newNotification.type
).value;
if (type === "email") {
newNotification.config = null;
} else if (type === "slack" || type === "webhook" || type === "discord") {
newNotification.address = "";
newNotification.config = newNotification.config || {};
if (name === "config") {
newNotification.config = value;
}
if (type === "webhook") {
newNotification.config.platform = "webhook";
}
if (type === "slack") {
newNotification.config.platform = "slack";
}
if (type === "discord") {
newNotification.config.platform = "discord";
}
} else if (type === "pager_duty") {
newNotification.config = newNotification.config || {};
if (name === "config") {
newNotification.config = value;
}
newNotification.config.platform = "pager_duty";
}
// Field-level validation
let fieldError;
if (name === "notificationName") {
const { error } = notificationEmailValidation.extract(name).validate(value);
fieldError = error?.message;
}
if (type === "email" && name === "address") {
const { error } = notificationEmailValidation.extract(name).validate(value);
fieldError = error?.message;
}
if (
(type === "slack" || type === "webhook" || type === "discord") &&
name === "config"
) {
// Validate only webhookUrl inside config
const { error } = notificationWebhookValidation.extract("config").validate(value);
fieldError = error?.message;
}
// Set field-level error
const { error } = notificationValidation.extract(name).validate(value);
setErrors((prev) => ({
...prev,
[name]: fieldError,
[name]: error?.message,
}));
setNotification(newNotification);
@@ -192,36 +111,9 @@ const CreateNotifications = () => {
type: NOTIFICATION_TYPES.find((type) => type._id === notification.type).value,
};
if (form.type === "slack" || form.type === "discord") {
form.type = "webhook";
}
let error = null;
if (form.type === "email") {
error = notificationEmailValidation.validate(
{ notificationName: form.notificationName, address: form.address },
{ abortEarly: false }
).error;
} else if (form.type === "webhook") {
form.config = {
platform: form.config.platform,
webhookUrl: form.config.webhookUrl,
};
error = notificationWebhookValidation.validate(
{ notificationName: form.notificationName, config: form.config },
{ abortEarly: false }
).error;
} else if (form.type === "pager_duty") {
form.config = {
platform: form.config.platform,
routingKey: form.config.routingKey,
};
error = notificationPagerDutyValidation.validate(
{ notificationName: form.notificationName, config: form.config },
{ abortEarly: false }
).error;
}
error = notificationValidation.validate(form, { abortEarly: false }).error;
if (error) {
const newErrors = {};
@@ -236,6 +128,7 @@ const CreateNotifications = () => {
testNotification(form);
};
const type = NOTIFICATION_TYPES.find((type) => type._id === notification.type).value;
return (
<Stack gap={theme.spacing(10)}>
<Breadcrumbs list={BREADCRUMBS} />
@@ -287,164 +180,24 @@ const CreateNotifications = () => {
/>
</Stack>
</ConfigBox>
{notification.type === 1 && (
<ConfigBox>
<Box>
<Typography component="h2">
{t("createNotifications.emailSettings.title")}
</Typography>
<Typography component="p">
{t("createNotifications.emailSettings.description")}
</Typography>
</Box>
<Stack gap={theme.spacing(12)}>
<TextInput
label={t("createNotifications.emailSettings.emailLabel")}
name="address"
placeholder={t("createNotifications.emailSettings.emailPlaceholder")}
value={notification.address}
onChange={onChange}
error={Boolean(errors.address)}
helperText={errors["address"]}
/>
</Stack>
</ConfigBox>
)}
{notification.type === 2 && (
<ConfigBox>
<Box>
<Typography component="h2">
{t("createNotifications.slackSettings.title")}
</Typography>
<Typography component="p">
{t("createNotifications.slackSettings.description")}
</Typography>
</Box>
<Stack gap={theme.spacing(12)}>
<TextInput
label={t("createNotifications.slackSettings.webhookLabel")}
value={notification.config.webhookUrl || ""}
error={Boolean(errors.config)}
helperText={errors["config"]}
onChange={(e) => {
const updatedConfig = {
...notification.config,
webhookUrl: e.target.value,
};
<ConfigBox>
<Box>
<Typography component="h2">{t(TITLE_MAP[type])}</Typography>
<Typography component="p">{t(DESCRIPTION_MAP[type])}</Typography>
</Box>
<Stack gap={theme.spacing(12)}>
<TextInput
label={t(LABEL_MAP[type])}
name="address"
placeholder={t(PLACEHOLDER_MAP[type])}
value={notification.address}
onChange={onChange}
error={Boolean(errors.address)}
helperText={errors["address"]}
/>
</Stack>
</ConfigBox>
onChange({
target: {
name: "config",
value: updatedConfig,
},
});
}}
/>
</Stack>
</ConfigBox>
)}
{notification.type === 3 && (
<ConfigBox>
<Box>
<Typography component="h2">
{t("createNotifications.pagerdutySettings.title")}
</Typography>
<Typography component="p">
{t("createNotifications.pagerdutySettings.description")}
</Typography>
</Box>
<Stack gap={theme.spacing(12)}>
<TextInput
label={t("createNotifications.pagerdutySettings.integrationKeyLabel")}
placeholder={t(
"createNotifications.pagerdutySettings.integrationKeyPlaceholder"
)}
value={notification.config.routingKey || ""}
error={Boolean(errors.config)}
helperText={errors["config"]}
onChange={(e) => {
const updatedConfig = {
...notification.config,
routingKey: e.target.value,
};
onChange({
target: {
name: "config",
value: updatedConfig,
},
});
}}
/>
</Stack>
</ConfigBox>
)}
{notification.type === 4 && (
<ConfigBox>
<Box>
<Typography component="h2">
{t("createNotifications.genericWebhookSettings.title")}
</Typography>
<Typography component="p">
{t("createNotifications.genericWebhookSettings.description")}
</Typography>
</Box>
<Stack gap={theme.spacing(12)}>
<TextInput
label={t("createNotifications.genericWebhookSettings.webhookLabel")}
value={notification.config.webhookUrl || ""}
error={Boolean(errors.config)}
helperText={errors["config"]}
onChange={(e) => {
const updatedConfig = {
...notification.config,
webhookUrl: e.target.value,
};
onChange({
target: {
name: "config",
value: updatedConfig,
},
});
}}
/>
</Stack>
</ConfigBox>
)}
{notification.type === 5 && (
<ConfigBox>
<Box>
<Typography component="h2">
{t("createNotifications.discordSettings.title")}
</Typography>
<Typography component="p">
{t("createNotifications.discordSettings.description")}
</Typography>
</Box>
<Stack gap={theme.spacing(12)}>
<TextInput
label={t("createNotifications.discordSettings.webhookLabel")}
value={notification.config.webhookUrl || ""}
error={Boolean(errors.config)}
helperText={errors["config"]}
onChange={(e) => {
const updatedConfig = {
...notification.config,
webhookUrl: e.target.value,
};
onChange({
target: {
name: "config",
value: updatedConfig,
},
});
}}
/>
</Stack>
</ConfigBox>
)}
<Stack
direction="row"
justifyContent="flex-end"
+1 -17
View File
@@ -44,27 +44,11 @@ const Notifications = () => {
return row.notificationName;
},
},
{
id: "type",
content: "Type",
render: (row) => {
return row.type;
},
},
{
id: "target",
content: "Target",
render: (row) => {
const type = row.type;
if (type === "email") {
return row.address;
}
if (type === "webhook") {
return row.config?.webhookUrl;
}
if (type === "pager_duty") {
return row.config?.routingKey;
}
return row.address;
},
},
{
+32
View File
@@ -5,3 +5,35 @@ export const NOTIFICATION_TYPES = [
{ _id: 4, name: "Webhook", value: "webhook" },
{ _id: 5, name: "Discord", value: "discord" },
];
export const TITLE_MAP = {
email: "createNotifications.emailSettings.title",
slack: "createNotifications.slackSettings.title",
pager_duty: "createNotifications.pagerDutySettings.title",
webhook: "createNotifications.webhookSettings.title",
discord: "createNotifications.discordSettings.title",
};
export const DESCRIPTION_MAP = {
email: "createNotifications.emailSettings.description",
slack: "createNotifications.slackSettings.description",
pager_duty: "createNotifications.pagerDutySettings.description",
webhook: "createNotifications.webhookSettings.description",
discord: "createNotifications.discordSettings.description",
};
export const LABEL_MAP = {
email: "createNotifications.emailSettings.emailLabel",
slack: "createNotifications.slackSettings.webhookLabel",
pager_duty: "createNotifications.pagerDutySettings.integrationKeyLabel",
webhook: "createNotifications.webhookSettings.webhookLabel",
discord: "createNotifications.discordSettings.webhookLabel",
};
export const PLACEHOLDER_MAP = {
email: "createNotifications.emailSettings.emailPlaceholder",
slack: "createNotifications.slackSettings.webhookPlaceholder",
pager_duty: "createNotifications.pagerDutySettings.integrationKeyPlaceholder",
webhook: "createNotifications.webhookSettings.webhookPlaceholder",
discord: "createNotifications.discordSettings.webhookPlaceholder",
};
+9 -49
View File
@@ -404,57 +404,19 @@ const infrastructureMonitorValidation = joi.object({
notifications: joi.array().items(joi.string()),
});
const notificationEmailValidation = joi.object({
const notificationValidation = joi.object({
notificationName: joi.string().required().messages({
"string.empty": "Notification name is required",
"any.required": "Notification name is required",
}),
address: joi
.string()
.email({ tlds: { allow: false } })
.required()
.messages({
"string.empty": "E-mail address cannot be empty",
"string.email": "Please enter a valid e-mail address",
"string.base": "E-mail address must be a string",
"any.required": "E-mail address is required",
}),
});
const notificationWebhookValidation = joi.object({
notificationName: joi.string().required().messages({
"string.empty": "Notification name is required",
"any.required": "Notification name is required",
address: joi.string().required().messages({
"string.empty": "This field cannot be empty",
"string.base": "This field must be a string",
"any.required": "This field is required",
}),
config: joi
.object({
webhookUrl: joi.string().uri().required().messages({
"string.empty": "Webhook URL is required",
"string.uri": "Webhook URL must be a valid URI",
"any.required": "Webhook URL is required",
}),
platform: joi.string().required().messages({
"string.base": "Platform must be a string",
"any.required": "Platform is required",
}),
})
.unknown(true),
});
const notificationPagerDutyValidation = joi.object({
notificationName: joi.string().required().messages({
"string.empty": "Notification name is required",
"any.required": "Notification name is required",
}),
config: joi.object({
platform: joi.string().required().messages({
"string.base": "Platform must be a string",
"any.required": "Platform is required",
}),
routingKey: joi.string().required().messages({
"string.empty": "Routing key is required",
"any.required": "Routing key is required",
}),
type: joi.string().required().messages({
"string.empty": "This field is required",
"any.required": "This field is required",
}),
});
@@ -469,7 +431,5 @@ export {
infrastructureMonitorValidation,
statusPageValidation,
logoImageValidation,
notificationEmailValidation,
notificationWebhookValidation,
notificationPagerDutyValidation,
notificationValidation,
};
+2 -2
View File
@@ -707,13 +707,13 @@
"webhookLabel": "Slack webhook URL",
"webhookPlaceholder": "https://hooks.slack.com/services/..."
},
"pagerdutySettings": {
"pagerDutySettings": {
"title": "PagerDuty",
"description": "Configure your PagerDuty integration here",
"integrationKeyLabel": "Integration key",
"integrationKeyPlaceholder": "1234567890"
},
"genericWebhookSettings": {
"webhookSettings": {
"title": "Webhook",
"description": "Configure your webhook here",
"webhookLabel": "Webhook URL",
+1 -73
View File
@@ -23,85 +23,13 @@ class NotificationController {
this.stringService = stringService;
this.statusService = statusService;
this.db = db;
this.triggerNotification = this.triggerNotification.bind(this);
}
async triggerNotification(req, res, next) {
try {
await triggerNotificationBodyValidation.validateAsync(req.body, {
abortEarly: false,
stripUnknown: true,
});
} catch (error) {
next(handleValidationError(error, SERVICE_NAME));
return;
}
try {
const { monitorId, type, platform, config, status = false } = req.body;
// Create a simplified networkResponse similar to what would come from monitoring
const networkResponse = {
monitorId,
status,
};
// Use the statusService to get monitor details and handle status change logic
// This returns { monitor, statusChanged, prevStatus } exactly like your job queue uses
const statusResult = await this.statusService.updateStatus(networkResponse);
if (type === NOTIFICATION_TYPES.WEBHOOK) {
const notification = {
type,
platform,
config,
};
await this.notificationService.sendWebhookNotification(
statusResult, // Contains monitor, statusChanged, and prevStatus
notification
);
}
return res.success({
msg: this.stringService.webhookSendSuccess,
});
} catch (error) {
next(handleError(error, SERVICE_NAME, "triggerNotification"));
}
}
createTestNetworkResponse() {
return {
monitor: {
_id: "test-monitor-id",
name: "Test Monitor",
url: "https://example.com",
},
status: true,
statusChanged: true,
prevStatus: false,
};
}
testNotification = async (req, res, next) => {
try {
const notification = req.body;
let success;
if (notification?.type === "email") {
success = await this.notificationService.sendTestEmail(notification);
}
if (notification?.type === "webhook") {
success =
await this.notificationService.sendTestWebhookNotification(notification);
}
if (notification?.type === "pager_duty") {
success =
await this.notificationService.sendTestPagerDutyNotification(notification);
}
const success = await this.notificationService.sendTestNotification(notification);
if (!success) {
return res.error({
+62 -7
View File
@@ -69,6 +69,15 @@ const MonitorSchema = mongoose.Schema(
type: Number,
default: undefined,
},
notifications: [
{
type: mongoose.Schema.Types.ObjectId,
ref: "Notification",
},
],
secret: {
type: String,
},
thresholds: {
type: {
usage_cpu: { type: Number },
@@ -78,14 +87,33 @@ const MonitorSchema = mongoose.Schema(
},
_id: false,
},
notifications: [
{
type: mongoose.Schema.Types.ObjectId,
ref: "Notification",
alertThreshold: {
type: Number,
default: 5,
},
cpuAlertThreshold: {
type: Number,
default: function () {
return this.alertThreshold;
},
},
memoryAlertThreshold: {
type: Number,
default: function () {
return this.alertThreshold;
},
},
diskAlertThreshold: {
type: Number,
default: function () {
return this.alertThreshold;
},
},
tempAlertThreshold: {
type: Number,
default: function () {
return this.alertThreshold;
},
],
secret: {
type: String,
},
},
{
@@ -132,6 +160,33 @@ MonitorSchema.pre("deleteMany", async function (next) {
next();
});
MonitorSchema.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();
});
MonitorSchema.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();
});
MonitorSchema.index({ teamId: 1, type: 1 });
export default mongoose.model("Monitor", MonitorSchema);
+1 -50
View File
@@ -1,19 +1,5 @@
import mongoose from "mongoose";
const configSchema = mongoose.Schema(
{
webhookUrl: { type: String },
botToken: { type: String },
chatId: { type: String },
platform: {
type: String,
enum: ["slack", "pager_duty", "webhook", "discord"],
},
routingKey: { type: String },
},
{ _id: false }
);
const NotificationSchema = mongoose.Schema(
{
userId: {
@@ -30,14 +16,7 @@ const NotificationSchema = mongoose.Schema(
},
type: {
type: String,
enum: ["email", "sms", "webhook", "pager_duty"],
},
platform: {
type: String,
},
config: {
type: configSchema,
default: () => ({}),
enum: ["email", "slack", "discord", "webhook", "pager_duty"],
},
notificationName: {
type: String,
@@ -49,34 +28,6 @@ const NotificationSchema = mongoose.Schema(
phone: {
type: String,
},
alertThreshold: {
type: Number,
default: 5,
},
cpuAlertThreshold: {
type: Number,
default: function () {
return this.alertThreshold;
},
},
memoryAlertThreshold: {
type: Number,
default: function () {
return this.alertThreshold;
},
},
diskAlertThreshold: {
type: Number,
default: function () {
return this.alertThreshold;
},
},
tempAlertThreshold: {
type: Number,
default: function () {
return this.alertThreshold;
},
},
},
{
timestamps: true,
+10 -3
View File
@@ -73,6 +73,7 @@ import StatusService from "./service/statusService.js";
// Notification Service and dependencies
import NotificationService from "./service/notificationService.js";
import NotificationUtils from "./service/notificationUtils.js";
// Buffer Service and dependencies
import BufferService from "./service/bufferService.js";
@@ -173,13 +174,19 @@ const startApp = async () => {
);
const bufferService = new BufferService({ db, logger });
const statusService = new StatusService({ db, logger, buffer: bufferService });
const notificationService = new NotificationService(
const notificationUtils = new NotificationUtils({
stringService,
emailService,
});
const notificationService = new NotificationService({
emailService,
db,
logger,
networkService,
stringService
);
stringService,
notificationUtils,
});
const redisService = new RedisService({ Redis: IORedis, logger });
-2
View File
@@ -11,8 +11,6 @@ class NotificationRoutes {
initializeRoutes() {
this.router.use(verifyJWT);
this.router.post("/trigger", this.notificationController.triggerNotification);
this.router.post("/test", this.notificationController.testNotification);
this.router.post("/test/all", this.notificationController.testAllNotifications);
-1
View File
@@ -1,5 +1,4 @@
import { Pulse } from "@pulsecron/pulse";
import { ObjectId } from "mongodb";
const SERVICE_NAME = "PulseQueue";
class PulseQueue {
+4 -10
View File
@@ -432,9 +432,9 @@ class NetworkService {
throw err;
}
async requestWebhook(platform, url, message) {
async requestWebhook(type, url, body) {
try {
const response = await this.axios.post(url, message, {
const response = await this.axios.post(url, body, {
headers: {
"Content-Type": "application/json",
},
@@ -444,7 +444,7 @@ class NetworkService {
type: "webhook",
status: true,
code: response.status,
message: `Successfully sent ${platform} notification`,
message: `Successfully sent ${type} notification`,
payload: response.data,
};
} catch (error) {
@@ -452,19 +452,13 @@ class NetworkService {
message: error.message,
service: this.SERVICE_NAME,
method: "requestWebhook",
url,
platform,
error: error.message,
statusCode: error.response?.status,
responseData: error.response?.data,
requestPayload: message,
});
return {
type: "webhook",
status: false,
code: error.response?.status || this.NETWORK_ERROR,
message: `Failed to send ${platform} notification`,
message: `Failed to send ${type} notification`,
payload: error.response?.data,
};
}
+83 -472
View File
@@ -1,512 +1,123 @@
const SERVICE_NAME = "NotificationService";
const TELEGRAM_API_BASE_URL = "https://api.telegram.org/bot";
const PLATFORM_TYPES = ["telegram", "slack", "discord", "webhook"];
const MESSAGE_FORMATTERS = {
telegram: (messageText, chatId) => ({ chat_id: chatId, text: messageText }),
slack: (messageText) => ({ text: messageText }),
discord: (messageText) => ({ content: messageText }),
webhook: (messageText) => ({ text: messageText }),
};
class NotificationService {
static SERVICE_NAME = SERVICE_NAME;
/**
* Creates an instance of NotificationService.
*
* @param {Object} emailService - The email service used for sending notifications.
* @param {Object} db - The database instance for storing notification data.
* @param {Object} logger - The logger instance for logging activities.
* @param {Object} networkService - The network service for sending webhook notifications.
*/
constructor(emailService, db, logger, networkService, stringService) {
this.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;
}
/**
* Formats a notification message based on the monitor status and platform.
*
* @param {Object} monitor - The monitor object.
* @param {string} monitor.name - The name of the monitor.
* @param {string} monitor.url - The URL of the monitor.
* @param {boolean} status - The current status of the monitor (true for up, false for down).
* @param {string} platform - The notification platform (e.g., "telegram", "slack", "discord").
* @param {string} [chatId] - The chat ID for platforms that require it (e.g., Telegram).
* @returns {Object|null} The formatted message object for the specified platform, or null if the platform is unsupported.
*/
sendNotification = async ({ notification, subject, content, html }) => {
const { type, address } = notification;
formatNotificationMessage(monitor, status, platform, chatId, code, timestamp) {
// 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");
}
if (!PLATFORM_TYPES.includes(platform)) {
return undefined;
}
return MESSAGE_FORMATTERS[platform](messageText, chatId);
}
formatHardwareNotificationMessage = (
monitor,
status,
platform,
chatId,
code,
timestamp,
alerts
) => {
const messageText = alerts.map((alert) => alert).join("\n");
return MESSAGE_FORMATTERS[platform](messageText, chatId);
};
sendTestWebhookNotification = async (notification) => {
const config = notification.config;
const response = await this.networkService.requestWebhook(
config.platform,
config.webhookUrl,
"This is a test notification"
);
return response.status;
};
/**
* Sends a webhook notification to a specified platform.
*
* @param {Object} networkResponse - The response object from the network.
* @param {Object} networkResponse.monitor - The monitor object.
* @param {boolean} networkResponse.status - The monitor's status (true for up, false for down).
* @param {Object} notification - The notification settings.
* @param {string} notification.platform - The target platform ("telegram", "slack", "discord").
* @param {Object} notification.config - The configuration object for the webhook.
* @param {string} notification.config.webhookUrl - The webhook URL for the platform.
* @param {string} [notification.config.botToken] - The bot token for Telegram notifications.
* @param {string} [notification.config.chatId] - The chat ID for Telegram notifications.
* @returns {Promise<boolean>} A promise that resolves to true if the notification was sent successfully, otherwise false.
*/
async sendWebhookNotification(networkResponse, notification, alerts = []) {
const { monitor, status, code } = networkResponse;
const { webhookUrl, platform, botToken, chatId } = notification.config;
// Early return if platform is not supported
if (!PLATFORM_TYPES.includes(platform)) {
this.logger.warn({
message: this.stringService.getWebhookUnsupportedPlatform(platform),
service: this.SERVICE_NAME,
method: "sendWebhookNotification",
details: { platform },
});
return false;
}
// Early return for telegram if required fields are missing
if (platform === "telegram" && (!botToken || !chatId)) {
this.logger.warn({
message: "Missing required fields for Telegram notification",
service: this.SERVICE_NAME,
method: "sendWebhookNotification",
details: { platform },
});
return false;
}
let url = webhookUrl;
if (platform === "telegram") {
url = `${TELEGRAM_API_BASE_URL}${botToken}/sendMessage`;
}
let message;
if (monitor.type === "hardware") {
message = this.formatHardwareNotificationMessage(
monitor,
status,
platform,
chatId,
code,
networkResponse.timestamp,
alerts
);
} else {
message = this.formatNotificationMessage(
monitor,
status,
platform,
chatId,
code, // Pass the code field directly
networkResponse.timestamp
);
}
try {
const response = await this.networkService.requestWebhook(platform, url, message);
return response.status;
} catch (error) {
this.logger.error({
message: this.stringService.getWebhookSendError(platform),
service: this.SERVICE_NAME,
method: "sendWebhookNotification",
stack: error.stack,
});
return false;
}
}
/**
* Sends an email notification for hardware infrastructure alerts
*
* @async
* @function sendHardwareEmail
* @param {Object} networkResponse - Response object containing monitor information
* @param {string} address - Email address to send the notification to
* @param {Array} [alerts=[]] - List of hardware alerts to include in the email
* @returns {Promise<boolean>} - Indicates whether email was sent successfully
* @throws {Error}
*/
async sendHardwareEmail(networkResponse, address, alerts = []) {
if (alerts.length === 0) return false;
const { monitor, status, prevStatus } = 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);
this.emailService.sendEmail(address, subject, html).catch((error) => {
this.logger.warn({
message: error.message,
service: this.SERVICE_NAME,
method: "sendHardwareEmail",
stack: error.stack,
});
});
return true;
}
sendTestEmail = async (notification) => {
const to = notification?.address;
if (!to || typeof to !== "string") {
throw new Error(this.stringService.errorForValidEmailAddress);
}
const subject = this.stringService.testEmailSubject;
const context = { testName: "Monitoring System" };
try {
const html = await this.emailService.buildEmail("testEmailTemplate", context);
const messageId = await this.emailService.sendEmail(to, subject, html);
if (messageId) {
return true;
}
} catch (error) {
this.logger.warn({
message: error.message,
service: this.SERVICE_NAME,
method: "sendTestEmail",
stack: error.stack,
});
return false;
}
};
/**
* Sends an email notification about monitor status change
*
* @async
* @function sendEmail
* @param {Object} networkResponse - Response object containing monitor status information
* @param {string} address - Email address to send the notification to
* @returns {Promise<boolean>} - Indicates email was sent successfully
*/
async sendEmail(networkResponse, address) {
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);
this.emailService.sendEmail(address, subject, html).catch((error) => {
this.logger.warn({
message: error.message,
service: this.SERVICE_NAME,
method: "sendEmail",
stack: error.stack,
});
});
return true;
}
async sendTestPagerDutyNotification(notification) {
const { routingKey } = notification.config;
const response = await this.networkService.requestPagerDuty({
message: "This is a test notification",
monitorUrl: "Test notification",
routingKey,
});
return response;
}
async sendPagerDutyNotification(networkResponse, notification, alerts = []) {
const { monitor, status, code } = networkResponse;
const { routingKey, platform } = notification.config;
let message;
if (monitor.type === "hardware") {
message = this.formatHardwareNotificationMessage(
monitor,
status,
platform,
null,
code,
networkResponse.timestamp,
alerts
);
} else {
message = this.formatNotificationMessage(
monitor,
status,
platform,
null,
code, // Pass the code field directly
networkResponse.timestamp
);
}
try {
const response = await this.networkService.requestPagerDuty({
message,
routingKey,
monitorUrl: monitor.url,
});
return response;
} catch (error) {
this.logger.error({
message: "Failed to send PagerDuty notification",
details: error.details,
service: error.service || this.SERVICE_NAME,
method: error.method || "sendPagerDutyNotification",
stack: error.stack,
});
return false;
}
}
async handleStatusNotifications(networkResponse) {
try {
// If status hasn't changed, we're done
if (networkResponse.statusChanged === false) return false;
// if prevStatus is undefined, monitor is resuming, we're done
if (networkResponse.prevStatus === undefined) return false;
const notificationIDs = networkResponse.monitor?.notifications ?? [];
const notifications = await this.db.getNotificationsByIds(notificationIDs);
for (const notification of notifications) {
if (notification.type === "email") {
await this.sendEmail(networkResponse, notification.address);
} else if (notification.type === "webhook") {
await this.sendWebhookNotification(networkResponse, notification);
} else if (notification.type === "pager_duty") {
await this.sendPagerDutyNotification(networkResponse, notification);
}
}
if (type === "email") {
const messageId = await this.emailService.sendEmail(address, subject, html);
if (!messageId) return false;
return true;
} catch (error) {
this.logger.error({
message: error.message,
service: this.SERVICE_NAME,
method: "handleNotifications",
stack: error.stack,
});
}
}
/**
* Handles status change notifications for a monitor
*
* @async
* @function handleStatusNotifications
* @param {Object} networkResponse - Response object containing monitor status information
* @returns {Promise<boolean>} - Indicates whether notifications were processed
* @throws {Error}
*/
async handleHardwareNotifications(networkResponse) {
const thresholds = networkResponse?.monitor?.thresholds;
if (thresholds === undefined) return false; // No thresholds set, we're done
// Get thresholds from monitor
const {
usage_cpu: cpuThreshold = -1,
usage_memory: memoryThreshold = -1,
usage_disk: diskThreshold = -1,
} = thresholds;
// Create a body for webhooks
let body = { text: content };
if (type === "discord") {
body = { content };
}
// Get metrics from response
const metrics = networkResponse?.payload?.data ?? null;
if (metrics === null) return false;
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,
});
const {
cpu: { usage_percent: cpuUsage = -1 } = {},
memory: { usage_percent: memoryUsage = -1 } = {},
disk = [],
} = metrics;
return response;
}
};
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,
};
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 ?? [];
const notifications = await this.db.getNotificationsByIds(notificationIDs);
if (notificationIDs.length === 0) return false;
for (const notification of notifications) {
const alertsToSend = [];
const alertTypes = ["cpu", "memory", "disk"];
if (networkResponse.monitor.type === "hardware") {
const thresholds = networkResponse?.monitor?.thresholds;
for (const type of alertTypes) {
// Iterate over each alert type to see if any need to be decremented
if (alerts[type] === true) {
notification[`${type}AlertThreshold`]--; // Decrement threshold if an alert is triggered
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
if (notification[`${type}AlertThreshold`] <= 0) {
// If threshold drops below 0, reset and send notification
notification[`${type}AlertThreshold`] = notification.alertThreshold;
const alerts = await this.notificationUtils.buildHardwareAlerts(networkResponse);
if (alerts.length === 0) return false;
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]());
}
}
}
const { subject, html } = await this.notificationUtils.buildHardwareEmail(
networkResponse,
alerts
);
const content =
await this.notificationUtils.buildHardwareNotificationMessage(alerts);
await notification.save();
if (alertsToSend.length === 0) continue; // No alerts to send, we're done
if (notification.type === "email") {
await this.sendHardwareEmail(networkResponse, notification.address, alertsToSend);
} else if (notification.type === "webhook") {
await this.sendWebhookNotification(networkResponse, notification, alertsToSend);
} else if (notification.type === "pager_duty") {
await this.sendPagerDutyNotification(networkResponse, notification, alertsToSend);
}
const success = await this.notifyAll({ notificationIDs, subject, html, content });
return success;
}
return true;
// Status monitors
const { subject, html } =
await this.notificationUtils.buildStatusEmail(networkResponse);
const content = await this.notificationUtils.buildWebhookMessage(networkResponse);
const success = this.notifyAll({ notificationIDs, subject, html, content });
return success;
}
/**
* Handles notifications for different monitor types
*
* @async
* @function handleNotifications
* @param {Object} networkResponse - Response object containing monitor information
* @returns {Promise<boolean>} - Indicates whether notifications were processed successfully
*/
async handleNotifications(networkResponse) {
try {
if (networkResponse.monitor.type === "hardware") {
this.handleHardwareNotifications(networkResponse);
}
this.handleStatusNotifications(networkResponse);
return true;
} catch (error) {
this.logger.error({
message: error.message,
service: this.SERVICE_NAME,
method: "handleNotifications",
stack: error.stack,
});
}
}
async testAllNotifications(notificationIDs) {
async notifyAll({ notificationIDs, subject, html, content }) {
const notifications = await this.db.getNotificationsByIds(notificationIDs);
// Map each notification to a test promise
const testPromises = notifications.map(async (notification) => {
const promises = notifications.map(async (notification) => {
try {
if (notification.type === "email") {
await this.sendTestEmail(notification);
} else if (notification.type === "webhook") {
await this.sendTestWebhookNotification(notification);
} else if (notification.type === "pager_duty") {
await this.sendTestPagerDutyNotification(notification);
}
await this.sendNotification({ notification, subject, content, html });
return true;
} catch (err) {
return false;
}
});
const results = await Promise.all(testPromises);
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;
+146
View File
@@ -0,0 +1,146 @@
class NotificationUtils {
constructor({ stringService, emailService }) {
this.stringService = stringService;
this.emailService = emailService;
}
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");
}
return messageText;
};
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 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 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]());
}
}
}
await monitor.save();
return alertsToSend;
};
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) => {
return alerts.map((alert) => alert).join("\n");
};
}
export default NotificationUtils;
+8 -17
View File
@@ -570,24 +570,15 @@ const createNotificationBodyValidation = joi.object({
"string.email": "Please enter a valid e-mail address",
}),
}),
type: joi.string().valid("email", "webhook", "pager_duty").required().messages({
"string.empty": "Notification type is required",
"any.required": "Notification type is required",
"any.only": "Notification type must be email, webhook, or pager_duty",
}),
config: joi.when("type", {
is: "webhook",
then: joi.object({
webhookUrl: joi.string().uri().required().messages({
"string.uri": "Webhook URL must be a valid URI",
"any.required": "Webhook URL is required",
}),
platform: joi.string().required().messages({
"string.empty": "Platform is required",
"any.required": "Platform is required",
}),
type: joi
.string()
.valid("email", "webhook", "slack", "discord", "pager_duty")
.required()
.messages({
"string.empty": "Notification type is required",
"any.required": "Notification type is required",
"any.only": "Notification type must be email, webhook, or pager_duty",
}),
}),
});
//****************************************