@ -1,15 +1,23 @@
const Prometheus = require ( 'prom-client' ) ;
const https = require ( 'https' ) ;
const dayjs = require ( "dayjs" ) ;
const utc = require ( 'dayjs/plugin/utc' )
var timezone = require ( 'dayjs/plugin/timezone' )
dayjs . extend ( utc )
dayjs . extend ( timezone )
const axios = require ( "axios" ) ;
const { tcping , ping } = require ( "../util-server" ) ;
const { debug , UP , DOWN , PENDING } = require ( "../util" ) ;
const { tcping , ping , checkCertificate } = require ( "../util-server" ) ;
const { R } = require ( "redbean-node" ) ;
const { BeanModel } = require ( "redbean-node/dist/bean-model" ) ;
const { Notification } = require ( "../notification" )
// Use Custom agent to disable session reuse
// https://github.com/nodejs/node/issues/3940
const customAgent = new https . Agent ( {
maxCachedSessions : 0
} ) ;
const commonLabels = [
'monitor_name' ,
'monitor_type' ,
@ -18,24 +26,24 @@ const commonLabels = [
'monitor_port' ,
]
const monitor _response _time = new Prometheus . Gauge ( {
name : 'monitor_response_time' ,
help : 'Monitor Response Time (ms)' ,
labelNames : commonLabels
} ) ;
const monitor _status = new Prometheus . Gauge ( {
name : 'monitor_status' ,
help : 'Monitor Status (1 = UP, 0= DOWN)' ,
labelNames : commonLabels
} ) ;
/ * *
* status :
* 0 = DOWN
* 1 = UP
* /
class Monitor extends BeanModel {
async toJSON ( ) {
let notificationIDList = { } ;
@ -54,6 +62,7 @@ class Monitor extends BeanModel {
url : this . url ,
hostname : this . hostname ,
port : this . port ,
maxretries : this . maxretries ,
weight : this . weight ,
active : this . active ,
type : this . type ,
@ -65,6 +74,7 @@ class Monitor extends BeanModel {
start ( io ) {
let previousBeat = null ;
let retries = 0 ;
const monitorLabelValues = {
monitor _name : this . name ,
@ -74,21 +84,23 @@ class Monitor extends BeanModel {
monitor _port : this . port
}
const beat = async ( ) => {
if ( ! previousBeat ) {
previousBeat = await R . findOne ( "heartbeat" , " monitor_id = ? ORDER BY time DESC" , [
this . id
] )
}
const isFirstBeat = ! previousBeat ;
let bean = R . dispense ( "heartbeat" )
bean . monitor _id = this . id ;
bean . time = R . isoDateTime ( dayjs . utc ( ) ) ;
bean . status = 0 ;
bean . status = DOWN ;
// Duration
if ( previous Beat) {
if ( ! isFirst Beat) {
bean . duration = dayjs ( bean . time ) . diff ( dayjs ( previousBeat . time ) , 'second' ) ;
} else {
bean . duration = 0 ;
@ -98,13 +110,27 @@ class Monitor extends BeanModel {
if ( this . type === "http" || this . type === "keyword" ) {
let startTime = dayjs ( ) . valueOf ( ) ;
let res = await axios . get ( this . url , {
headers : { 'User-Agent' : 'Uptime-Kuma' }
} )
headers : { "User-Agent" : "Uptime-Kuma" } ,
httpsAgent : customAgent ,
} ) ;
bean . msg = ` ${ res . status } - ${ res . statusText } `
bean . ping = dayjs ( ) . valueOf ( ) - startTime ;
// Check certificate if https is used
let certInfoStartTime = dayjs ( ) . valueOf ( ) ;
if ( this . getUrl ( ) ? . protocol === "https:" ) {
try {
await this . updateTlsInfo ( checkCertificate ( res ) ) ;
} catch ( e ) {
console . error ( e . message )
}
}
debug ( "Cert Info Query Time: " + ( dayjs ( ) . valueOf ( ) - certInfoStartTime ) + "ms" )
if ( this . type === "http" ) {
bean . status = 1 ;
bean . status = UP ;
} else {
let data = res . data ;
@ -116,7 +142,7 @@ class Monitor extends BeanModel {
if ( data . includes ( this . keyword ) ) {
bean . msg += ", keyword is found"
bean . status = 1 ;
bean . status = UP ;
} else {
throw new Error ( bean . msg + ", but keyword is not found" )
}
@ -127,30 +153,52 @@ class Monitor extends BeanModel {
} else if ( this . type === "port" ) {
bean . ping = await tcping ( this . hostname , this . port ) ;
bean . msg = ""
bean . status = 1 ;
bean . status = UP ;
} else if ( this . type === "ping" ) {
bean . ping = await ping ( this . hostname ) ;
bean . msg = ""
bean . status = 1 ;
bean . status = UP ;
}
retries = 0 ;
} catch ( error ) {
if ( ( this . maxretries > 0 ) && ( retries < this . maxretries ) ) {
retries ++ ;
bean . status = PENDING ;
}
bean . msg = error . message ;
}
// Mark as important if status changed
if ( ! previousBeat || previousBeat . status !== bean . status ) {
// * ? -> 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 ) ;
// Mark as important if status changed, ignore pending pings,
// Don't notify if disrupted changes to up
if ( isImportant ) {
bean . important = true ;
// Do not send if first beat is UP
if ( previousBeat || bean . status !== 1 ) {
// Send only if the first beat is DOWN
if ( ! isFirstBeat || bean . status === DOWN ) {
let notificationList = await R . getAll ( ` SELECT notification.* FROM notification, monitor_notification WHERE monitor_id = ? AND monitor_notification.notification_id = notification.id ` , [
this . id
] )
let text ;
if ( bean . status === 1 ) {
if ( bean . status === UP ) {
text = "✅ Up"
} else {
text = "🔴 Down"
@ -171,11 +219,12 @@ class Monitor extends BeanModel {
bean . important = false ;
}
monitor _status . set ( monitorLabelValues , bean . status )
if ( bean . status === 1 ) {
if ( bean . status === UP ) {
console . info ( ` Monitor # ${ this . id } ' ${ this . name } ': Successful Response: ${ bean . ping } ms | Interval: ${ this . interval } seconds | Type: ${ this . type } ` )
} else if ( bean . status === PENDING ) {
console . warn ( ` Monitor # ${ this . id } ' ${ this . name } ': Pending: ${ bean . msg } | Type: ${ this . type } ` )
} else {
console . warn ( ` Monitor # ${ this . id } ' ${ this . name } ': Failing: ${ bean . msg } | Type: ${ this . type } ` )
}
@ -198,10 +247,35 @@ class Monitor extends BeanModel {
clearInterval ( this . heartbeatInterval )
}
// Helper Method:
// returns URL object for further usage
// returns null if url is invalid
getUrl ( ) {
try {
return new URL ( this . url ) ;
} catch ( _ ) {
return null ;
}
}
// Store TLS info to database
async updateTlsInfo ( checkCertificateResult ) {
let tls _info _bean = await R . findOne ( "monitor_tls_info" , "monitor_id = ?" , [
this . id
] ) ;
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 ) ;
await R . store ( tls _info _bean ) ;
}
static async sendStats ( io , monitorID , userID ) {
Monitor . sendAvgPing ( 24 , io , monitorID , userID ) ;
Monitor . sendUptime ( 24 , io , monitorID , userID ) ;
Monitor . sendUptime ( 24 * 30 , io , monitorID , userID ) ;
Monitor . sendCertInfo ( io , monitorID , userID ) ;
}
/ * *
@ -222,6 +296,15 @@ class Monitor extends BeanModel {
io . to ( userID ) . emit ( "avgPing" , monitorID , avgPing ) ;
}
static async sendCertInfo ( io , monitorID , userID ) {
let tls _info = await R . findOne ( "monitor_tls_info" , "monitor_id = ?" , [
monitorID
] ) ;
if ( tls _info != null ) {
io . to ( userID ) . emit ( "certInfo" , monitorID , tls _info . info _json ) ;
}
}
/ * *
* Uptime with calculation
* Calculation based on :
@ -270,7 +353,7 @@ class Monitor extends BeanModel {
}
total += value ;
if ( row . status === 0 ) {
if ( row . status === 0 || row . status === 2 ) {
downtime += value ;
}
}