Merge branch 'develop' into 1063-fe-hardware-montioring-monitor-gauge

This commit is contained in:
Shemy Gan committed 2024-10-29 10:25:26 -04:00
commit 239bd9c14e
27 files changed
+1341 -1683

No files matched your search

+1
View File
@@ -0,0 +1 @@
VITE_APP_API_BASE_URL=UPTIME_APP_API_BASE_URL
+2
View File
@@ -24,3 +24,5 @@ dist-ssr
*.sw?
.env
!env.sh
Executable
+8
View File
@@ -0,0 +1,8 @@
#!/bin/sh
for i in $(env | grep UPTIME_APP_)
do
key=$(echo $i | cut -d '=' -f 1)
value=$(echo $i | cut -d '=' -f 2-)
echo $key=$value
find /usr/share/nginx/html -type f \( -name '*.js' -o -name '*.css' \) -exec sed -i "s|${key}|${value}|g" '{}' +
done
+3 -3
View File
@@ -2805,9 +2805,9 @@
}
},
"node_modules/chart.js": {
"version": "4.4.5",
"resolved": "https://registry.npmjs.org/chart.js/-/chart.js-4.4.5.tgz",
"integrity": "sha512-CVVjg1RYTJV9OCC8WeJPMx8gsV8K6WIyIEQUE3ui4AR9Hfgls9URri6Ja3hyMVBbTF8Q2KFa19PE815gWcWhng==",
"version": "4.4.6",
"resolved": "https://registry.npmjs.org/chart.js/-/chart.js-4.4.6.tgz",
"integrity": "sha512-8Y406zevUPbbIBA/HRk33khEmQPk5+cxeflWE/2rx1NJsjVWMPw/9mSP9rxHP5eqi6LNoPBVMfZHxbwLSgldYA==",
"license": "MIT",
"dependencies": {
"@kurkle/color": "^0.3.0"
+1
View File
@@ -6,6 +6,7 @@
"scripts": {
"dev": "vite",
"build": "vite build",
"build-dev": "vite build --mode development",
"lint": "eslint . --ext js,jsx --report-unused-disable-directives --max-warnings 0",
"preview": "vite preview"
},
@@ -74,7 +74,7 @@ const IncidentTable = ({ monitors, selectedMonitor, filter }) => {
sortOrder: "desc",
limit: null,
dateRange: null,
sitler: filter,
filter: filter,
page: paginationController.page,
rowsPerPage: paginationController.rowsPerPage,
});
+1 -1
View File
@@ -8,7 +8,7 @@ RUN npm install
COPY ../../Client .
RUN npm run build
RUN npm run build-dev
RUN npm install -g serve
+4 -4
View File
@@ -6,10 +6,10 @@ cd ../..
# Define an array of services and their Dockerfiles
declare -A services=(
["bluewave/uptime_client"]="./Docker/dist/client.Dockerfile"
["bluewave/database_mongo"]="./Docker/dist/mongoDB.Dockerfile"
["bluewave/uptime_redis"]="./Docker/dist/redis.Dockerfile"
["bluewave/uptime_server"]="./Docker/dist/server.Dockerfile"
["bluewaveuptime/uptime_client"]="./Docker/dist/client.Dockerfile"
["bluewaveuptime/uptime_database_mongo"]="./Docker/dist/mongoDB.Dockerfile"
["bluewaveuptime/uptime_redis"]="./Docker/dist/redis.Dockerfile"
["bluewaveuptime/uptime_server"]="./Docker/dist/server.Dockerfile"
)
# Loop through each service and build the corresponding image
+2
View File
@@ -15,5 +15,7 @@ FROM nginx:1.27.1-alpine
COPY ./Docker/dist/nginx/conf.d/default.conf /etc/nginx/conf.d/default.conf
COPY --from=build /app/dist /usr/share/nginx/html
COPY --from=build /app/env.sh /docker-entrypoint.d/env.sh
RUN chmod +x /docker-entrypoint.d/env.sh
CMD ["nginx", "-g", "daemon off;"]
+2
View File
@@ -1,6 +1,8 @@
services:
client:
image: bluewaveuptime/uptime_client:latest
environment:
UPTIME_APP_API_BASE_URL: "http://localhost:5000/api/v1"
ports:
- "80:80"
- "443:443"
+2 -1
View File
@@ -13,5 +13,6 @@ RUN npm run build
FROM nginx:1.27.1-alpine
COPY --from=build /app/dist /usr/share/nginx/html
COPY --from=build /app/env.sh /docker-entrypoint.d/env.sh
RUN chmod +x /docker-entrypoint.d/env.sh
CMD ["nginx", "-g", "daemon off;"]
+2
View File
@@ -1,6 +1,8 @@
services:
client:
image: uptime_client:latest
environment:
UPTIME_APP_API_BASE_URL: "http://localhost:5000/api/v1"
ports:
- "80:80"
- "443:443"
+6 -3
View File
@@ -296,9 +296,12 @@ const getMonitorById = async (monitorId) => {
const notifications = await Notification.find({
monitorId: monitorId,
});
monitor.notifications = notifications;
const monitorWithNotifications = await monitor.save();
return monitorWithNotifications;
const updatedMonitor = await Monitor.findByIdAndUpdate(
monitorId,
{ notifications },
{ new: true }
).populate("notifications");
return updatedMonitor;
} catch (error) {
error.service = SERVICE_NAME;
error.method = "getMonitorById";
+8 -1
View File
@@ -40,6 +40,9 @@ import mjml2html from "mjml";
import SettingsService from "./service/settingsService.js";
import AppSettings from "./db/models/AppSettings.js";
import StatusService from "./service/statusService.js";
import NotificationService from "./service/notificationService.js";
import db from "./db/mongo/MongoDB.js";
const SERVICE_NAME = "Server";
@@ -125,10 +128,14 @@ const startApp = async () => {
nodemailer,
logger
);
const networkService = new NetworkService(db, emailService, axios, ping, logger, http);
const networkService = new NetworkService(axios, ping, logger, http);
const statusService = new StatusService(db, logger);
const notificationService = new NotificationService(emailService, db, logger);
const jobQueue = await JobQueue.createJobQueue(
db,
networkService,
statusService,
notificationService,
settingsService,
logger,
Queue,
+144 -60
View File
@@ -11,12 +11,24 @@ const SERVICE_NAME = "JobQueue";
*/
class JobQueue {
/**
* Constructs a new JobQueue
* @constructor
* @param {SettingsService} settingsService - The settings service
* @throws {Error}
* @class JobQueue
* @classdesc Manages job queue and workers.
*
* @param {Object} statusService - Service for handling status updates.
* @param {Object} notificationService - Service for handling notifications.
* @param {Object} settingsService - Service for retrieving settings.
* @param {Object} logger - Logger for logging information.
* @param {Function} Queue - Queue constructor.
* @param {Function} Worker - Worker constructor.
*/
constructor(settingsService, logger, Queue, Worker) {
constructor(
statusService,
notificationService,
settingsService,
logger,
Queue,
Worker
) {
const settings = settingsService.getSettings() || {};
const { redisHost = "127.0.0.1", redisPort = 6379 } = settings;
@@ -31,27 +43,45 @@ class JobQueue {
this.workers = [];
this.db = null;
this.networkService = null;
this.statusService = statusService;
this.notificationService = notificationService;
this.settingsService = settingsService;
this.logger = logger;
this.Worker = Worker;
}
/**
* Static factory method to create a JobQueue
* @static
* @async
* @returns {Promise<JobQueue>} - Returns a new JobQueue
* Creates and initializes a JobQueue instance.
*
* @param {Object} db - Database service for accessing monitors.
* @param {Object} networkService - Service for network operations.
* @param {Object} statusService - Service for handling status updates.
* @param {Object} notificationService - Service for handling notifications.
* @param {Object} settingsService - Service for retrieving settings.
* @param {Object} logger - Logger for logging information.
* @param {Function} Queue - Queue constructor.
* @param {Function} Worker - Worker constructor.
* @returns {Promise<JobQueue>} - The initialized JobQueue instance.
* @throws {Error} - Throws an error if initialization fails.
*/
static async createJobQueue(
db,
networkService,
statusService,
notificationService,
settingsService,
logger,
Queue,
Worker
) {
const queue = new JobQueue(settingsService, logger, Queue, Worker);
const queue = new JobQueue(
statusService,
notificationService,
settingsService,
logger,
Queue,
Worker
);
try {
queue.db = db;
queue.networkService = networkService;
@@ -71,62 +101,104 @@ class JobQueue {
}
}
/**
* Checks if the given monitor is in a maintenance window.
*
* @param {string} monitorId - The ID of the monitor to check.
* @returns {Promise<boolean>} - Returns true if the monitor is in a maintenance window, otherwise false.
* @throws {Error} - Throws an error if the database query fails.
*/
async isInMaintenanceWindow(monitorId) {
const maintenanceWindows = await this.db.getMaintenanceWindowsByMonitorId(monitorId);
// Check for active maintenance window:
const maintenanceWindowIsActive = maintenanceWindows.reduce((acc, window) => {
if (window.active) {
const start = new Date(window.start);
const end = new Date(window.end);
const now = new Date();
const repeatInterval = window.repeat || 0;
// If start is < now and end > now, we're in maintenance
if (start <= now && end >= now) return true;
// If maintenance window was set in the past with a repeat,
// we need to advance start and end to see if we are in range
while (start < now && repeatInterval !== 0) {
start.setTime(start.getTime() + repeatInterval);
end.setTime(end.getTime() + repeatInterval);
if (start <= now && end >= now) {
return true;
}
}
return false;
}
return acc;
}, false);
return maintenanceWindowIsActive;
}
/**
* Creates a job handler function for processing jobs.
*
* @returns {Function} An async function that processes a job.
*/
createJobHandler() {
return async (job) => {
try {
// Get all maintenance windows for this monitor
const monitorId = job.data._id;
const maintenanceWindowActive = await this.isInMaintenanceWindow(monitorId);
// If a maintenance window is active, we're done
if (maintenanceWindowActive) {
this.logger.info({
message: `Monitor ${monitorId} is in maintenance window`,
service: SERVICE_NAME,
method: "createWorker",
});
return;
}
// Get the current status
const networkResponse = await this.networkService.getStatus(job);
// Handle status change
const { monitor, statusChanged, prevStatus } =
await this.statusService.updateStatus(networkResponse);
//If status hasn't changed, we're done
if (statusChanged === false) return;
// if prevStatus is undefined, monitor is resuming, we're done
if (prevStatus === undefined) return;
this.notificationService.handleNotifications({
...networkResponse,
monitor,
prevStatus,
});
} catch (error) {
this.logger.error({
message: error.message,
service: SERVICE_NAME,
method: "createWorker",
details: `Error processing job ${job.id}: ${error.message}`,
stack: error.stack,
});
}
};
}
/**
* Creates a worker for the queue
* Operations are carried out in the async callback
* @returns {Worker} The newly created worker
*/
createWorker() {
const worker = new this.Worker(
QUEUE_NAME,
async (job) => {
try {
// Get all maintenance windows for this monitor
const monitorId = job.data._id;
const maintenanceWindows =
await this.db.getMaintenanceWindowsByMonitorId(monitorId);
// Check for active maintenance window:
const maintenanceWindowActive = maintenanceWindows.reduce((acc, window) => {
if (window.active) {
const start = new Date(window.start);
const end = new Date(window.end);
const now = new Date();
const repeatInterval = window.repeat || 0;
while ((start < now) & (repeatInterval !== 0)) {
start.setTime(start.getTime() + repeatInterval);
end.setTime(end.getTime() + repeatInterval);
}
if (start < now && end > now) {
return true;
}
}
return acc;
}, false);
if (!maintenanceWindowActive) {
await this.networkService.getStatus(job);
} else {
this.logger.info({
message: `Monitor ${monitorId} is in maintenance window`,
service: SERVICE_NAME,
method: "createWorker",
});
}
} catch (error) {
this.logger.error({
message: error.message,
service: SERVICE_NAME,
method: "createWorker",
details: `Error processing job ${job.id}: ${error.message}`,
stack: error.stack,
});
}
},
{
connection: this.connection,
}
);
const worker = new this.Worker(QUEUE_NAME, this.createJobHandler(), {
connection: this.connection,
});
return worker;
}
@@ -233,6 +305,12 @@ class JobQueue {
}
}
/**
* Retrieves the statistics of jobs and workers.
*
* @returns {Promise<Object>} - An object containing job statistics and the number of workers.
* @throws {Error} - Throws an error if the job statistics retrieval fails.
*/
async getJobStats() {
try {
const jobs = await this.queue.getJobs();
@@ -313,6 +391,12 @@ class JobQueue {
}
}
/**
* Retrieves the metrics of the job queue.
*
* @returns {Promise<Object>} - An object containing various job queue metrics.
* @throws {Error} - Throws an error if the metrics retrieval fails.
*/
async getMetrics() {
try {
const metrics = {
+150 -435
View File
@@ -1,32 +1,22 @@
import { errorMessages, successMessages } from "../utils/messages.js";
/**
* NetworkService
* Constructs a new NetworkService instance.
*
* This service handles all network requests on the back end
* This includes pings, http requests, and pagespeed checks
* @param {Object} axios - The axios instance for HTTP requests.
* @param {Object} ping - The ping utility for network checks.
* @param {Object} logger - The logger instance for logging.
* @param {Object} http - The HTTP utility for network operations.
*/
class NetworkService {
/**
* Creates an instance of NetworkService.
*
* @param {Object} db - The database service.
* @param {Object} emailService - The email service.
* @param {Object} axios - The axios HTTP client.
* @param {Object} ping - The ping service.
* @param {Object} logger - The logging service.
* @param {Object} http - The HTTP service.
*/
constructor(db, emailService, axios, ping, logger, http) {
this.db = db;
this.emailService = emailService;
constructor(axios, ping, logger, http) {
this.TYPE_PING = "ping";
this.TYPE_HTTP = "http";
this.TYPE_PAGESPEED = "pagespeed";
this.TYPE_HARDWARE = "hardware";
this.SERVICE_NAME = "NetworkService";
this.NETWORK_ERROR = 5000;
this.PING_ERROR = 5001;
this.axios = axios;
this.ping = ping;
this.logger = logger;
@@ -34,459 +24,184 @@ class NetworkService {
}
/**
* Handles notifications for a given monitor configuration.
* Times the execution of an asynchronous operation.
*
* @param {Object} config - The configuration object for the notification.
* @param {Object} config.monitor - The monitor object containing the monitor ID.
* @param {string} config.template - The email template to be used.
* @param {Object} config.context - The context for the email template.
* @param {string} config.subject - The subject of the email.
* @param {Function} operation - The asynchronous operation to be timed.
* @returns {Promise<Object>} An object containing the response, response time, and optionally an error.
* @property {Object|null} response - The response from the operation, or null if an error occurred.
* @property {number} responseTime - The time taken for the operation to complete, in milliseconds.
* @property {Error} [error] - The error object if an error occurred during the operation.
*/
async handleNotification(config) {
try {
const notifications = await this.db.getNotificationsByMonitorId(config.monitor._id);
for (const notification of notifications) {
if (notification.type === "email") {
await this.emailService.buildAndSendEmail(
config.template,
config.context,
notification.address,
config.subject
);
}
}
} catch (error) {
this.logger.error({
message: error.message,
service: this.SERVICE_NAME,
method: "handleNotification",
details: `notification error for monitor: ${monitor._id}`,
stack: error.stack,
});
}
}
/**
* Handles the status update for a monitor job.
*
* @param {Object} job - The job object containing job details.
* @param {boolean} isAlive - The status of the monitor (true if up, false if down).
* @param {Object} [hardwareData] - The hardware data for the monitor.
* @returns {Promise<void>}
*/
async handleStatusUpdate(job, isAlive, hardwareData) {
let monitor;
const { _id } = job.data;
// Look up the monitor, if it doesn't exist, it's probably been removed, return
try {
monitor = await this.db.getMonitorById(_id);
} catch (error) {
this.logger.error({
message: error.message,
service: this.SERVICE_NAME,
method: "handleStatusUpdate",
stack: error.stack,
details: `monitor lookup error for monitor: ${_id}`,
});
return;
}
// Otherwise, try to update monitor status
try {
if (monitor.status === undefined || monitor.status !== isAlive) {
const oldStatus = monitor.status;
monitor.status = isAlive;
await monitor.save();
if (oldStatus !== undefined && oldStatus !== isAlive) {
const config = {
monitor: monitor,
template: isAlive === true ? "serverIsUpTemplate" : "serverIsDownTemplate",
context: { monitorName: monitor.name, monitorUrl: monitor.url },
subject: `Monitor ${monitor.name} is ${isAlive === true ? "up" : "down"}`,
};
this.handleNotification(config);
}
}
if (monitor.type === this.TYPE_HARDWARE) {
this.handleHardwareUpdate(monitor, hardwareData);
}
} catch (error) {
this.logger.error({
message: error.message,
service: this.SERVICE_NAME,
method: "handleStatusUpdate",
stack: error.stack,
details: `status update error for monitor: ${_id}`,
});
}
}
async handleHardwareUpdate(monitor, hardwareData) {
//Get Thresholds
const thresholds = monitor?.thresholds;
if (thresholds === undefined) {
return;
}
//Get Values
const cpuUsage = hardwareData?.cpu?.usage_percent;
const memoryUsage = hardwareData?.memory?.usage_percent;
const diskUsage = hardwareData?.disk[0]?.usage_percent;
// Return early if no values
if (cpuUsage === undefined && memoryUsage === undefined && diskUsage === undefined) {
return;
}
// Return early if all values are below thresholds
if (
cpuUsage < thresholds.usage_cpu &&
memoryUsage < thresholds.usage_memory &&
diskUsage < thresholds.usage_disk
) {
return;
}
let context = {
message: "Threshold(s) exceeded:",
};
if (cpuUsage > thresholds.usage_cpu) {
context.cpu = `CPU USAGE: ${cpuUsage * 100}% > ${thresholds.usage_cpu * 100}%`;
}
if (memoryUsage > thresholds.usage_memory) {
context.memory = `MEMORY USAGE: ${memoryUsage * 100}% > ${thresholds.usage_memory * 100}%`;
}
if (diskUsage > thresholds.usage_disk) {
context.disk = `DISK USAGE: ${diskUsage * 100}% > ${thresholds.usage_disk * 100}%`;
}
const config = {
monitor: monitor,
template: "thresholdViolatedTemplate",
context: context,
subject: `Threshold Violated for ${monitor.name}`,
};
this.handleNotification(config);
}
/**
* Measures the response time of an asynchronous operation.
* @param {Function} operation - An asynchronous operation to measure.
* @returns {Promise<{responseTime: number, response: any}>} An object containing the response time in milliseconds and the response from the operation.
* @throws {Error} The error object from the operation, contains response time.
*/
async measureResponseTime(operation) {
async timeRequest(operation) {
const startTime = Date.now();
try {
const response = await operation();
const endTime = Date.now();
return { responseTime: endTime - startTime, response };
const responseTime = endTime - startTime;
return { response, responseTime };
} catch (error) {
const endTime = Date.now();
error.responseTime = endTime - startTime;
error.service === undefined ? (error.service = this.SERVICE_NAME) : null;
error.method === undefined ? (error.method = "measureResponseTime") : null;
throw error;
const responseTime = endTime - startTime;
return { response: null, responseTime, error };
}
}
/**
* Handles the ping operation for a given job, measures its response time, and logs the result.
* @param {Object} job - The job object containing data for the ping operation.
* @returns {Promise<{boolean}} The result of logging and storing the check
* Sends a ping request to the specified URL and returns the response.
*
* @param {Object} job - The job object containing the data for the ping request.
* @param {Object} job.data - The data object within the job.
* @param {string} job.data.url - The URL to ping.
* @param {string} job.data._id - The monitor ID for the ping request.
* @returns {Promise<Object>} An object containing the ping response details.
* @property {string} monitorId - The monitor ID for the ping request.
* @property {string} type - The type of request, which is "ping".
* @property {number} responseTime - The time taken for the ping request to complete, in milliseconds.
* @property {Object} payload - The response payload from the ping request.
* @property {boolean} status - The status of the ping request (true if successful, false otherwise).
* @property {number} code - The response code (200 if successful, error code otherwise).
* @property {string} message - The message indicating the result of the ping request.
*/
async handlePing(job) {
let isAlive;
const operation = async () => {
const response = await this.ping.promise.probe(job.data.url);
return response;
};
try {
const { responseTime, response } = await this.measureResponseTime(operation);
isAlive = response.alive;
const checkData = {
monitorId: job.data._id,
status: isAlive,
responseTime,
message: isAlive
? successMessages.PING_SUCCESS
: errorMessages.PING_CANNOT_RESOLVE,
};
await this.logAndStoreCheck(checkData, this.db.createCheck);
} catch (error) {
isAlive = false;
const checkData = {
monitorId: job.data._id,
status: isAlive,
message: errorMessages.PING_CANNOT_RESOLVE,
responseTime: error.responseTime,
};
await this.logAndStoreCheck(checkData, this.db.createCheck);
} finally {
this.handleStatusUpdate(job, isAlive);
}
}
/**
* Handles the http operation for a given job, measures its response time, and logs the result.
* @param {Object} job - The job object containing data for the ping operation.
* @returns {Promise<{boolean}} The result of logging and storing the check
*/
async handleHttp(job) {
// Define operation for timing
const operation = async () => {
const response = await this.axios.get(job.data.url);
return response;
};
let isAlive;
// attempt connection
try {
const { responseTime, response } = await this.measureResponseTime(operation);
// check if response is in the 200 range, if so, service is up
isAlive = response.status >= 200 && response.status < 300;
//Create a check with relevant data
const checkData = {
monitorId: job.data._id,
status: isAlive,
responseTime,
statusCode: response.status,
message: this.http.STATUS_CODES[response.status],
};
await this.logAndStoreCheck(checkData, this.db.createCheck);
} catch (error) {
const statusCode = error.response?.status || this.NETWORK_ERROR;
let message = this.http.STATUS_CODES[statusCode] || "Network Error";
isAlive = false;
const checkData = {
monitorId: job.data._id,
status: isAlive,
statusCode,
responseTime: error.responseTime,
message,
};
await this.logAndStoreCheck(checkData, this.db.createCheck);
} finally {
this.handleStatusUpdate(job, isAlive);
}
}
/**
* Handles PageSpeed job types by fetching and processing PageSpeed insights.
*
* This method sends a request to the Google PageSpeed Insights API to get performance metrics
* for the specified URL, then logs and stores the check results.
*
* @param {Object} job - The job object containing data related to the PageSpeed check.
* @param {string} job.data.url - The URL to be analyzed by the PageSpeed Insights API.
* @param {string} job.data._id - The unique identifier for the monitor associated with the check.
*
* @returns {Promise<void>} A promise that resolves when the check results have been logged and stored.
*
* @throws {Error} Throws an error if there is an issue with fetching or processing the PageSpeed insights.
*/
async handlePagespeed(job) {
let isAlive;
try {
const url = job.data.url;
const response = await this.axios.get(
`https://pagespeedonline.googleapis.com/pagespeedonline/v5/runPagespeed?url=${url}&category=seo&category=accessibility&category=best-practices&category=performance`
);
const pageSpeedResults = response.data;
const categories = pageSpeedResults.lighthouseResult?.categories;
const audits = pageSpeedResults.lighthouseResult?.audits;
const {
"cumulative-layout-shift": cls,
"speed-index": si,
"first-contentful-paint": fcp,
"largest-contentful-paint": lcp,
"total-blocking-time": tbt,
} = audits;
// Weights
// First Contentful Paint 10%
// Speed Index 10%
// Largest Contentful Paint 25%
// Total Blocking Time 30%
// Cumulative Layout Shift 25%
isAlive = true;
const checkData = {
monitorId: job.data._id,
status: isAlive,
statusCode: response.status,
message: this.http.STATUS_CODES[response.status],
accessibility: (categories.accessibility?.score || 0) * 100,
bestPractices: (categories["best-practices"]?.score || 0) * 100,
seo: (categories.seo?.score || 0) * 100,
performance: (categories.performance?.score || 0) * 100,
audits: {
cls,
si,
fcp,
lcp,
tbt,
},
};
this.logAndStoreCheck(checkData, this.db.createPageSpeedCheck);
} catch (error) {
isAlive = false;
const statusCode = error.response?.status || this.NETWORK_ERROR;
const message = this.http.STATUS_CODES[statusCode] || "Network Error";
const checkData = {
monitorId: job.data._id,
status: isAlive,
statusCode,
message,
accessibility: 0,
bestPractices: 0,
seo: 0,
performance: 0,
};
this.logAndStoreCheck(checkData, this.db.createPageSpeedCheck);
} finally {
this.handleStatusUpdate(job, isAlive);
}
}
async handleHardware(job) {
async requestPing(job) {
const url = job.data.url;
let isAlive;
//TODO Fetch hardware data
//For now, fake hardware data:
const { response, responseTime, error } = await this.timeRequest(() =>
this.ping.promise.probe(url)
);
const hardwareData = {
const pingResponse = {
monitorId: job.data._id,
cpu: {
physical_core: 1,
logical_core: 1,
frequency: 266,
temperature: null,
free_percent: null,
usage_percent: 1,
},
memory: {
total_bytes: 4,
available_bytes: 4,
used_bytes: 2,
usage_percent: 1,
},
disk: [
{
read_speed_bytes: 3,
write_speed_bytes: 3,
total_bytes: 10,
free_bytes: 2,
usage_percent: 1,
},
],
host: {
os: "Linux",
platform: "Ubuntu",
kernel_version: "24.04",
},
type: "ping",
responseTime,
payload: response,
};
try {
isAlive = true;
this.logAndStoreCheck(hardwareData, this.db.createHardwareCheck);
} catch (error) {
isAlive = false;
const nullData = {
monitorId: job.data._id,
cpu: {
physical_core: 0,
logical_core: 0,
frequency: 0,
temperature: 0,
free_percent: 0,
usage_percent: 0,
},
memory: {
total_bytes: 0,
available_bytes: 0,
used_bytes: 0,
usage_percent: 0,
},
disk: [
{
read_speed_bytes: 0,
write_speed_bytes: 0,
total_bytes: 0,
free_bytes: 0,
usage_percent: 0,
},
],
host: {
os: "",
platform: "",
kernel_version: "",
},
};
this.logAndStoreCheck(nullData, this.db.createHardwareCheck);
} finally {
this.handleStatusUpdate(job, isAlive, hardwareData);
if (error) {
pingResponse.status = false;
pingResponse.code = this.PING_ERROR;
pingResponse.message = errorMessages.PING_CANNOT_RESOLVE;
return pingResponse;
}
pingResponse.code = 200;
pingResponse.status = response.alive;
pingResponse.message = successMessages.PING_SUCCESS;
return pingResponse;
}
/**
* Retrieves the status of a given job based on its type.
* For unsupported job types, it logs an error and returns false.
* Sends an HTTP GET request to the specified URL and returns the response.
*
* @param {Object} job - The job object containing data necessary for processing.
* @returns {Promise<boolean>} The status of the job if it is supported and processed successfully, otherwise false.
* @param {Object} job - The job object containing the data for the HTTP request.
* @param {Object} job.data - The data object within the job.
* @param {string} job.data.url - The URL to send the HTTP GET request to.
* @param {string} job.data._id - The monitor ID for the HTTP request.
* @returns {Promise<Object>} An object containing the HTTP response details.
* @property {string} monitorId - The monitor ID for the HTTP request.
* @property {string} type - The type of request, which is "http".
* @property {number} responseTime - The time taken for the HTTP request to complete, in milliseconds.
* @property {Object} payload - The response payload from the HTTP request.
* @property {boolean} status - The status of the HTTP request (true if successful, false otherwise).
* @property {number} code - The response code (200 if successful, error code otherwise).
* @property {string} message - The message indicating the result of the HTTP request.
*/
async requestHttp(job) {
const url = job.data.url;
const { response, responseTime, error } = await this.timeRequest(() =>
this.axios.get(url)
);
const httpResponse = {
monitorId: job.data._id,
type: "http",
responseTime,
payload: response?.data,
};
if (error) {
const code = error.response?.status || this.NETWORK_ERROR;
httpResponse.code = code;
httpResponse.status = false;
httpResponse.message = this.http.STATUS_CODES[code] || "Network Error";
return httpResponse;
}
httpResponse.status = true;
httpResponse.code = response.status;
httpResponse.message = this.http.STATUS_CODES[response.status];
return httpResponse;
}
/**
* Sends a request to the Google PageSpeed Insights API for the specified URL and returns the response.
*
* @param {Object} job - The job object containing the data for the PageSpeed request.
* @param {Object} job.data - The data object within the job.
* @param {string} job.data.url - The URL to analyze with PageSpeed Insights.
* @param {string} job.data._id - The monitor ID for the PageSpeed request.
* @returns {Promise<Object>} An object containing the PageSpeed response details.
* @property {string} monitorId - The monitor ID for the PageSpeed request.
* @property {string} type - The type of request, which is "pagespeed".
* @property {number} responseTime - The time taken for the PageSpeed request to complete, in milliseconds.
* @property {Object} payload - The response payload from the PageSpeed request.
* @property {boolean} status - The status of the PageSpeed request (true if successful, false otherwise).
* @property {number} code - The response code (200 if successful, error code otherwise).
* @property {string} message - The message indicating the result of the PageSpeed request.
*/
async requestPagespeed(job) {
const url = job.data.url;
const { response, responseTime, error } = await this.timeRequest(() =>
this.axios.get(
`https://pagespeedonline.googleapis.com/pagespeedonline/v5/runPagespeed?url=${url}&category=seo&category=accessibility&category=best-practices&category=performance`
)
);
const pagespeedResponse = {
monitorId: job.data._id,
type: "pagespeed",
responseTime,
payload: response?.data,
};
if (error) {
const code = error.response?.status || this.NETWORK_ERROR;
pagespeedResponse.code = code;
pagespeedResponse.status = false;
pagespeedResponse.message = this.http.STATUS_CODES[code] || "Network Error";
return pagespeedResponse;
}
pagespeedResponse.status = true;
pagespeedResponse.code = response.status;
pagespeedResponse.message = this.http.STATUS_CODES[response.status];
return pagespeedResponse;
}
async requestHandleHardware(job) {}
/**
* Gets the status of a job based on its type and returns the appropriate response.
*
* @param {Object} job - The job object containing the data for the status request.
* @param {Object} job.data - The data object within the job.
* @param {string} job.data.type - The type of the job (e.g., "ping", "http", "pagespeed", "hardware").
* @returns {Promise<Object>} The response object from the appropriate request method.
* @throws {Error} Throws an error if the job type is unsupported.
*/
async getStatus(job) {
switch (job.data.type) {
case this.TYPE_PING:
return await this.handlePing(job);
return await this.requestPing(job);
case this.TYPE_HTTP:
return await this.handleHttp(job);
return await this.requestHttp(job);
case this.TYPE_PAGESPEED:
return await this.handlePagespeed(job);
return await this.requestPagespeed(job);
case this.TYPE_HARDWARE:
return await this.handleHardware(job);
return await this.requestHandleHardware(job);
default:
this.logger.error({
message: `Unsupported type: ${job.data.type}`,
service: this.SERVICE_NAME,
method: "getStatus",
});
return false;
}
}
/**
* Logs and stores the result of a check for a specific job.
*
* @param {Object} data - Data to be written
* @param {function} writeToDB - DB write method
*
* @returns {Promise<boolean>} The status of the inserted check if successful, otherwise false.
*/
async logAndStoreCheck(data, writeToDB) {
try {
const insertedCheck = await writeToDB(data);
if (insertedCheck !== null && insertedCheck !== undefined) {
return insertedCheck.status;
}
throw new Error();
} catch (error) {
this.logger.error({
message: error.message,
service: this.SERVICE_NAME,
method: "logAndStoreCheck",
details: `Error writing check for ${data.monitorId}`,
stack: error.stack,
});
return {};
}
}
}
+63
View File
@@ -0,0 +1,63 @@
class NotificationService {
/**
* 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.
*/
constructor(emailService, db, logger) {
this.SERVICE_NAME = "NotificationService";
this.emailService = emailService;
this.db = db;
this.logger = logger;
}
/**
* Sends an email notification based on the network response.
*
* @param {Object} networkResponse - The response from the network monitor.
* @param {Object} networkResponse.monitor - The monitor object containing details about the monitored service.
* @param {string} networkResponse.monitor.name - The name of the monitor.
* @param {string} networkResponse.monitor.url - The URL of the monitor.
* @param {boolean} networkResponse.status - The current status of the monitor (true for up, false for down).
* @param {boolean} networkResponse.prevStatus - The previous status of the monitor (true for up, false for down).
* @param {string} address - The email address to send the notification to.
*/
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"}`;
this.emailService.buildAndSendEmail(template, context, address, subject);
}
/**
* Handles notifications based on the network response.
*
* @param {Object} networkResponse - The response from the network monitor.
* @param {string} networkResponse.monitorId - The ID of the monitor.
*/
async handleNotifications(networkResponse) {
try {
const notifications = await this.db.getNotificationsByMonitorId(
networkResponse.monitorId
);
for (const notification of notifications) {
if (notification.type === "email") {
this.sendEmail(networkResponse, notification.address);
}
// Handle other types of notifications here
}
} catch (error) {
this.logger.warn({
message: error.message,
service: this.SERVICE_NAME,
method: "handleNotifications",
stack: error.stack,
});
}
}
}
export default NotificationService;
+139
View File
@@ -0,0 +1,139 @@
class StatusService {
/**
* Creates an instance of StatusService.
*
* @param {Object} db - The database instance.
* @param {Object} logger - The logger instance.
*/
constructor(db, logger) {
this.db = db;
this.logger = logger;
this.SERVICE_NAME = "StatusService";
}
/**
* Updates the status of a monitor based on the network response.
*
* @param {Object} networkResponse - The network response containing monitorId and status.
* @param {string} networkResponse.monitorId - The ID of the monitor.
* @param {string} networkResponse.status - The new status of the monitor.
* @returns {Promise<Object>} - A promise that resolves to an object containing the monitor, statusChanged flag, and previous status if the status changed, or false if an error occurred.
* @returns {Promise<Object>} returnObject - The object returned by the function.
* @returns {Object} returnObject.monitor - The monitor object.
* @returns {boolean} returnObject.statusChanged - Flag indicating if the status has changed.
* @returns {boolean} returnObject.prevStatus - The previous status of the monitor
*/
updateStatus = async (networkResponse) => {
this.insertCheck(networkResponse);
try {
const { monitorId, status } = networkResponse;
const monitor = await this.db.getMonitorById(monitorId);
// No change in monitor status, return early
if (monitor.status === status) return { statusChanged: false };
// Monitor status changed, save prev status and update monitor
this.logger.info({
service: this.SERVICE_NAME,
message: `${monitor.name} went from ${monitor.status === true ? "up" : "down"} to ${status === true ? "up" : "down"}`,
prevStatus: monitor.status,
newStatus: status,
});
const prevStatus = monitor.status;
monitor.status = status;
await monitor.save();
return {
monitor,
statusChanged: true,
prevStatus: prevStatus,
};
//
} catch (error) {
this.logger.error({
service: this.SERVICE_NAME,
message: error.message,
method: "updateStatus",
stack: error.stack,
});
throw error;
}
};
/**
* Builds a check object from the network response.
*
* @param {Object} networkResponse - The network response object.
* @param {string} networkResponse.monitorId - The monitor ID.
* @param {string} networkResponse.type - The type of the response.
* @param {string} networkResponse.status - The status of the response.
* @param {number} networkResponse.responseTime - The response time.
* @param {number} networkResponse.code - The status code.
* @param {string} networkResponse.message - The message.
* @param {Object} networkResponse.payload - The payload of the response.
* @returns {Object} The check object.
*/
buildCheck = (networkResponse) => {
const { monitorId, type, status, responseTime, code, message, payload } =
networkResponse;
const check = {
monitorId,
status,
statusCode: code,
responseTime,
message,
};
if (type === "pagespeed") {
const categories = payload.lighthouseResult?.categories;
const audits = payload.lighthouseResult?.audits;
const {
"cumulative-layout-shift": cls = 0,
"speed-index": si = 0,
"first-contentful-paint": fcp = 0,
"largest-contentful-paint": lcp = 0,
"total-blocking-time": tbt = 0,
} = audits;
check.accessibility = (categories.accessibility?.score || 0) * 100;
check.bestPractices = (categories["best-practices"]?.score || 0) * 100;
check.seo = (categories.seo?.score || 0) * 100;
check.performance = (categories.performance?.score || 0) * 100;
check.audits = { cls, si, fcp, lcp, tbt };
}
return check;
};
/**
* Inserts a check into the database based on the network response.
*
* @param {Object} networkResponse - The network response object.
* @param {string} networkResponse.monitorId - The monitor ID.
* @param {string} networkResponse.type - The type of the response.
* @param {string} networkResponse.status - The status of the response.
* @param {number} networkResponse.responseTime - The response time.
* @param {number} networkResponse.code - The status code.
* @param {string} networkResponse.message - The message.
* @param {Object} networkResponse.payload - The payload of the response.
* @returns {Promise<void>} A promise that resolves when the check is inserted.
*/
insertCheck = async (networkResponse) => {
try {
const operationMap = {
http: this.db.createCheck,
ping: this.db.createCheck,
pagespeed: this.db.createPageSpeedCheck,
};
const operation = operationMap[networkResponse.type];
const check = this.buildCheck(networkResponse);
await operation(check);
} catch (error) {
this.logger.error({
message: error.message,
service: this.SERVICE_NAME,
method: "insertCheck",
details: `Error inserting check for monitor: ${networkResponse.monitorId}`,
stack: error.stack,
});
}
};
}
export default StatusService;
+44 -72
View File
@@ -1,73 +1,45 @@
<mjml>
<mj-head>
<mj-font
name="Roboto"
href="https://fonts.googleapis.com/css?family=Roboto:300,500"
></mj-font>
<mj-attributes>
<mj-all font-family="Roboto, Helvetica, sans-serif"></mj-all>
<mj-text
font-weight="300"
font-size="16px"
color="#616161"
line-height="24px"
></mj-text>
<mj-section padding="0px"></mj-section>
</mj-attributes>
</mj-head>
<mj-body>
<mj-section padding="20px 0">
<mj-column width="100%">
<mj-text
align="left"
font-size="10px"
>
Message from BlueWave Uptime Service
</mj-text>
</mj-column>
<mj-column
width="45%"
padding-top="20px"
>
<mj-text
align="center"
font-weight="500"
padding="0px"
font-size="18px"
color="red"
>
Google.com is down
</mj-text>
<mj-divider
border-width="2px"
border-color="#616161"
></mj-divider>
</mj-column>
</mj-section>
<mj-section>
<mj-column width="100%">
<mj-text>
<p>Hello {{name}}!</p>
<p>
We detected an incident on one of your monitors. Your service is currently
down. We'll send a message to you once it is up again.
</p>
<p><b>Monitor name:</b> {{monitor}}</p>
<p><b>URL:</b> {{url}}</p>
<p><b>Problem:</b> {{problem}}</p>
<p><b>Start date:</b> {{startDate}}</p>
</mj-text>
</mj-column>
<mj-column width="100%">
<mj-divider
border-width="1px"
border-color="#E0E0E0"
></mj-divider>
<mj-button background-color="#1570EF"> View incident details </mj-button>
<mj-text font-size="12px">
<p>This email was sent by BlueWave Uptime.</p>
</mj-text>
</mj-column>
</mj-section>
</mj-body>
</mjml>
<mj-head>
<mj-font name="Roboto" href="https://fonts.googleapis.com/css?family=Roboto:300,500"></mj-font>
<mj-attributes>
<mj-all font-family="Roboto, Helvetica, sans-serif"></mj-all>
<mj-text font-weight="300" font-size="16px" color="#616161" line-height="24px"></mj-text>
<mj-section padding="0px"></mj-section>
</mj-attributes>
</mj-head>
<mj-body>
<mj-section padding="20px 0">
<mj-column width="100%">
<mj-text align="left" font-size="10px">
Message from BlueWave Uptime Service
</mj-text>
</mj-column>
<mj-column width="45%" padding-top="20px">
<mj-text align="center" font-weight="500" padding="0px" font-size="18px" color="red">
Google.com is down
</mj-text>
<mj-divider border-width="2px" border-color="#616161"></mj-divider>
</mj-column>
</mj-section>
<mj-section>
<mj-column width="100%">
<mj-text>
<p>Hello {{name}}!</p>
<p>
We detected an incident on one of your monitors. Your service is currently
down. We'll send a message to you once it is up again.
</p>
<p><b>Monitor name:</b> {{monitor}}</p>
<p><b>URL:</b> {{url}}</p>
</mj-text>
</mj-column>
<mj-column width="100%">
<mj-divider border-width="1px" border-color="#E0E0E0"></mj-divider>
<mj-button background-color="#1570EF"> View incident details </mj-button>
<mj-text font-size="12px">
<p>This email was sent by BlueWave Uptime.</p>
</mj-text>
</mj-column>
</mj-section>
</mj-body>
</mjml>
+41 -71
View File
@@ -1,72 +1,42 @@
<mjml>
<mj-head>
<mj-font
name="Roboto"
href="https://fonts.googleapis.com/css?family=Roboto:300,500"
></mj-font>
<mj-attributes>
<mj-all font-family="Roboto, Helvetica, sans-serif"></mj-all>
<mj-text
font-weight="300"
font-size="16px"
color="#616161"
line-height="24px"
></mj-text>
<mj-section padding="0px"></mj-section>
</mj-attributes>
</mj-head>
<mj-body>
<mj-section padding="20px 0">
<mj-column width="100%">
<mj-text
align="left"
font-size="10px"
>
Message from BlueWave Uptime Service
</mj-text>
</mj-column>
<mj-column
width="45%"
padding-top="20px"
>
<mj-text
align="center"
font-weight="500"
padding="0px"
font-size="18px"
color="green"
>
{{monitor}} is up
</mj-text>
<mj-divider
border-width="2px"
border-color="#616161"
></mj-divider>
</mj-column>
</mj-section>
<mj-section>
<mj-column width="100%">
<mj-text>
<p>Hello {{name}}!</p>
<p>Your latest incident is resolved and your monitored service is up again.</p>
<p><b>Monitor name:</b> {{monitor}}</p>
<p><b>URL:</b> {{url}}</p>
<p><b>Problem:</b> {{problem}}</p>
<p><b>Start date:</b> {{startDate}}</p>
<p><b>Resolved date:</b> {{resolvedDate}}</p>
<p><b>Duration:</b>{{duration}}</p>
</mj-text>
</mj-column>
<mj-column width="100%">
<mj-divider
border-width="1px"
border-color="#E0E0E0"
></mj-divider>
<mj-button background-color="#1570EF"> View incident details </mj-button>
<mj-text font-size="12px">
<p>This email was sent by BlueWave Uptime.</p>
</mj-text>
</mj-column>
</mj-section>
</mj-body>
</mjml>
<mj-head>
<mj-font name="Roboto" href="https://fonts.googleapis.com/css?family=Roboto:300,500"></mj-font>
<mj-attributes>
<mj-all font-family="Roboto, Helvetica, sans-serif"></mj-all>
<mj-text font-weight="300" font-size="16px" color="#616161" line-height="24px"></mj-text>
<mj-section padding="0px"></mj-section>
</mj-attributes>
</mj-head>
<mj-body>
<mj-section padding="20px 0">
<mj-column width="100%">
<mj-text align="left" font-size="10px">
Message from BlueWave Uptime Service
</mj-text>
</mj-column>
<mj-column width="45%" padding-top="20px">
<mj-text align="center" font-weight="500" padding="0px" font-size="18px" color="green">
{{monitor}} is up
</mj-text>
<mj-divider border-width="2px" border-color="#616161"></mj-divider>
</mj-column>
</mj-section>
<mj-section>
<mj-column width="100%">
<mj-text>
<p>Hello {{name}}!</p>
<p>Your latest incident is resolved and your monitored service is up again.</p>
<p><b>Monitor name:</b> {{monitor}}</p>
<p><b>URL:</b> {{url}}</p>
</mj-text>
</mj-column>
<mj-column width="100%">
<mj-divider border-width="1px" border-color="#E0E0E0"></mj-divider>
<mj-button background-color="#1570EF"> View incident details </mj-button>
<mj-text font-size="12px">
<p>This email was sent by BlueWave Uptime.</p>
</mj-text>
</mj-column>
</mj-section>
</mj-body>
</mjml>
@@ -419,12 +419,6 @@ describe("Monitor Controller - createMonitor", () => {
expect(next.firstCall.args[0]).to.be.an("error");
expect(next.firstCall.args[0].message).to.equal("Job error");
});
it("should fail validation with empty notifications", async () => {
req.body.notifications = [];
await createMonitor(req, res, next);
expect(next.firstCall.args[0]).to.be.an("error");
expect(next.firstCall.args[0].status).to.equal(422);
});
it("should return success message and data if all operations succeed", async () => {
const monitor = { _id: "123", save: sinon.stub() };
req.db.createMonitor.returns(monitor);
@@ -790,12 +784,6 @@ describe("Monitor Controller - editMonitor", () => {
expect(next.firstCall.args[0]).to.be.an("error");
expect(next.firstCall.args[0].message).to.equal("Add Job error");
});
it("should fail validation with empty notifications", async () => {
req.body.notifications = [];
await editMonitor(req, res, next);
expect(next.firstCall.args[0]).to.be.an("error");
expect(next.firstCall.args[0].status).to.equal(422);
});
it("should return success message with data if all operations succeed", async () => {
const monitor = { _id: "123" };
req.db.getMonitorById.returns({ teamId: "123" });
+240 -178
View File
@@ -44,21 +44,41 @@ class WorkerStub {
}
describe("JobQueue", () => {
let settingsService, logger, db, networkService;
let settingsService,
logger,
db,
networkService,
statusService,
notificationService,
jobQueue;
beforeEach(() => {
beforeEach(async () => {
settingsService = { getSettings: sinon.stub() };
statusService = { updateStatus: sinon.stub() };
notificationService = { handleNotifications: sinon.stub() };
logger = { error: sinon.stub(), info: sinon.stub() };
db = {
getAllMonitors: sinon.stub().returns([]),
getMaintenanceWindowsByMonitorId: sinon.stub().returns([]),
};
networkService = { getStatus: sinon.stub() };
jobQueue = await JobQueue.createJobQueue(
db,
networkService,
statusService,
notificationService,
settingsService,
logger,
QueueStub,
WorkerStub
);
});
afterEach(() => {
sinon.restore();
});
describe("createJobQueue", () => {
it("should create a new JobQueue and add jobs for active monitors", async () => {
db.getAllMonitors.returns([
@@ -68,6 +88,8 @@ describe("JobQueue", () => {
const jobQueue = await JobQueue.createJobQueue(
db,
networkService,
statusService,
notificationService,
settingsService,
logger,
QueueStub,
@@ -81,9 +103,11 @@ describe("JobQueue", () => {
it("should reject with an error if an error occurs", async () => {
db.getAllMonitors.throws("Error");
try {
await JobQueue.createJobQueue(
const jobQueue = await JobQueue.createJobQueue(
db,
networkService,
statusService,
notificationService,
settingsService,
logger,
QueueStub,
@@ -94,6 +118,7 @@ describe("JobQueue", () => {
expect(error.method).to.equal("createJobQueue");
}
});
it("should reject with an error if an error occurs, should not overwrite error data", async () => {
const error = new Error("Error");
error.service = "otherService";
@@ -101,9 +126,11 @@ describe("JobQueue", () => {
db.getAllMonitors.throws(error);
try {
await JobQueue.createJobQueue(
const jobQueue = await JobQueue.createJobQueue(
db,
networkService,
statusService,
notificationService,
settingsService,
logger,
QueueStub,
@@ -115,58 +142,53 @@ describe("JobQueue", () => {
}
});
});
describe("Constructor", () => {
it("should construct a new JobQueue with default port and host if not provided", () => {
it("should construct a new JobQueue with default port and host if not provided", async () => {
settingsService.getSettings.returns({});
const jobQueue = new JobQueue(settingsService, logger, QueueStub, WorkerStub);
expect(jobQueue.connection.host).to.equal("127.0.0.1");
expect(jobQueue.connection.port).to.equal(6379);
});
it("should construct a new JobQueue with provided port and host", () => {
it("should construct a new JobQueue with provided port and host", async () => {
settingsService.getSettings.returns({ redisHost: "localhost", redisPort: 1234 });
const jobQueue = new JobQueue(settingsService, logger, QueueStub, WorkerStub);
const jobQueue = await JobQueue.createJobQueue(
db,
networkService,
statusService,
notificationService,
settingsService,
logger,
QueueStub,
WorkerStub
);
expect(jobQueue.connection.host).to.equal("localhost");
expect(jobQueue.connection.port).to.equal(1234);
});
});
describe("createWorker", () => {
it("should create a new worker", async () => {
const jobQueue = new JobQueue(settingsService, logger, QueueStub, WorkerStub);
const worker = jobQueue.createWorker();
expect(worker).to.be.instanceOf(WorkerStub);
});
it("worker should handle a maintenanceWindow error", async () => {
describe("isMaintenanceWindow", () => {
it("should throw an error if error occurs", async () => {
db.getMaintenanceWindowsByMonitorId.throws("Error");
const jobQueue = await JobQueue.createJobQueue(
db,
networkService,
statusService,
notificationService,
settingsService,
logger,
QueueStub,
WorkerStub
);
const worker = jobQueue.createWorker();
await worker.workerTask();
expect(logger.error.calledOnce).to.be.true;
try {
jobQueue.isInMaintenanceWindow(1);
} catch (error) {
expect(error.service).to.equal("JobQueue");
expect(error.method).to.equal("createWorker");
}
});
it("worker should handle a maintenanceWindow that is not active", async () => {
db.getMaintenanceWindowsByMonitorId.returns([
{ start: 123, end: 123, repeat: 123456 },
]);
const jobQueue = await JobQueue.createJobQueue(
db,
networkService,
settingsService,
logger,
QueueStub,
WorkerStub
);
const worker = jobQueue.createWorker();
await worker.workerTask();
expect(networkService.getStatus.calledOnce).to.be.true;
});
it("worker should handle a maintenanceWindow that is active", async () => {
it("should return true if in maintenance window with no repeat", async () => {
db.getMaintenanceWindowsByMonitorId.returns([
{
active: true,
@@ -178,42 +200,193 @@ describe("JobQueue", () => {
const jobQueue = await JobQueue.createJobQueue(
db,
networkService,
statusService,
notificationService,
settingsService,
logger,
QueueStub,
WorkerStub
);
const worker = jobQueue.createWorker();
await worker.workerTask();
expect(networkService.getStatus.calledOnce).to.be.false;
const inWindow = await jobQueue.isInMaintenanceWindow(1);
expect(inWindow).to.be.true;
});
it("worker should handle a maintenanceWindow that is active, has a repeat, but is not in maintenance zone", async () => {
it("should return true if in maintenance window with repeat", async () => {
db.getMaintenanceWindowsByMonitorId.returns([
{
active: true,
start: new Date(Date.now() - 10000).toISOString(),
end: new Date(Date.now() + 5000).toISOString(),
repeat: 10000,
end: new Date(Date.now() - 5000).toISOString(),
repeat: 1000,
},
]);
const jobQueue = await JobQueue.createJobQueue(
db,
networkService,
statusService,
notificationService,
settingsService,
logger,
QueueStub,
WorkerStub
);
const worker = jobQueue.createWorker();
await worker.workerTask();
expect(networkService.getStatus.calledOnce).to.be.true;
const inWindow = await jobQueue.isInMaintenanceWindow(1);
expect(inWindow).to.be.true;
});
it("should return false if in end < start", async () => {
db.getMaintenanceWindowsByMonitorId.returns([
{
active: true,
start: new Date(Date.now() - 5000).toISOString(),
end: new Date(Date.now() - 10000).toISOString(),
repeat: 1000,
},
]);
const jobQueue = await JobQueue.createJobQueue(
db,
networkService,
statusService,
notificationService,
settingsService,
logger,
QueueStub,
WorkerStub
);
const inWindow = await jobQueue.isInMaintenanceWindow(1);
expect(inWindow).to.be.false;
});
it("should return false if not in maintenance window", async () => {
db.getMaintenanceWindowsByMonitorId.returns([
{
active: false,
start: new Date(Date.now() - 5000).toISOString(),
end: new Date(Date.now() - 10000).toISOString(),
repeat: 1000,
},
]);
const jobQueue = await JobQueue.createJobQueue(
db,
networkService,
statusService,
notificationService,
settingsService,
logger,
QueueStub,
WorkerStub
);
const inWindow = await jobQueue.isInMaintenanceWindow(1);
expect(inWindow).to.be.false;
});
});
describe("createJobHandler", () => {
it("resolve to an error if an error is thrown within", async () => {
const jobQueue = await JobQueue.createJobQueue(
db,
networkService,
statusService,
notificationService,
settingsService,
logger,
QueueStub,
WorkerStub
);
jobQueue.isInMaintenanceWindow = sinon.stub().throws("Error");
try {
const handler = jobQueue.createJobHandler();
await handler({ data: { _id: 1 } });
} catch (error) {
expect(error.service).to.equal("JobQueue");
expect(error.details).to.equal(`Error processing job 1: Error`);
}
});
it("should log info if job is in maintenance window", async () => {
const jobQueue = await JobQueue.createJobQueue(
db,
networkService,
statusService,
notificationService,
settingsService,
logger,
QueueStub,
WorkerStub
);
jobQueue.isInMaintenanceWindow = sinon.stub().returns(true);
const handler = jobQueue.createJobHandler();
await handler({ data: { _id: 1 } });
expect(logger.info.calledOnce).to.be.true;
expect(logger.info.firstCall.args[0].message).to.equal(
"Monitor 1 is in maintenance window"
);
});
it("should return if status has not changed", async () => {
const jobQueue = await JobQueue.createJobQueue(
db,
networkService,
statusService,
notificationService,
settingsService,
logger,
QueueStub,
WorkerStub
);
jobQueue.isInMaintenanceWindow = sinon.stub().returns(false);
statusService.updateStatus = sinon.stub().returns({ statusChanged: false });
const handler = jobQueue.createJobHandler();
await handler({ data: { _id: 1 } });
expect(jobQueue.notificationService.handleNotifications.notCalled).to.be.true;
});
it("should return if status has changed, but prevStatus was undefined (monitor paused)", async () => {
const jobQueue = await JobQueue.createJobQueue(
db,
networkService,
statusService,
notificationService,
settingsService,
logger,
QueueStub,
WorkerStub
);
jobQueue.isInMaintenanceWindow = sinon.stub().returns(false);
statusService.updateStatus = sinon
.stub()
.returns({ statusChanged: true, prevStatus: undefined });
const handler = jobQueue.createJobHandler();
await handler({ data: { _id: 1 } });
expect(jobQueue.notificationService.handleNotifications.notCalled).to.be.true;
});
it("should call notification service if status changed and monitor was not paused", async () => {
const jobQueue = await JobQueue.createJobQueue(
db,
networkService,
statusService,
notificationService,
settingsService,
logger,
QueueStub,
WorkerStub
);
jobQueue.isInMaintenanceWindow = sinon.stub().returns(false);
statusService.updateStatus = sinon
.stub()
.returns({ statusChanged: true, prevStatus: false });
const handler = jobQueue.createJobHandler();
await handler({ data: { _id: 1 } });
expect(jobQueue.notificationService.handleNotifications.calledOnce).to.be.true;
});
});
describe("getWorkerStats", () => {
it("should throw an error if getRepeatable Jobs fails", async () => {
const jobQueue = await JobQueue.createJobQueue(
db,
networkService,
statusService,
notificationService,
settingsService,
logger,
QueueStub,
@@ -233,6 +406,8 @@ describe("JobQueue", () => {
const jobQueue = await JobQueue.createJobQueue(
db,
networkService,
statusService,
notificationService,
settingsService,
logger,
QueueStub,
@@ -252,11 +427,14 @@ describe("JobQueue", () => {
}
});
});
describe("scaleWorkers", () => {
it("should scale workers to 5 if no workers", async () => {
const jobQueue = await JobQueue.createJobQueue(
db,
networkService,
statusService,
notificationService,
settingsService,
logger,
QueueStub,
@@ -268,6 +446,8 @@ describe("JobQueue", () => {
const jobQueue = await JobQueue.createJobQueue(
db,
networkService,
statusService,
notificationService,
settingsService,
logger,
QueueStub,
@@ -286,6 +466,8 @@ describe("JobQueue", () => {
const jobQueue = await JobQueue.createJobQueue(
db,
networkService,
statusService,
notificationService,
settingsService,
logger,
QueueStub,
@@ -309,6 +491,8 @@ describe("JobQueue", () => {
const jobQueue = await JobQueue.createJobQueue(
db,
networkService,
statusService,
notificationService,
settingsService,
logger,
QueueStub,
@@ -329,6 +513,8 @@ describe("JobQueue", () => {
const jobQueue = await JobQueue.createJobQueue(
db,
networkService,
statusService,
notificationService,
settingsService,
logger,
QueueStub,
@@ -340,11 +526,13 @@ describe("JobQueue", () => {
});
});
describe("getJobs", async () => {
describe("getJobs", () => {
it("should return jobs", async () => {
const jobQueue = await JobQueue.createJobQueue(
db,
networkService,
statusService,
notificationService,
settingsService,
logger,
QueueStub,
@@ -357,6 +545,8 @@ describe("JobQueue", () => {
const jobQueue = await JobQueue.createJobQueue(
db,
networkService,
statusService,
notificationService,
settingsService,
logger,
QueueStub,
@@ -377,6 +567,8 @@ describe("JobQueue", () => {
const jobQueue = await JobQueue.createJobQueue(
db,
networkService,
statusService,
notificationService,
settingsService,
logger,
QueueStub,
@@ -398,28 +590,12 @@ describe("JobQueue", () => {
});
});
describe("getJobStats", async () => {
describe("getJobStats", () => {
it("should return job stats for no jobs", async () => {
const jobQueue = await JobQueue.createJobQueue(
db,
networkService,
settingsService,
logger,
QueueStub,
WorkerStub
);
const jobStats = await jobQueue.getJobStats();
expect(jobStats).to.deep.equal({ jobs: [], workers: 5 });
});
it("should return job stats for jobs", async () => {
const jobQueue = await JobQueue.createJobQueue(
db,
networkService,
settingsService,
logger,
QueueStub,
WorkerStub
);
jobQueue.queue.getJobs = async () => {
return [{ data: { url: "test" }, getState: async () => "completed" }];
};
@@ -430,14 +606,6 @@ describe("JobQueue", () => {
});
});
it("should reject with an error if mapping jobs fails", async () => {
const jobQueue = await JobQueue.createJobQueue(
db,
networkService,
settingsService,
logger,
QueueStub,
WorkerStub
);
jobQueue.queue.getJobs = async () => {
return [
{
@@ -457,14 +625,6 @@ describe("JobQueue", () => {
}
});
it("should reject with an error if mapping jobs fails but respect existing error data", async () => {
const jobQueue = await JobQueue.createJobQueue(
db,
networkService,
settingsService,
logger,
QueueStub,
WorkerStub
);
jobQueue.queue.getJobs = async () => {
return [
{
@@ -488,28 +648,12 @@ describe("JobQueue", () => {
});
});
describe("addJob", async () => {
describe("addJob", () => {
it("should add a job to the queue", async () => {
const jobQueue = await JobQueue.createJobQueue(
db,
networkService,
settingsService,
logger,
QueueStub,
WorkerStub
);
jobQueue.addJob("test", { url: "test" });
expect(jobQueue.queue.jobs.length).to.equal(1);
});
it("should reject with an error if adding fails", async () => {
const jobQueue = await JobQueue.createJobQueue(
db,
networkService,
settingsService,
logger,
QueueStub,
WorkerStub
);
jobQueue.queue.add = async () => {
throw new Error("Error adding job");
};
@@ -522,14 +666,6 @@ describe("JobQueue", () => {
}
});
it("should reject with an error if adding fails but respect existing error data", async () => {
const jobQueue = await JobQueue.createJobQueue(
db,
networkService,
settingsService,
logger,
QueueStub,
WorkerStub
);
jobQueue.queue.add = async () => {
const error = new Error("Error adding job");
error.service = "otherService";
@@ -545,16 +681,8 @@ describe("JobQueue", () => {
}
});
});
describe("deleteJob", async () => {
describe("deleteJob", () => {
it("should delete a job from the queue", async () => {
const jobQueue = await JobQueue.createJobQueue(
db,
networkService,
settingsService,
logger,
QueueStub,
WorkerStub
);
jobQueue.getWorkerStats = sinon.stub().returns({ load: 1, jobs: [{}] });
jobQueue.scaleWorkers = sinon.stub();
const monitor = { _id: 1 };
@@ -567,14 +695,6 @@ describe("JobQueue", () => {
// expect(jobQueue.scaleWorkers.calledOnce).to.be.true;
});
it("should log an error if job is not found", async () => {
const jobQueue = await JobQueue.createJobQueue(
db,
networkService,
settingsService,
logger,
QueueStub,
WorkerStub
);
jobQueue.getWorkerStats = sinon.stub().returns({ load: 1, jobs: [{}] });
jobQueue.scaleWorkers = sinon.stub();
const monitor = { _id: 1 };
@@ -584,14 +704,6 @@ describe("JobQueue", () => {
expect(logger.error.calledOnce).to.be.true;
});
it("should reject with an error if removeRepeatable fails", async () => {
const jobQueue = await JobQueue.createJobQueue(
db,
networkService,
settingsService,
logger,
QueueStub,
WorkerStub
);
jobQueue.queue.removeRepeatable = async () => {
const error = new Error("removeRepeatable error");
throw error;
@@ -606,14 +718,6 @@ describe("JobQueue", () => {
}
});
it("should reject with an error if removeRepeatable fails but respect existing error data", async () => {
const jobQueue = await JobQueue.createJobQueue(
db,
networkService,
settingsService,
logger,
QueueStub,
WorkerStub
);
jobQueue.queue.removeRepeatable = async () => {
const error = new Error("removeRepeatable error");
error.service = "otherService";
@@ -632,14 +736,6 @@ describe("JobQueue", () => {
});
describe("getMetrics", () => {
it("should return metrics for the job queue", async () => {
const jobQueue = await JobQueue.createJobQueue(
db,
networkService,
settingsService,
logger,
QueueStub,
WorkerStub
);
jobQueue.queue.getWaitingCount = async () => 1;
jobQueue.queue.getActiveCount = async () => 2;
jobQueue.queue.getCompletedCount = async () => 3;
@@ -657,14 +753,6 @@ describe("JobQueue", () => {
});
});
it("should log an error if metrics operations fail", async () => {
const jobQueue = await JobQueue.createJobQueue(
db,
networkService,
settingsService,
logger,
QueueStub,
WorkerStub
);
jobQueue.queue.getWaitingCount = async () => {
throw new Error("Error");
};
@@ -676,14 +764,6 @@ describe("JobQueue", () => {
describe("obliterate", () => {
it("should return true if obliteration is successful", async () => {
const jobQueue = await JobQueue.createJobQueue(
db,
networkService,
settingsService,
logger,
QueueStub,
WorkerStub
);
jobQueue.queue.pause = async () => true;
jobQueue.getJobs = async () => [{ key: 1, id: 1 }];
jobQueue.queue.removeRepeatableByKey = async () => true;
@@ -693,15 +773,6 @@ describe("JobQueue", () => {
expect(obliteration).to.be.true;
});
it("should throw an error if obliteration fails", async () => {
const jobQueue = await JobQueue.createJobQueue(
db,
networkService,
settingsService,
logger,
QueueStub,
WorkerStub
);
jobQueue.getMetrics = async () => {
throw new Error("Error");
};
@@ -714,15 +785,6 @@ describe("JobQueue", () => {
}
});
it("should throw an error if obliteration fails but respect existing error data", async () => {
const jobQueue = await JobQueue.createJobQueue(
db,
networkService,
settingsService,
logger,
QueueStub,
WorkerStub
);
jobQueue.getMetrics = async () => {
const error = new Error("Error");
error.service = "otherService";
File diff suppressed because it is too large. Load diff
@@ -0,0 +1,95 @@
import sinon from "sinon";
import NotificationService from "../../service/notificationService.js";
import { expect } from "chai";
describe("NotificationService", () => {
let emailService, db, logger, notificationService;
beforeEach(() => {
db = {
getNotificationsByMonitorId: sinon.stub(),
};
emailService = {
buildAndSendEmail: sinon.stub(),
};
logger = {
warn: sinon.stub(),
};
notificationService = new NotificationService(emailService, db, logger);
});
afterEach(() => {
sinon.restore();
});
describe("constructor", () => {
it("should create a new instance of NotificationService", () => {
expect(notificationService).to.be.an.instanceOf(NotificationService);
});
});
describe("sendEmail", async () => {
it("should send an email notification with Up Template", async () => {
const networkResponse = {
monitor: {
name: "Test Monitor",
url: "http://test.com",
},
status: true,
prevStatus: false,
};
const address = "test@test.com";
await notificationService.sendEmail(networkResponse, address);
expect(notificationService.emailService.buildAndSendEmail.calledOnce).to.be.true;
expect(
notificationService.emailService.buildAndSendEmail.calledWith(
"serverIsUpTemplate",
{ monitor: "Test Monitor", url: "http://test.com" }
)
);
});
it("should send an email notification with Down Template", async () => {
const networkResponse = {
monitor: {
name: "Test Monitor",
url: "http://test.com",
},
status: false,
prevStatus: true,
};
const address = "test@test.com";
await notificationService.sendEmail(networkResponse, address);
expect(notificationService.emailService.buildAndSendEmail.calledOnce).to.be.true;
});
it("should send an email notification with Up Template", async () => {
const networkResponse = {
monitor: {
name: "Test Monitor",
url: "http://test.com",
},
status: true,
prevStatus: false,
};
const address = "test@test.com";
await notificationService.sendEmail(networkResponse, address);
expect(notificationService.emailService.buildAndSendEmail.calledOnce).to.be.true;
});
});
describe("handleNotifications", async () => {
it("should handle notifications based on the network response", async () => {
notificationService.sendEmail = sinon.stub();
notificationService.db.getNotificationsByMonitorId.resolves([
{ type: "email", address: "www.google.com" },
]);
await notificationService.handleNotifications({ monitorId: "123" });
expect(notificationService.sendEmail.calledOnce).to.be.true;
});
it("should handle an error when getting notifications", async () => {
const testError = new Error("Test Error");
notificationService.db.getNotificationsByMonitorId.rejects(testError);
await notificationService.handleNotifications({ monitorId: "123" });
expect(notificationService.logger.warn.calledOnce).to.be.true;
});
});
});
+193
View File
@@ -0,0 +1,193 @@
import sinon from "sinon";
import StatusService from "../../service/statusService.js";
import { afterEach, describe } from "node:test";
describe("StatusService", () => {
let db, logger, statusService;
beforeEach(() => {
db = {
getMonitorById: sinon.stub(),
createCheck: sinon.stub(),
createPagespeedCheck: sinon.stub(),
};
logger = {
info: sinon.stub(),
error: sinon.stub(),
};
statusService = new StatusService(db, logger);
});
afterEach(() => {
sinon.restore();
});
describe("constructor", () => {
it("should create an instance of StatusService", () => {
expect(statusService).to.be.an.instanceOf(StatusService);
});
});
describe("updateStatus", async () => {
beforeEach(() => {
// statusService.insertCheck = sinon.stub().resolves;
});
afterEach(() => {
sinon.restore();
});
it("should throw an error if an error occurs", async () => {
const error = new Error("Test error");
statusService.db.getMonitorById = sinon.stub().throws(error);
try {
await statusService.updateStatus({ monitorId: "test", status: true });
} catch (error) {
expect(error.message).to.equal("Test error");
}
// expect(statusService.insertCheck.calledOnce).to.be.true;
});
it("should return {statusChanged: false} if status hasn't changed", async () => {
statusService.db.getMonitorById = sinon.stub().returns({ status: true });
const result = await statusService.updateStatus({
monitorId: "test",
status: true,
});
expect(result).to.deep.equal({ statusChanged: false });
// expect(statusService.insertCheck.calledOnce).to.be.true;
});
it("should return {statusChanged: true} if status has changed from down to up", async () => {
statusService.db.getMonitorById = sinon
.stub()
.returns({ status: false, save: sinon.stub() });
const result = await statusService.updateStatus({
monitorId: "test",
status: true,
});
expect(result.statusChanged).to.be.true;
expect(result.monitor.status).to.be.true;
expect(result.prevStatus).to.be.false;
// expect(statusService.insertCheck.calledOnce).to.be.true;
});
it("should return {statusChanged: true} if status has changed from up to down", async () => {
statusService.db.getMonitorById = sinon
.stub()
.returns({ status: true, save: sinon.stub() });
const result = await statusService.updateStatus({
monitorId: "test",
status: false,
});
expect(result.statusChanged).to.be.true;
expect(result.monitor.status).to.be.false;
expect(result.prevStatus).to.be.true;
// expect(statusService.insertCheck.calledOnce).to.be.true;
});
});
describe("buildCheck", () => {
it("should build a check object", () => {
const check = statusService.buildCheck({
monitorId: "test",
type: "test",
status: true,
responseTime: 100,
code: 200,
message: "Test message",
payload: { test: "test" },
});
expect(check.monitorId).to.equal("test");
expect(check.status).to.be.true;
expect(check.statusCode).to.equal(200);
expect(check.responseTime).to.equal(100);
expect(check.message).to.equal("Test message");
});
it("should build a check object for pagespeed type", () => {
const check = statusService.buildCheck({
monitorId: "test",
type: "pagespeed",
status: true,
responseTime: 100,
code: 200,
message: "Test message",
payload: {
lighthouseResult: {
categories: {
accessibility: { score: 1 },
"best-practices": { score: 1 },
performance: { score: 1 },
seo: { score: 1 },
},
audits: {
"cumulative-layout-shift": { score: 1 },
"speed-index": { score: 1 },
"first-contentful-paint": { score: 1 },
"largest-contentful-paint": { score: 1 },
"total-blocking-time": { score: 1 },
},
},
},
});
expect(check.monitorId).to.equal("test");
expect(check.status).to.be.true;
expect(check.statusCode).to.equal(200);
expect(check.responseTime).to.equal(100);
expect(check.message).to.equal("Test message");
expect(check.accessibility).to.equal(100);
expect(check.bestPractices).to.equal(100);
expect(check.performance).to.equal(100);
expect(check.seo).to.equal(100);
expect(check.audits).to.deep.equal({
cls: { score: 1 },
si: { score: 1 },
fcp: { score: 1 },
lcp: { score: 1 },
tbt: { score: 1 },
});
});
it("should build a check object for pagespeed type with missing data", () => {
const check = statusService.buildCheck({
monitorId: "test",
type: "pagespeed",
status: true,
responseTime: 100,
code: 200,
message: "Test message",
payload: {
lighthouseResult: {
categories: {},
audits: {},
},
},
});
expect(check.monitorId).to.equal("test");
expect(check.status).to.be.true;
expect(check.statusCode).to.equal(200);
expect(check.responseTime).to.equal(100);
expect(check.message).to.equal("Test message");
expect(check.accessibility).to.equal(0);
expect(check.bestPractices).to.equal(0);
expect(check.performance).to.equal(0);
expect(check.seo).to.equal(0);
expect(check.audits).to.deep.equal({
cls: 0,
si: 0,
fcp: 0,
lcp: 0,
tbt: 0,
});
});
});
describe("insertCheck", () => {
it("should log an error if one is thrown", async () => {
const testError = new Error("Test error");
statusService.db.createCheck = sinon.stub().throws(testError);
try {
await statusService.insertCheck({ monitorId: "test" });
} catch (error) {
expect(error.message).to.equal(testError.message);
}
expect(statusService.logger.error.calledOnce).to.be.true;
});
it("should insert a check into the database", async () => {
await statusService.insertCheck({ monitorId: "test", type: "http" });
expect(statusService.db.createCheck.calledOnce).to.be.true;
});
});
});
+1 -1
View File
@@ -234,7 +234,7 @@ const editMonitorBodyValidation = joi.object({
name: joi.string(),
description: joi.string(),
interval: joi.number(),
notifications: joi.array().items(joi.object()).min(1),
notifications: joi.array().items(joi.object()),
});
const pauseMonitorParamValidation = joi.object({
+9
View File
@@ -12,6 +12,15 @@ icon: sign-posts-wrench
---
## Quickstart for users (remote server) <a href="#user-quickstart" id="user-quickstart"></a>
1. Download our [Docker compose file](https://github.com/bluewave-labs/bluewave-uptime/blob/develop/Docker/dist/docker-compose.yaml)
2. Edit the `UPTIME_APP_API_BASE_URL` variable in the docker-compose file to point to your remote server.
3. Run `docker compose up` to start the application
4. Now the application is running at `http://<remote_server_ip>`
---
## Quickstart for developers <a href="#dev-quickstart" id="dev-quickstart"></a>
{% hint style="info" %}