@ -34,16 +34,25 @@ class UptimeCalculator {
* /
* /
minutelyUptimeDataList = new LimitQueue ( 24 * 60 ) ;
minutelyUptimeDataList = new LimitQueue ( 24 * 60 ) ;
/ * *
* Recent 30 - day uptime , each item is a 1 - hour interval
* Key : { number } DivisionKey
* @ type { LimitQueue < number , string > }
* /
hourlyUptimeDataList = new LimitQueue ( 30 * 24 ) ;
/ * *
/ * *
* Daily uptime data ,
* Daily uptime data ,
* Key : { number } DailyKey
* Key : { number } DailyKey
* /
* /
dailyUptimeDataList = new LimitQueue ( 365 ) ;
dailyUptimeDataList = new LimitQueue ( 365 ) ;
lastDailyUptimeData = null ;
lastUptimeData = null ;
lastUptimeData = null ;
lastHourlyUptimeData = null ;
lastDailyUptimeData = null ;
lastDailyStatBean = null ;
lastDailyStatBean = null ;
lastHourlyStatBean = null ;
lastMinutelyStatBean = null ;
lastMinutelyStatBean = null ;
/ * *
/ * *
@ -53,6 +62,10 @@ class UptimeCalculator {
* @ returns { Promise < UptimeCalculator > } UptimeCalculator
* @ returns { Promise < UptimeCalculator > } UptimeCalculator
* /
* /
static async getUptimeCalculator ( monitorID ) {
static async getUptimeCalculator ( monitorID ) {
if ( ! monitorID ) {
throw new Error ( "Monitor ID is required" ) ;
}
if ( ! UptimeCalculator . list [ monitorID ] ) {
if ( ! UptimeCalculator . list [ monitorID ] ) {
UptimeCalculator . list [ monitorID ] = new UptimeCalculator ( ) ;
UptimeCalculator . list [ monitorID ] = new UptimeCalculator ( ) ;
await UptimeCalculator . list [ monitorID ] . init ( monitorID ) ;
await UptimeCalculator . list [ monitorID ] . init ( monitorID ) ;
@ -108,13 +121,32 @@ class UptimeCalculator {
up : bean . up ,
up : bean . up ,
down : bean . down ,
down : bean . down ,
avgPing : bean . ping ,
avgPing : bean . ping ,
minPing : bean . pingMin ,
maxPing : bean . pingMax ,
} ) ;
}
// Load hourly data from database (recent 30 days only)
let hourlyStatBeans = await R . find ( "stat_hourly" , " monitor_id = ? AND timestamp > ? ORDER BY timestamp" , [
monitorID ,
this . getHourlyKey ( now . subtract ( 30 , "day" ) ) ,
] ) ;
for ( let bean of hourlyStatBeans ) {
let key = bean . timestamp ;
this . hourlyUptimeDataList . push ( key , {
up : bean . up ,
down : bean . down ,
avgPing : bean . ping ,
minPing : bean . pingMin ,
maxPing : bean . pingMax ,
} ) ;
} ) ;
}
}
// Load daily data from database (recent 365 days only)
// Load daily data from database (recent 365 days only)
let dailyStatBeans = await R . find ( "stat_daily" , " monitor_id = ? AND timestamp > ? ORDER BY timestamp" , [
let dailyStatBeans = await R . find ( "stat_daily" , " monitor_id = ? AND timestamp > ? ORDER BY timestamp" , [
monitorID ,
monitorID ,
this . getDailyKey ( now . subtract ( 365 , "day" ) . unix ( ) ) ,
this . getDailyKey ( now . subtract ( 365 , "day" ) ),
] ) ;
] ) ;
for ( let bean of dailyStatBeans ) {
for ( let bean of dailyStatBeans ) {
@ -123,6 +155,8 @@ class UptimeCalculator {
up : bean . up ,
up : bean . up ,
down : bean . down ,
down : bean . down ,
avgPing : bean . ping ,
avgPing : bean . ping ,
minPing : bean . pingMin ,
maxPing : bean . pingMax ,
} ) ;
} ) ;
}
}
}
}
@ -148,13 +182,16 @@ class UptimeCalculator {
}
}
let divisionKey = this . getMinutelyKey ( date ) ;
let divisionKey = this . getMinutelyKey ( date ) ;
let dailyKey = this . getDailyKey ( divisionKey ) ;
let hourlyKey = this . getHourlyKey ( date ) ;
let dailyKey = this . getDailyKey ( date ) ;
let minutelyData = this . minutelyUptimeDataList [ divisionKey ] ;
let minutelyData = this . minutelyUptimeDataList [ divisionKey ] ;
let hourlyData = this . hourlyUptimeDataList [ hourlyKey ] ;
let dailyData = this . dailyUptimeDataList [ dailyKey ] ;
let dailyData = this . dailyUptimeDataList [ dailyKey ] ;
if ( flatStatus === UP ) {
if ( flatStatus === UP ) {
minutelyData . up += 1 ;
minutelyData . up += 1 ;
hourlyData . up += 1 ;
dailyData . up += 1 ;
dailyData . up += 1 ;
// Only UP status can update the ping
// Only UP status can update the ping
@ -163,32 +200,57 @@ class UptimeCalculator {
// The first beat of the minute, the ping is the current ping
// The first beat of the minute, the ping is the current ping
if ( minutelyData . up === 1 ) {
if ( minutelyData . up === 1 ) {
minutelyData . avgPing = ping ;
minutelyData . avgPing = ping ;
minutelyData . minPing = ping ;
minutelyData . maxPing = ping ;
} else {
} else {
minutelyData . avgPing = ( minutelyData . avgPing * ( minutelyData . up - 1 ) + ping ) / minutelyData . up ;
minutelyData . avgPing = ( minutelyData . avgPing * ( minutelyData . up - 1 ) + ping ) / minutelyData . up ;
minutelyData . minPing = Math . min ( minutelyData . minPing , ping ) ;
minutelyData . maxPing = Math . max ( minutelyData . maxPing , ping ) ;
}
// Add avg ping
// The first beat of the hour, the ping is the current ping
if ( hourlyData . up === 1 ) {
hourlyData . avgPing = ping ;
hourlyData . minPing = ping ;
hourlyData . maxPing = ping ;
} else {
hourlyData . avgPing = ( hourlyData . avgPing * ( hourlyData . up - 1 ) + ping ) / hourlyData . up ;
hourlyData . minPing = Math . min ( hourlyData . minPing , ping ) ;
hourlyData . maxPing = Math . max ( hourlyData . maxPing , ping ) ;
}
}
// Add avg ping (daily)
// Add avg ping (daily)
// The first beat of the day, the ping is the current ping
// The first beat of the day, the ping is the current ping
if ( minutelyData . up === 1 ) {
if ( dai lyData. up === 1 ) {
dailyData . avgPing = ping ;
dailyData . avgPing = ping ;
dailyData . minPing = ping ;
dailyData . maxPing = ping ;
} else {
} else {
dailyData . avgPing = ( dailyData . avgPing * ( dailyData . up - 1 ) + ping ) / dailyData . up ;
dailyData . avgPing = ( dailyData . avgPing * ( dailyData . up - 1 ) + ping ) / dailyData . up ;
dailyData . minPing = Math . min ( dailyData . minPing , ping ) ;
dailyData . maxPing = Math . max ( dailyData . maxPing , ping ) ;
}
}
}
}
} else {
} else {
minutelyData . down += 1 ;
minutelyData . down += 1 ;
hourlyData . down += 1 ;
dailyData . down += 1 ;
dailyData . down += 1 ;
}
}
if ( dailyData !== this . lastDailyUptimeData ) {
this . lastDailyUptimeData = dailyData ;
}
if ( minutelyData !== this . lastUptimeData ) {
if ( minutelyData !== this . lastUptimeData ) {
this . lastUptimeData = minutelyData ;
this . lastUptimeData = minutelyData ;
}
}
if ( hourlyData !== this . lastHourlyUptimeData ) {
this . lastHourlyUptimeData = hourlyData ;
}
if ( dailyData !== this . lastDailyUptimeData ) {
this . lastDailyUptimeData = dailyData ;
}
// Don't store data in test mode
// Don't store data in test mode
if ( process . env . TEST _BACKEND ) {
if ( process . env . TEST _BACKEND ) {
log . debug ( "uptime-calc" , "Skip storing data in test mode" ) ;
log . debug ( "uptime-calc" , "Skip storing data in test mode" ) ;
@ -199,12 +261,24 @@ class UptimeCalculator {
dailyStatBean . up = dailyData . up ;
dailyStatBean . up = dailyData . up ;
dailyStatBean . down = dailyData . down ;
dailyStatBean . down = dailyData . down ;
dailyStatBean . ping = dailyData . avgPing ;
dailyStatBean . ping = dailyData . avgPing ;
dailyStatBean . pingMin = dailyData . minPing ;
dailyStatBean . pingMax = dailyData . maxPing ;
await R . store ( dailyStatBean ) ;
await R . store ( dailyStatBean ) ;
let hourlyStatBean = await this . getHourlyStatBean ( hourlyKey ) ;
hourlyStatBean . up = hourlyData . up ;
hourlyStatBean . down = hourlyData . down ;
hourlyStatBean . ping = hourlyData . avgPing ;
hourlyStatBean . pingMin = hourlyData . minPing ;
hourlyStatBean . pingMax = hourlyData . maxPing ;
await R . store ( hourlyStatBean ) ;
let minutelyStatBean = await this . getMinutelyStatBean ( divisionKey ) ;
let minutelyStatBean = await this . getMinutelyStatBean ( divisionKey ) ;
minutelyStatBean . up = minutelyData . up ;
minutelyStatBean . up = minutelyData . up ;
minutelyStatBean . down = minutelyData . down ;
minutelyStatBean . down = minutelyData . down ;
minutelyStatBean . ping = minutelyData . avgPing ;
minutelyStatBean . ping = minutelyData . avgPing ;
minutelyStatBean . pingMin = minutelyData . minPing ;
minutelyStatBean . pingMax = minutelyData . maxPing ;
await R . store ( minutelyStatBean ) ;
await R . store ( minutelyStatBean ) ;
// Remove the old data
// Remove the old data
@ -214,6 +288,11 @@ class UptimeCalculator {
this . getMinutelyKey ( date . subtract ( 24 , "hour" ) ) ,
this . getMinutelyKey ( date . subtract ( 24 , "hour" ) ) ,
] ) ;
] ) ;
await R . exec ( "DELETE FROM stat_hourly WHERE monitor_id = ? AND timestamp < ?" , [
this . monitorID ,
this . getHourlyKey ( date . subtract ( 30 , "day" ) ) ,
] ) ;
return date ;
return date ;
}
}
@ -242,6 +321,31 @@ class UptimeCalculator {
return this . lastDailyStatBean ;
return this . lastDailyStatBean ;
}
}
/ * *
* Get the hourly stat bean
* @ param { number } timestamp milliseconds
* @ returns { Promise < import ( "redbean-node" ) . Bean > } stat _hourly bean
* /
async getHourlyStatBean ( timestamp ) {
if ( this . lastHourlyStatBean && this . lastHourlyStatBean . timestamp === timestamp ) {
return this . lastHourlyStatBean ;
}
let bean = await R . findOne ( "stat_hourly" , " monitor_id = ? AND timestamp = ?" , [
this . monitorID ,
timestamp ,
] ) ;
if ( ! bean ) {
bean = R . dispense ( "stat_hourly" ) ;
bean . monitor _id = this . monitorID ;
bean . timestamp = timestamp ;
}
this . lastHourlyStatBean = bean ;
return this . lastHourlyStatBean ;
}
/ * *
/ * *
* Get the minutely stat bean
* Get the minutely stat bean
* @ param { number } timestamp milliseconds
* @ param { number } timestamp milliseconds
@ -268,11 +372,12 @@ class UptimeCalculator {
}
}
/ * *
/ * *
* Convert timestamp to minutely key
* @ param { dayjs . Dayjs } date The heartbeat date
* @ param { dayjs . Dayjs } date The heartbeat date
* @ returns { number } Timestamp
* @ returns { number } Timestamp
* /
* /
getMinutelyKey ( date ) {
getMinutelyKey ( date ) {
// Convert the current date to the nearest minute (e.g. 2021-01-01 12:34:56 -> 2021-01-01 12:34:00)
// Truncate value to minutes (e.g. 2021-01-01 12:34:56 -> 2021-01-01 12:34:00)
date = date . startOf ( "minute" ) ;
date = date . startOf ( "minute" ) ;
// Convert to timestamp in second
// Convert to timestamp in second
@ -283,6 +388,8 @@ class UptimeCalculator {
up : 0 ,
up : 0 ,
down : 0 ,
down : 0 ,
avgPing : 0 ,
avgPing : 0 ,
minPing : 0 ,
maxPing : 0 ,
} ) ;
} ) ;
}
}
@ -290,14 +397,37 @@ class UptimeCalculator {
}
}
/ * *
/ * *
* Convert timestamp to dai ly key
* Convert timestamp to hour ly key
* @ param { number} timestamp Timestamp
* @ param { dayjs. Dayjs } date The heartbeat date
* @ returns { number } Timestamp
* @ returns { number } Timestamp
* /
* /
getDailyKey ( timestamp ) {
getHourlyKey ( date ) {
let date = dayjs . unix ( timestamp ) ;
// Truncate value to hours (e.g. 2021-01-01 12:34:56 -> 2021-01-01 12:00:00)
date = date . startOf ( "hour" ) ;
// Convert to timestamp in second
let divisionKey = date . unix ( ) ;
if ( ! ( divisionKey in this . hourlyUptimeDataList ) ) {
this . hourlyUptimeDataList . push ( divisionKey , {
up : 0 ,
down : 0 ,
avgPing : 0 ,
minPing : 0 ,
maxPing : 0 ,
} ) ;
}
// Convert the date to the nearest day (e.g. 2021-01-01 12:34:56 -> 2021-01-01 00:00:00)
return divisionKey ;
}
/ * *
* Convert timestamp to daily key
* @ param { dayjs . Dayjs } date The heartbeat date
* @ returns { number } Timestamp
* /
getDailyKey ( date ) {
// Truncate value to start of day (e.g. 2021-01-01 12:34:56 -> 2021-01-01 00:00:00)
// Considering if the user keep changing could affect the calculation, so use UTC time to avoid this problem.
// Considering if the user keep changing could affect the calculation, so use UTC time to avoid this problem.
date = date . utc ( ) . startOf ( "day" ) ;
date = date . utc ( ) . startOf ( "day" ) ;
let dailyKey = date . unix ( ) ;
let dailyKey = date . unix ( ) ;
@ -307,12 +437,34 @@ class UptimeCalculator {
up : 0 ,
up : 0 ,
down : 0 ,
down : 0 ,
avgPing : 0 ,
avgPing : 0 ,
minPing : 0 ,
maxPing : 0 ,
} ) ;
} ) ;
}
}
return dailyKey ;
return dailyKey ;
}
}
/ * *
* Convert timestamp to key
* @ param { dayjs . Dayjs } datetime Datetime
* @ param { "day" | "hour" | "minute" } type the type of data which is expected to be returned
* @ returns { number } Timestamp
* @ throws { Error } If the type is invalid
* /
getKey ( datetime , type ) {
switch ( type ) {
case "day" :
return this . getDailyKey ( datetime ) ;
case "hour" :
return this . getHourlyKey ( datetime ) ;
case "minute" :
return this . getMinutelyKey ( datetime ) ;
default :
throw new Error ( "Invalid type" ) ;
}
}
/ * *
/ * *
* Flat status to UP or DOWN
* Flat status to UP or DOWN
* @ param { number } status the status which schould be turned into a flat status
* @ param { number } status the status which schould be turned into a flat status
@ -333,22 +485,22 @@ class UptimeCalculator {
/ * *
/ * *
* @ param { number } num the number of data points which are expected to be returned
* @ param { number } num the number of data points which are expected to be returned
* @ param { "day" | " minute"} type the type of data which is expected to be returned
* @ param { "day" | " hour" | " minute"} type the type of data which is expected to be returned
* @ returns { UptimeDataResult } UptimeDataResult
* @ returns { UptimeDataResult } UptimeDataResult
* @ throws { Error } The maximum number of minutes greater than 1440
* @ throws { Error } The maximum number of minutes greater than 1440
* /
* /
getData ( num , type = "day" ) {
getData ( num , type = "day" ) {
let key ;
if ( type === "day" ) {
if ( type === "hour" && num > 24 * 30 ) {
key = this . getDailyKey ( this . getCurrentDate ( ) . unix ( ) ) ;
throw new Error ( "The maximum number of hours is 720" ) ;
} else {
}
if ( num > 24 * 60 ) {
if ( type === "minute" && num > 24 * 60 ) {
throw new Error ( "The maximum number of minutes is 1440" ) ;
throw new Error ( "The maximum number of minutes is 1440" ) ;
}
key = this . getMinutelyKey ( this . getCurrentDate ( ) ) ;
}
}
// Get the current time period key based on the type
let key = this . getKey ( this . getCurrentDate ( ) , type ) ;
let total = {
let total = {
up : 0 ,
up : 0 ,
down : 0 ,
down : 0 ,
@ -357,20 +509,37 @@ class UptimeCalculator {
let totalPing = 0 ;
let totalPing = 0 ;
let endTimestamp ;
let endTimestamp ;
if ( type === "day" ) {
// Get the eariest timestamp of the required period based on the type
endTimestamp = key - 86400 * ( num - 1 ) ;
switch ( type ) {
} else {
case "day" :
endTimestamp = key - 60 * ( num - 1 ) ;
endTimestamp = key - 86400 * ( num - 1 ) ;
break ;
case "hour" :
endTimestamp = key - 3600 * ( num - 1 ) ;
break ;
case "minute" :
endTimestamp = key - 60 * ( num - 1 ) ;
break ;
default :
throw new Error ( "Invalid type" ) ;
}
}
// Sum up all data in the specified time range
// Sum up all data in the specified time range
while ( key >= endTimestamp ) {
while ( key >= endTimestamp ) {
let data ;
let data ;
if ( type === "day" ) {
switch ( type ) {
data = this . dailyUptimeDataList [ key ] ;
case "day" :
} else {
data = this . dailyUptimeDataList [ key ] ;
data = this . minutelyUptimeDataList [ key ] ;
break ;
case "hour" :
data = this . hourlyUptimeDataList [ key ] ;
break ;
case "minute" :
data = this . minutelyUptimeDataList [ key ] ;
break ;
default :
throw new Error ( "Invalid type" ) ;
}
}
if ( data ) {
if ( data ) {
@ -379,27 +548,53 @@ class UptimeCalculator {
totalPing += data . avgPing * data . up ;
totalPing += data . avgPing * data . up ;
}
}
// Previous day
// Set key to the pervious time period
if ( type === "day" ) {
switch ( type ) {
key -= 86400 ;
case "day" :
} else {
key -= 86400 ;
key -= 60 ;
break ;
case "hour" :
key -= 3600 ;
break ;
case "minute" :
key -= 60 ;
break ;
default :
throw new Error ( "Invalid type" ) ;
}
}
}
}
let uptimeData = new UptimeDataResult ( ) ;
let uptimeData = new UptimeDataResult ( ) ;
// If there is no data in the previous time ranges, use the last data?
if ( total . up === 0 && total . down === 0 ) {
if ( total . up === 0 && total . down === 0 ) {
if ( type === "day" && this . lastDailyUptimeData ) {
switch ( type ) {
total = this . lastDailyUptimeData ;
case "day" :
totalPing = total . avgPing * total . up ;
if ( this . lastDailyUptimeData ) {
} else if ( type === "minute" && this . lastUptimeData ) {
total = this . lastDailyUptimeData ;
total = this . lastUptimeData ;
totalPing = total . avgPing * total . up ;
totalPing = total . avgPing * total . up ;
} else {
} else {
return uptimeData ;
uptimeData . uptime = 0 ;
}
uptimeData . avgPing = null ;
break ;
return uptimeData ;
case "hour" :
if ( this . lastHourlyUptimeData ) {
total = this . lastHourlyUptimeData ;
totalPing = total . avgPing * total . up ;
} else {
return uptimeData ;
}
break ;
case "minute" :
if ( this . lastUptimeData ) {
total = this . lastUptimeData ;
totalPing = total . avgPing * total . up ;
} else {
return uptimeData ;
}
break ;
default :
throw new Error ( "Invalid type" ) ;
}
}
}
}
@ -416,6 +611,85 @@ class UptimeCalculator {
return uptimeData ;
return uptimeData ;
}
}
/ * *
* Get data in form of an array
* @ param { number } num the number of data points which are expected to be returned
* @ param { "day" | "hour" | "minute" } type the type of data which is expected to be returned
* @ returns { Array < object > } uptime data
* @ throws { Error } The maximum number of minutes greater than 1440
* /
getDataArray ( num , type = "day" ) {
if ( type === "hour" && num > 24 * 30 ) {
throw new Error ( "The maximum number of hours is 720" ) ;
}
if ( type === "minute" && num > 24 * 60 ) {
throw new Error ( "The maximum number of minutes is 1440" ) ;
}
// Get the current time period key based on the type
let key = this . getKey ( this . getCurrentDate ( ) , type ) ;
let result = [ ] ;
let endTimestamp ;
// Get the eariest timestamp of the required period based on the type
switch ( type ) {
case "day" :
endTimestamp = key - 86400 * ( num - 1 ) ;
break ;
case "hour" :
endTimestamp = key - 3600 * ( num - 1 ) ;
break ;
case "minute" :
endTimestamp = key - 60 * ( num - 1 ) ;
break ;
default :
throw new Error ( "Invalid type" ) ;
}
// Get datapoints in the specified time range
while ( key >= endTimestamp ) {
let data ;
switch ( type ) {
case "day" :
data = this . dailyUptimeDataList [ key ] ;
break ;
case "hour" :
data = this . hourlyUptimeDataList [ key ] ;
break ;
case "minute" :
data = this . minutelyUptimeDataList [ key ] ;
break ;
default :
throw new Error ( "Invalid type" ) ;
}
if ( data ) {
data . timestamp = key ;
result . push ( data ) ;
}
// Set key to the pervious time period
switch ( type ) {
case "day" :
key -= 86400 ;
break ;
case "hour" :
key -= 3600 ;
break ;
case "minute" :
key -= 60 ;
break ;
default :
throw new Error ( "Invalid type" ) ;
}
}
return result ;
}
/ * *
/ * *
* Get the uptime data by duration
* Get the uptime data by duration
* @ param { '24h' | '30d' | '1y' } duration Only accept 24 h , 30 d , 1 y
* @ param { '24h' | '30d' | '1y' } duration Only accept 24 h , 30 d , 1 y
@ -446,7 +720,7 @@ class UptimeCalculator {
* @ returns { UptimeDataResult } UptimeDataResult
* @ returns { UptimeDataResult } UptimeDataResult
* /
* /
get7Day ( ) {
get7Day ( ) {
return this . getData ( 7 ) ;
return this . getData ( 168, "hour" ) ;
}
}
/ * *
/ * *
@ -464,7 +738,7 @@ class UptimeCalculator {
}
}
/ * *
/ * *
* @ returns { dayjs . Dayjs } Current date
* @ returns { dayjs . Dayjs } Current date time in UTC
* /
* /
getCurrentDate ( ) {
getCurrentDate ( ) {
return dayjs . utc ( ) ;
return dayjs . utc ( ) ;
@ -476,12 +750,12 @@ class UptimeDataResult {
/ * *
/ * *
* @ type { number } Uptime
* @ type { number } Uptime
* /
* /
uptime ;
uptime = 0 ;
/ * *
/ * *
* @ type { number } Average ping
* @ type { number } Average ping
* /
* /
avgPing ;
avgPing = null ;
}
}
module . exports = {
module . exports = {