2021-07-01 06:03:06 +00:00
2021-06-25 13:55:49 +00:00
const dayjs = require ( "dayjs" ) ;
2021-06-27 08:10:55 +00:00
const utc = require ( 'dayjs/plugin/utc' )
var timezone = require ( 'dayjs/plugin/timezone' )
dayjs . extend ( utc )
dayjs . extend ( timezone )
const axios = require ( "axios" ) ;
2021-07-01 09:00:23 +00:00
const { tcping , ping } = require ( "../util-server" ) ;
2021-06-27 08:10:55 +00:00
const { R } = require ( "redbean-node" ) ;
2021-06-25 13:55:49 +00:00
const { BeanModel } = require ( "redbean-node/dist/bean-model" ) ;
2021-07-09 09:55:48 +00:00
const { Notification } = require ( "../notification" )
2021-06-27 08:10:55 +00:00
/ * *
* status :
* 0 = DOWN
* 1 = UP
* /
2021-06-25 13:55:49 +00:00
class Monitor extends BeanModel {
2021-07-09 09:55:48 +00:00
async toJSON ( ) {
let notificationIDList = { } ;
let list = await R . find ( "monitor_notification" , " monitor_id = ? " , [
this . id
] )
for ( let bean of list ) {
notificationIDList [ bean . notification _id ] = true ;
}
2021-06-25 13:55:49 +00:00
return {
id : this . id ,
name : this . name ,
url : this . url ,
2021-07-01 06:03:06 +00:00
hostname : this . hostname ,
port : this . port ,
2021-07-19 16:23:06 +00:00
maxretries : this . maxretries ,
2021-07-01 05:11:16 +00:00
weight : this . weight ,
2021-06-25 13:55:49 +00:00
active : this . active ,
type : this . type ,
interval : this . interval ,
2021-07-01 09:19:28 +00:00
keyword : this . keyword ,
2021-07-09 09:55:48 +00:00
notificationIDList
2021-06-25 13:55:49 +00:00
} ;
}
start ( io ) {
2021-06-29 08:06:20 +00:00
let previousBeat = null ;
2021-07-19 16:23:06 +00:00
let retries = 0 ;
2021-06-29 08:06:20 +00:00
2021-06-27 08:10:55 +00:00
const beat = async ( ) => {
2021-06-29 08:06:20 +00:00
if ( ! previousBeat ) {
previousBeat = await R . findOne ( "heartbeat" , " monitor_id = ? ORDER BY time DESC" , [
this . id
] )
}
2021-06-27 08:10:55 +00:00
let bean = R . dispense ( "heartbeat" )
bean . monitor _id = this . id ;
bean . time = R . isoDateTime ( dayjs . utc ( ) ) ;
bean . status = 0 ;
2021-06-30 18:02:54 +00:00
// Duration
if ( previousBeat ) {
bean . duration = dayjs ( bean . time ) . diff ( dayjs ( previousBeat . time ) , 'second' ) ;
} else {
bean . duration = 0 ;
}
2021-06-27 08:10:55 +00:00
try {
2021-07-01 09:19:28 +00:00
if ( this . type === "http" || this . type === "keyword" ) {
2021-06-27 08:10:55 +00:00
let startTime = dayjs ( ) . valueOf ( ) ;
2021-07-11 20:01:34 +00:00
let res = await axios . get ( this . url , {
headers : { 'User-Agent' : 'Uptime-Kuma' }
} )
2021-06-27 08:10:55 +00:00
bean . msg = ` ${ res . status } - ${ res . statusText } `
bean . ping = dayjs ( ) . valueOf ( ) - startTime ;
2021-07-01 09:19:28 +00:00
if ( this . type === "http" ) {
bean . status = 1 ;
} else {
2021-07-12 02:52:41 +00:00
let data = res . data ;
// Convert to string for object/array
if ( typeof data !== "string" ) {
data = JSON . stringify ( data )
}
if ( data . includes ( this . keyword ) ) {
2021-07-01 09:19:28 +00:00
bean . msg += ", keyword is found"
bean . status = 1 ;
} else {
throw new Error ( bean . msg + ", but keyword is not found" )
}
}
2021-07-01 06:03:06 +00:00
} else if ( this . type === "port" ) {
bean . ping = await tcping ( this . hostname , this . port ) ;
2021-07-01 13:47:14 +00:00
bean . msg = ""
2021-07-01 06:03:06 +00:00
bean . status = 1 ;
2021-07-01 09:00:23 +00:00
} else if ( this . type === "ping" ) {
bean . ping = await ping ( this . hostname ) ;
2021-07-01 13:47:14 +00:00
bean . msg = ""
2021-07-01 09:00:23 +00:00
bean . status = 1 ;
2021-06-27 08:10:55 +00:00
}
2021-07-19 16:23:06 +00:00
retries = 0 ;
2021-06-27 08:10:55 +00:00
} catch ( error ) {
2021-07-19 16:23:06 +00:00
if ( ( this . maxretries > 0 ) && ( retries < this . maxretries ) ) {
retries ++ ;
bean . status = 2 ;
}
2021-06-27 08:10:55 +00:00
bean . msg = error . message ;
}
2021-07-20 09:50:33 +00:00
// Mark as important if status changed, ignore pending pings,
// Don't notify if disrupted changes to up
2021-07-22 18:25:03 +00:00
if ( ( ! previousBeat ) || ( ( previousBeat . status !== bean . status ) && bean . status !== 2 && ! ( previousBeat . status === 2 && bean . status !== 0 ) ) ) {
2021-06-29 08:06:20 +00:00
bean . important = true ;
2021-07-09 09:55:48 +00:00
2021-07-09 17:15:21 +00:00
// Do not send if first beat is UP
if ( previousBeat || bean . status !== 1 ) {
let notificationList = await R . getAll ( ` SELECT notification.* FROM notification, monitor_notification WHERE monitor_id = ? AND monitor_notification.notification_id = notification.id ` , [
this . id
] )
2021-07-09 09:55:48 +00:00
2021-07-09 17:15:21 +00:00
let text ;
if ( bean . status === 1 ) {
text = "✅ Up"
} else {
text = "🔴 Down"
}
2021-07-09 09:55:48 +00:00
2021-07-09 17:15:21 +00:00
let msg = ` [ ${ this . name } ] [ ${ text } ] ${ bean . msg } ` ;
2021-07-09 09:55:48 +00:00
2021-07-09 17:15:21 +00:00
for ( let notification of notificationList ) {
2021-07-22 04:28:47 +00:00
try {
await Notification . send ( JSON . parse ( notification . config ) , msg , await this . toJSON ( ) , bean . toJSON ( ) )
} catch ( e ) {
console . error ( "Cannot send notification to " + notification . name )
}
2021-07-09 17:15:21 +00:00
}
}
2021-07-09 09:55:48 +00:00
2021-06-29 08:06:20 +00:00
} else {
bean . important = false ;
}
2021-07-20 22:41:38 +00:00
if ( bean . status === 1 ) {
console . info ( ` Monitor # ${ this . id } ' ${ this . name } ': Successful Response: ${ bean . ping } ms | Interval: ${ this . interval } seconds | Type: ${ this . type } ` )
} else {
console . warn ( ` Monitor # ${ this . id } ' ${ this . name } ': Failing: ${ bean . msg } | Type: ${ this . type } ` )
}
2021-06-29 08:06:20 +00:00
io . to ( this . user _id ) . emit ( "heartbeat" , bean . toJSON ( ) ) ;
2021-06-27 08:10:55 +00:00
await R . store ( bean )
2021-07-01 06:03:06 +00:00
Monitor . sendStats ( io , this . id , this . user _id )
2021-06-29 08:06:20 +00:00
previousBeat = bean ;
2021-06-25 13:55:49 +00:00
}
beat ( ) ;
this . heartbeatInterval = setInterval ( beat , this . interval * 1000 ) ;
}
stop ( ) {
clearInterval ( this . heartbeatInterval )
}
2021-06-30 13:04:58 +00:00
static async sendStats ( io , monitorID , userID ) {
Monitor . sendAvgPing ( 24 , io , monitorID , userID ) ;
2021-07-01 05:11:16 +00:00
Monitor . sendUptime ( 24 , io , monitorID , userID ) ;
Monitor . sendUptime ( 24 * 30 , io , monitorID , userID ) ;
2021-06-30 13:04:58 +00:00
}
2021-07-01 05:11:16 +00:00
/ * *
*
* @ param duration : int Hours
* /
2021-06-30 13:04:58 +00:00
static async sendAvgPing ( duration , io , monitorID , userID ) {
let avgPing = parseInt ( await R . getCell ( `
SELECT AVG ( ping )
FROM heartbeat
2021-07-10 04:04:40 +00:00
WHERE time > DATETIME ( 'now' , ? || ' hours' )
2021-07-01 05:11:16 +00:00
AND ping IS NOT NULL
2021-06-30 13:04:58 +00:00
AND monitor _id = ? ` , [
- duration ,
monitorID
] ) ) ;
io . to ( userID ) . emit ( "avgPing" , monitorID , avgPing ) ;
}
2021-07-01 05:11:16 +00:00
/ * *
2021-07-09 06:14:03 +00:00
* Uptime with calculation
* Calculation based on :
* https : //www.uptrends.com/support/kb/reporting/calculation-of-uptime-and-downtime
2021-07-01 05:11:16 +00:00
* @ param duration : int Hours
* /
static async sendUptime ( duration , io , monitorID , userID ) {
2021-07-01 09:00:23 +00:00
let sec = duration * 3600 ;
2021-07-11 12:07:03 +00:00
let heartbeatList = await R . getAll ( `
2021-07-09 06:14:03 +00:00
SELECT duration , time , status
2021-07-01 05:11:16 +00:00
FROM heartbeat
2021-07-10 04:04:40 +00:00
WHERE time > DATETIME ( 'now' , ? || ' hours' )
2021-07-01 05:11:16 +00:00
AND monitor _id = ? ` , [
- duration ,
monitorID
2021-07-01 09:00:23 +00:00
] ) ;
let downtime = 0 ;
2021-07-09 06:14:03 +00:00
let total = 0 ;
let uptime ;
2021-07-01 09:00:23 +00:00
2021-07-11 12:07:03 +00:00
// Special handle for the first heartbeat only
if ( heartbeatList . length === 1 ) {
2021-07-01 13:47:14 +00:00
2021-07-11 12:07:03 +00:00
if ( heartbeatList [ 0 ] . status === 1 ) {
uptime = 1 ;
} else {
uptime = 0 ;
}
} else {
for ( let row of heartbeatList ) {
let value = parseInt ( row . duration )
let time = row . time
2021-07-01 09:00:23 +00:00
2021-07-11 12:07:03 +00:00
// Handle if heartbeat duration longer than the target duration
// e.g. Heartbeat duration = 28hrs, but target duration = 24hrs
if ( value > sec ) {
let trim = dayjs . utc ( ) . diff ( dayjs ( time ) , 'second' ) ;
value = sec - trim ;
if ( value < 0 ) {
value = 0 ;
}
}
total += value ;
2021-07-19 16:23:06 +00:00
if ( row . status === 0 || row . status === 2 ) {
2021-07-11 12:07:03 +00:00
downtime += value ;
2021-07-01 09:00:23 +00:00
}
}
2021-07-01 05:11:16 +00:00
2021-07-11 12:07:03 +00:00
uptime = ( total - downtime ) / total ;
if ( uptime < 0 ) {
uptime = 0 ;
2021-07-06 05:44:33 +00:00
}
2021-07-01 09:00:23 +00:00
}
2021-07-09 06:14:03 +00:00
2021-07-01 05:11:16 +00:00
io . to ( userID ) . emit ( "uptime" , monitorID , duration , uptime ) ;
2021-06-30 13:04:58 +00:00
}
2021-06-25 13:55:49 +00:00
}
module . exports = Monitor ;