mirror of
https://github.com/louislam/uptime-kuma.git
synced 2024-11-27 16:54:04 +00:00
Uptime calculation improvement and 1-year uptime (#2750)
This commit is contained in:
parent
eec221247f
commit
076331bf00
22 changed files with 1306 additions and 264 deletions
|
@ -1,6 +1,7 @@
|
||||||
module.exports = {
|
module.exports = {
|
||||||
ignorePatterns: [
|
ignorePatterns: [
|
||||||
"test/*",
|
"test/*.js",
|
||||||
|
"test/cypress",
|
||||||
"server/modules/apicache/*",
|
"server/modules/apicache/*",
|
||||||
"src/util.js"
|
"src/util.js"
|
||||||
],
|
],
|
||||||
|
|
41
db/knex_migrations/2023-08-16-0000-create-uptime.js
Normal file
41
db/knex_migrations/2023-08-16-0000-create-uptime.js
Normal file
|
@ -0,0 +1,41 @@
|
||||||
|
exports.up = function (knex) {
|
||||||
|
return knex.schema
|
||||||
|
.createTable("stat_minutely", function (table) {
|
||||||
|
table.increments("id");
|
||||||
|
table.comment("This table contains the minutely aggregate statistics for each monitor");
|
||||||
|
table.integer("monitor_id").unsigned().notNullable()
|
||||||
|
.references("id").inTable("monitor")
|
||||||
|
.onDelete("CASCADE")
|
||||||
|
.onUpdate("CASCADE");
|
||||||
|
table.integer("timestamp")
|
||||||
|
.notNullable()
|
||||||
|
.comment("Unix timestamp rounded down to the nearest minute");
|
||||||
|
table.float("ping").notNullable().comment("Average ping in milliseconds");
|
||||||
|
table.smallint("up").notNullable();
|
||||||
|
table.smallint("down").notNullable();
|
||||||
|
|
||||||
|
table.unique([ "monitor_id", "timestamp" ]);
|
||||||
|
})
|
||||||
|
.createTable("stat_daily", function (table) {
|
||||||
|
table.increments("id");
|
||||||
|
table.comment("This table contains the daily aggregate statistics for each monitor");
|
||||||
|
table.integer("monitor_id").unsigned().notNullable()
|
||||||
|
.references("id").inTable("monitor")
|
||||||
|
.onDelete("CASCADE")
|
||||||
|
.onUpdate("CASCADE");
|
||||||
|
table.integer("timestamp")
|
||||||
|
.notNullable()
|
||||||
|
.comment("Unix timestamp rounded down to the nearest day");
|
||||||
|
table.float("ping").notNullable().comment("Average ping in milliseconds");
|
||||||
|
table.smallint("up").notNullable();
|
||||||
|
table.smallint("down").notNullable();
|
||||||
|
|
||||||
|
table.unique([ "monitor_id", "timestamp" ]);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
exports.down = function (knex) {
|
||||||
|
return knex.schema
|
||||||
|
.dropTable("stat_minutely")
|
||||||
|
.dropTable("stat_daily");
|
||||||
|
};
|
16
db/knex_migrations/2023-08-18-0301-heartbeat.js
Normal file
16
db/knex_migrations/2023-08-18-0301-heartbeat.js
Normal file
|
@ -0,0 +1,16 @@
|
||||||
|
exports.up = function (knex) {
|
||||||
|
// Add new column heartbeat.end_time
|
||||||
|
return knex.schema
|
||||||
|
.alterTable("heartbeat", function (table) {
|
||||||
|
table.datetime("end_time").nullable().defaultTo(null);
|
||||||
|
});
|
||||||
|
|
||||||
|
};
|
||||||
|
|
||||||
|
exports.down = function (knex) {
|
||||||
|
// Rename heartbeat.start_time to heartbeat.time
|
||||||
|
return knex.schema
|
||||||
|
.alterTable("heartbeat", function (table) {
|
||||||
|
table.dropColumn("end_time");
|
||||||
|
});
|
||||||
|
};
|
|
@ -5,12 +5,10 @@ https://knexjs.org/guide/migrations.html#knexfile-in-other-languages
|
||||||
## Basic rules
|
## Basic rules
|
||||||
- All tables must have a primary key named `id`
|
- All tables must have a primary key named `id`
|
||||||
- Filename format: `YYYY-MM-DD-HHMM-patch-name.js`
|
- Filename format: `YYYY-MM-DD-HHMM-patch-name.js`
|
||||||
- Avoid native SQL syntax, use knex methods, because Uptime Kuma supports multiple databases
|
- Avoid native SQL syntax, use knex methods, because Uptime Kuma supports SQLite and MariaDB.
|
||||||
|
|
||||||
## Template
|
## Template
|
||||||
|
|
||||||
Filename: YYYYMMDDHHMMSS_name.js
|
|
||||||
|
|
||||||
```js
|
```js
|
||||||
exports.up = function(knex) {
|
exports.up = function(knex) {
|
||||||
|
|
||||||
|
|
105
package-lock.json
generated
105
package-lock.json
generated
|
@ -119,6 +119,7 @@
|
||||||
"stylelint": "^15.10.1",
|
"stylelint": "^15.10.1",
|
||||||
"stylelint-config-standard": "~25.0.0",
|
"stylelint-config-standard": "~25.0.0",
|
||||||
"terser": "~5.15.0",
|
"terser": "~5.15.0",
|
||||||
|
"test": "~3.3.0",
|
||||||
"timezones-list": "~3.0.1",
|
"timezones-list": "~3.0.1",
|
||||||
"typescript": "~4.4.4",
|
"typescript": "~4.4.4",
|
||||||
"v-pagination-3": "~0.1.7",
|
"v-pagination-3": "~0.1.7",
|
||||||
|
@ -5981,6 +5982,18 @@
|
||||||
"resolved": "https://registry.npmjs.org/abbrev/-/abbrev-1.1.1.tgz",
|
"resolved": "https://registry.npmjs.org/abbrev/-/abbrev-1.1.1.tgz",
|
||||||
"integrity": "sha512-nne9/IiQ/hzIhY6pdDnbBtz7DjPTKrY00P/zvPSm5pOFkl6xuGrGnXn/VtTNNfNtAfZ9/1RtehkszU9qcTii0Q=="
|
"integrity": "sha512-nne9/IiQ/hzIhY6pdDnbBtz7DjPTKrY00P/zvPSm5pOFkl6xuGrGnXn/VtTNNfNtAfZ9/1RtehkszU9qcTii0Q=="
|
||||||
},
|
},
|
||||||
|
"node_modules/abort-controller": {
|
||||||
|
"version": "3.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/abort-controller/-/abort-controller-3.0.0.tgz",
|
||||||
|
"integrity": "sha512-h8lQ8tacZYnR3vNQTgibj+tODHI5/+l06Au2Pcriv/Gmet0eaj4TwWH41sO9wnHDiQsEj19q0drzdWdeAHtweg==",
|
||||||
|
"dev": true,
|
||||||
|
"dependencies": {
|
||||||
|
"event-target-shim": "^5.0.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=6.5"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/accepts": {
|
"node_modules/accepts": {
|
||||||
"version": "1.3.8",
|
"version": "1.3.8",
|
||||||
"resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz",
|
"resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz",
|
||||||
|
@ -9377,6 +9390,15 @@
|
||||||
"node": ">= 0.6"
|
"node": ">= 0.6"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/event-target-shim": {
|
||||||
|
"version": "5.0.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/event-target-shim/-/event-target-shim-5.0.1.tgz",
|
||||||
|
"integrity": "sha512-i/2XbnSz/uxRCU6+NdVJgKWDTM427+MqYbkQzD321DuCQJUqOuJKIA0IM2+W2xtYHdKOmZ4dR6fExsd4SXL+WQ==",
|
||||||
|
"dev": true,
|
||||||
|
"engines": {
|
||||||
|
"node": ">=6"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/event-to-promise": {
|
"node_modules/event-to-promise": {
|
||||||
"version": "0.7.0",
|
"version": "0.7.0",
|
||||||
"resolved": "https://registry.npmjs.org/event-to-promise/-/event-to-promise-0.7.0.tgz",
|
"resolved": "https://registry.npmjs.org/event-to-promise/-/event-to-promise-0.7.0.tgz",
|
||||||
|
@ -15529,6 +15551,15 @@
|
||||||
"node": ">=6"
|
"node": ">=6"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/process": {
|
||||||
|
"version": "0.11.10",
|
||||||
|
"resolved": "https://registry.npmjs.org/process/-/process-0.11.10.tgz",
|
||||||
|
"integrity": "sha512-cdGef/drWFoydD1JsMzuFf8100nZl+GT+yacc2bEced5f9Rjk4z+WtFUTBu9PhOi9j/jfmBPu0mMEY4wIdAF8A==",
|
||||||
|
"dev": true,
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 0.6.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/process-nextick-args": {
|
"node_modules/process-nextick-args": {
|
||||||
"version": "2.0.1",
|
"version": "2.0.1",
|
||||||
"resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz",
|
||||||
|
@ -17297,6 +17328,23 @@
|
||||||
"node": ">=8"
|
"node": ">=8"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/string.prototype.replaceall": {
|
||||||
|
"version": "1.0.7",
|
||||||
|
"resolved": "https://registry.npmjs.org/string.prototype.replaceall/-/string.prototype.replaceall-1.0.7.tgz",
|
||||||
|
"integrity": "sha512-xB2WV2GlSCSJT5dMGdhdH1noMPiAB91guiepwTYyWY9/0Vq/TZ7RPmnOSUGAEvry08QIK7EMr28aAii+9jC6kw==",
|
||||||
|
"dev": true,
|
||||||
|
"dependencies": {
|
||||||
|
"call-bind": "^1.0.2",
|
||||||
|
"define-properties": "^1.1.4",
|
||||||
|
"es-abstract": "^1.20.4",
|
||||||
|
"get-intrinsic": "^1.1.3",
|
||||||
|
"has-symbols": "^1.0.3",
|
||||||
|
"is-regex": "^1.1.4"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/sponsors/ljharb"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/string.prototype.trim": {
|
"node_modules/string.prototype.trim": {
|
||||||
"version": "1.2.7",
|
"version": "1.2.7",
|
||||||
"resolved": "https://registry.npmjs.org/string.prototype.trim/-/string.prototype.trim-1.2.7.tgz",
|
"resolved": "https://registry.npmjs.org/string.prototype.trim/-/string.prototype.trim-1.2.7.tgz",
|
||||||
|
@ -17825,6 +17873,23 @@
|
||||||
"integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==",
|
"integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==",
|
||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
|
"node_modules/test": {
|
||||||
|
"version": "3.3.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/test/-/test-3.3.0.tgz",
|
||||||
|
"integrity": "sha512-JKlEohxDIJRjwBH/+BrTcAPHljBALrAHw3Zs99RqZlaC605f6BggqXhxkdqZThbSHgaYPwpNJlf9bTSWkb/1rA==",
|
||||||
|
"dev": true,
|
||||||
|
"dependencies": {
|
||||||
|
"minimist": "^1.2.6",
|
||||||
|
"readable-stream": "^4.3.0",
|
||||||
|
"string.prototype.replaceall": "^1.0.6"
|
||||||
|
},
|
||||||
|
"bin": {
|
||||||
|
"node--test": "bin/node--test.js",
|
||||||
|
"node--test-name-pattern": "bin/node--test-name-pattern.js",
|
||||||
|
"node--test-only": "bin/node--test-only.js",
|
||||||
|
"test": "bin/node-core-test.js"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/test-exclude": {
|
"node_modules/test-exclude": {
|
||||||
"version": "6.0.0",
|
"version": "6.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/test-exclude/-/test-exclude-6.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/test-exclude/-/test-exclude-6.0.0.tgz",
|
||||||
|
@ -17839,6 +17904,46 @@
|
||||||
"node": ">=8"
|
"node": ">=8"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/test/node_modules/buffer": {
|
||||||
|
"version": "6.0.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/buffer/-/buffer-6.0.3.tgz",
|
||||||
|
"integrity": "sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA==",
|
||||||
|
"dev": true,
|
||||||
|
"funding": [
|
||||||
|
{
|
||||||
|
"type": "github",
|
||||||
|
"url": "https://github.com/sponsors/feross"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "patreon",
|
||||||
|
"url": "https://www.patreon.com/feross"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "consulting",
|
||||||
|
"url": "https://feross.org/support"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"dependencies": {
|
||||||
|
"base64-js": "^1.3.1",
|
||||||
|
"ieee754": "^1.2.1"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/test/node_modules/readable-stream": {
|
||||||
|
"version": "4.4.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-4.4.2.tgz",
|
||||||
|
"integrity": "sha512-Lk/fICSyIhodxy1IDK2HazkeGjSmezAWX2egdtJnYhtzKEsBPJowlI6F6LPb5tqIQILrMbx22S5o3GuJavPusA==",
|
||||||
|
"dev": true,
|
||||||
|
"dependencies": {
|
||||||
|
"abort-controller": "^3.0.0",
|
||||||
|
"buffer": "^6.0.3",
|
||||||
|
"events": "^3.3.0",
|
||||||
|
"process": "^0.11.10",
|
||||||
|
"string_decoder": "^1.3.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": "^12.22.0 || ^14.17.0 || >=16.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/text-table": {
|
"node_modules/text-table": {
|
||||||
"version": "0.2.0",
|
"version": "0.2.0",
|
||||||
"resolved": "https://registry.npmjs.org/text-table/-/text-table-0.2.0.tgz",
|
"resolved": "https://registry.npmjs.org/text-table/-/text-table-0.2.0.tgz",
|
||||||
|
|
|
@ -24,8 +24,11 @@
|
||||||
"start-server": "node server/server.js",
|
"start-server": "node server/server.js",
|
||||||
"start-server-dev": "cross-env NODE_ENV=development node server/server.js",
|
"start-server-dev": "cross-env NODE_ENV=development node server/server.js",
|
||||||
"build": "vite build --config ./config/vite.config.js",
|
"build": "vite build --config ./config/vite.config.js",
|
||||||
"test": "node test/prepare-test-server.js && npm run jest-backend",
|
"test": "node test/prepare-test-server.js && npm run test-backend",
|
||||||
"test-with-build": "npm run build && npm test",
|
"test-with-build": "npm run build && npm test",
|
||||||
|
"test-backend": "node test/backend-test-entry.js && npm run jest-backend",
|
||||||
|
"test-backend:14": "cross-env TEST_BACKEND=1 NODE_OPTIONS=\"--experimental-abortcontroller --no-warnings\" node--test test/backend-test",
|
||||||
|
"test-backend:18": "cross-env TEST_BACKEND=1 node --test test/backend-test",
|
||||||
"jest-backend": "cross-env TEST_BACKEND=1 jest --runInBand --detectOpenHandles --forceExit --config=./config/jest-backend.config.js",
|
"jest-backend": "cross-env TEST_BACKEND=1 jest --runInBand --detectOpenHandles --forceExit --config=./config/jest-backend.config.js",
|
||||||
"tsc": "tsc",
|
"tsc": "tsc",
|
||||||
"vite-preview-dist": "vite preview --host --config ./config/vite.config.js",
|
"vite-preview-dist": "vite preview --host --config ./config/vite.config.js",
|
||||||
|
@ -181,6 +184,7 @@
|
||||||
"stylelint": "^15.10.1",
|
"stylelint": "^15.10.1",
|
||||||
"stylelint-config-standard": "~25.0.0",
|
"stylelint-config-standard": "~25.0.0",
|
||||||
"terser": "~5.15.0",
|
"terser": "~5.15.0",
|
||||||
|
"test": "~3.3.0",
|
||||||
"timezones-list": "~3.0.1",
|
"timezones-list": "~3.0.1",
|
||||||
"typescript": "~4.4.4",
|
"typescript": "~4.4.4",
|
||||||
"v-pagination-3": "~0.1.7",
|
"v-pagination-3": "~0.1.7",
|
||||||
|
|
|
@ -45,8 +45,6 @@ async function sendNotificationList(socket) {
|
||||||
* @returns {Promise<void>}
|
* @returns {Promise<void>}
|
||||||
*/
|
*/
|
||||||
async function sendHeartbeatList(socket, monitorID, toUser = false, overwrite = false) {
|
async function sendHeartbeatList(socket, monitorID, toUser = false, overwrite = false) {
|
||||||
const timeLogger = new TimeLogger();
|
|
||||||
|
|
||||||
let list = await R.getAll(`
|
let list = await R.getAll(`
|
||||||
SELECT * FROM heartbeat
|
SELECT * FROM heartbeat
|
||||||
WHERE monitor_id = ?
|
WHERE monitor_id = ?
|
||||||
|
@ -63,8 +61,6 @@ async function sendHeartbeatList(socket, monitorID, toUser = false, overwrite =
|
||||||
} else {
|
} else {
|
||||||
socket.emit("heartbeatList", monitorID, result, overwrite);
|
socket.emit("heartbeatList", monitorID, result, overwrite);
|
||||||
}
|
}
|
||||||
|
|
||||||
timeLogger.print(`[Monitor: ${monitorID}] sendHeartbeatList`);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
@ -183,6 +183,12 @@ class Database {
|
||||||
|
|
||||||
let config = {};
|
let config = {};
|
||||||
|
|
||||||
|
let mariadbPoolConfig = {
|
||||||
|
afterCreate: function (conn, done) {
|
||||||
|
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
log.info("db", `Database Type: ${dbConfig.type}`);
|
log.info("db", `Database Type: ${dbConfig.type}`);
|
||||||
|
|
||||||
if (dbConfig.type === "sqlite") {
|
if (dbConfig.type === "sqlite") {
|
||||||
|
@ -233,7 +239,9 @@ class Database {
|
||||||
user: dbConfig.username,
|
user: dbConfig.username,
|
||||||
password: dbConfig.password,
|
password: dbConfig.password,
|
||||||
database: dbConfig.dbName,
|
database: dbConfig.dbName,
|
||||||
}
|
timezone: "UTC",
|
||||||
|
},
|
||||||
|
pool: mariadbPoolConfig,
|
||||||
};
|
};
|
||||||
} else if (dbConfig.type === "embedded-mariadb") {
|
} else if (dbConfig.type === "embedded-mariadb") {
|
||||||
let embeddedMariaDB = EmbeddedMariaDB.getInstance();
|
let embeddedMariaDB = EmbeddedMariaDB.getInstance();
|
||||||
|
@ -245,7 +253,8 @@ class Database {
|
||||||
socketPath: embeddedMariaDB.socketPath,
|
socketPath: embeddedMariaDB.socketPath,
|
||||||
user: "node",
|
user: "node",
|
||||||
database: "kuma",
|
database: "kuma",
|
||||||
}
|
},
|
||||||
|
pool: mariadbPoolConfig,
|
||||||
};
|
};
|
||||||
} else {
|
} else {
|
||||||
throw new Error("Unknown Database type: " + dbConfig.type);
|
throw new Error("Unknown Database type: " + dbConfig.type);
|
||||||
|
@ -350,6 +359,7 @@ class Database {
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
* TODO
|
||||||
* @returns {Promise<void>}
|
* @returns {Promise<void>}
|
||||||
*/
|
*/
|
||||||
static async rollbackLatestPatch() {
|
static async rollbackLatestPatch() {
|
||||||
|
@ -582,14 +592,6 @@ class Database {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Aquire a direct connection to database
|
|
||||||
* @returns {any} Database connection
|
|
||||||
*/
|
|
||||||
static getBetterSQLite3Database() {
|
|
||||||
return R.knex.client.acquireConnection();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Special handle, because tarn.js throw a promise reject that cannot be caught
|
* Special handle, because tarn.js throw a promise reject that cannot be caught
|
||||||
* @returns {Promise<void>}
|
* @returns {Promise<void>}
|
||||||
|
@ -603,7 +605,9 @@ class Database {
|
||||||
log.info("db", "Closing the database");
|
log.info("db", "Closing the database");
|
||||||
|
|
||||||
// Flush WAL to main database
|
// Flush WAL to main database
|
||||||
|
if (Database.dbConfig.type === "sqlite") {
|
||||||
await R.exec("PRAGMA wal_checkpoint(TRUNCATE)");
|
await R.exec("PRAGMA wal_checkpoint(TRUNCATE)");
|
||||||
|
}
|
||||||
|
|
||||||
while (true) {
|
while (true) {
|
||||||
Database.noReject = true;
|
Database.noReject = true;
|
||||||
|
@ -616,29 +620,34 @@ class Database {
|
||||||
log.info("db", "Waiting to close the database");
|
log.info("db", "Waiting to close the database");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
log.info("db", "SQLite closed");
|
log.info("db", "Database closed");
|
||||||
|
|
||||||
process.removeListener("unhandledRejection", listener);
|
process.removeListener("unhandledRejection", listener);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get the size of the database
|
* Get the size of the database (SQLite only)
|
||||||
* @returns {number} Size of database
|
* @returns {number} Size of database
|
||||||
*/
|
*/
|
||||||
static getSize() {
|
static getSize() {
|
||||||
|
if (Database.dbConfig.type === "sqlite") {
|
||||||
log.debug("db", "Database.getSize()");
|
log.debug("db", "Database.getSize()");
|
||||||
let stats = fs.statSync(Database.sqlitePath);
|
let stats = fs.statSync(Database.sqlitePath);
|
||||||
log.debug("db", stats);
|
log.debug("db", stats);
|
||||||
return stats.size;
|
return stats.size;
|
||||||
}
|
}
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Shrink the database
|
* Shrink the database
|
||||||
* @returns {Promise<void>}
|
* @returns {Promise<void>}
|
||||||
*/
|
*/
|
||||||
static async shrink() {
|
static async shrink() {
|
||||||
|
if (Database.dbConfig.type === "sqlite") {
|
||||||
await R.exec("VACUUM");
|
await R.exec("VACUUM");
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
*
|
*
|
||||||
|
|
|
@ -43,7 +43,9 @@ const clearOldData = async () => {
|
||||||
[ parsedPeriod * -24 ]
|
[ parsedPeriod * -24 ]
|
||||||
);
|
);
|
||||||
|
|
||||||
|
if (Database.dbConfig.type === "sqlite") {
|
||||||
await R.exec("PRAGMA optimize;");
|
await R.exec("PRAGMA optimize;");
|
||||||
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
log.error("clearOldData", `Failed to clear old data: ${e.message}`);
|
log.error("clearOldData", `Failed to clear old data: ${e.message}`);
|
||||||
}
|
}
|
||||||
|
|
|
@ -18,11 +18,10 @@ const apicache = require("../modules/apicache");
|
||||||
const { UptimeKumaServer } = require("../uptime-kuma-server");
|
const { UptimeKumaServer } = require("../uptime-kuma-server");
|
||||||
const { CacheableDnsHttpAgent } = require("../cacheable-dns-http-agent");
|
const { CacheableDnsHttpAgent } = require("../cacheable-dns-http-agent");
|
||||||
const { DockerHost } = require("../docker");
|
const { DockerHost } = require("../docker");
|
||||||
const { UptimeCacheList } = require("../uptime-cache-list");
|
|
||||||
const Gamedig = require("gamedig");
|
const Gamedig = require("gamedig");
|
||||||
const jsonata = require("jsonata");
|
const jsonata = require("jsonata");
|
||||||
const jwt = require("jsonwebtoken");
|
const jwt = require("jsonwebtoken");
|
||||||
const Database = require("../database");
|
const { UptimeCalculator } = require("../uptime-calculator");
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* status:
|
* status:
|
||||||
|
@ -346,13 +345,6 @@ class Monitor extends BeanModel {
|
||||||
bean.status = flipStatus(bean.status);
|
bean.status = flipStatus(bean.status);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Duration
|
|
||||||
if (!isFirstBeat) {
|
|
||||||
bean.duration = dayjs(bean.time).diff(dayjs(previousBeat.time), "second");
|
|
||||||
} else {
|
|
||||||
bean.duration = 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
if (await Monitor.isUnderMaintenance(this.id)) {
|
if (await Monitor.isUnderMaintenance(this.id)) {
|
||||||
bean.msg = "Monitor under maintenance";
|
bean.msg = "Monitor under maintenance";
|
||||||
|
@ -971,11 +963,17 @@ class Monitor extends BeanModel {
|
||||||
log.warn("monitor", `Monitor #${this.id} '${this.name}': Failing: ${bean.msg} | Interval: ${beatInterval} seconds | Type: ${this.type} | Down Count: ${bean.downCount} | Resend Interval: ${this.resendInterval}`);
|
log.warn("monitor", `Monitor #${this.id} '${this.name}': Failing: ${bean.msg} | Interval: ${beatInterval} seconds | Type: ${this.type} | Down Count: ${bean.downCount} | Resend Interval: ${this.resendInterval}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Calculate uptime
|
||||||
|
let uptimeCalculator = await UptimeCalculator.getUptimeCalculator(this.id);
|
||||||
|
let endTimeDayjs = await uptimeCalculator.update(bean.status, parseFloat(bean.ping));
|
||||||
|
bean.end_time = R.isoDateTimeMillis(endTimeDayjs);
|
||||||
|
|
||||||
|
// Send to frontend
|
||||||
log.debug("monitor", `[${this.name}] Send to socket`);
|
log.debug("monitor", `[${this.name}] Send to socket`);
|
||||||
UptimeCacheList.clearCache(this.id);
|
|
||||||
io.to(this.user_id).emit("heartbeat", bean.toJSON());
|
io.to(this.user_id).emit("heartbeat", bean.toJSON());
|
||||||
Monitor.sendStats(io, this.id, this.user_id);
|
Monitor.sendStats(io, this.id, this.user_id);
|
||||||
|
|
||||||
|
// Store to database
|
||||||
log.debug("monitor", `[${this.name}] Store`);
|
log.debug("monitor", `[${this.name}] Store`);
|
||||||
await R.store(bean);
|
await R.store(bean);
|
||||||
|
|
||||||
|
@ -1149,44 +1147,31 @@ class Monitor extends BeanModel {
|
||||||
*/
|
*/
|
||||||
static async sendStats(io, monitorID, userID) {
|
static async sendStats(io, monitorID, userID) {
|
||||||
const hasClients = getTotalClientInRoom(io, userID) > 0;
|
const hasClients = getTotalClientInRoom(io, userID) > 0;
|
||||||
|
let uptimeCalculator = await UptimeCalculator.getUptimeCalculator(monitorID);
|
||||||
|
|
||||||
if (hasClients) {
|
if (hasClients) {
|
||||||
await Monitor.sendAvgPing(24, io, monitorID, userID);
|
// Send 24 hour average ping
|
||||||
await Monitor.sendUptime(24, io, monitorID, userID);
|
let data24h = await uptimeCalculator.get24Hour();
|
||||||
await Monitor.sendUptime(24 * 30, io, monitorID, userID);
|
io.to(userID).emit("avgPing", monitorID, (data24h.avgPing) ? data24h.avgPing.toFixed(2) : null);
|
||||||
|
|
||||||
|
// Send 24 hour uptime
|
||||||
|
io.to(userID).emit("uptime", monitorID, 24, data24h.uptime);
|
||||||
|
|
||||||
|
// Send 30 day uptime
|
||||||
|
let data30d = await uptimeCalculator.get30Day();
|
||||||
|
io.to(userID).emit("uptime", monitorID, 720, data30d.uptime);
|
||||||
|
|
||||||
|
// Send 1-year uptime
|
||||||
|
let data1y = await uptimeCalculator.get1Year();
|
||||||
|
io.to(userID).emit("uptime", monitorID, "1y", data1y.uptime);
|
||||||
|
|
||||||
|
// Send Cert Info
|
||||||
await Monitor.sendCertInfo(io, monitorID, userID);
|
await Monitor.sendCertInfo(io, monitorID, userID);
|
||||||
} else {
|
} else {
|
||||||
log.debug("monitor", "No clients in the room, no need to send stats");
|
log.debug("monitor", "No clients in the room, no need to send stats");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Send the average ping to user
|
|
||||||
* @param {number} duration Hours
|
|
||||||
* @param {Server} io Socket instance to send data to
|
|
||||||
* @param {number} monitorID ID of monitor to read
|
|
||||||
* @param {number} userID ID of user to send data to
|
|
||||||
* @returns {void}
|
|
||||||
*/
|
|
||||||
static async sendAvgPing(duration, io, monitorID, userID) {
|
|
||||||
const timeLogger = new TimeLogger();
|
|
||||||
const sqlHourOffset = Database.sqlHourOffset();
|
|
||||||
|
|
||||||
let avgPing = parseInt(await R.getCell(`
|
|
||||||
SELECT AVG(ping)
|
|
||||||
FROM heartbeat
|
|
||||||
WHERE time > ${sqlHourOffset}
|
|
||||||
AND ping IS NOT NULL
|
|
||||||
AND monitor_id = ? `, [
|
|
||||||
-duration,
|
|
||||||
monitorID,
|
|
||||||
]));
|
|
||||||
|
|
||||||
timeLogger.print(`[Monitor: ${monitorID}] avgPing`);
|
|
||||||
|
|
||||||
io.to(userID).emit("avgPing", monitorID, avgPing);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Send certificate information to client
|
* Send certificate information to client
|
||||||
* @param {Server} io Socket server instance
|
* @param {Server} io Socket server instance
|
||||||
|
@ -1203,101 +1188,6 @@ class Monitor extends BeanModel {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Uptime with calculation
|
|
||||||
* Calculation based on:
|
|
||||||
* https://www.uptrends.com/support/kb/reporting/calculation-of-uptime-and-downtime
|
|
||||||
* @param {number} duration Hours
|
|
||||||
* @param {number} monitorID ID of monitor to calculate
|
|
||||||
* @param {boolean} forceNoCache Should the uptime be recalculated?
|
|
||||||
* @returns {number} Uptime of monitor
|
|
||||||
*/
|
|
||||||
static async calcUptime(duration, monitorID, forceNoCache = false) {
|
|
||||||
|
|
||||||
if (!forceNoCache) {
|
|
||||||
let cachedUptime = UptimeCacheList.getUptime(monitorID, duration);
|
|
||||||
if (cachedUptime != null) {
|
|
||||||
return cachedUptime;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const timeLogger = new TimeLogger();
|
|
||||||
|
|
||||||
const startTime = R.isoDateTime(dayjs.utc().subtract(duration, "hour"));
|
|
||||||
|
|
||||||
// Handle if heartbeat duration longer than the target duration
|
|
||||||
// e.g. If the last beat's duration is bigger that the 24hrs window, it will use the duration between the (beat time - window margin) (THEN case in SQL)
|
|
||||||
let result = await R.getRow(`
|
|
||||||
SELECT
|
|
||||||
-- SUM all duration, also trim off the beat out of time window
|
|
||||||
SUM(
|
|
||||||
CASE
|
|
||||||
WHEN (JULIANDAY(\`time\`) - JULIANDAY(?)) * 86400 < duration
|
|
||||||
THEN (JULIANDAY(\`time\`) - JULIANDAY(?)) * 86400
|
|
||||||
ELSE duration
|
|
||||||
END
|
|
||||||
) AS total_duration,
|
|
||||||
|
|
||||||
-- SUM all uptime duration, also trim off the beat out of time window
|
|
||||||
SUM(
|
|
||||||
CASE
|
|
||||||
WHEN (status = 1 OR status = 3)
|
|
||||||
THEN
|
|
||||||
CASE
|
|
||||||
WHEN (JULIANDAY(\`time\`) - JULIANDAY(?)) * 86400 < duration
|
|
||||||
THEN (JULIANDAY(\`time\`) - JULIANDAY(?)) * 86400
|
|
||||||
ELSE duration
|
|
||||||
END
|
|
||||||
END
|
|
||||||
) AS uptime_duration
|
|
||||||
FROM heartbeat
|
|
||||||
WHERE time > ?
|
|
||||||
AND monitor_id = ?
|
|
||||||
`, [
|
|
||||||
startTime, startTime, startTime, startTime, startTime,
|
|
||||||
monitorID,
|
|
||||||
]);
|
|
||||||
|
|
||||||
timeLogger.print(`[Monitor: ${monitorID}][${duration}] sendUptime`);
|
|
||||||
|
|
||||||
let totalDuration = result.total_duration;
|
|
||||||
let uptimeDuration = result.uptime_duration;
|
|
||||||
let uptime = 0;
|
|
||||||
|
|
||||||
if (totalDuration > 0) {
|
|
||||||
uptime = uptimeDuration / totalDuration;
|
|
||||||
if (uptime < 0) {
|
|
||||||
uptime = 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
} else {
|
|
||||||
// Handle new monitor with only one beat, because the beat's duration = 0
|
|
||||||
let status = parseInt(await R.getCell("SELECT `status` FROM heartbeat WHERE monitor_id = ?", [ monitorID ]));
|
|
||||||
|
|
||||||
if (status === UP) {
|
|
||||||
uptime = 1;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Cache
|
|
||||||
UptimeCacheList.addUptime(monitorID, duration, uptime);
|
|
||||||
|
|
||||||
return uptime;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Send Uptime
|
|
||||||
* @param {number} duration Hours
|
|
||||||
* @param {Server} io Socket server instance
|
|
||||||
* @param {number} monitorID ID of monitor to send
|
|
||||||
* @param {number} userID ID of user to send to
|
|
||||||
* @returns {void}
|
|
||||||
*/
|
|
||||||
static async sendUptime(duration, io, monitorID, userID) {
|
|
||||||
const uptime = await this.calcUptime(duration, monitorID);
|
|
||||||
io.to(userID).emit("uptime", monitorID, duration, uptime);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Has status of monitor changed since last beat?
|
* Has status of monitor changed since last beat?
|
||||||
* @param {boolean} isFirstBeat Is this the first beat of this monitor?
|
* @param {boolean} isFirstBeat Is this the first beat of this monitor?
|
||||||
|
|
|
@ -7,11 +7,11 @@ const dayjs = require("dayjs");
|
||||||
const { UP, MAINTENANCE, DOWN, PENDING, flipStatus, log } = require("../../src/util");
|
const { UP, MAINTENANCE, DOWN, PENDING, flipStatus, log } = require("../../src/util");
|
||||||
const StatusPage = require("../model/status_page");
|
const StatusPage = require("../model/status_page");
|
||||||
const { UptimeKumaServer } = require("../uptime-kuma-server");
|
const { UptimeKumaServer } = require("../uptime-kuma-server");
|
||||||
const { UptimeCacheList } = require("../uptime-cache-list");
|
|
||||||
const { makeBadge } = require("badge-maker");
|
const { makeBadge } = require("badge-maker");
|
||||||
const { badgeConstants } = require("../config");
|
const { badgeConstants } = require("../config");
|
||||||
const { Prometheus } = require("../prometheus");
|
const { Prometheus } = require("../prometheus");
|
||||||
const Database = require("../database");
|
const Database = require("../database");
|
||||||
|
const { UptimeCalculator } = require("../uptime-calculator");
|
||||||
|
|
||||||
let router = express.Router();
|
let router = express.Router();
|
||||||
|
|
||||||
|
@ -89,7 +89,7 @@ router.get("/api/push/:pushToken", async (request, response) => {
|
||||||
await R.store(bean);
|
await R.store(bean);
|
||||||
|
|
||||||
io.to(monitor.user_id).emit("heartbeat", bean.toJSON());
|
io.to(monitor.user_id).emit("heartbeat", bean.toJSON());
|
||||||
UptimeCacheList.clearCache(monitor.id);
|
|
||||||
Monitor.sendStats(io, monitor.id, monitor.user_id);
|
Monitor.sendStats(io, monitor.id, monitor.user_id);
|
||||||
new Prometheus(monitor).update(bean, undefined);
|
new Prometheus(monitor).update(bean, undefined);
|
||||||
|
|
||||||
|
@ -206,9 +206,13 @@ router.get("/api/badge/:id/uptime/:duration?", cache("5 minutes"), async (reques
|
||||||
try {
|
try {
|
||||||
const requestedMonitorId = parseInt(request.params.id, 10);
|
const requestedMonitorId = parseInt(request.params.id, 10);
|
||||||
// if no duration is given, set value to 24 (h)
|
// if no duration is given, set value to 24 (h)
|
||||||
const requestedDuration = request.params.duration !== undefined ? parseInt(request.params.duration, 10) : 24;
|
let requestedDuration = request.params.duration !== undefined ? request.params.duration : "24h";
|
||||||
const overrideValue = value && parseFloat(value);
|
const overrideValue = value && parseFloat(value);
|
||||||
|
|
||||||
|
if (requestedDuration === "24") {
|
||||||
|
requestedDuration = "24h";
|
||||||
|
}
|
||||||
|
|
||||||
let publicMonitor = await R.getRow(`
|
let publicMonitor = await R.getRow(`
|
||||||
SELECT monitor_group.monitor_id FROM monitor_group, \`group\`
|
SELECT monitor_group.monitor_id FROM monitor_group, \`group\`
|
||||||
WHERE monitor_group.group_id = \`group\`.id
|
WHERE monitor_group.group_id = \`group\`.id
|
||||||
|
@ -225,10 +229,8 @@ router.get("/api/badge/:id/uptime/:duration?", cache("5 minutes"), async (reques
|
||||||
badgeValues.message = "N/A";
|
badgeValues.message = "N/A";
|
||||||
badgeValues.color = badgeConstants.naColor;
|
badgeValues.color = badgeConstants.naColor;
|
||||||
} else {
|
} else {
|
||||||
const uptime = overrideValue ?? await Monitor.calcUptime(
|
const uptimeCalculator = await UptimeCalculator.getUptimeCalculator(requestedMonitorId);
|
||||||
requestedDuration,
|
const uptime = overrideValue ?? uptimeCalculator.getDataByDuration(requestedDuration).uptime;
|
||||||
requestedMonitorId
|
|
||||||
);
|
|
||||||
|
|
||||||
// limit the displayed uptime percentage to four (two, when displayed as percent) decimal digits
|
// limit the displayed uptime percentage to four (two, when displayed as percent) decimal digits
|
||||||
const cleanUptime = (uptime * 100).toPrecision(4);
|
const cleanUptime = (uptime * 100).toPrecision(4);
|
||||||
|
@ -274,21 +276,19 @@ router.get("/api/badge/:id/ping/:duration?", cache("5 minutes"), async (request,
|
||||||
const requestedMonitorId = parseInt(request.params.id, 10);
|
const requestedMonitorId = parseInt(request.params.id, 10);
|
||||||
|
|
||||||
// Default duration is 24 (h) if not defined in queryParam, limited to 720h (30d)
|
// Default duration is 24 (h) if not defined in queryParam, limited to 720h (30d)
|
||||||
const requestedDuration = Math.min(request.params.duration ? parseInt(request.params.duration, 10) : 24, 720);
|
let requestedDuration = request.params.duration !== undefined ? request.params.duration : "24h";
|
||||||
const overrideValue = value && parseFloat(value);
|
const overrideValue = value && parseFloat(value);
|
||||||
|
|
||||||
|
if (requestedDuration === "24") {
|
||||||
|
requestedDuration = "24h";
|
||||||
|
}
|
||||||
|
|
||||||
const sqlHourOffset = Database.sqlHourOffset();
|
const sqlHourOffset = Database.sqlHourOffset();
|
||||||
|
|
||||||
const publicAvgPing = parseInt(await R.getCell(`
|
// Check if monitor is public
|
||||||
SELECT AVG(ping) FROM monitor_group, \`group\`, heartbeat
|
|
||||||
WHERE monitor_group.group_id = \`group\`.id
|
const uptimeCalculator = await UptimeCalculator.getUptimeCalculator(requestedMonitorId);
|
||||||
AND heartbeat.time > ${sqlHourOffset}
|
const publicAvgPing = uptimeCalculator.getDataByDuration(requestedDuration).avgPing;
|
||||||
AND heartbeat.ping IS NOT NULL
|
|
||||||
AND public = 1
|
|
||||||
AND heartbeat.monitor_id = ?
|
|
||||||
`,
|
|
||||||
[ -requestedDuration, requestedMonitorId ]
|
|
||||||
));
|
|
||||||
|
|
||||||
const badgeValues = { style };
|
const badgeValues = { style };
|
||||||
|
|
||||||
|
|
|
@ -7,6 +7,7 @@ const { R } = require("redbean-node");
|
||||||
const Monitor = require("../model/monitor");
|
const Monitor = require("../model/monitor");
|
||||||
const { badgeConstants } = require("../config");
|
const { badgeConstants } = require("../config");
|
||||||
const { makeBadge } = require("badge-maker");
|
const { makeBadge } = require("badge-maker");
|
||||||
|
const { UptimeCalculator } = require("../uptime-calculator");
|
||||||
|
|
||||||
let router = express.Router();
|
let router = express.Router();
|
||||||
|
|
||||||
|
@ -92,8 +93,8 @@ router.get("/api/status-page/heartbeat/:slug", cache("1 minutes"), async (reques
|
||||||
list = R.convertToBeans("heartbeat", list);
|
list = R.convertToBeans("heartbeat", list);
|
||||||
heartbeatList[monitorID] = list.reverse().map(row => row.toPublicJSON());
|
heartbeatList[monitorID] = list.reverse().map(row => row.toPublicJSON());
|
||||||
|
|
||||||
const type = 24;
|
const uptimeCalculator = await UptimeCalculator.getUptimeCalculator(monitorID);
|
||||||
uptimeList[`${monitorID}_${type}`] = await Monitor.calcUptime(type, monitorID);
|
uptimeList[`${monitorID}_24`] = uptimeCalculator.get24Hour().uptime;
|
||||||
}
|
}
|
||||||
|
|
||||||
response.json({
|
response.json({
|
||||||
|
|
|
@ -84,7 +84,7 @@ log.info("server", "Importing this project modules");
|
||||||
log.debug("server", "Importing Monitor");
|
log.debug("server", "Importing Monitor");
|
||||||
const Monitor = require("./model/monitor");
|
const Monitor = require("./model/monitor");
|
||||||
log.debug("server", "Importing Settings");
|
log.debug("server", "Importing Settings");
|
||||||
const { getSettings, setSettings, setting, initJWTSecret, checkLogin, startUnitTest, FBSD, doubleCheckPassword, startE2eTests,
|
const { getSettings, setSettings, setting, initJWTSecret, checkLogin, FBSD, doubleCheckPassword, startE2eTests,
|
||||||
allowDevAllOrigin
|
allowDevAllOrigin
|
||||||
} = require("./util-server");
|
} = require("./util-server");
|
||||||
|
|
||||||
|
@ -1659,10 +1659,6 @@ let needSetup = false;
|
||||||
startMonitors();
|
startMonitors();
|
||||||
checkVersion.startInterval();
|
checkVersion.startInterval();
|
||||||
|
|
||||||
if (testMode) {
|
|
||||||
startUnitTest();
|
|
||||||
}
|
|
||||||
|
|
||||||
if (e2eTestMode) {
|
if (e2eTestMode) {
|
||||||
startE2eTests();
|
startE2eTests();
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,51 +0,0 @@
|
||||||
const { log } = require("../src/util");
|
|
||||||
class UptimeCacheList {
|
|
||||||
/**
|
|
||||||
* list[monitorID][duration]
|
|
||||||
*/
|
|
||||||
static list = {};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get the uptime for a specific period
|
|
||||||
* @param {number} monitorID ID of monitor to query
|
|
||||||
* @param {number} duration Duration to query
|
|
||||||
* @returns {(number|null)} Uptime for provided duration, if it exists
|
|
||||||
*/
|
|
||||||
static getUptime(monitorID, duration) {
|
|
||||||
if (UptimeCacheList.list[monitorID] && UptimeCacheList.list[monitorID][duration]) {
|
|
||||||
log.debug("UptimeCacheList", "getUptime: " + monitorID + " " + duration);
|
|
||||||
return UptimeCacheList.list[monitorID][duration];
|
|
||||||
} else {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Add uptime for specified monitor
|
|
||||||
* @param {number} monitorID ID of monitor to insert for
|
|
||||||
* @param {number} duration Duration to insert for
|
|
||||||
* @param {number} uptime Uptime to add
|
|
||||||
* @returns {void}
|
|
||||||
*/
|
|
||||||
static addUptime(monitorID, duration, uptime) {
|
|
||||||
log.debug("UptimeCacheList", "addUptime: " + monitorID + " " + duration);
|
|
||||||
if (!UptimeCacheList.list[monitorID]) {
|
|
||||||
UptimeCacheList.list[monitorID] = {};
|
|
||||||
}
|
|
||||||
UptimeCacheList.list[monitorID][duration] = uptime;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Clear cache for specified monitor
|
|
||||||
* @param {number} monitorID ID of monitor to clear
|
|
||||||
* @returns {void}
|
|
||||||
*/
|
|
||||||
static clearCache(monitorID) {
|
|
||||||
log.debug("UptimeCacheList", "clearCache: " + monitorID);
|
|
||||||
delete UptimeCacheList.list[monitorID];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
module.exports = {
|
|
||||||
UptimeCacheList,
|
|
||||||
};
|
|
483
server/uptime-calculator.js
Normal file
483
server/uptime-calculator.js
Normal file
|
@ -0,0 +1,483 @@
|
||||||
|
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 {
|
||||||
|
|
||||||
|
static list = {};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* For testing purposes, we can set the current date to a specific date.
|
||||||
|
* @type {dayjs.Dayjs}
|
||||||
|
*/
|
||||||
|
static currentDate = null;
|
||||||
|
|
||||||
|
monitorID;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Recent 24-hour uptime, each item is a 1-minute interval
|
||||||
|
* Key: {number} DivisionKey
|
||||||
|
*/
|
||||||
|
minutelyUptimeDataList = new LimitQueue(24 * 60);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Daily uptime data,
|
||||||
|
* Key: {number} DailyKey
|
||||||
|
*/
|
||||||
|
dailyUptimeDataList = new LimitQueue(365);
|
||||||
|
|
||||||
|
lastDailyUptimeData = null;
|
||||||
|
lastUptimeData = null;
|
||||||
|
|
||||||
|
lastDailyStatBean = null;
|
||||||
|
lastMinutelyStatBean = null;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param monitorID
|
||||||
|
* @returns {Promise<UptimeCalculator>}
|
||||||
|
*/
|
||||||
|
static async getUptimeCalculator(monitorID) {
|
||||||
|
if (!UptimeCalculator.list[monitorID]) {
|
||||||
|
UptimeCalculator.list[monitorID] = new UptimeCalculator();
|
||||||
|
await UptimeCalculator.list[monitorID].init(monitorID);
|
||||||
|
}
|
||||||
|
return UptimeCalculator.list[monitorID];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param monitorID
|
||||||
|
*/
|
||||||
|
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();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {number} monitorID
|
||||||
|
*/
|
||||||
|
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 key = bean.timestamp;
|
||||||
|
this.minutelyUptimeDataList.push(key, {
|
||||||
|
up: bean.up,
|
||||||
|
down: bean.down,
|
||||||
|
avgPing: bean.ping,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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").unix()),
|
||||||
|
]);
|
||||||
|
|
||||||
|
for (let bean of dailyStatBeans) {
|
||||||
|
let key = bean.timestamp;
|
||||||
|
this.dailyUptimeDataList.push(key, {
|
||||||
|
up: bean.up,
|
||||||
|
down: bean.down,
|
||||||
|
avgPing: bean.ping,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @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();
|
||||||
|
|
||||||
|
// Don't count MAINTENANCE into uptime
|
||||||
|
if (status === MAINTENANCE) {
|
||||||
|
return date;
|
||||||
|
}
|
||||||
|
|
||||||
|
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 dailyKey = this.getDailyKey(divisionKey);
|
||||||
|
|
||||||
|
let minutelyData = this.minutelyUptimeDataList[divisionKey];
|
||||||
|
let dailyData = this.dailyUptimeDataList[dailyKey];
|
||||||
|
|
||||||
|
if (flatStatus === UP) {
|
||||||
|
minutelyData.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;
|
||||||
|
} else {
|
||||||
|
minutelyData.avgPing = (minutelyData.avgPing * (minutelyData.up - 1) + ping) / minutelyData.up;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add avg ping (daily)
|
||||||
|
// The first beat of the day, the ping is the current ping
|
||||||
|
if (minutelyData.up === 1) {
|
||||||
|
dailyData.avgPing = ping;
|
||||||
|
} else {
|
||||||
|
dailyData.avgPing = (dailyData.avgPing * (dailyData.up - 1) + ping) / dailyData.up;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
} else {
|
||||||
|
minutelyData.down += 1;
|
||||||
|
dailyData.down += 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (dailyData !== this.lastDailyUptimeData) {
|
||||||
|
this.lastDailyUptimeData = dailyData;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (minutelyData !== this.lastUptimeData) {
|
||||||
|
this.lastUptimeData = minutelyData;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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.ping;
|
||||||
|
await R.store(dailyStatBean);
|
||||||
|
|
||||||
|
let minutelyStatBean = await this.getMinutelyStatBean(divisionKey);
|
||||||
|
minutelyStatBean.up = minutelyData.up;
|
||||||
|
minutelyStatBean.down = minutelyData.down;
|
||||||
|
minutelyStatBean.ping = minutelyData.ping;
|
||||||
|
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")),
|
||||||
|
]);
|
||||||
|
|
||||||
|
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 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;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {dayjs.Dayjs} date The heartbeat date
|
||||||
|
* @returns {number} Timestamp
|
||||||
|
*/
|
||||||
|
getMinutelyKey(date) {
|
||||||
|
// Convert the current date to the nearest minute (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)) {
|
||||||
|
let last = this.minutelyUptimeDataList.getLastKey();
|
||||||
|
if (last && last > divisionKey) {
|
||||||
|
log.warn("uptime-calc", "The system time has been changed? The uptime data may be inaccurate.");
|
||||||
|
}
|
||||||
|
|
||||||
|
this.minutelyUptimeDataList.push(divisionKey, {
|
||||||
|
up: 0,
|
||||||
|
down: 0,
|
||||||
|
avgPing: 0,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return divisionKey;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Convert timestamp to daily key
|
||||||
|
* @param {number} timestamp Timestamp
|
||||||
|
* @returns {number} Timestamp
|
||||||
|
*/
|
||||||
|
getDailyKey(timestamp) {
|
||||||
|
let date = dayjs.unix(timestamp);
|
||||||
|
|
||||||
|
// Convert the date to the nearest 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]) {
|
||||||
|
let last = this.dailyUptimeDataList.getLastKey();
|
||||||
|
if (last && last > dailyKey) {
|
||||||
|
log.warn("uptime-calc", "The system time has been changed? The uptime data may be inaccurate.");
|
||||||
|
}
|
||||||
|
|
||||||
|
this.dailyUptimeDataList.push(dailyKey, {
|
||||||
|
up: 0,
|
||||||
|
down: 0,
|
||||||
|
avgPing: 0,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return dailyKey;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Flat status to UP or DOWN
|
||||||
|
* @param {number} status
|
||||||
|
* @returns {number}
|
||||||
|
* @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
|
||||||
|
* @param {string} type "day" | "minute"
|
||||||
|
*/
|
||||||
|
getData(num, type = "day") {
|
||||||
|
let key;
|
||||||
|
|
||||||
|
if (type === "day") {
|
||||||
|
key = this.getDailyKey(this.getCurrentDate().unix());
|
||||||
|
} else {
|
||||||
|
if (num > 24 * 60) {
|
||||||
|
throw new Error("The maximum number of minutes is 1440");
|
||||||
|
}
|
||||||
|
key = this.getMinutelyKey(this.getCurrentDate());
|
||||||
|
}
|
||||||
|
|
||||||
|
let total = {
|
||||||
|
up: 0,
|
||||||
|
down: 0,
|
||||||
|
};
|
||||||
|
|
||||||
|
let totalPing = 0;
|
||||||
|
let endTimestamp;
|
||||||
|
|
||||||
|
if (type === "day") {
|
||||||
|
endTimestamp = key - 86400 * (num - 1);
|
||||||
|
} else {
|
||||||
|
endTimestamp = key - 60 * (num - 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Sum up all data in the specified time range
|
||||||
|
while (key >= endTimestamp) {
|
||||||
|
let data;
|
||||||
|
|
||||||
|
if (type === "day") {
|
||||||
|
data = this.dailyUptimeDataList[key];
|
||||||
|
} else {
|
||||||
|
data = this.minutelyUptimeDataList[key];
|
||||||
|
}
|
||||||
|
|
||||||
|
if (data) {
|
||||||
|
total.up += data.up;
|
||||||
|
total.down += data.down;
|
||||||
|
totalPing += data.avgPing * data.up;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Previous day
|
||||||
|
if (type === "day") {
|
||||||
|
key -= 86400;
|
||||||
|
} else {
|
||||||
|
key -= 60;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let uptimeData = new UptimeDataResult();
|
||||||
|
|
||||||
|
if (total.up === 0 && total.down === 0) {
|
||||||
|
if (type === "day" && this.lastDailyUptimeData) {
|
||||||
|
total = this.lastDailyUptimeData;
|
||||||
|
totalPing = total.avgPing * total.up;
|
||||||
|
} else if (type === "minute" && this.lastUptimeData) {
|
||||||
|
total = this.lastUptimeData;
|
||||||
|
totalPing = total.avgPing * total.up;
|
||||||
|
} else {
|
||||||
|
uptimeData.uptime = 0;
|
||||||
|
uptimeData.avgPing = null;
|
||||||
|
return uptimeData;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let avgPing;
|
||||||
|
|
||||||
|
if (total.up === 0) {
|
||||||
|
avgPing = null;
|
||||||
|
} else {
|
||||||
|
avgPing = totalPing / total.up;
|
||||||
|
}
|
||||||
|
|
||||||
|
uptimeData.uptime = total.up / (total.up + total.down);
|
||||||
|
uptimeData.avgPing = avgPing;
|
||||||
|
return uptimeData;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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(7);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @returns {UptimeDataResult} UptimeDataResult
|
||||||
|
*/
|
||||||
|
get30Day() {
|
||||||
|
return this.getData(30);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @returns {UptimeDataResult} UptimeDataResult
|
||||||
|
*/
|
||||||
|
get1Year() {
|
||||||
|
return this.getData(365);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @returns {dayjs.Dayjs} Current date
|
||||||
|
*/
|
||||||
|
getCurrentDate() {
|
||||||
|
return dayjs.utc();
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
class UptimeDataResult {
|
||||||
|
/**
|
||||||
|
* @type {number} Uptime
|
||||||
|
*/
|
||||||
|
uptime;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @type {number} Average ping
|
||||||
|
*/
|
||||||
|
avgPing;
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
UptimeCalculator,
|
||||||
|
UptimeDataResult,
|
||||||
|
};
|
|
@ -847,29 +847,6 @@ exports.doubleCheckPassword = async (socket, currentPassword) => {
|
||||||
return user;
|
return user;
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
|
||||||
* Start Unit tests
|
|
||||||
* @returns {void}
|
|
||||||
*/
|
|
||||||
exports.startUnitTest = async () => {
|
|
||||||
console.log("Starting unit test...");
|
|
||||||
const npm = /^win/.test(process.platform) ? "npm.cmd" : "npm";
|
|
||||||
const child = childProcess.spawn(npm, [ "run", "jest-backend" ]);
|
|
||||||
|
|
||||||
child.stdout.on("data", (data) => {
|
|
||||||
console.log(data.toString());
|
|
||||||
});
|
|
||||||
|
|
||||||
child.stderr.on("data", (data) => {
|
|
||||||
console.log(data.toString());
|
|
||||||
});
|
|
||||||
|
|
||||||
child.on("close", function (code) {
|
|
||||||
console.log("Jest exit code: " + code);
|
|
||||||
process.exit(code);
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Start end-to-end tests
|
* Start end-to-end tests
|
||||||
* @returns {void}
|
* @returns {void}
|
||||||
|
|
79
server/utils/array-with-key.js
Normal file
79
server/utils/array-with-key.js
Normal file
|
@ -0,0 +1,79 @@
|
||||||
|
/**
|
||||||
|
* An object that can be used as an array with a key
|
||||||
|
* Like PHP's array
|
||||||
|
*/
|
||||||
|
class ArrayWithKey {
|
||||||
|
__stack = [];
|
||||||
|
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
constructor() {
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param key
|
||||||
|
* @param value
|
||||||
|
*/
|
||||||
|
push(key, value) {
|
||||||
|
this[key] = value;
|
||||||
|
this.__stack.push(key);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
pop() {
|
||||||
|
let key = this.__stack.pop();
|
||||||
|
let prop = this[key];
|
||||||
|
delete this[key];
|
||||||
|
return prop;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
getLastKey() {
|
||||||
|
if (this.__stack.length === 0) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return this.__stack[this.__stack.length - 1];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
shift() {
|
||||||
|
let key = this.__stack.shift();
|
||||||
|
let value = this[key];
|
||||||
|
delete this[key];
|
||||||
|
return {
|
||||||
|
key,
|
||||||
|
value,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
length() {
|
||||||
|
return this.__stack.length;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the last element
|
||||||
|
* @returns {*|null} The last element, or null if the array is empty
|
||||||
|
*/
|
||||||
|
last() {
|
||||||
|
let key = this.getLastKey();
|
||||||
|
if (key === null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return this[key];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
ArrayWithKey
|
||||||
|
};
|
37
server/utils/limit-queue.js
Normal file
37
server/utils/limit-queue.js
Normal file
|
@ -0,0 +1,37 @@
|
||||||
|
const { ArrayWithKey } = require("./array-with-key");
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Limit Queue
|
||||||
|
* The first element will be removed when the length exceeds the limit
|
||||||
|
*/
|
||||||
|
class LimitQueue extends ArrayWithKey {
|
||||||
|
|
||||||
|
__limit;
|
||||||
|
__onExceed = null;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {number} limit
|
||||||
|
*/
|
||||||
|
constructor(limit) {
|
||||||
|
super();
|
||||||
|
this.__limit = limit;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @inheritDoc
|
||||||
|
*/
|
||||||
|
push(key, value) {
|
||||||
|
super.push(key, value);
|
||||||
|
if (this.length() > this.__limit) {
|
||||||
|
let item = this.shift();
|
||||||
|
if (this.__onExceed) {
|
||||||
|
this.__onExceed(item);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
LimitQueue
|
||||||
|
};
|
|
@ -84,10 +84,12 @@ export default {
|
||||||
},
|
},
|
||||||
|
|
||||||
title() {
|
title() {
|
||||||
|
if (this.type === "1y") {
|
||||||
|
return `1${this.$t("-year")}`;
|
||||||
|
}
|
||||||
if (this.type === "720") {
|
if (this.type === "720") {
|
||||||
return `30${this.$t("-day")}`;
|
return `30${this.$t("-day")}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
return `24${this.$t("-hour")}`;
|
return `24${this.$t("-hour")}`;
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
|
@ -95,6 +95,8 @@
|
||||||
<CountUp :value="avgPing" />
|
<CountUp :value="avgPing" />
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- Uptime (24-hour) -->
|
||||||
<div class="col-12 col-sm col row d-flex align-items-center d-sm-block">
|
<div class="col-12 col-sm col row d-flex align-items-center d-sm-block">
|
||||||
<h4 class="col-4 col-sm-12">{{ $t("Uptime") }}</h4>
|
<h4 class="col-4 col-sm-12">{{ $t("Uptime") }}</h4>
|
||||||
<p class="col-4 col-sm-12 mb-0 mb-sm-2">(24{{ $t("-hour") }})</p>
|
<p class="col-4 col-sm-12 mb-0 mb-sm-2">(24{{ $t("-hour") }})</p>
|
||||||
|
@ -102,6 +104,8 @@
|
||||||
<Uptime :monitor="monitor" type="24" />
|
<Uptime :monitor="monitor" type="24" />
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- Uptime (30-day) -->
|
||||||
<div class="col-12 col-sm col row d-flex align-items-center d-sm-block">
|
<div class="col-12 col-sm col row d-flex align-items-center d-sm-block">
|
||||||
<h4 class="col-4 col-sm-12">{{ $t("Uptime") }}</h4>
|
<h4 class="col-4 col-sm-12">{{ $t("Uptime") }}</h4>
|
||||||
<p class="col-4 col-sm-12 mb-0 mb-sm-2">(30{{ $t("-day") }})</p>
|
<p class="col-4 col-sm-12 mb-0 mb-sm-2">(30{{ $t("-day") }})</p>
|
||||||
|
@ -110,6 +114,15 @@
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- Uptime (1-year) -->
|
||||||
|
<div class="col-12 col-sm col row d-flex align-items-center d-sm-block">
|
||||||
|
<h4 class="col-4 col-sm-12">{{ $t("Uptime") }}</h4>
|
||||||
|
<p class="col-4 col-sm-12 mb-0 mb-sm-2">(1{{ $t("-year") }})</p>
|
||||||
|
<span class="col-4 col-sm-12 num">
|
||||||
|
<Uptime :monitor="monitor" type="1y" />
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div v-if="tlsInfo" class="col-12 col-sm col row d-flex align-items-center d-sm-block">
|
<div v-if="tlsInfo" class="col-12 col-sm col row d-flex align-items-center d-sm-block">
|
||||||
<h4 class="col-4 col-sm-12">{{ $t("Cert Exp.") }}</h4>
|
<h4 class="col-4 col-sm-12">{{ $t("Cert Exp.") }}</h4>
|
||||||
<p class="col-4 col-sm-12 mb-0 mb-sm-2">(<Datetime :value="tlsInfo.certInfo.validTo" date-only />)</p>
|
<p class="col-4 col-sm-12 mb-0 mb-sm-2">(<Datetime :value="tlsInfo.certInfo.validTo" date-only />)</p>
|
||||||
|
|
20
test/backend-test-entry.js
Normal file
20
test/backend-test-entry.js
Normal file
|
@ -0,0 +1,20 @@
|
||||||
|
// Check Node.js version
|
||||||
|
const semver = require("semver");
|
||||||
|
const childProcess = require("child_process");
|
||||||
|
|
||||||
|
const nodeVersion = process.versions.node;
|
||||||
|
console.log("Node.js version: " + nodeVersion);
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
// Node.js version >= 18
|
||||||
|
if (semver.satisfies(nodeVersion, ">= 18")) {
|
||||||
|
console.log("Use the native test runner: `node --test`");
|
||||||
|
childProcess.execSync("npm run test-backend:18", { stdio: "inherit" });
|
||||||
|
} else {
|
||||||
|
// 14 - 16 here
|
||||||
|
console.log("Use `test` package: `node--test`")
|
||||||
|
childProcess.execSync("npm run test-backend:14", { stdio: "inherit" });
|
||||||
|
}
|
||||||
|
|
||||||
|
|
423
test/backend-test/test-uptime-calculator.js
Normal file
423
test/backend-test/test-uptime-calculator.js
Normal file
|
@ -0,0 +1,423 @@
|
||||||
|
const semver = require("semver");
|
||||||
|
let test;
|
||||||
|
const nodeVersion = process.versions.node;
|
||||||
|
// Node.js version >= 18
|
||||||
|
if (semver.satisfies(nodeVersion, ">= 18")) {
|
||||||
|
test = require("node:test");
|
||||||
|
} else {
|
||||||
|
test = require("test");
|
||||||
|
}
|
||||||
|
|
||||||
|
const assert = require("node:assert");
|
||||||
|
const { UptimeCalculator } = require("../../server/uptime-calculator");
|
||||||
|
const dayjs = require("dayjs");
|
||||||
|
const { UP, DOWN, PENDING, MAINTENANCE } = require("../../src/util");
|
||||||
|
dayjs.extend(require("dayjs/plugin/utc"));
|
||||||
|
dayjs.extend(require("../../server/modules/dayjs/plugin/timezone"));
|
||||||
|
dayjs.extend(require("dayjs/plugin/customParseFormat"));
|
||||||
|
|
||||||
|
test("Test Uptime Calculator - custom date", async (t) => {
|
||||||
|
let c1 = new UptimeCalculator();
|
||||||
|
|
||||||
|
// Test custom date
|
||||||
|
UptimeCalculator.currentDate = dayjs.utc("2021-01-01T00:00:00.000Z");
|
||||||
|
assert.strictEqual(c1.getCurrentDate().unix(), dayjs.utc("2021-01-01T00:00:00.000Z").unix());
|
||||||
|
});
|
||||||
|
|
||||||
|
test("Test update - UP", async (t) => {
|
||||||
|
UptimeCalculator.currentDate = dayjs.utc("2023-08-12 20:46:59");
|
||||||
|
let c2 = new UptimeCalculator();
|
||||||
|
let date = await c2.update(UP);
|
||||||
|
assert.strictEqual(date.unix(), dayjs.utc("2023-08-12 20:46:59").unix());
|
||||||
|
});
|
||||||
|
|
||||||
|
test("Test update - MAINTENANCE", async (t) => {
|
||||||
|
UptimeCalculator.currentDate = dayjs.utc("2023-08-12 20:47:20");
|
||||||
|
let c2 = new UptimeCalculator();
|
||||||
|
let date = await c2.update(MAINTENANCE);
|
||||||
|
assert.strictEqual(date.unix(), dayjs.utc("2023-08-12 20:47:20").unix());
|
||||||
|
});
|
||||||
|
|
||||||
|
test("Test update - DOWN", async (t) => {
|
||||||
|
UptimeCalculator.currentDate = dayjs.utc("2023-08-12 20:47:20");
|
||||||
|
let c2 = new UptimeCalculator();
|
||||||
|
let date = await c2.update(DOWN);
|
||||||
|
assert.strictEqual(date.unix(), dayjs.utc("2023-08-12 20:47:20").unix());
|
||||||
|
});
|
||||||
|
|
||||||
|
test("Test update - PENDING", async (t) => {
|
||||||
|
UptimeCalculator.currentDate = dayjs.utc("2023-08-12 20:47:20");
|
||||||
|
let c2 = new UptimeCalculator();
|
||||||
|
let date = await c2.update(PENDING);
|
||||||
|
assert.strictEqual(date.unix(), dayjs.utc("2023-08-12 20:47:20").unix());
|
||||||
|
});
|
||||||
|
|
||||||
|
test("Test flatStatus", async (t) => {
|
||||||
|
let c2 = new UptimeCalculator();
|
||||||
|
assert.strictEqual(c2.flatStatus(UP), UP);
|
||||||
|
//assert.strictEqual(c2.flatStatus(MAINTENANCE), UP);
|
||||||
|
assert.strictEqual(c2.flatStatus(DOWN), DOWN);
|
||||||
|
assert.strictEqual(c2.flatStatus(PENDING), DOWN);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("Test getMinutelyKey", async (t) => {
|
||||||
|
let c2 = new UptimeCalculator();
|
||||||
|
let divisionKey = c2.getMinutelyKey(dayjs.utc("2023-08-12 20:46:00"));
|
||||||
|
assert.strictEqual(divisionKey, dayjs.utc("2023-08-12 20:46:00").unix());
|
||||||
|
|
||||||
|
// Edge case 1
|
||||||
|
c2 = new UptimeCalculator();
|
||||||
|
divisionKey = c2.getMinutelyKey(dayjs.utc("2023-08-12 20:46:01"));
|
||||||
|
assert.strictEqual(divisionKey, dayjs.utc("2023-08-12 20:46:00").unix());
|
||||||
|
|
||||||
|
// Edge case 2
|
||||||
|
c2 = new UptimeCalculator();
|
||||||
|
divisionKey = c2.getMinutelyKey(dayjs.utc("2023-08-12 20:46:59"));
|
||||||
|
assert.strictEqual(divisionKey, dayjs.utc("2023-08-12 20:46:00").unix());
|
||||||
|
});
|
||||||
|
|
||||||
|
test("Test getDailyKey", async (t) => {
|
||||||
|
let c2 = new UptimeCalculator();
|
||||||
|
let dailyKey = c2.getDailyKey(dayjs.utc("2023-08-12 20:46:00").unix());
|
||||||
|
assert.strictEqual(dailyKey, dayjs.utc("2023-08-12").unix());
|
||||||
|
|
||||||
|
c2 = new UptimeCalculator();
|
||||||
|
dailyKey = c2.getDailyKey(dayjs.utc("2023-08-12 23:45:30").unix());
|
||||||
|
assert.strictEqual(dailyKey, dayjs.utc("2023-08-12").unix());
|
||||||
|
|
||||||
|
// Edge case 1
|
||||||
|
c2 = new UptimeCalculator();
|
||||||
|
dailyKey = c2.getDailyKey(dayjs.utc("2023-08-12 23:59:59").unix());
|
||||||
|
assert.strictEqual(dailyKey, dayjs.utc("2023-08-12").unix());
|
||||||
|
|
||||||
|
// Edge case 2
|
||||||
|
c2 = new UptimeCalculator();
|
||||||
|
dailyKey = c2.getDailyKey(dayjs.utc("2023-08-12 00:00:00").unix());
|
||||||
|
assert.strictEqual(dailyKey, dayjs.utc("2023-08-12").unix());
|
||||||
|
});
|
||||||
|
|
||||||
|
test("Test lastDailyUptimeData", async (t) => {
|
||||||
|
let c2 = new UptimeCalculator();
|
||||||
|
await c2.update(UP);
|
||||||
|
assert.strictEqual(c2.lastDailyUptimeData.up, 1);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("Test get24Hour Uptime and Avg Ping", async (t) => {
|
||||||
|
UptimeCalculator.currentDate = dayjs.utc("2023-08-12 20:46:59");
|
||||||
|
|
||||||
|
// No data
|
||||||
|
let c2 = new UptimeCalculator();
|
||||||
|
let data = c2.get24Hour();
|
||||||
|
assert.strictEqual(data.uptime, 0);
|
||||||
|
assert.strictEqual(data.avgPing, null);
|
||||||
|
|
||||||
|
// 1 Up
|
||||||
|
c2 = new UptimeCalculator();
|
||||||
|
await c2.update(UP, 100);
|
||||||
|
let uptime = c2.get24Hour().uptime;
|
||||||
|
assert.strictEqual(uptime, 1);
|
||||||
|
assert.strictEqual(c2.get24Hour().avgPing, 100);
|
||||||
|
|
||||||
|
// 2 Up
|
||||||
|
c2 = new UptimeCalculator();
|
||||||
|
await c2.update(UP, 100);
|
||||||
|
await c2.update(UP, 200);
|
||||||
|
uptime = c2.get24Hour().uptime;
|
||||||
|
assert.strictEqual(uptime, 1);
|
||||||
|
assert.strictEqual(c2.get24Hour().avgPing, 150);
|
||||||
|
|
||||||
|
// 3 Up
|
||||||
|
c2 = new UptimeCalculator();
|
||||||
|
await c2.update(UP, 0);
|
||||||
|
await c2.update(UP, 100);
|
||||||
|
await c2.update(UP, 400);
|
||||||
|
uptime = c2.get24Hour().uptime;
|
||||||
|
assert.strictEqual(uptime, 1);
|
||||||
|
assert.strictEqual(c2.get24Hour().avgPing, 166.66666666666666);
|
||||||
|
|
||||||
|
// 1 MAINTENANCE
|
||||||
|
c2 = new UptimeCalculator();
|
||||||
|
await c2.update(MAINTENANCE);
|
||||||
|
uptime = c2.get24Hour().uptime;
|
||||||
|
assert.strictEqual(uptime, 0);
|
||||||
|
assert.strictEqual(c2.get24Hour().avgPing, null);
|
||||||
|
|
||||||
|
// 1 PENDING
|
||||||
|
c2 = new UptimeCalculator();
|
||||||
|
await c2.update(PENDING);
|
||||||
|
uptime = c2.get24Hour().uptime;
|
||||||
|
assert.strictEqual(uptime, 0);
|
||||||
|
assert.strictEqual(c2.get24Hour().avgPing, null);
|
||||||
|
|
||||||
|
// 1 DOWN
|
||||||
|
c2 = new UptimeCalculator();
|
||||||
|
await c2.update(DOWN);
|
||||||
|
uptime = c2.get24Hour().uptime;
|
||||||
|
assert.strictEqual(uptime, 0);
|
||||||
|
assert.strictEqual(c2.get24Hour().avgPing, null);
|
||||||
|
|
||||||
|
// 2 DOWN
|
||||||
|
c2 = new UptimeCalculator();
|
||||||
|
await c2.update(DOWN);
|
||||||
|
await c2.update(DOWN);
|
||||||
|
uptime = c2.get24Hour().uptime;
|
||||||
|
assert.strictEqual(uptime, 0);
|
||||||
|
assert.strictEqual(c2.get24Hour().avgPing, null);
|
||||||
|
|
||||||
|
// 1 DOWN, 1 UP
|
||||||
|
c2 = new UptimeCalculator();
|
||||||
|
await c2.update(DOWN);
|
||||||
|
await c2.update(UP, 0.5);
|
||||||
|
uptime = c2.get24Hour().uptime;
|
||||||
|
assert.strictEqual(uptime, 0.5);
|
||||||
|
assert.strictEqual(c2.get24Hour().avgPing, 0.5);
|
||||||
|
|
||||||
|
// 1 UP, 1 DOWN
|
||||||
|
c2 = new UptimeCalculator();
|
||||||
|
await c2.update(UP, 123);
|
||||||
|
await c2.update(DOWN);
|
||||||
|
uptime = c2.get24Hour().uptime;
|
||||||
|
assert.strictEqual(uptime, 0.5);
|
||||||
|
assert.strictEqual(c2.get24Hour().avgPing, 123);
|
||||||
|
|
||||||
|
// Add 24 hours
|
||||||
|
c2 = new UptimeCalculator();
|
||||||
|
await c2.update(UP, 0);
|
||||||
|
await c2.update(UP, 0);
|
||||||
|
await c2.update(UP, 0);
|
||||||
|
await c2.update(UP, 1);
|
||||||
|
await c2.update(DOWN);
|
||||||
|
uptime = c2.get24Hour().uptime;
|
||||||
|
assert.strictEqual(uptime, 0.8);
|
||||||
|
assert.strictEqual(c2.get24Hour().avgPing, 0.25);
|
||||||
|
|
||||||
|
UptimeCalculator.currentDate = UptimeCalculator.currentDate.add(24, "hour");
|
||||||
|
|
||||||
|
// After 24 hours, even if there is no data, the uptime should be still 80%
|
||||||
|
uptime = c2.get24Hour().uptime;
|
||||||
|
assert.strictEqual(uptime, 0.8);
|
||||||
|
assert.strictEqual(c2.get24Hour().avgPing, 0.25);
|
||||||
|
|
||||||
|
// Add more 24 hours (48 hours)
|
||||||
|
UptimeCalculator.currentDate = UptimeCalculator.currentDate.add(24, "hour");
|
||||||
|
|
||||||
|
// After 48 hours, even if there is no data, the uptime should be still 80%
|
||||||
|
uptime = c2.get24Hour().uptime;
|
||||||
|
assert.strictEqual(uptime, 0.8);
|
||||||
|
assert.strictEqual(c2.get24Hour().avgPing, 0.25);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("Test get7DayUptime", async (t) => {
|
||||||
|
UptimeCalculator.currentDate = dayjs.utc("2023-08-12 20:46:59");
|
||||||
|
|
||||||
|
// No data
|
||||||
|
let c2 = new UptimeCalculator();
|
||||||
|
let uptime = c2.get7Day().uptime;
|
||||||
|
assert.strictEqual(uptime, 0);
|
||||||
|
|
||||||
|
// 1 Up
|
||||||
|
c2 = new UptimeCalculator();
|
||||||
|
await c2.update(UP);
|
||||||
|
uptime = c2.get7Day().uptime;
|
||||||
|
assert.strictEqual(uptime, 1);
|
||||||
|
|
||||||
|
// 2 Up
|
||||||
|
c2 = new UptimeCalculator();
|
||||||
|
await c2.update(UP);
|
||||||
|
await c2.update(UP);
|
||||||
|
uptime = c2.get7Day().uptime;
|
||||||
|
assert.strictEqual(uptime, 1);
|
||||||
|
|
||||||
|
// 3 Up
|
||||||
|
c2 = new UptimeCalculator();
|
||||||
|
await c2.update(UP);
|
||||||
|
await c2.update(UP);
|
||||||
|
await c2.update(UP);
|
||||||
|
uptime = c2.get7Day().uptime;
|
||||||
|
assert.strictEqual(uptime, 1);
|
||||||
|
|
||||||
|
// 1 MAINTENANCE
|
||||||
|
c2 = new UptimeCalculator();
|
||||||
|
await c2.update(MAINTENANCE);
|
||||||
|
uptime = c2.get7Day().uptime;
|
||||||
|
assert.strictEqual(uptime, 0);
|
||||||
|
|
||||||
|
// 1 PENDING
|
||||||
|
c2 = new UptimeCalculator();
|
||||||
|
await c2.update(PENDING);
|
||||||
|
uptime = c2.get7Day().uptime;
|
||||||
|
assert.strictEqual(uptime, 0);
|
||||||
|
|
||||||
|
// 1 DOWN
|
||||||
|
c2 = new UptimeCalculator();
|
||||||
|
await c2.update(DOWN);
|
||||||
|
uptime = c2.get7Day().uptime;
|
||||||
|
assert.strictEqual(uptime, 0);
|
||||||
|
|
||||||
|
// 2 DOWN
|
||||||
|
c2 = new UptimeCalculator();
|
||||||
|
await c2.update(DOWN);
|
||||||
|
await c2.update(DOWN);
|
||||||
|
uptime = c2.get7Day().uptime;
|
||||||
|
assert.strictEqual(uptime, 0);
|
||||||
|
|
||||||
|
// 1 DOWN, 1 UP
|
||||||
|
c2 = new UptimeCalculator();
|
||||||
|
await c2.update(DOWN);
|
||||||
|
await c2.update(UP);
|
||||||
|
uptime = c2.get7Day().uptime;
|
||||||
|
assert.strictEqual(uptime, 0.5);
|
||||||
|
|
||||||
|
// 1 UP, 1 DOWN
|
||||||
|
c2 = new UptimeCalculator();
|
||||||
|
await c2.update(UP);
|
||||||
|
await c2.update(DOWN);
|
||||||
|
uptime = c2.get7Day().uptime;
|
||||||
|
assert.strictEqual(uptime, 0.5);
|
||||||
|
|
||||||
|
// Add 7 days
|
||||||
|
c2 = new UptimeCalculator();
|
||||||
|
await c2.update(UP);
|
||||||
|
await c2.update(UP);
|
||||||
|
await c2.update(UP);
|
||||||
|
await c2.update(UP);
|
||||||
|
await c2.update(DOWN);
|
||||||
|
uptime = c2.get7Day().uptime;
|
||||||
|
assert.strictEqual(uptime, 0.8);
|
||||||
|
UptimeCalculator.currentDate = UptimeCalculator.currentDate.add(7, "day");
|
||||||
|
|
||||||
|
// After 7 days, even if there is no data, the uptime should be still 80%
|
||||||
|
uptime = c2.get7Day().uptime;
|
||||||
|
assert.strictEqual(uptime, 0.8);
|
||||||
|
|
||||||
|
});
|
||||||
|
|
||||||
|
test("Test get30DayUptime (1 check per day)", async (t) => {
|
||||||
|
UptimeCalculator.currentDate = dayjs.utc("2023-08-12 20:46:59");
|
||||||
|
|
||||||
|
let c2 = new UptimeCalculator();
|
||||||
|
let uptime = c2.get30Day().uptime;
|
||||||
|
assert.strictEqual(uptime, 0);
|
||||||
|
|
||||||
|
let up = 0;
|
||||||
|
let down = 0;
|
||||||
|
let flip = true;
|
||||||
|
for (let i = 0; i < 30; i++) {
|
||||||
|
UptimeCalculator.currentDate = UptimeCalculator.currentDate.add(1, "day");
|
||||||
|
|
||||||
|
if (flip) {
|
||||||
|
await c2.update(UP);
|
||||||
|
up++;
|
||||||
|
} else {
|
||||||
|
await c2.update(DOWN);
|
||||||
|
down++;
|
||||||
|
}
|
||||||
|
|
||||||
|
uptime = c2.get30Day().uptime;
|
||||||
|
assert.strictEqual(uptime, up / (up + down));
|
||||||
|
|
||||||
|
flip = !flip;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Last 7 days
|
||||||
|
// Down, Up, Down, Up, Down, Up, Down
|
||||||
|
// So 3 UP
|
||||||
|
assert.strictEqual(c2.get7Day().uptime, 3 / 7);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("Test get1YearUptime (1 check per day)", async (t) => {
|
||||||
|
UptimeCalculator.currentDate = dayjs.utc("2023-08-12 20:46:59");
|
||||||
|
|
||||||
|
let c2 = new UptimeCalculator();
|
||||||
|
let uptime = c2.get1Year().uptime;
|
||||||
|
assert.strictEqual(uptime, 0);
|
||||||
|
|
||||||
|
let flip = true;
|
||||||
|
for (let i = 0; i < 365; i++) {
|
||||||
|
UptimeCalculator.currentDate = UptimeCalculator.currentDate.add(1, "day");
|
||||||
|
|
||||||
|
if (flip) {
|
||||||
|
await c2.update(UP);
|
||||||
|
} else {
|
||||||
|
await c2.update(DOWN);
|
||||||
|
}
|
||||||
|
|
||||||
|
uptime = c2.get30Day().time;
|
||||||
|
flip = !flip;
|
||||||
|
}
|
||||||
|
|
||||||
|
assert.strictEqual(c2.get1Year().uptime, 183 / 365);
|
||||||
|
assert.strictEqual(c2.get30Day().uptime, 15 / 30);
|
||||||
|
assert.strictEqual(c2.get7Day().uptime, 4 / 7);
|
||||||
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Code from here: https://stackoverflow.com/a/64550489/1097815
|
||||||
|
*/
|
||||||
|
function memoryUsage() {
|
||||||
|
const formatMemoryUsage = (data) => `${Math.round(data / 1024 / 1024 * 100) / 100} MB`;
|
||||||
|
const memoryData = process.memoryUsage();
|
||||||
|
|
||||||
|
const memoryUsage = {
|
||||||
|
rss: `${formatMemoryUsage(memoryData.rss)} -> Resident Set Size - total memory allocated for the process execution`,
|
||||||
|
heapTotal: `${formatMemoryUsage(memoryData.heapTotal)} -> total size of the allocated heap`,
|
||||||
|
heapUsed: `${formatMemoryUsage(memoryData.heapUsed)} -> actual memory used during the execution`,
|
||||||
|
external: `${formatMemoryUsage(memoryData.external)} -> V8 external memory`,
|
||||||
|
};
|
||||||
|
return memoryUsage;
|
||||||
|
}
|
||||||
|
|
||||||
|
test("Worst case", async (t) => {
|
||||||
|
console.log("Memory usage before preparation", memoryUsage());
|
||||||
|
|
||||||
|
let c = new UptimeCalculator();
|
||||||
|
let up = 0;
|
||||||
|
let down = 0;
|
||||||
|
let interval = 20;
|
||||||
|
|
||||||
|
await t.test("Prepare data", async () => {
|
||||||
|
UptimeCalculator.currentDate = dayjs.utc("2023-08-12 20:46:59");
|
||||||
|
|
||||||
|
// Since 2023-08-12 will be out of 365 range, it starts from 2023-08-13 actually
|
||||||
|
let actualStartDate = dayjs.utc("2023-08-13 00:00:00").unix();
|
||||||
|
|
||||||
|
// Simulate 1s interval for a year
|
||||||
|
for (let i = 0; i < 365 * 24 * 60 * 60; i += interval) {
|
||||||
|
UptimeCalculator.currentDate = UptimeCalculator.currentDate.add(interval, "second");
|
||||||
|
|
||||||
|
//Randomly UP, DOWN, MAINTENANCE, PENDING
|
||||||
|
let rand = Math.random();
|
||||||
|
if (rand < 0.25) {
|
||||||
|
c.update(UP);
|
||||||
|
if (UptimeCalculator.currentDate.unix() > actualStartDate) {
|
||||||
|
up++;
|
||||||
|
}
|
||||||
|
} else if (rand < 0.5) {
|
||||||
|
c.update(DOWN);
|
||||||
|
if (UptimeCalculator.currentDate.unix() > actualStartDate) {
|
||||||
|
down++;
|
||||||
|
}
|
||||||
|
} else if (rand < 0.75) {
|
||||||
|
c.update(MAINTENANCE);
|
||||||
|
if (UptimeCalculator.currentDate.unix() > actualStartDate) {
|
||||||
|
//up++;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
c.update(PENDING);
|
||||||
|
if (UptimeCalculator.currentDate.unix() > actualStartDate) {
|
||||||
|
down++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
console.log("Final Date: ", UptimeCalculator.currentDate.format("YYYY-MM-DD HH:mm:ss"));
|
||||||
|
console.log("Memory usage before preparation", memoryUsage());
|
||||||
|
|
||||||
|
assert.strictEqual(c.minutelyUptimeDataList.length(), 1440);
|
||||||
|
assert.strictEqual(c.dailyUptimeDataList.length(), 365);
|
||||||
|
});
|
||||||
|
|
||||||
|
await t.test("get1YearUptime()", async () => {
|
||||||
|
assert.strictEqual(c.get1Year().uptime, up / (up + down));
|
||||||
|
});
|
||||||
|
|
||||||
|
});
|
Loading…
Reference in a new issue