Merge branch 'bluewave-labs:develop' into network-page-BE-part-1
This commit is contained in:
16 files changed
+391
-985
No files matched your search
@@ -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);
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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;
|
||||
},
|
||||
},
|
||||
{
|
||||
|
||||
@@ -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",
|
||||
};
|
||||
@@ -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,
|
||||
};
|
||||
@@ -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",
|
||||
|
||||
@@ -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({
|
||||
|
||||
@@ -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,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
@@ -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 });
|
||||
|
||||
|
||||
@@ -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,5 +1,4 @@
|
||||
import { Pulse } from "@pulsecron/pulse";
|
||||
import { ObjectId } from "mongodb";
|
||||
|
||||
const SERVICE_NAME = "PulseQueue";
|
||||
class PulseQueue {
|
||||
|
||||
@@ -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,
|
||||
};
|
||||
}
|
||||
|
||||
Executable → Regular
+83
-472
@@ -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;
|
||||
@@ -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;
|
||||
@@ -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",
|
||||
}),
|
||||
}),
|
||||
});
|
||||
|
||||
//****************************************
|
||||
|
||||
Reference in new issue
Block a user