2026-01-12 19:32:23 +00:00
import MonitorStats from "../../db/models/MonitorStats.js" ;
2026-01-12 22:58:46 +00:00
import { CheckModel } from "@/db/models/index.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
2026-01-14 19:28:17 +00:00
constructor ({ db , logger , buffer , incidentService , monitorsRepository }) {
2025-04-20 11:29:53 -07:00
this . db = db ;
this . logger = logger ;
this . buffer = buffer ;
2025-11-06 17:06:33 -05:00
this . incidentService = incidentService ;
2026-01-13 20:20:00 +00:00
this . monitorsRepository = monitorsRepository ;
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 {
2026-01-13 20:20:00 +00:00
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" ;
};
2025-11-18 16:01:47 -05:00
/**
* Saves check if needed and adds to incident buffer
* Removes check from checks buffer if it was saved immediately
*
* @param {Object} check - The check object
* @param {Object} monitor - The monitor object
* @param {string} action - The incident action ("create" or "resolve")
* @param {string} errorContext - Context for error messages
* @returns {Promise<void>}
*/
handleIncidentForCheck = async ( check , monitor , action , errorContext = "incident handling" ) => {
try {
let savedCheck = check ;
if ( ! check . _id ) {
try {
2026-01-12 22:58:46 +00:00
const checkModel = new CheckModel ( check );
2025-11-18 16:01:47 -05:00
savedCheck = await checkModel . save ();
this . buffer . removeCheckFromBuffer ( check );
} catch ( checkError ) {
this . logger . error ({
service : this . SERVICE_NAME ,
method : "handleIncidentForCheck" ,
message : `Failed to save check immediately for ${ errorContext } : ${ checkError . message } ` ,
2026-01-13 20:20:00 +00:00
monitorId : monitor . id ,
2025-11-18 16:01:47 -05:00
stack : checkError . stack ,
});
savedCheck = null ;
}
}
if ( savedCheck && savedCheck . _id ) {
try {
this . buffer . addIncidentToBuffer ({ monitor , check : savedCheck , action });
} catch ( incidentError ) {
this . logger . error ({
service : this . SERVICE_NAME ,
method : "handleIncidentForCheck" ,
message : `Failed to add incident to buffer for ${ errorContext } : ${ incidentError . message } ` ,
2026-01-13 20:20:00 +00:00
monitorId : monitor . id ,
2025-11-18 16:01:47 -05:00
action ,
stack : incidentError . stack ,
});
}
}
} catch ( error ) {
this . logger . error ({
service : this . SERVICE_NAME ,
method : "handleIncidentForCheck" ,
message : `Error in ${ errorContext } : ${ error . message } ` ,
2026-01-13 20:20:00 +00:00
monitorId : monitor ? . id ,
2025-11-18 16:01:47 -05:00
stack : error . stack ,
});
}
};
2025-04-20 11:29:53 -07:00
/**
* 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 {
2026-01-15 20:55:01 +00:00
const { monitorId , teamId , status , code } = networkResponse ;
const monitor = await this . monitorsRepository . findById ( monitorId , teamId );
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 ) {
2026-01-15 18:33:03 +00:00
const updated = await this . monitorsRepository . updateById ( monitor . id , monitor . teamId , monitor );
2025-04-20 11:29:53 -07:00
return {
2026-01-13 20:20:00 +00:00
monitor : updated ,
2025-04-20 11:29:53 -07:00
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 ,
});
2025-11-06 17:06:33 -05:00
2025-11-18 16:01:47 -05:00
if ( newStatus === false ) {
await this . handleIncidentForCheck ( check , monitor , "create" , "status change to down" );
} else if ( prevStatus === false ) {
await this . handleIncidentForCheck ( check , monitor , "resolve" , "status change to up" );
2025-11-06 17:06:33 -05:00
}
2025-11-18 16:01:47 -05:00
}
2025-11-06 17:06:33 -05:00
2025-11-18 16:01:47 -05:00
if ( monitor . status === false && ! statusChanged ) {
2025-11-06 17:06:33 -05:00
try {
2026-01-13 20:20:00 +00:00
const lastManuallyResolvedIncident = await this . db . incidentModule . getLastManuallyResolvedIncident ( monitor . id );
2025-11-18 16:01:47 -05:00
let calculatedFailureRate = failureRate ;
if ( lastManuallyResolvedIncident && lastManuallyResolvedIncident . endTime ) {
try {
const checksAfterResolution = await Check . find ({
2026-01-13 20:20:00 +00:00
monitorId : monitor . id ,
2025-11-18 16:01:47 -05:00
createdAt : { $gt : lastManuallyResolvedIncident . endTime },
})
. sort ({ createdAt : 1 })
. limit ( monitor . statusWindowSize )
. select ( "status" )
. lean ();
if ( checksAfterResolution . length > 0 ) {
const checksStatuses = checksAfterResolution . map (( c ) => c . status );
const failuresAfterResolution = checksStatuses . filter (( s ) => s === false ). length ;
calculatedFailureRate = ( failuresAfterResolution / monitor . statusWindow . length ) * 100 ;
} else {
calculatedFailureRate = 0 ;
}
} catch ( checkQueryError ) {
this . logger . error ({
service : this . SERVICE_NAME ,
method : "updateStatus" ,
message : `Failed to query checks after manual resolution: ${ checkQueryError . message } ` ,
2026-01-13 20:20:00 +00:00
monitorId : monitor . id ,
2025-11-18 16:01:47 -05:00
stack : checkQueryError . stack ,
});
}
2025-11-06 17:06:33 -05:00
}
2025-11-18 16:01:47 -05:00
if ( calculatedFailureRate >= monitor . statusWindowThreshold ) {
await this . handleIncidentForCheck ( check , monitor , "create" , "threshold check without status change" );
}
} catch ( error ) {
2025-11-06 17:06:33 -05:00
this . logger . error ({
service : this . SERVICE_NAME ,
method : "updateStatus" ,
2025-11-18 16:01:47 -05:00
message : `Error handling threshold check without status change: ${ error . message } ` ,
2026-01-13 20:20:00 +00:00
monitorId : monitor . id ,
2025-11-18 16:01:47 -05:00
stack : error . stack ,
2025-11-06 17:06:33 -05:00
});
}
2025-08-18 13:40:56 -07:00
}
monitor . status = newStatus ;
2026-01-15 18:33:03 +00:00
const updated = await this . monitorsRepository . updateById ( monitor . id , monitor . teamId , monitor );
2025-05-19 12:28:18 -07:00
2025-04-20 11:29:53 -07:00
return {
2026-01-13 20:20:00 +00:00
monitor : updated ,
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 = {
2026-01-12 19:32:23 +00:00
metadata : {
monitorId ,
teamId ,
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 ?? {};
2026-01-12 23:57:39 +00:00
const mapAudit = ( audit ) => {
if ( ! audit || typeof audit !== "object" ) {
return undefined ;
}
return {
id : audit . id ,
title : audit . title ,
2026-01-13 20:20:00 +00:00
score : typeof audit . score === "number" ? audit . score : ( audit . score ?? null ),
2026-01-12 23:57:39 +00:00
displayValue : audit . displayValue ,
numericValue : typeof audit . numericValue === "number" ? audit . numericValue : undefined ,
numericUnit : audit . numericUnit ,
};
};
2025-04-20 11:29:53 -07:00
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 ;
2026-01-12 23:57:39 +00:00
check . audits = {
cls : mapAudit ( audits ? .[ "cumulative-layout-shift" ]),
si : mapAudit ( audits ? .[ "speed-index" ]),
fcp : mapAudit ( audits ? .[ "first-contentful-paint" ]),
lcp : mapAudit ( audits ? .[ "largest-contentful-paint" ]),
tbt : mapAudit ( audits ? .[ "total-blocking-time" ]),
};
2025-04-20 11:29:53 -07:00
}
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 ;