2025-07-23 13:03:57 -07:00
import MonitorStats from "../../db/models/MonitorStats.js" ;
2025-04-20 11:29:53 -07:00
const SERVICE_NAME = "StatusService" ;
class StatusService {
static SERVICE_NAME = SERVICE_NAME ;
2025-07-25 15:15:14 -07:00
2025-04-20 11:29:53 -07:00
/**
2025-08-18 10:55:12 -07:00
* @param {{
* buffer: import("./bufferService.js").BufferService
* }}
*/ constructor ({ db , logger , buffer }) {
2025-04-20 11:29:53 -07:00
this . db = db ;
this . logger = logger ;
this . buffer = buffer ;
2025-07-25 15:15:14 -07:00
}
get serviceName () {
return StatusService . SERVICE_NAME ;
2025-04-20 11:29:53 -07:00
}
async updateRunningStats ({ monitor , networkResponse }) {
try {
const monitorId = monitor . _id ;
2025-08-18 10:55:12 -07:00
const { responseTime , status } = networkResponse ;
2025-04-20 11:29:53 -07:00
// Get stats
let stats = await MonitorStats . findOne ({ monitorId });
if ( ! stats ) {
stats = new MonitorStats ({
monitorId ,
avgResponseTime : 0 ,
totalChecks : 0 ,
totalUpChecks : 0 ,
totalDownChecks : 0 ,
uptimePercentage : 0 ,
lastCheck : null ,
});
}
// Update stats
// Last response time
stats . lastResponseTime = responseTime ;
// Avg response time:
let avgResponseTime = stats . avgResponseTime ;
if ( typeof responseTime !== "undefined" && responseTime !== null ) {
if ( avgResponseTime === 0 ) {
avgResponseTime = responseTime ;
} else {
2025-07-21 10:34:16 -07:00
avgResponseTime = ( avgResponseTime * ( stats . totalChecks - 1 ) + responseTime ) / stats . totalChecks ;
2025-04-20 11:29:53 -07:00
}
}
stats . avgResponseTime = avgResponseTime ;
// Total checks
stats . totalChecks ++ ;
if ( status === true ) {
stats . totalUpChecks ++ ;
// Update the timeSinceLastFailure if needed
if ( stats . timeOfLastFailure === 0 ) {
stats . timeOfLastFailure = new Date (). getTime ();
}
} else {
stats . totalDownChecks ++ ;
stats . timeOfLastFailure = 0 ;
}
// Calculate uptime percentage
let uptimePercentage ;
if ( stats . totalChecks > 0 ) {
uptimePercentage = stats . totalUpChecks / stats . totalChecks ;
} else {
uptimePercentage = status === true ? 100 : 0 ;
}
stats . uptimePercentage = uptimePercentage ;
// latest check
stats . lastCheckTimestamp = new Date (). getTime ();
await stats . save ();
return true ;
} catch ( error ) {
this . logger . error ({
service : this . SERVICE_NAME ,
message : error . message ,
method : "updateRunningStats" ,
stack : error . stack ,
});
return false ;
}
}
getStatusString = ( status ) => {
if ( status === true ) return "up" ;
if ( status === false ) return "down" ;
return "unknown" ;
};
/**
* 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 containinfg 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 ) => {
2025-08-18 10:55:12 -07:00
const check = this . buildCheck ( networkResponse );
await this . insertCheck ( check );
2025-04-20 11:29:53 -07:00
try {
const { monitorId , status , code } = networkResponse ;
2025-07-30 12:25:12 -07:00
const monitor = await this . db . monitorModule . getMonitorById ( monitorId );
2025-05-19 12:28:18 -07:00
2025-04-20 11:29:53 -07:00
// Update running stats
this . updateRunningStats ({ monitor , networkResponse });
2025-05-19 12:28:18 -07:00
2025-08-18 15:34:15 -07:00
// If the status window size has changed, empty
while ( monitor . statusWindow . length > monitor . statusWindowSize ) {
monitor . statusWindow . shift ();
}
2025-08-18 13:40:56 -07:00
// Update status sliding window
monitor . statusWindow . push ( status );
if ( monitor . statusWindow . length > monitor . statusWindowSize ) {
monitor . statusWindow . shift ();
}
2025-08-18 15:34:15 -07:00
if ( monitor . status === undefined || monitor . status === null ) {
2025-08-18 13:40:56 -07:00
monitor . status = status ;
}
let newStatus = monitor . status ;
let statusChanged = false ;
const prevStatus = monitor . status ;
// Return early if not enough data points
if ( monitor . statusWindow . length < monitor . statusWindowSize ) {
await monitor . save ();
2025-04-20 11:29:53 -07:00
return {
monitor ,
statusChanged : false ,
2025-08-18 13:40:56 -07:00
prevStatus ,
2025-04-20 11:29:53 -07:00
code ,
2025-08-18 13:40:56 -07:00
timestamp : Date . now (),
2025-04-20 11:29:53 -07:00
};
2025-08-18 13:40:56 -07:00
}
2025-05-19 12:28:18 -07:00
2025-08-18 13:40:56 -07:00
// Check if threshold has been met
const failures = monitor . statusWindow . filter (( s ) => s === false ). length ;
2025-08-18 15:34:15 -07:00
const failureRate = ( failures / monitor . statusWindow . length ) * 100 ;
2025-08-18 13:40:56 -07:00
// If threshold has been met and the monitor is not already down, mark down:
2025-08-18 15:34:15 -07:00
if ( failureRate >= monitor . statusWindowThreshold && monitor . status !== false ) {
2025-08-18 13:40:56 -07:00
newStatus = false ;
statusChanged = true ;
}
// If the failure rate is below the threshold and the monitor is down, recover:
2025-08-30 16:19:17 -07:00
else if ( failureRate < monitor . statusWindowThreshold && monitor . status === false ) {
2025-08-18 13:40:56 -07:00
newStatus = true ;
statusChanged = true ;
}
if ( statusChanged ) {
this . logger . info ({
service : this . SERVICE_NAME ,
message : ` ${ monitor . name } went from ${ this . getStatusString ( prevStatus ) } to ${ this . getStatusString ( newStatus ) } ` ,
prevStatus ,
newStatus ,
});
}
monitor . status = newStatus ;
2025-04-20 11:29:53 -07:00
await monitor . save ();
2025-05-19 12:28:18 -07:00
2025-04-20 11:29:53 -07:00
return {
monitor ,
2025-08-18 13:40:56 -07:00
statusChanged ,
2025-04-20 11:29:53 -07:00
prevStatus ,
code ,
timestamp : new Date (). getTime (),
};
} catch ( error ) {
2025-05-19 12:28:18 -07:00
error . service = this . SERVICE_NAME ;
error . method = "updateStatus" ;
2025-04-20 11:29:53 -07:00
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 ,
teamId ,
type ,
status ,
responseTime ,
code ,
message ,
payload ,
first_byte_took ,
body_read_took ,
dns_took ,
conn_took ,
connect_took ,
tls_took ,
2025-08-15 16:04:43 -07:00
timings ,
2025-04-20 11:29:53 -07:00
} = networkResponse ;
const check = {
monitorId ,
teamId ,
2025-08-18 10:55:12 -07:00
type ,
2025-04-20 11:29:53 -07:00
status ,
statusCode : code ,
responseTime ,
2025-08-15 16:04:43 -07:00
timings : timings || {},
2025-04-20 11:29:53 -07:00
message ,
first_byte_took ,
body_read_took ,
dns_took ,
conn_took ,
connect_took ,
tls_took ,
};
if ( type === "pagespeed" ) {
if ( typeof payload === "undefined" ) {
2025-06-06 14:05:59 +08:00
this . logger . warn ({
message : "Failed to build check" ,
service : this . SERVICE_NAME ,
method : "buildCheck" ,
details : "empty payload" ,
});
2025-04-20 11:29:53 -07:00
return undefined ;
}
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 };
}
if ( type === "hardware" ) {
2025-07-29 00:17:34 +05:30
const { cpu , memory , disk , host , net } = payload ? . data ?? {};
2025-04-20 11:29:53 -07:00
const { errors } = payload ? . errors ?? [];
check . cpu = cpu ?? {};
check . memory = memory ?? {};
check . disk = disk ?? {};
check . host = host ?? {};
check . errors = errors ?? [];
2025-06-08 00:13:47 +05:30
check . capture = payload ? . capture ?? {};
2025-07-29 00:17:34 +05:30
check . net = net ?? {};
2025-04-20 11:29:53 -07:00
}
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.
*/
2025-08-18 10:55:12 -07:00
insertCheck = async ( check ) => {
2025-04-20 11:29:53 -07:00
try {
if ( typeof check === "undefined" ) {
this . logger . warn ({
message : "Failed to build check" ,
service : this . SERVICE_NAME ,
method : "insertCheck" ,
});
2025-08-18 10:55:12 -07:00
return false ;
2025-04-20 11:29:53 -07:00
}
2025-08-18 10:55:12 -07:00
this . buffer . addToBuffer ({ check });
return true ;
2025-04-20 11:29:53 -07:00
} catch ( error ) {
this . logger . error ({
message : error . message ,
2025-06-06 14:05:59 +08:00
service : error . service || this . SERVICE_NAME ,
method : error . method || "insertCheck" ,
2025-08-18 10:55:12 -07:00
details : error . details || `Error inserting check for monitor: ${ check ? . monitorId } ` ,
2025-04-20 11:29:53 -07:00
stack : error . stack ,
});
}
};
}
export default StatusService ;