mirror of
				https://github.com/louislam/uptime-kuma.git
				synced 2025-11-04 13:46:13 +08:00 
			
		
		
		
	
		
			
				
	
	
		
			815 lines
		
	
	
		
			25 KiB
		
	
	
	
		
			JavaScript
		
	
	
	
	
	
			
		
		
	
	
			815 lines
		
	
	
		
			25 KiB
		
	
	
	
		
			JavaScript
		
	
	
	
	
	
const dayjs = require("dayjs");
 | 
						|
const { UP, MAINTENANCE, DOWN, PENDING } = require("../src/util");
 | 
						|
const { LimitQueue } = require("./utils/limit-queue");
 | 
						|
const { log } = require("../src/util");
 | 
						|
const { R } = require("redbean-node");
 | 
						|
 | 
						|
/**
 | 
						|
 * Calculates the uptime of a monitor.
 | 
						|
 */
 | 
						|
class UptimeCalculator {
 | 
						|
    /**
 | 
						|
     * @private
 | 
						|
     * @type {{string:UptimeCalculator}}
 | 
						|
     */
 | 
						|
 | 
						|
    static list = {};
 | 
						|
 | 
						|
    /**
 | 
						|
     * For testing purposes, we can set the current date to a specific date.
 | 
						|
     * @type {dayjs.Dayjs}
 | 
						|
     */
 | 
						|
    static currentDate = null;
 | 
						|
 | 
						|
    /**
 | 
						|
     * monitorID the id of the monitor
 | 
						|
     * @type {number}
 | 
						|
     */
 | 
						|
    monitorID;
 | 
						|
 | 
						|
    /**
 | 
						|
     * Recent 24-hour uptime, each item is a 1-minute interval
 | 
						|
     * Key: {number} DivisionKey
 | 
						|
     * @type {LimitQueue<number,string>}
 | 
						|
     */
 | 
						|
    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,
 | 
						|
     * Key: {number} DailyKey
 | 
						|
     */
 | 
						|
    dailyUptimeDataList = new LimitQueue(365);
 | 
						|
 | 
						|
    lastUptimeData = null;
 | 
						|
    lastHourlyUptimeData = null;
 | 
						|
    lastDailyUptimeData = null;
 | 
						|
 | 
						|
    lastDailyStatBean = null;
 | 
						|
    lastHourlyStatBean = null;
 | 
						|
    lastMinutelyStatBean = null;
 | 
						|
 | 
						|
    /**
 | 
						|
     * Get the uptime calculator for a monitor
 | 
						|
     * Initializes and returns the monitor if it does not exist
 | 
						|
     * @param {number} monitorID the id of the monitor
 | 
						|
     * @returns {Promise<UptimeCalculator>} UptimeCalculator
 | 
						|
     */
 | 
						|
    static async getUptimeCalculator(monitorID) {
 | 
						|
        if (!monitorID) {
 | 
						|
            throw new Error("Monitor ID is required");
 | 
						|
        }
 | 
						|
 | 
						|
        if (!UptimeCalculator.list[monitorID]) {
 | 
						|
            UptimeCalculator.list[monitorID] = new UptimeCalculator();
 | 
						|
            await UptimeCalculator.list[monitorID].init(monitorID);
 | 
						|
        }
 | 
						|
        return UptimeCalculator.list[monitorID];
 | 
						|
    }
 | 
						|
 | 
						|
    /**
 | 
						|
     * Remove a monitor from the list
 | 
						|
     * @param {number} monitorID the id of the monitor
 | 
						|
     * @returns {Promise<void>}
 | 
						|
     */
 | 
						|
    static async remove(monitorID) {
 | 
						|
        delete UptimeCalculator.list[monitorID];
 | 
						|
    }
 | 
						|
 | 
						|
    /**
 | 
						|
     *
 | 
						|
     */
 | 
						|
    constructor() {
 | 
						|
        if (process.env.TEST_BACKEND) {
 | 
						|
            // Override the getCurrentDate() method to return a specific date
 | 
						|
            // Only for testing
 | 
						|
            this.getCurrentDate = () => {
 | 
						|
                if (UptimeCalculator.currentDate) {
 | 
						|
                    return UptimeCalculator.currentDate;
 | 
						|
                } else {
 | 
						|
                    return dayjs.utc();
 | 
						|
                }
 | 
						|
            };
 | 
						|
        }
 | 
						|
    }
 | 
						|
 | 
						|
    /**
 | 
						|
     * Initialize the uptime calculator for a monitor
 | 
						|
     * @param {number} monitorID the id of the monitor
 | 
						|
     * @returns {Promise<void>}
 | 
						|
     */
 | 
						|
    async init(monitorID) {
 | 
						|
        this.monitorID = monitorID;
 | 
						|
 | 
						|
        let now = this.getCurrentDate();
 | 
						|
 | 
						|
        // Load minutely data from database (recent 24 hours only)
 | 
						|
        let minutelyStatBeans = await R.find("stat_minutely", " monitor_id = ? AND timestamp > ? ORDER BY timestamp", [
 | 
						|
            monitorID,
 | 
						|
            this.getMinutelyKey(now.subtract(24, "hour")),
 | 
						|
        ]);
 | 
						|
 | 
						|
        for (let bean of minutelyStatBeans) {
 | 
						|
            let data = {
 | 
						|
                up: bean.up,
 | 
						|
                down: bean.down,
 | 
						|
                avgPing: bean.ping,
 | 
						|
                minPing: bean.pingMin,
 | 
						|
                maxPing: bean.pingMax,
 | 
						|
            };
 | 
						|
 | 
						|
            if (bean.extras != null) {
 | 
						|
                data = {
 | 
						|
                    ...data,
 | 
						|
                    ...JSON.parse(bean.extras),
 | 
						|
                };
 | 
						|
            }
 | 
						|
 | 
						|
            let key = bean.timestamp;
 | 
						|
            this.minutelyUptimeDataList.push(key, data);
 | 
						|
        }
 | 
						|
 | 
						|
        // 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 data = {
 | 
						|
                up: bean.up,
 | 
						|
                down: bean.down,
 | 
						|
                avgPing: bean.ping,
 | 
						|
                minPing: bean.pingMin,
 | 
						|
                maxPing: bean.pingMax,
 | 
						|
            };
 | 
						|
 | 
						|
            if (bean.extras != null) {
 | 
						|
                data = {
 | 
						|
                    ...data,
 | 
						|
                    ...JSON.parse(bean.extras),
 | 
						|
                };
 | 
						|
            }
 | 
						|
 | 
						|
            this.hourlyUptimeDataList.push(bean.timestamp, data);
 | 
						|
        }
 | 
						|
 | 
						|
        // Load daily data from database (recent 365 days only)
 | 
						|
        let dailyStatBeans = await R.find("stat_daily", " monitor_id = ? AND timestamp > ? ORDER BY timestamp", [
 | 
						|
            monitorID,
 | 
						|
            this.getDailyKey(now.subtract(365, "day")),
 | 
						|
        ]);
 | 
						|
 | 
						|
        for (let bean of dailyStatBeans) {
 | 
						|
            let data = {
 | 
						|
                up: bean.up,
 | 
						|
                down: bean.down,
 | 
						|
                avgPing: bean.ping,
 | 
						|
                minPing: bean.pingMin,
 | 
						|
                maxPing: bean.pingMax,
 | 
						|
            };
 | 
						|
 | 
						|
            if (bean.extras != null) {
 | 
						|
                data = {
 | 
						|
                    ...data,
 | 
						|
                    ...JSON.parse(bean.extras),
 | 
						|
                };
 | 
						|
            }
 | 
						|
 | 
						|
            this.dailyUptimeDataList.push(bean.timestamp, data);
 | 
						|
        }
 | 
						|
    }
 | 
						|
 | 
						|
    /**
 | 
						|
     * @param {number} status status
 | 
						|
     * @param {number} ping Ping
 | 
						|
     * @returns {dayjs.Dayjs} date
 | 
						|
     * @throws {Error} Invalid status
 | 
						|
     */
 | 
						|
    async update(status, ping = 0) {
 | 
						|
        let date = this.getCurrentDate();
 | 
						|
 | 
						|
        let flatStatus = this.flatStatus(status);
 | 
						|
 | 
						|
        if (flatStatus === DOWN && ping > 0) {
 | 
						|
            log.warn("uptime-calc", "The ping is not effective when the status is DOWN");
 | 
						|
        }
 | 
						|
 | 
						|
        let divisionKey = this.getMinutelyKey(date);
 | 
						|
        let hourlyKey = this.getHourlyKey(date);
 | 
						|
        let dailyKey = this.getDailyKey(date);
 | 
						|
 | 
						|
        let minutelyData = this.minutelyUptimeDataList[divisionKey];
 | 
						|
        let hourlyData = this.hourlyUptimeDataList[hourlyKey];
 | 
						|
        let dailyData = this.dailyUptimeDataList[dailyKey];
 | 
						|
 | 
						|
        if (status === MAINTENANCE) {
 | 
						|
            minutelyData.maintenance = minutelyData.maintenance ? minutelyData.maintenance + 1 : 1;
 | 
						|
            hourlyData.maintenance = hourlyData.maintenance ? hourlyData.maintenance + 1 : 1;
 | 
						|
            dailyData.maintenance = dailyData.maintenance ? dailyData.maintenance + 1 : 1;
 | 
						|
 | 
						|
        } else if (flatStatus === UP) {
 | 
						|
            minutelyData.up += 1;
 | 
						|
            hourlyData.up += 1;
 | 
						|
            dailyData.up += 1;
 | 
						|
 | 
						|
            // Only UP status can update the ping
 | 
						|
            if (!isNaN(ping)) {
 | 
						|
                // Add avg ping
 | 
						|
                // The first beat of the minute, the ping is the current ping
 | 
						|
                if (minutelyData.up === 1) {
 | 
						|
                    minutelyData.avgPing = ping;
 | 
						|
                    minutelyData.minPing = ping;
 | 
						|
                    minutelyData.maxPing = ping;
 | 
						|
                } else {
 | 
						|
                    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)
 | 
						|
                // The first beat of the day, the ping is the current ping
 | 
						|
                if (dailyData.up === 1) {
 | 
						|
                    dailyData.avgPing = ping;
 | 
						|
                    dailyData.minPing = ping;
 | 
						|
                    dailyData.maxPing = ping;
 | 
						|
                } else {
 | 
						|
                    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 if (flatStatus === DOWN) {
 | 
						|
            minutelyData.down += 1;
 | 
						|
            hourlyData.down += 1;
 | 
						|
            dailyData.down += 1;
 | 
						|
        }
 | 
						|
 | 
						|
        if (minutelyData !== this.lastUptimeData) {
 | 
						|
            this.lastUptimeData = minutelyData;
 | 
						|
        }
 | 
						|
 | 
						|
        if (hourlyData !== this.lastHourlyUptimeData) {
 | 
						|
            this.lastHourlyUptimeData = hourlyData;
 | 
						|
        }
 | 
						|
 | 
						|
        if (dailyData !== this.lastDailyUptimeData) {
 | 
						|
            this.lastDailyUptimeData = dailyData;
 | 
						|
        }
 | 
						|
 | 
						|
        // Don't store data in test mode
 | 
						|
        if (process.env.TEST_BACKEND) {
 | 
						|
            log.debug("uptime-calc", "Skip storing data in test mode");
 | 
						|
            return date;
 | 
						|
        }
 | 
						|
 | 
						|
        let dailyStatBean = await this.getDailyStatBean(dailyKey);
 | 
						|
        dailyStatBean.up = dailyData.up;
 | 
						|
        dailyStatBean.down = dailyData.down;
 | 
						|
        dailyStatBean.ping = dailyData.avgPing;
 | 
						|
        dailyStatBean.pingMin = dailyData.minPing;
 | 
						|
        dailyStatBean.pingMax = dailyData.maxPing;
 | 
						|
        {
 | 
						|
            // eslint-disable-next-line no-unused-vars
 | 
						|
            const { up, down, avgPing, minPing, maxPing, timestamp, ...extras } = dailyData;
 | 
						|
            if (Object.keys(extras).length > 0) {
 | 
						|
                dailyStatBean.extras = JSON.stringify(extras);
 | 
						|
            }
 | 
						|
        }
 | 
						|
        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;
 | 
						|
        {
 | 
						|
            // eslint-disable-next-line no-unused-vars
 | 
						|
            const { up, down, avgPing, minPing, maxPing, timestamp, ...extras } = hourlyData;
 | 
						|
            if (Object.keys(extras).length > 0) {
 | 
						|
                hourlyStatBean.extras = JSON.stringify(extras);
 | 
						|
            }
 | 
						|
        }
 | 
						|
        await R.store(hourlyStatBean);
 | 
						|
 | 
						|
        let minutelyStatBean = await this.getMinutelyStatBean(divisionKey);
 | 
						|
        minutelyStatBean.up = minutelyData.up;
 | 
						|
        minutelyStatBean.down = minutelyData.down;
 | 
						|
        minutelyStatBean.ping = minutelyData.avgPing;
 | 
						|
        minutelyStatBean.pingMin = minutelyData.minPing;
 | 
						|
        minutelyStatBean.pingMax = minutelyData.maxPing;
 | 
						|
        {
 | 
						|
            // eslint-disable-next-line no-unused-vars
 | 
						|
            const { up, down, avgPing, minPing, maxPing, timestamp, ...extras } = minutelyData;
 | 
						|
            if (Object.keys(extras).length > 0) {
 | 
						|
                minutelyStatBean.extras = JSON.stringify(extras);
 | 
						|
            }
 | 
						|
        }
 | 
						|
        await R.store(minutelyStatBean);
 | 
						|
 | 
						|
        // Remove the old data
 | 
						|
        log.debug("uptime-calc", "Remove old data");
 | 
						|
        await R.exec("DELETE FROM stat_minutely WHERE monitor_id = ? AND timestamp < ?", [
 | 
						|
            this.monitorID,
 | 
						|
            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;
 | 
						|
    }
 | 
						|
 | 
						|
    /**
 | 
						|
     * Get the daily stat bean
 | 
						|
     * @param {number} timestamp milliseconds
 | 
						|
     * @returns {Promise<import("redbean-node").Bean>} stat_daily bean
 | 
						|
     */
 | 
						|
    async getDailyStatBean(timestamp) {
 | 
						|
        if (this.lastDailyStatBean && this.lastDailyStatBean.timestamp === timestamp) {
 | 
						|
            return this.lastDailyStatBean;
 | 
						|
        }
 | 
						|
 | 
						|
        let bean = await R.findOne("stat_daily", " monitor_id = ? AND timestamp = ?", [
 | 
						|
            this.monitorID,
 | 
						|
            timestamp,
 | 
						|
        ]);
 | 
						|
 | 
						|
        if (!bean) {
 | 
						|
            bean = R.dispense("stat_daily");
 | 
						|
            bean.monitor_id = this.monitorID;
 | 
						|
            bean.timestamp = timestamp;
 | 
						|
        }
 | 
						|
 | 
						|
        this.lastDailyStatBean = bean;
 | 
						|
        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
 | 
						|
     * @param {number} timestamp milliseconds
 | 
						|
     * @returns {Promise<import("redbean-node").Bean>} stat_minutely bean
 | 
						|
     */
 | 
						|
    async getMinutelyStatBean(timestamp) {
 | 
						|
        if (this.lastMinutelyStatBean && this.lastMinutelyStatBean.timestamp === timestamp) {
 | 
						|
            return this.lastMinutelyStatBean;
 | 
						|
        }
 | 
						|
 | 
						|
        let bean = await R.findOne("stat_minutely", " monitor_id = ? AND timestamp = ?", [
 | 
						|
            this.monitorID,
 | 
						|
            timestamp,
 | 
						|
        ]);
 | 
						|
 | 
						|
        if (!bean) {
 | 
						|
            bean = R.dispense("stat_minutely");
 | 
						|
            bean.monitor_id = this.monitorID;
 | 
						|
            bean.timestamp = timestamp;
 | 
						|
        }
 | 
						|
 | 
						|
        this.lastMinutelyStatBean = bean;
 | 
						|
        return this.lastMinutelyStatBean;
 | 
						|
    }
 | 
						|
 | 
						|
    /**
 | 
						|
     * Convert timestamp to minutely key
 | 
						|
     * @param {dayjs.Dayjs} date The heartbeat date
 | 
						|
     * @returns {number} Timestamp
 | 
						|
     */
 | 
						|
    getMinutelyKey(date) {
 | 
						|
        // Truncate value to minutes (e.g. 2021-01-01 12:34:56 -> 2021-01-01 12:34:00)
 | 
						|
        date = date.startOf("minute");
 | 
						|
 | 
						|
        // Convert to timestamp in second
 | 
						|
        let divisionKey = date.unix();
 | 
						|
 | 
						|
        if (! (divisionKey in this.minutelyUptimeDataList)) {
 | 
						|
            this.minutelyUptimeDataList.push(divisionKey, {
 | 
						|
                up: 0,
 | 
						|
                down: 0,
 | 
						|
                avgPing: 0,
 | 
						|
                minPing: 0,
 | 
						|
                maxPing: 0,
 | 
						|
            });
 | 
						|
        }
 | 
						|
 | 
						|
        return divisionKey;
 | 
						|
    }
 | 
						|
 | 
						|
    /**
 | 
						|
     * Convert timestamp to hourly key
 | 
						|
     * @param {dayjs.Dayjs} date The heartbeat date
 | 
						|
     * @returns {number} Timestamp
 | 
						|
     */
 | 
						|
    getHourlyKey(date) {
 | 
						|
        // 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,
 | 
						|
            });
 | 
						|
        }
 | 
						|
 | 
						|
        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.
 | 
						|
        date = date.utc().startOf("day");
 | 
						|
        let dailyKey = date.unix();
 | 
						|
 | 
						|
        if (!this.dailyUptimeDataList[dailyKey]) {
 | 
						|
            this.dailyUptimeDataList.push(dailyKey, {
 | 
						|
                up: 0,
 | 
						|
                down: 0,
 | 
						|
                avgPing: 0,
 | 
						|
                minPing: 0,
 | 
						|
                maxPing: 0,
 | 
						|
            });
 | 
						|
        }
 | 
						|
 | 
						|
        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
 | 
						|
     * @param {number} status the status which should be turned into a flat status
 | 
						|
     * @returns {UP|DOWN|PENDING} The flat status
 | 
						|
     * @throws {Error} Invalid status
 | 
						|
     */
 | 
						|
    flatStatus(status) {
 | 
						|
        switch (status) {
 | 
						|
            case UP:
 | 
						|
            case MAINTENANCE:
 | 
						|
                return UP;
 | 
						|
            case DOWN:
 | 
						|
            case PENDING:
 | 
						|
                return DOWN;
 | 
						|
        }
 | 
						|
        throw new Error("Invalid status");
 | 
						|
    }
 | 
						|
 | 
						|
    /**
 | 
						|
     * @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 {UptimeDataResult} UptimeDataResult
 | 
						|
     * @throws {Error} The maximum number of minutes greater than 1440
 | 
						|
     */
 | 
						|
    getData(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 total = {
 | 
						|
            up: 0,
 | 
						|
            down: 0,
 | 
						|
        };
 | 
						|
 | 
						|
        let totalPing = 0;
 | 
						|
        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");
 | 
						|
        }
 | 
						|
 | 
						|
        // Sum up all data 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) {
 | 
						|
                total.up += data.up;
 | 
						|
                total.down += data.down;
 | 
						|
                totalPing += data.avgPing * data.up;
 | 
						|
            }
 | 
						|
 | 
						|
            // Set key to the previous 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");
 | 
						|
            }
 | 
						|
        }
 | 
						|
 | 
						|
        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) {
 | 
						|
            switch (type) {
 | 
						|
                case "day":
 | 
						|
                    if (this.lastDailyUptimeData) {
 | 
						|
                        total = this.lastDailyUptimeData;
 | 
						|
                        totalPing = total.avgPing * total.up;
 | 
						|
                    } else {
 | 
						|
                        return uptimeData;
 | 
						|
                    }
 | 
						|
                    break;
 | 
						|
                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");
 | 
						|
            }
 | 
						|
        }
 | 
						|
 | 
						|
        let avgPing;
 | 
						|
 | 
						|
        if (total.up === 0) {
 | 
						|
            avgPing = null;
 | 
						|
        } else {
 | 
						|
            avgPing = totalPing / total.up;
 | 
						|
        }
 | 
						|
 | 
						|
        if (total.up + total.down === 0) {
 | 
						|
            uptimeData.uptime = 0;
 | 
						|
        } else {
 | 
						|
            uptimeData.uptime = total.up / (total.up + total.down);
 | 
						|
        }
 | 
						|
        uptimeData.avgPing = avgPing;
 | 
						|
        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 previous 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
 | 
						|
     * @param {'24h'|'30d'|'1y'} duration Only accept 24h, 30d, 1y
 | 
						|
     * @returns {UptimeDataResult} UptimeDataResult
 | 
						|
     * @throws {Error} Invalid duration
 | 
						|
     */
 | 
						|
    getDataByDuration(duration) {
 | 
						|
        if (duration === "24h") {
 | 
						|
            return this.get24Hour();
 | 
						|
        } else if (duration === "30d") {
 | 
						|
            return this.get30Day();
 | 
						|
        } else if (duration === "1y") {
 | 
						|
            return this.get1Year();
 | 
						|
        } else {
 | 
						|
            throw new Error("Invalid duration");
 | 
						|
        }
 | 
						|
    }
 | 
						|
 | 
						|
    /**
 | 
						|
     * 1440 = 24 * 60mins
 | 
						|
     * @returns {UptimeDataResult} UptimeDataResult
 | 
						|
     */
 | 
						|
    get24Hour() {
 | 
						|
        return this.getData(1440, "minute");
 | 
						|
    }
 | 
						|
 | 
						|
    /**
 | 
						|
     * @returns {UptimeDataResult} UptimeDataResult
 | 
						|
     */
 | 
						|
    get7Day() {
 | 
						|
        return this.getData(168, "hour");
 | 
						|
    }
 | 
						|
 | 
						|
    /**
 | 
						|
     * @returns {UptimeDataResult} UptimeDataResult
 | 
						|
     */
 | 
						|
    get30Day() {
 | 
						|
        return this.getData(30);
 | 
						|
    }
 | 
						|
 | 
						|
    /**
 | 
						|
     * @returns {UptimeDataResult} UptimeDataResult
 | 
						|
     */
 | 
						|
    get1Year() {
 | 
						|
        return this.getData(365);
 | 
						|
    }
 | 
						|
 | 
						|
    /**
 | 
						|
     * @returns {dayjs.Dayjs} Current datetime in UTC
 | 
						|
     */
 | 
						|
    getCurrentDate() {
 | 
						|
        return dayjs.utc();
 | 
						|
    }
 | 
						|
 | 
						|
}
 | 
						|
 | 
						|
class UptimeDataResult {
 | 
						|
    /**
 | 
						|
     * @type {number} Uptime
 | 
						|
     */
 | 
						|
    uptime = 0;
 | 
						|
 | 
						|
    /**
 | 
						|
     * @type {number} Average ping
 | 
						|
     */
 | 
						|
    avgPing = null;
 | 
						|
}
 | 
						|
 | 
						|
module.exports = {
 | 
						|
    UptimeCalculator,
 | 
						|
    UptimeDataResult,
 | 
						|
};
 |