diff --git a/README.md b/README.md new file mode 100644 index 00000000..34948fe5 --- /dev/null +++ b/README.md @@ -0,0 +1,29 @@ +# Uptime Kuma + +# Features + +* Monitoring uptime for HTTP(s) / TCP / Ping. +* Fancy, Reactive, Fast UI/UX. +* Notifications via Webhook, Telegram, Discord and email (SMTP). +* 20 seconds interval. + +# How to Use + +npm + +Docker + +One-click Deploy to DigitalOcean + +# Motivation + +* I was looking for a self-hosted monitoring tool like "Uptime Robot", but it is hard to find a suitable one. +* Want to build a fancy UI. +* Learn Vue 3 and vite.js. +* Show the power of Bootstrap 5. +* Try to use WebSocket with SPA instead of REST API. +* Deploy my first Docker image to Docker Hub. + + +If you love this project, please consider giving me a ⭐. + diff --git a/server/model/monitor.js b/server/model/monitor.js index 8346040b..3b19d2ff 100644 --- a/server/model/monitor.js +++ b/server/model/monitor.js @@ -5,7 +5,7 @@ var timezone = require('dayjs/plugin/timezone') dayjs.extend(utc) dayjs.extend(timezone) const axios = require("axios"); -const {tcping} = require("../util-server"); +const {tcping, ping} = require("../util-server"); const {R} = require("redbean-node"); const {BeanModel} = require("redbean-node/dist/bean-model"); @@ -67,6 +67,10 @@ class Monitor extends BeanModel { } else if (this.type === "port") { bean.ping = await tcping(this.hostname, this.port); bean.status = 1; + + } else if (this.type === "ping") { + bean.ping = await ping(this.hostname); + bean.status = 1; } } catch (error) { @@ -125,19 +129,49 @@ class Monitor extends BeanModel { * @param duration : int Hours */ static async sendUptime(duration, io, monitorID, userID) { - let downtime = parseInt(await R.getCell(` - SELECT SUM(duration) + let sec = duration * 3600; + + let downtimeList = await R.getAll(` + SELECT duration, time FROM heartbeat WHERE time > DATE('now', ? || ' hours') AND status = 0 AND monitor_id = ? `, [ -duration, monitorID - ])); + ]); + + let downtime = 0; + + for (let row of downtimeList) { + let value = parseInt(row.duration) + let time = row.time + + // Handle if heartbeat duration longer than the target duration + // e.g. Heartbeat duration = 28hrs, but target duration = 24hrs + if (value <= sec) { + downtime += value; + } else { + console.log("Now: " + dayjs.utc()); + console.log("Time: " + dayjs(time)) + + let trim = dayjs.utc().diff(dayjs(time), 'second'); + console.log("trim: " + trim); + value = sec - trim; + + if (value < 0) { + value = 0; + } + downtime += value; + } + } - let sec = duration * 3600; let uptime = (sec - downtime) / sec; + if (uptime < 0) { + uptime = 0; + } + io.to(userID).emit("uptime", monitorID, duration, uptime); } } diff --git a/server/ping-lite.js b/server/ping-lite.js new file mode 100644 index 00000000..e290d887 --- /dev/null +++ b/server/ping-lite.js @@ -0,0 +1,118 @@ +// https://github.com/ben-bradley/ping-lite/blob/master/ping-lite.js +// Fixed on Windows + +var spawn = require('child_process').spawn, + events = require('events'), + fs = require('fs'), + WIN = /^win/.test(process.platform), + LIN = /^linux/.test(process.platform), + MAC = /^darwin/.test(process.platform); + +module.exports = Ping; + +function Ping(host, options) { + if (!host) + throw new Error('You must specify a host to ping!'); + + this._host = host; + this._options = options = (options || {}); + + events.EventEmitter.call(this); + + if (WIN) { + this._bin = 'c:/windows/system32/ping.exe'; + this._args = (options.args) ? options.args : [ '-n', '1', '-w', '5000', host ]; + this._regmatch = /[><=]([0-9.]+?)ms/; + } + else if (LIN) { + this._bin = '/bin/ping'; + this._args = (options.args) ? options.args : [ '-n', '-w', '2', '-c', '1', host ]; + this._regmatch = /=([0-9.]+?) ms/; // need to verify this + } + else if (MAC) { + this._bin = '/sbin/ping'; + this._args = (options.args) ? options.args : [ '-n', '-t', '2', '-c', '1', host ]; + this._regmatch = /=([0-9.]+?) ms/; + } + else { + throw new Error('Could not detect your ping binary.'); + } + + if (!fs.existsSync(this._bin)) + throw new Error('Could not detect '+this._bin+' on your system'); + + this._i = 0; + + return this; +} + +Ping.prototype.__proto__ = events.EventEmitter.prototype; + +// SEND A PING +// =========== +Ping.prototype.send = function(callback) { + var self = this; + callback = callback || function(err, ms) { + if (err) return self.emit('error', err); + else return self.emit('result', ms); + }; + + var _ended, _exited, _errored; + + this._ping = spawn(this._bin, this._args); // spawn the binary + + this._ping.on('error', function(err) { // handle binary errors + _errored = true; + callback(err); + }); + + this._ping.stdout.on('data', function(data) { // log stdout + this._stdout = (this._stdout || '') + data; + }); + + this._ping.stdout.on('end', function() { + _ended = true; + if (_exited && !_errored) onEnd.call(self._ping); + }); + + this._ping.stderr.on('data', function(data) { // log stderr + this._stderr = (this._stderr || '') + data; + }); + + this._ping.on('exit', function(code) { // handle complete + _exited = true; + if (_ended && !_errored) onEnd.call(self._ping); + }); + + function onEnd() { + var stdout = this.stdout._stdout, + stderr = this.stderr._stderr, + ms; + + if (stderr) + return callback(new Error(stderr)); + else if (!stdout) + return callback(new Error('No stdout detected')); + + ms = stdout.match(self._regmatch); // parse out the ##ms response + ms = (ms && ms[1]) ? Number(ms[1]) : ms; + + callback(null, ms); + } +}; + +// CALL Ping#send(callback) ON A TIMER +// =================================== +Ping.prototype.start = function(callback) { + var self = this; + this._i = setInterval(function() { + self.send(callback); + }, (self._options.interval || 5000)); + self.send(callback); +}; + +// STOP SENDING PINGS +// ================== +Ping.prototype.stop = function() { + clearInterval(this._i); +}; diff --git a/server/util-server.js b/server/util-server.js index 8e03284a..ca8e9f8e 100644 --- a/server/util-server.js +++ b/server/util-server.js @@ -1,4 +1,5 @@ const tcpp = require('tcp-ping'); +const Ping = require("./ping-lite"); exports.tcping = function (hostname, port) { return new Promise((resolve, reject) => { @@ -20,3 +21,19 @@ exports.tcping = function (hostname, port) { }); }); } + +exports.ping = function (hostname) { + return new Promise((resolve, reject) => { + const ping = new Ping(hostname); + + ping.send(function(err, ms) { + if (err) { + reject(err) + } else if (ms === null) { + reject(new Error("timeout")) + } else { + resolve(Math.round(ms)) + } + }); + }); +} diff --git a/src/components/CountUp.vue b/src/components/CountUp.vue index 5cfc8d4c..b929e52e 100644 --- a/src/components/CountUp.vue +++ b/src/components/CountUp.vue @@ -48,7 +48,7 @@ export default { let frames = 12; let step = Math.floor(diff / frames); - if (! this.isNum || (diff > 0 && step < 1) || (diff < 0 && step > 1) || diff === 0) { + if (isNaN(step) || ! this.isNum || (diff > 0 && step < 1) || (diff < 0 && step > 1) || diff === 0) { // Lazy to NOT this condition, hahaha. } else { for (let i = 1; i < frames; i++) { diff --git a/src/components/Uptime.vue b/src/components/Uptime.vue index f752b8f3..ad8114fc 100644 --- a/src/components/Uptime.vue +++ b/src/components/Uptime.vue @@ -18,7 +18,7 @@ export default { let key = this.monitor.id + "_" + this.type; - if (this.$root.uptimeList[key]) { + if (this.$root.uptimeList[key] !== undefined) { return Math.round(this.$root.uptimeList[key] * 10000) / 100 + "%"; } else { return "N/A" diff --git a/src/pages/DashboardHome.vue b/src/pages/DashboardHome.vue index 396afdd0..27c8adf3 100644 --- a/src/pages/DashboardHome.vue +++ b/src/pages/DashboardHome.vue @@ -5,60 +5,6 @@
- -
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-

Up

{{ stats.up }} @@ -76,32 +22,43 @@ {{ stats.pause }}
-
- -
-
-

Latest Incident

- -
- MySQL was down. +
+
+

Uptime

+

(24-hour)

+
- -
- No issues was found. +
+

Uptime

+

(30-day)

+
-
-
- -

Overall Uptime

- -
-
100.00% (24 hours)
-
100.00% (7 days)
-
100.00% (30 days)
-
+
-
+
+ + + + + + + + + + + + + + + + + + + + + +
NameStatusDateTimeMessage
{{ beat.name }}{{ beat.msg }}
No important events
@@ -109,7 +66,10 @@ @@ -151,5 +130,18 @@ export default { font-size: 30px; color: $primary; font-weight: bold; + display: block; +} + +.shadow-box { + padding: 20px; +} + +table { + font-size: 14px; + + tr { + transition: all ease-in-out 0.2ms; + } } diff --git a/src/pages/Details.vue b/src/pages/Details.vue index 21bdc611..764a3f55 100644 --- a/src/pages/Details.vue +++ b/src/pages/Details.vue @@ -2,7 +2,8 @@

{{ monitor.name }}

{{ monitor.url }} - {{ monitor.hostname }}:{{ monitor.port }} + TCP Ping {{ monitor.hostname }}:{{ monitor.port }} + Ping: {{ monitor.hostname }}

diff --git a/src/pages/EditMonitor.vue b/src/pages/EditMonitor.vue index 75f7bf5a..57ab8118 100644 --- a/src/pages/EditMonitor.vue +++ b/src/pages/EditMonitor.vue @@ -27,22 +27,19 @@
- +
+ + +
+
+ + +
- +