2021-07-30 14:18:26 +03:00
const https = require ( "https" ) ;
2021-06-25 16:55:49 +03:00
const dayjs = require ( "dayjs" ) ;
2021-07-30 14:18:26 +03:00
const utc = require ( "dayjs/plugin/utc" )
let timezone = require ( "dayjs/plugin/timezone" )
2021-06-27 11:10:55 +03:00
dayjs . extend ( utc )
dayjs . extend ( timezone )
const axios = require ( "axios" ) ;
2021-07-30 14:18:26 +03:00
const { Prometheus } = require ( "../prometheus" ) ;
2021-08-16 21:09:40 +03:00
const { debug , UP , DOWN , PENDING , flipStatus , TimeLogger } = require ( "../../src/util" ) ;
2021-08-30 09:55:33 +03:00
const { tcping , ping , dnsResolve , checkCertificate , checkStatusCode , getTotalClientInRoom } = require ( "../util-server" ) ;
2021-07-30 14:18:26 +03:00
const { R } = require ( "redbean-node" ) ;
const { BeanModel } = require ( "redbean-node/dist/bean-model" ) ;
const { Notification } = require ( "../notification" )
2021-08-12 19:13:46 +03:00
const version = require ( "../../package.json" ) . version ;
2021-06-27 11:10:55 +03:00
/ * *
* status :
* 0 = DOWN
* 1 = UP
2021-07-27 20:53:59 +03:00
* 2 = PENDING
2021-06-27 11:10:55 +03:00
* /
2021-06-25 16:55:49 +03:00
class Monitor extends BeanModel {
2021-07-09 12:55:48 +03:00
async toJSON ( ) {
let notificationIDList = { } ;
let list = await R . find ( "monitor_notification" , " monitor_id = ? " , [
2021-07-30 14:18:26 +03:00
this . id ,
2021-07-09 12:55:48 +03:00
] )
for ( let bean of list ) {
notificationIDList [ bean . notification _id ] = true ;
}
2021-06-25 16:55:49 +03:00
return {
id : this . id ,
name : this . name ,
url : this . url ,
2021-07-01 09:03:06 +03:00
hostname : this . hostname ,
port : this . port ,
2021-07-19 19:23:06 +03:00
maxretries : this . maxretries ,
2021-07-01 08:11:16 +03:00
weight : this . weight ,
2021-06-25 16:55:49 +03:00
active : this . active ,
type : this . type ,
interval : this . interval ,
2021-07-01 12:19:28 +03:00
keyword : this . keyword ,
2021-07-30 17:11:14 +03:00
ignoreTls : this . getIgnoreTls ( ) ,
2021-07-30 19:01:04 +03:00
upsideDown : this . isUpsideDown ( ) ,
2021-08-05 14:04:38 +03:00
maxredirects : this . maxredirects ,
accepted _statuscodes : this . getAcceptedStatuscodes ( ) ,
2021-08-23 01:05:48 +03:00
dns _resolve _type : this . dns _resolve _type ,
dns _resolve _server : this . dns _resolve _server ,
2021-08-28 22:20:25 +03:00
dns _last _result : this . dns _last _result ,
2021-07-30 14:18:26 +03:00
notificationIDList ,
2021-06-25 16:55:49 +03:00
} ;
}
2021-07-30 17:11:14 +03:00
/ * *
* Parse to boolean
* @ returns { boolean }
* /
getIgnoreTls ( ) {
return Boolean ( this . ignoreTls )
}
/ * *
* Parse to boolean
* @ returns { boolean }
* /
2021-07-30 19:01:04 +03:00
isUpsideDown ( ) {
2021-07-30 17:11:14 +03:00
return Boolean ( this . upsideDown ) ;
}
2021-08-05 14:04:38 +03:00
getAcceptedStatuscodes ( ) {
return JSON . parse ( this . accepted _statuscodes _json ) ;
}
2021-06-25 16:55:49 +03:00
start ( io ) {
2021-06-29 11:06:20 +03:00
let previousBeat = null ;
2021-07-19 19:23:06 +03:00
let retries = 0 ;
2021-06-29 11:06:20 +03:00
2021-07-27 19:52:31 +03:00
let prometheus = new Prometheus ( this ) ;
2021-07-22 18:00:11 +03:00
2021-06-27 11:10:55 +03:00
const beat = async ( ) => {
2021-07-24 06:42:14 +03:00
2021-08-10 12:51:30 +03:00
// Expose here for prometheus update
// undefined if not https
let tlsInfo = undefined ;
2021-06-29 11:06:20 +03:00
if ( ! previousBeat ) {
previousBeat = await R . findOne ( "heartbeat" , " monitor_id = ? ORDER BY time DESC" , [
2021-07-30 14:18:26 +03:00
this . id ,
2021-06-29 11:06:20 +03:00
] )
}
2021-07-24 06:42:14 +03:00
const isFirstBeat = ! previousBeat ;
2021-06-27 11:10:55 +03:00
let bean = R . dispense ( "heartbeat" )
bean . monitor _id = this . id ;
bean . time = R . isoDateTime ( dayjs . utc ( ) ) ;
2021-07-24 06:42:14 +03:00
bean . status = DOWN ;
2021-06-27 11:10:55 +03:00
2021-07-30 19:01:04 +03:00
if ( this . isUpsideDown ( ) ) {
bean . status = flipStatus ( bean . status ) ;
}
2021-06-30 21:02:54 +03:00
// Duration
2021-07-24 06:42:14 +03:00
if ( ! isFirstBeat ) {
2021-07-30 14:18:26 +03:00
bean . duration = dayjs ( bean . time ) . diff ( dayjs ( previousBeat . time ) , "second" ) ;
2021-06-30 21:02:54 +03:00
} else {
bean . duration = 0 ;
}
2021-06-27 11:10:55 +03:00
try {
2021-07-01 12:19:28 +03:00
if ( this . type === "http" || this . type === "keyword" ) {
2021-08-23 13:52:24 +03:00
// Do not do any queries/high loading things before the "bean.ping"
2021-06-27 11:10:55 +03:00
let startTime = dayjs ( ) . valueOf ( ) ;
2021-07-30 17:11:14 +03:00
2021-07-11 23:01:34 +03:00
let res = await axios . get ( this . url , {
2021-08-11 18:12:38 +03:00
timeout : this . interval * 1000 * 0.8 ,
2021-07-30 14:18:26 +03:00
headers : {
2021-08-10 15:23:15 +03:00
"Accept" : "*/*" ,
2021-08-11 20:31:07 +03:00
"User-Agent" : "Uptime-Kuma/" + version ,
2021-07-30 14:18:26 +03:00
} ,
2021-07-30 17:11:14 +03:00
httpsAgent : new https . Agent ( {
2021-08-23 13:52:24 +03:00
maxCachedSessions : 0 , // Use Custom agent to disable session reuse (https://github.com/nodejs/node/issues/3940)
2021-07-30 17:11:14 +03:00
rejectUnauthorized : ! this . getIgnoreTls ( ) ,
} ) ,
2021-08-05 14:04:38 +03:00
maxRedirects : this . maxredirects ,
validateStatus : ( status ) => {
return checkStatusCode ( status , this . getAcceptedStatuscodes ( ) ) ;
} ,
2021-07-23 06:22:37 +03:00
} ) ;
2021-06-27 11:10:55 +03:00
bean . msg = ` ${ res . status } - ${ res . statusText } `
bean . ping = dayjs ( ) . valueOf ( ) - startTime ;
2021-07-01 12:19:28 +03:00
2021-07-22 11:04:32 +03:00
// Check certificate if https is used
2021-07-23 07:58:05 +03:00
let certInfoStartTime = dayjs ( ) . valueOf ( ) ;
2021-07-22 11:13:58 +03:00
if ( this . getUrl ( ) ? . protocol === "https:" ) {
2021-07-23 07:58:05 +03:00
try {
2021-08-10 12:51:30 +03:00
tlsInfo = await this . updateTlsInfo ( checkCertificate ( res ) ) ;
2021-07-23 07:58:05 +03:00
} catch ( e ) {
2021-08-08 08:47:29 +03:00
if ( e . message !== "No TLS certificate in response" ) {
console . error ( e . message )
}
2021-07-23 07:58:05 +03:00
}
2021-07-21 07:09:09 +03:00
}
2021-07-01 12:19:28 +03:00
2021-07-23 07:58:05 +03:00
debug ( "Cert Info Query Time: " + ( dayjs ( ) . valueOf ( ) - certInfoStartTime ) + "ms" )
2021-07-01 12:19:28 +03:00
if ( this . type === "http" ) {
2021-07-24 06:42:14 +03:00
bean . status = UP ;
2021-07-01 12:19:28 +03:00
} else {
2021-07-12 05:52:41 +03: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 12:19:28 +03:00
bean . msg += ", keyword is found"
2021-07-24 06:42:14 +03:00
bean . status = UP ;
2021-07-01 12:19:28 +03:00
} else {
throw new Error ( bean . msg + ", but keyword is not found" )
}
}
2021-07-01 09:03:06 +03:00
} else if ( this . type === "port" ) {
bean . ping = await tcping ( this . hostname , this . port ) ;
2021-07-01 16:47:14 +03:00
bean . msg = ""
2021-07-24 06:42:14 +03:00
bean . status = UP ;
2021-07-01 12:00:23 +03:00
} else if ( this . type === "ping" ) {
bean . ping = await ping ( this . hostname ) ;
2021-07-01 16:47:14 +03:00
bean . msg = ""
2021-07-24 06:42:14 +03:00
bean . status = UP ;
2021-08-23 01:05:48 +03:00
} else if ( this . type === "dns" ) {
let startTime = dayjs ( ) . valueOf ( ) ;
2021-08-23 01:05:48 +03:00
let dnsMessage = "" ;
2021-08-23 01:05:48 +03:00
2021-08-23 01:05:48 +03:00
let dnsRes = await dnsResolve ( this . hostname , this . dns _resolve _server , this . dns _resolve _type ) ;
bean . ping = dayjs ( ) . valueOf ( ) - startTime ;
2021-08-23 01:05:48 +03:00
2021-08-25 10:31:42 +03:00
if ( this . dns _resolve _type == "A" || this . dns _resolve _type == "AAAA" || this . dns _resolve _type == "TXT" ) {
2021-08-24 12:47:12 +03:00
dnsMessage += "Records: " ;
2021-08-25 10:31:42 +03:00
dnsMessage += dnsRes . join ( " | " ) ;
2021-08-24 12:47:12 +03:00
} else if ( this . dns _resolve _type == "CNAME" || this . dns _resolve _type == "PTR" ) {
2021-08-23 01:05:48 +03:00
dnsMessage = dnsRes [ 0 ] ;
2021-08-23 17:30:11 +03:00
} else if ( this . dns _resolve _type == "CAA" ) {
2021-08-23 01:05:48 +03:00
dnsMessage = dnsRes [ 0 ] . issue ;
2021-08-23 17:30:11 +03:00
} else if ( this . dns _resolve _type == "MX" ) {
2021-08-23 01:05:48 +03:00
dnsRes . forEach ( record => {
2021-08-24 12:47:12 +03:00
dnsMessage += ` Hostname: ${ record . exchange } - Priority: ${ record . priority } | ` ;
2021-08-23 01:05:48 +03:00
} ) ;
2021-08-23 01:05:48 +03:00
dnsMessage = dnsMessage . slice ( 0 , - 2 )
2021-08-23 17:30:11 +03:00
} else if ( this . dns _resolve _type == "NS" ) {
2021-08-24 12:47:12 +03:00
dnsMessage += "Servers: " ;
2021-08-25 10:31:42 +03:00
dnsMessage += dnsRes . join ( " | " ) ;
2021-08-23 17:30:11 +03:00
} else if ( this . dns _resolve _type == "SOA" ) {
2021-08-23 01:05:48 +03:00
dnsMessage += ` NS-Name: ${ dnsRes . nsname } | Hostmaster: ${ dnsRes . hostmaster } | Serial: ${ dnsRes . serial } | Refresh: ${ dnsRes . refresh } | Retry: ${ dnsRes . retry } | Expire: ${ dnsRes . expire } | MinTTL: ${ dnsRes . minttl } ` ;
2021-08-23 17:30:11 +03:00
} else if ( this . dns _resolve _type == "SRV" ) {
2021-08-23 01:05:48 +03:00
dnsRes . forEach ( record => {
dnsMessage += ` Name: ${ record . name } | Port: ${ record . port } | Priority: ${ record . priority } | Weight: ${ record . weight } | ` ;
} ) ;
2021-08-23 01:05:48 +03:00
dnsMessage = dnsMessage . slice ( 0 , - 2 )
2021-08-23 01:05:48 +03:00
}
2021-08-28 22:29:24 +03:00
if ( this . dnsLastResult !== dnsMessage ) {
2021-08-28 22:20:25 +03:00
R . exec ( "UPDATE `monitor` SET dns_last_result = ? WHERE id = ? " , [
dnsMessage ,
this . id
] ) ;
}
2021-08-23 01:05:48 +03:00
bean . msg = dnsMessage ;
bean . status = UP ;
2021-06-27 11:10:55 +03:00
}
2021-07-30 19:01:04 +03:00
if ( this . isUpsideDown ( ) ) {
bean . status = flipStatus ( bean . status ) ;
if ( bean . status === DOWN ) {
throw new Error ( "Flip UP to DOWN" ) ;
}
}
2021-07-19 19:23:06 +03:00
retries = 0 ;
2021-06-27 11:10:55 +03:00
} catch ( error ) {
2021-07-30 19:01:04 +03:00
bean . msg = error . message ;
// If UP come in here, it must be upside down mode
// Just reset the retries
if ( this . isUpsideDown ( ) && bean . status === UP ) {
retries = 0 ;
} else if ( ( this . maxretries > 0 ) && ( retries < this . maxretries ) ) {
2021-07-19 19:23:06 +03:00
retries ++ ;
2021-07-24 06:42:14 +03:00
bean . status = PENDING ;
2021-07-19 19:23:06 +03:00
}
2021-06-27 11:10:55 +03:00
}
2021-07-24 06:42:14 +03:00
// * ? -> ANY STATUS = important [isFirstBeat]
// UP -> PENDING = not important
// * UP -> DOWN = important
// UP -> UP = not important
// PENDING -> PENDING = not important
// * PENDING -> DOWN = important
// PENDING -> UP = not important
// DOWN -> PENDING = this case not exists
// DOWN -> DOWN = not important
// * DOWN -> UP = important
let isImportant = isFirstBeat ||
( previousBeat . status === UP && bean . status === DOWN ) ||
( previousBeat . status === DOWN && bean . status === UP ) ||
( previousBeat . status === PENDING && bean . status === DOWN ) ;
2021-07-20 12:50:33 +03:00
// Mark as important if status changed, ignore pending pings,
// Don't notify if disrupted changes to up
2021-07-24 06:42:14 +03:00
if ( isImportant ) {
2021-06-29 11:06:20 +03:00
bean . important = true ;
2021-07-09 12:55:48 +03:00
2021-07-24 06:42:14 +03:00
// Send only if the first beat is DOWN
if ( ! isFirstBeat || bean . status === DOWN ) {
2021-07-30 14:18:26 +03:00
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 20:15:21 +03:00
] )
2021-07-09 12:55:48 +03:00
2021-07-09 20:15:21 +03:00
let text ;
2021-07-24 06:42:14 +03:00
if ( bean . status === UP ) {
2021-07-09 20:15:21 +03:00
text = "✅ Up"
} else {
text = "🔴 Down"
}
2021-07-09 12:55:48 +03:00
2021-07-09 20:15:21 +03:00
let msg = ` [ ${ this . name } ] [ ${ text } ] ${ bean . msg } ` ;
2021-07-09 12:55:48 +03:00
2021-07-30 14:18:26 +03:00
for ( let notification of notificationList ) {
2021-07-22 07:28:47 +03:00
try {
await Notification . send ( JSON . parse ( notification . config ) , msg , await this . toJSON ( ) , bean . toJSON ( ) )
} catch ( e ) {
2021-08-23 15:57:42 +03:00
console . error ( "Cannot send notification to " + notification . name ) ;
console . log ( e ) ;
2021-07-22 07:28:47 +03:00
}
2021-07-09 20:15:21 +03:00
}
}
2021-07-09 12:55:48 +03:00
2021-06-29 11:06:20 +03:00
} else {
bean . important = false ;
}
2021-07-24 06:42:14 +03:00
if ( bean . status === UP ) {
2021-07-21 01:41:38 +03:00
console . info ( ` Monitor # ${ this . id } ' ${ this . name } ': Successful Response: ${ bean . ping } ms | Interval: ${ this . interval } seconds | Type: ${ this . type } ` )
2021-07-24 06:42:14 +03:00
} else if ( bean . status === PENDING ) {
2021-07-27 20:53:59 +03:00
console . warn ( ` Monitor # ${ this . id } ' ${ this . name } ': Pending: ${ bean . msg } | Max retries: ${ this . maxretries } | Type: ${ this . type } ` )
2021-07-21 01:41:38 +03:00
} else {
console . warn ( ` Monitor # ${ this . id } ' ${ this . name } ': Failing: ${ bean . msg } | Type: ${ this . type } ` )
}
2021-06-29 11:06:20 +03:00
io . to ( this . user _id ) . emit ( "heartbeat" , bean . toJSON ( ) ) ;
2021-07-01 09:03:06 +03:00
Monitor . sendStats ( io , this . id , this . user _id )
2021-06-29 11:06:20 +03:00
2021-08-23 13:52:24 +03:00
await R . store ( bean ) ;
prometheus . update ( bean , tlsInfo ) ;
2021-06-29 11:06:20 +03:00
previousBeat = bean ;
2021-08-23 13:52:24 +03:00
this . heartbeatInterval = setTimeout ( beat , this . interval * 1000 ) ;
2021-06-25 16:55:49 +03:00
}
beat ( ) ;
}
stop ( ) {
2021-08-23 13:52:24 +03:00
clearTimeout ( this . heartbeatInterval ) ;
2021-06-25 16:55:49 +03:00
}
2021-06-30 16:04:58 +03:00
2021-07-30 06:23:04 +03:00
/ * *
* Helper Method :
* returns URL object for further usage
* returns null if url is invalid
* @ returns { null | URL }
* /
2021-07-22 11:04:32 +03:00
getUrl ( ) {
try {
return new URL ( this . url ) ;
} catch ( _ ) {
return null ;
}
}
2021-07-30 06:23:04 +03:00
/ * *
* Store TLS info to database
* @ param checkCertificateResult
2021-08-10 12:51:30 +03:00
* @ returns { Promise < object > }
2021-07-30 06:23:04 +03:00
* /
2021-07-22 11:04:32 +03:00
async updateTlsInfo ( checkCertificateResult ) {
let tls _info _bean = await R . findOne ( "monitor_tls_info" , "monitor_id = ?" , [
2021-07-30 14:18:26 +03:00
this . id ,
2021-07-22 11:04:32 +03:00
] ) ;
if ( tls _info _bean == null ) {
tls _info _bean = R . dispense ( "monitor_tls_info" ) ;
tls _info _bean . monitor _id = this . id ;
}
tls _info _bean . info _json = JSON . stringify ( checkCertificateResult ) ;
2021-07-23 07:58:05 +03:00
await R . store ( tls _info _bean ) ;
2021-08-10 12:51:30 +03:00
return checkCertificateResult ;
2021-07-22 11:04:32 +03:00
}
2021-06-30 16:04:58 +03:00
static async sendStats ( io , monitorID , userID ) {
2021-08-30 09:55:33 +03:00
const hasClients = getTotalClientInRoom ( io , userID ) > 0 ;
if ( hasClients ) {
await Monitor . sendAvgPing ( 24 , io , monitorID , userID ) ;
await Monitor . sendUptime ( 24 , io , monitorID , userID ) ;
await Monitor . sendUptime ( 24 * 30 , io , monitorID , userID ) ;
await Monitor . sendCertInfo ( io , monitorID , userID ) ;
} else {
debug ( "No clients in the room, no need to send stats" ) ;
}
2021-06-30 16:04:58 +03:00
}
2021-07-01 08:11:16 +03:00
/ * *
*
* @ param duration : int Hours
* /
2021-06-30 16:04:58 +03:00
static async sendAvgPing ( duration , io , monitorID , userID ) {
2021-08-16 21:09:40 +03:00
const timeLogger = new TimeLogger ( ) ;
2021-06-30 16:04:58 +03:00
let avgPing = parseInt ( await R . getCell ( `
SELECT AVG ( ping )
FROM heartbeat
2021-07-10 07:04:40 +03:00
WHERE time > DATETIME ( 'now' , ? || ' hours' )
2021-07-01 08:11:16 +03:00
AND ping IS NOT NULL
2021-06-30 16:04:58 +03:00
AND monitor _id = ? ` , [
- duration ,
2021-07-30 14:18:26 +03:00
monitorID ,
2021-06-30 16:04:58 +03:00
] ) ) ;
2021-08-16 21:09:40 +03:00
timeLogger . print ( ` [Monitor: ${ monitorID } ] avgPing ` ) ;
2021-06-30 16:04:58 +03:00
io . to ( userID ) . emit ( "avgPing" , monitorID , avgPing ) ;
}
2021-07-22 11:04:32 +03:00
static async sendCertInfo ( io , monitorID , userID ) {
2021-07-30 14:18:26 +03:00
let tls _info = await R . findOne ( "monitor_tls_info" , "monitor_id = ?" , [
monitorID ,
2021-07-22 11:04:32 +03:00
] ) ;
if ( tls _info != null ) {
io . to ( userID ) . emit ( "certInfo" , monitorID , tls _info . info _json ) ;
}
2021-07-21 07:09:09 +03:00
}
2021-07-01 08:11:16 +03:00
/ * *
2021-07-09 09:14:03 +03:00
* Uptime with calculation
* Calculation based on :
* https : //www.uptrends.com/support/kb/reporting/calculation-of-uptime-and-downtime
2021-07-01 08:11:16 +03:00
* @ param duration : int Hours
* /
static async sendUptime ( duration , io , monitorID , userID ) {
2021-08-16 21:09:40 +03:00
const timeLogger = new TimeLogger ( ) ;
2021-07-01 12:00:23 +03:00
let sec = duration * 3600 ;
2021-07-11 15:07:03 +03:00
let heartbeatList = await R . getAll ( `
2021-07-09 09:14:03 +03:00
SELECT duration , time , status
2021-07-01 08:11:16 +03:00
FROM heartbeat
2021-07-10 07:04:40 +03:00
WHERE time > DATETIME ( 'now' , ? || ' hours' )
2021-07-01 08:11:16 +03:00
AND monitor _id = ? ` , [
- duration ,
2021-07-30 14:18:26 +03:00
monitorID ,
2021-07-01 12:00:23 +03:00
] ) ;
2021-08-16 21:09:40 +03:00
timeLogger . print ( ` [Monitor: ${ monitorID } ][ ${ duration } ] sendUptime ` ) ;
2021-07-01 12:00:23 +03:00
let downtime = 0 ;
2021-07-09 09:14:03 +03:00
let total = 0 ;
let uptime ;
2021-07-01 12:00:23 +03:00
2021-07-11 15:07:03 +03:00
// Special handle for the first heartbeat only
if ( heartbeatList . length === 1 ) {
2021-07-01 16:47:14 +03:00
2021-07-11 15:07:03 +03: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 12:00:23 +03:00
2021-07-11 15:07:03 +03:00
// Handle if heartbeat duration longer than the target duration
// e.g. Heartbeat duration = 28hrs, but target duration = 24hrs
if ( value > sec ) {
2021-07-30 14:18:26 +03:00
let trim = dayjs . utc ( ) . diff ( dayjs ( time ) , "second" ) ;
2021-07-11 15:07:03 +03:00
value = sec - trim ;
if ( value < 0 ) {
value = 0 ;
}
}
total += value ;
2021-07-19 19:23:06 +03:00
if ( row . status === 0 || row . status === 2 ) {
2021-07-11 15:07:03 +03:00
downtime += value ;
2021-07-01 12:00:23 +03:00
}
}
2021-07-01 08:11:16 +03:00
2021-07-11 15:07:03 +03:00
uptime = ( total - downtime ) / total ;
if ( uptime < 0 ) {
uptime = 0 ;
2021-07-06 08:44:33 +03:00
}
2021-07-01 12:00:23 +03:00
}
2021-07-01 08:11:16 +03:00
io . to ( userID ) . emit ( "uptime" , monitorID , duration , uptime ) ;
2021-06-30 16:04:58 +03:00
}
2021-06-25 16:55:49 +03:00
}
module . exports = Monitor ;