Compare commits

..

8 Commits

Author SHA1 Message Date
Frank Elsinga
c247ecdb10 tested reverting a change to the testing harness 2024-10-04 03:57:25 +02:00
Frank Elsinga
28c5191195 fixed the skip condition correctly targetting the platforms I want 2024-10-04 03:57:25 +02:00
Frank Elsinga
a6a662e751 made sure that skipping of the testcase uses the provided mechanisms 2024-10-04 03:57:25 +02:00
Frank Elsinga
e97fd40b54 excluded arm from testcontainer runs 2024-10-04 03:57:25 +02:00
UptimeKumaBot
e80a6ef728 Translations Update from Weblate (#5072)
Co-authored-by: Louis Lam <louislam@users.noreply.github.com>
Co-authored-by: Cyril59310 <archas.cyril@hotmail.fr>
Co-authored-by: Marco <marco@nanoweb.ch>
Co-authored-by: Ilkka Myller <ilkka.myller@nodefield.com>
Co-authored-by: Michal <black23@gmail.com>
Co-authored-by: Harry Suryapambagya <harsxv@gmail.com>
Co-authored-by: Lee Min Waan <rakis9076@gmail.com>
Co-authored-by: AnnAngela <naganjue@vip.qq.com>
Co-authored-by: kindercat <156439718+kindercat@users.noreply.github.com>
Co-authored-by: stanol <stanol777@gmail.com>
Co-authored-by: Hyeonho Kang <hyeonhokang10@o365.ice.go.kr>
Co-authored-by: Aindriú Mac Giolla Eoin <aindriu80@gmail.com>
Co-authored-by: MrEddX <mreddx@chatrix.one>
Co-authored-by: Sajjad Jazini <s.jazini@vatanmail.ir>
Co-authored-by: Ossie Stinga <ossie.stinga@gmail.com>
Co-authored-by: Elia Ronchetti <e.ronchetti@campus.unimib.it>
Co-authored-by: B3CKDOOR <B3CKDOOR@users.noreply.weblate.kuma.pet>
Co-authored-by: Ivan M <ivan.meishutovich@gmail.com>
Co-authored-by: AlanFarley <alanfarley2010@live.com>
Co-authored-by: Mohammad Hesam Imani <hesamimany@gmail.com>
Co-authored-by: Shinwoo PARK <devpysweb@gmail.com>
Co-authored-by: Lucius Gaitán <lucius@luciusgaitan.com>
Co-authored-by: Ömer Faruk GÖL <omerfarukgol@hotmail.com>
Co-authored-by: Satveek Gupta <guptasatveek@gmail.com>
Co-authored-by: Aung ko <aungko.use@gmail.com>
2024-10-04 03:57:25 +02:00
Frank Elsinga
e262056512 Better description for shrink database button (#4814)
Co-authored-by: Adam Stachowicz <saibamenppl@gmail.com>
2024-10-04 03:57:25 +02:00
Frank Elsinga
4bbd9430f3 made sure that CI is happy 2024-10-04 02:48:42 +02:00
Frank Elsinga
18d80b254b implemented a testcase for mqtt monitors 2024-10-04 02:30:06 +02:00
21 changed files with 1265 additions and 520 deletions

View File

@@ -15,14 +15,14 @@ on:
jobs:
auto-test:
needs: [ check-linters ]
needs: [ check-linters, e2e-test ]
runs-on: ${{ matrix.os }}
timeout-minutes: 15
strategy:
matrix:
os: [macos-latest, ubuntu-latest, windows-latest, ARM64]
node: [ 18, 20 ]
node: [ 18, 20.5 ]
# See supported Node.js release schedule at https://nodejs.org/en/about/releases/
steps:
@@ -42,7 +42,7 @@ jobs:
# As a lot of dev dependencies are not supported on ARMv7, we have to test it separately and just test if `npm ci --production` works
armv7-simple-test:
needs: [ ]
needs: [ check-linters ]
runs-on: ${{ matrix.os }}
timeout-minutes: 15
if: ${{ github.repository == 'louislam/uptime-kuma' }}
@@ -77,7 +77,7 @@ jobs:
- run: npm run lint:prod
e2e-test:
needs: [ ]
needs: [ check-linters ]
runs-on: ARM64
steps:
- run: git config --global core.autocrlf false # Mainly for Windows

View File

@@ -16,7 +16,9 @@ export default defineConfig({
},
define: {
"FRONTEND_VERSION": JSON.stringify(process.env.npm_package_version),
"process.env": {},
"DEVCONTAINER": JSON.stringify(process.env.DEVCONTAINER),
"GITHUB_CODESPACES_PORT_FORWARDING_DOMAIN": JSON.stringify(process.env.GITHUB_CODESPACES_PORT_FORWARDING_DOMAIN),
"CODESPACE_NAME": JSON.stringify(process.env.CODESPACE_NAME),
},
plugins: [
vue(),

View File

@@ -4,6 +4,7 @@ const tar = require("tar");
const packageJSON = require("../package.json");
const fs = require("fs");
const rmSync = require("./fs-rmSync.js");
const version = packageJSON.version;
const filename = "dist.tar.gz";
@@ -28,9 +29,8 @@ function download(url) {
if (fs.existsSync("./dist")) {
if (fs.existsSync("./dist-backup")) {
fs.rmSync("./dist-backup", {
recursive: true,
force: true,
rmSync("./dist-backup", {
recursive: true
});
}
@@ -43,9 +43,8 @@ function download(url) {
tarStream.on("close", () => {
if (fs.existsSync("./dist-backup")) {
fs.rmSync("./dist-backup", {
recursive: true,
force: true,
rmSync("./dist-backup", {
recursive: true
});
}
console.log("Done");

23
extra/fs-rmSync.js Normal file
View File

@@ -0,0 +1,23 @@
const fs = require("fs");
/**
* Detect if `fs.rmSync` is available
* to avoid the runtime deprecation warning triggered for using `fs.rmdirSync` with `{ recursive: true }` in Node.js v16,
* or the `recursive` property removing completely in the future Node.js version.
* See the link below.
* @todo Once we drop the support for Node.js v14 (or at least versions before v14.14.0), we can safely replace this function with `fs.rmSync`, since `fs.rmSync` was add in Node.js v14.14.0 and currently we supports all the Node.js v14 versions that include the versions before the v14.14.0, and this function have almost the same signature with `fs.rmSync`.
* @link https://nodejs.org/docs/latest-v16.x/api/deprecations.html#dep0147-fsrmdirpath--recursive-true- the deprecation information of `fs.rmdirSync`
* @link https://nodejs.org/docs/latest-v16.x/api/fs.html#fsrmsyncpath-options the document of `fs.rmSync`
* @param {fs.PathLike} path Valid types for path values in "fs".
* @param {fs.RmDirOptions} options options for `fs.rmdirSync`, if `fs.rmSync` is available and property `recursive` is true, it will automatically have property `force` with value `true`.
* @returns {void}
*/
const rmSync = (path, options) => {
if (typeof fs.rmSync === "function") {
if (options.recursive) {
options.force = true;
}
return fs.rmSync(path, options);
}
return fs.rmdirSync(path, options);
};
module.exports = rmSync;

View File

@@ -2,6 +2,7 @@
import fs from "fs";
import util from "util";
import rmSync from "../fs-rmSync.js";
/**
* Copy across the required language files
@@ -15,10 +16,7 @@ import util from "util";
*/
function copyFiles(langCode, baseLang) {
if (fs.existsSync("./languages")) {
fs.rmSync("./languages", {
recursive: true,
force: true,
});
rmSync("./languages", { recursive: true });
}
fs.mkdirSync("./languages");
@@ -95,9 +93,6 @@ console.log("Updating: " + langCode);
copyFiles(langCode, baseLangCode);
await updateLanguage(langCode, baseLangCode);
fs.rmSync("./languages", {
recursive: true,
force: true,
});
rmSync("./languages", { recursive: true });
console.log("Done. Fixing formatting by ESLint...");

988
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -68,7 +68,8 @@
"sort-contributors": "node extra/sort-contributors.js",
"quick-run-nightly": "docker run --rm --env NODE_ENV=development -p 3001:3001 louislam/uptime-kuma:nightly2",
"start-dev-container": "cd docker && docker-compose -f docker-compose-dev.yml up --force-recreate",
"rebase-pr-to-1.23.X": "node extra/rebase-pr.js 1.23.X"
"rebase-pr-to-1.23.X": "node extra/rebase-pr.js 1.23.X",
"start-server-node14-win": "private\\node14\\node.exe server/server.js"
},
"dependencies": {
"@grpc/grpc-js": "~1.8.22",
@@ -154,6 +155,7 @@
"@fortawesome/vue-fontawesome": "~3.0.0-5",
"@playwright/test": "~1.39.0",
"@popperjs/core": "~2.10.2",
"@testcontainers/hivemq": "^10.13.1",
"@types/bootstrap": "~5.1.9",
"@types/node": "^20.8.6",
"@typescript-eslint/eslint-plugin": "^6.7.5",
@@ -189,6 +191,7 @@
"stylelint-config-standard": "~25.0.0",
"terser": "~5.15.0",
"test": "~3.3.0",
"testcontainers": "^10.13.1",
"typescript": "~4.4.4",
"v-pagination-3": "~0.1.7",
"vite": "~5.2.8",

View File

@@ -72,12 +72,23 @@ class Monitor extends BeanModel {
/**
* Return an object that ready to parse to JSON
* @param {object} preloadData to prevent n+1 problems, we query the data in a batch outside of this function
* @param {boolean} includeSensitiveData Include sensitive data in
* JSON
* @returns {object} Object ready to parse
* @returns {Promise<object>} Object ready to parse
*/
toJSON(preloadData = {}, includeSensitiveData = true) {
async toJSON(includeSensitiveData = true) {
let notificationIDList = {};
let list = await R.find("monitor_notification", " monitor_id = ? ", [
this.id,
]);
for (let bean of list) {
notificationIDList[bean.notification_id] = true;
}
const tags = await this.getTags();
let screenshot = null;
@@ -85,7 +96,7 @@ class Monitor extends BeanModel {
screenshot = "/screenshots/" + jwt.sign(this.id, UptimeKumaServer.getInstance().jwtSecret) + ".png";
}
const path = preloadData.paths.get(this.id) || [];
const path = await this.getPath();
const pathName = path.join(" / ");
let data = {
@@ -95,15 +106,15 @@ class Monitor extends BeanModel {
path,
pathName,
parent: this.parent,
childrenIDs: preloadData.childrenIDs.get(this.id) || [],
childrenIDs: await Monitor.getAllChildrenIDs(this.id),
url: this.url,
method: this.method,
hostname: this.hostname,
port: this.port,
maxretries: this.maxretries,
weight: this.weight,
active: preloadData.activeStatus.get(this.id),
forceInactive: preloadData.forceInactive.get(this.id),
active: await this.isActive(),
forceInactive: !await Monitor.isParentActive(this.id),
type: this.type,
timeout: this.timeout,
interval: this.interval,
@@ -123,9 +134,9 @@ class Monitor extends BeanModel {
docker_container: this.docker_container,
docker_host: this.docker_host,
proxyId: this.proxy_id,
notificationIDList: preloadData.notifications.get(this.id) || {},
tags: preloadData.tags.get(this.id) || [],
maintenance: preloadData.maintenanceStatus.get(this.id),
notificationIDList,
tags: tags,
maintenance: await Monitor.isUnderMaintenance(this.id),
mqttTopic: this.mqttTopic,
mqttSuccessMessage: this.mqttSuccessMessage,
mqttCheckType: this.mqttCheckType,
@@ -191,6 +202,16 @@ class Monitor extends BeanModel {
return data;
}
/**
* Checks if the monitor is active based on itself and its parents
* @returns {Promise<boolean>} Is the monitor active?
*/
async isActive() {
const parentActive = await Monitor.isParentActive(this.id);
return (this.active === 1) && parentActive;
}
/**
* Get all tags applied to this monitor
* @returns {Promise<LooseObject<any>[]>} List of tags on the
@@ -326,7 +347,7 @@ class Monitor extends BeanModel {
let previousBeat = null;
let retries = 0;
this.prometheus = new Prometheus(this);
this.prometheus = await Prometheus.createAndInitMetrics(this);
const beat = async () => {
@@ -978,7 +999,7 @@ class Monitor extends BeanModel {
await R.store(bean);
log.debug("monitor", `[${this.name}] prometheus.update`);
this.prometheus?.update(bean, tlsInfo);
await this.prometheus?.update(bean, tlsInfo);
previousBeat = bean;
@@ -1176,18 +1197,6 @@ class Monitor extends BeanModel {
return checkCertificateResult;
}
/**
* Checks if the monitor is active based on itself and its parents
* @param {number} monitorID ID of monitor to send
* @param {boolean} active is active
* @returns {Promise<boolean>} Is the monitor active?
*/
static async isActive(monitorID, active) {
const parentActive = await Monitor.isParentActive(monitorID);
return (active === 1) && parentActive;
}
/**
* Send statistics to clients
* @param {Server} io Socket server instance
@@ -1324,10 +1333,7 @@ class Monitor extends BeanModel {
for (let notification of notificationList) {
try {
const heartbeatJSON = bean.toJSON();
const monitorData = [{ id: monitor.id,
active: monitor.active
}];
const preloadData = await Monitor.preparePreloadData(monitorData);
// Prevent if the msg is undefined, notifications such as Discord cannot send out.
if (!heartbeatJSON["msg"]) {
heartbeatJSON["msg"] = "N/A";
@@ -1338,7 +1344,7 @@ class Monitor extends BeanModel {
heartbeatJSON["timezoneOffset"] = UptimeKumaServer.getInstance().getTimezoneOffset();
heartbeatJSON["localDateTime"] = dayjs.utc(heartbeatJSON["time"]).tz(heartbeatJSON["timezone"]).format(SQL_DATETIME_FORMAT);
await Notification.send(JSON.parse(notification.config), msg, monitor.toJSON(preloadData, false), heartbeatJSON);
await Notification.send(JSON.parse(notification.config), msg, await monitor.toJSON(false), heartbeatJSON);
} catch (e) {
log.error("monitor", "Cannot send notification to " + notification.name);
log.error("monitor", e);
@@ -1501,111 +1507,6 @@ class Monitor extends BeanModel {
}
}
/**
* Gets monitor notification of multiple monitor
* @param {Array} monitorIDs IDs of monitor to get
* @returns {Promise<LooseObject<any>>} object
*/
static async getMonitorNotification(monitorIDs) {
return await R.getAll(`
SELECT monitor_notification.monitor_id, monitor_notification.notification_id
FROM monitor_notification
WHERE monitor_notification.monitor_id IN (?)
`, [
monitorIDs,
]);
}
/**
* Gets monitor tags of multiple monitor
* @param {Array} monitorIDs IDs of monitor to get
* @returns {Promise<LooseObject<any>>} object
*/
static async getMonitorTag(monitorIDs) {
return await R.getAll(`
SELECT monitor_tag.monitor_id, tag.name, tag.color
FROM monitor_tag
JOIN tag ON monitor_tag.tag_id = tag.id
WHERE monitor_tag.monitor_id IN (?)
`, [
monitorIDs,
]);
}
/**
* prepare preloaded data for efficient access
* @param {Array} monitorData IDs & active field of monitor to get
* @returns {Promise<LooseObject<any>>} object
*/
static async preparePreloadData(monitorData) {
const notificationsMap = new Map();
const tagsMap = new Map();
const maintenanceStatusMap = new Map();
const childrenIDsMap = new Map();
const activeStatusMap = new Map();
const forceInactiveMap = new Map();
const pathsMap = new Map();
if (monitorData.length > 0) {
const monitorIDs = monitorData.map(monitor => monitor.id);
const notifications = await Monitor.getMonitorNotification(monitorIDs);
const tags = await Monitor.getMonitorTag(monitorIDs);
const maintenanceStatuses = await Promise.all(monitorData.map(monitor => Monitor.isUnderMaintenance(monitor.id)));
const childrenIDs = await Promise.all(monitorData.map(monitor => Monitor.getAllChildrenIDs(monitor.id)));
const activeStatuses = await Promise.all(monitorData.map(monitor => Monitor.isActive(monitor.id, monitor.active)));
const forceInactiveStatuses = await Promise.all(monitorData.map(monitor => Monitor.isParentActive(monitor.id)));
const paths = await Promise.all(monitorData.map(monitor => Monitor.getAllPath(monitor.id, monitor.name)));
notifications.forEach(row => {
if (!notificationsMap.has(row.monitor_id)) {
notificationsMap.set(row.monitor_id, {});
}
notificationsMap.get(row.monitor_id)[row.notification_id] = true;
});
tags.forEach(row => {
if (!tagsMap.has(row.monitor_id)) {
tagsMap.set(row.monitor_id, []);
}
tagsMap.get(row.monitor_id).push({
name: row.name,
color: row.color
});
});
monitorData.forEach((monitor, index) => {
maintenanceStatusMap.set(monitor.id, maintenanceStatuses[index]);
});
monitorData.forEach((monitor, index) => {
childrenIDsMap.set(monitor.id, childrenIDs[index]);
});
monitorData.forEach((monitor, index) => {
activeStatusMap.set(monitor.id, activeStatuses[index]);
});
monitorData.forEach((monitor, index) => {
forceInactiveMap.set(monitor.id, !forceInactiveStatuses[index]);
});
monitorData.forEach((monitor, index) => {
pathsMap.set(monitor.id, paths[index]);
});
}
return {
notifications: notificationsMap,
tags: tagsMap,
maintenanceStatus: maintenanceStatusMap,
childrenIDs: childrenIDsMap,
activeStatus: activeStatusMap,
forceInactive: forceInactiveMap,
paths: pathsMap,
};
}
/**
* Gets Parent of the monitor
* @param {number} monitorID ID of monitor to get
@@ -1638,18 +1539,16 @@ class Monitor extends BeanModel {
/**
* Gets the full path
* @param {number} monitorID ID of the monitor to get
* @param {string} name of the monitor to get
* @returns {Promise<string[]>} Full path (includes groups and the name) of the monitor
*/
static async getAllPath(monitorID, name) {
const path = [ name ];
async getPath() {
const path = [ this.name ];
if (this.parent === null) {
return path;
}
let parent = await Monitor.getParent(monitorID);
let parent = await Monitor.getParent(this.id);
while (parent !== null) {
path.unshift(parent.name);
parent = await Monitor.getParent(parent.id);

View File

@@ -139,22 +139,17 @@ class Slack extends NotificationProvider {
const title = "Uptime Kuma Alert";
let data = {
"text": `${title}\n${msg}`,
"channel": notification.slackchannel,
"username": notification.slackusername,
"icon_emoji": notification.slackiconemo,
"attachments": [],
};
if (notification.slackrichmessage) {
data.attachments.push(
"attachments": [
{
"color": (heartbeatJSON["status"] === UP) ? "#2eb886" : "#e01e5a",
"blocks": Slack.buildBlocks(baseURL, monitorJSON, heartbeatJSON, title, msg),
}
);
} else {
data.text = `${title}\n${msg}`;
}
]
};
if (notification.slackbutton) {
await Slack.deprecateURL(notification.slackbutton);

View File

@@ -32,17 +32,20 @@ class WeCom extends NotificationProvider {
* @returns {object} Message
*/
composeMessage(heartbeatJSON, msg) {
let title = "UptimeKuma Message";
let title;
if (msg != null && heartbeatJSON != null && heartbeatJSON["status"] === UP) {
title = "UptimeKuma Monitor Up";
}
if (msg != null && heartbeatJSON != null && heartbeatJSON["status"] === DOWN) {
title = "UptimeKuma Monitor Down";
}
if (msg != null) {
title = "UptimeKuma Message";
}
return {
msgtype: "text",
text: {
content: title + "\n" + msg
content: title + msg
}
};
}

View File

@@ -1,3 +1,4 @@
const { R } = require("redbean-node");
const PrometheusClient = require("prom-client");
const { log } = require("../src/util");
@@ -9,36 +10,102 @@ const commonLabels = [
"monitor_port",
];
const monitorCertDaysRemaining = new PrometheusClient.Gauge({
name: "monitor_cert_days_remaining",
help: "The number of days remaining until the certificate expires",
labelNames: commonLabels
});
const monitorCertIsValid = new PrometheusClient.Gauge({
name: "monitor_cert_is_valid",
help: "Is the certificate still valid? (1 = Yes, 0= No)",
labelNames: commonLabels
});
const monitorResponseTime = new PrometheusClient.Gauge({
name: "monitor_response_time",
help: "Monitor Response Time (ms)",
labelNames: commonLabels
});
const monitorStatus = new PrometheusClient.Gauge({
name: "monitor_status",
help: "Monitor Status (1 = UP, 0= DOWN, 2= PENDING, 3= MAINTENANCE)",
labelNames: commonLabels
});
class Prometheus {
monitorLabelValues = {};
/**
* @param {object} monitor Monitor object to monitor
* Metric: monitor_cert_days_remaining
* @type {PrometheusClient.Gauge<string> | null}
*/
constructor(monitor) {
static monitorCertDaysRemaining = null;
/**
* Metric: monitor_cert_is_valid
* @type {PrometheusClient.Gauge<string> | null}
*/
static monitorCertIsValid = null;
/**
* Metric: monitor_response_time
* @type {PrometheusClient.Gauge<string> | null}
*/
static monitorResponseTime = null;
/**
* Metric: monitor_status
* @type {PrometheusClient.Gauge<string> | null}
*/
static monitorStatus = null;
/**
* All registered metric labels.
* @type {string[] | null}
*/
static monitorLabelNames = null;
/**
* Monitor labels/values combination.
* @type {{}}
*/
monitorLabelValues;
/**
* Initialize metrics and get all label names the first time called.
* @returns {void}
*/
static async initMetrics() {
if (!this.monitorLabelNames) {
let labelNames = await R.getCol("SELECT name FROM tag");
this.monitorLabelNames = [ ...commonLabels, ...labelNames ];
}
if (!this.monitorCertDaysRemaining) {
this.monitorCertDaysRemaining = new PrometheusClient.Gauge({
name: "monitor_cert_days_remaining",
help: "The number of days remaining until the certificate expires",
labelNames: this.monitorLabelNames
});
}
if (!this.monitorCertIsValid) {
this.monitorCertIsValid = new PrometheusClient.Gauge({
name: "monitor_cert_is_valid",
help: "Is the certificate still valid? (1 = Yes, 0 = No)",
labelNames: this.monitorLabelNames
});
}
if (!this.monitorResponseTime) {
this.monitorResponseTime = new PrometheusClient.Gauge({
name: "monitor_response_time",
help: "Monitor Response Time (ms)",
labelNames: this.monitorLabelNames
});
}
if (!this.monitorStatus) {
this.monitorStatus = new PrometheusClient.Gauge({
name: "monitor_status",
help: "Monitor Status (1 = UP, 0 = DOWN, 2 = PENDING, 3 = MAINTENANCE)",
labelNames: this.monitorLabelNames
});
}
}
/**
* Wrapper to create a `Prometheus` instance and ensure metrics are initialized.
* @param {Monitor} monitor Monitor object to monitor
* @returns {Promise<Prometheus>} `Prometheus` instance
*/
static async createAndInitMetrics(monitor) {
await Prometheus.initMetrics();
let tags = await monitor.getTags();
return new Prometheus(monitor, tags);
}
/**
* Creates a prometheus metric instance.
*
* Note: Make sure to call `Prometheus.initMetrics()` once prior creating Prometheus instances.
* @param {Monitor} monitor Monitor object to monitor
* @param {Promise<LooseObject<any>[]>} tags Tags of the monitor
*/
constructor(monitor, tags) {
this.monitorLabelValues = {
monitor_name: monitor.name,
monitor_type: monitor.type,
@@ -46,6 +113,12 @@ class Prometheus {
monitor_hostname: monitor.hostname,
monitor_port: monitor.port
};
Object.values(tags)
// only label names that were known at first metric creation.
.filter(tag => Prometheus.monitorLabelNames.includes(tag.name))
.forEach(tag => {
this.monitorLabelValues[tag.name] = tag.value;
});
}
/**
@@ -55,7 +128,6 @@ class Prometheus {
* @returns {void}
*/
update(heartbeat, tlsInfo) {
if (typeof tlsInfo !== "undefined") {
try {
let isValid;
@@ -64,7 +136,7 @@ class Prometheus {
} else {
isValid = 0;
}
monitorCertIsValid.set(this.monitorLabelValues, isValid);
Prometheus.monitorCertIsValid.set(this.monitorLabelValues, isValid);
} catch (e) {
log.error("prometheus", "Caught error");
log.error("prometheus", e);
@@ -72,7 +144,7 @@ class Prometheus {
try {
if (tlsInfo.certInfo != null) {
monitorCertDaysRemaining.set(this.monitorLabelValues, tlsInfo.certInfo.daysRemaining);
Prometheus.monitorCertDaysRemaining.set(this.monitorLabelValues, tlsInfo.certInfo.daysRemaining);
}
} catch (e) {
log.error("prometheus", "Caught error");
@@ -82,7 +154,7 @@ class Prometheus {
if (heartbeat) {
try {
monitorStatus.set(this.monitorLabelValues, heartbeat.status);
Prometheus.monitorStatus.set(this.monitorLabelValues, heartbeat.status);
} catch (e) {
log.error("prometheus", "Caught error");
log.error("prometheus", e);
@@ -90,10 +162,10 @@ class Prometheus {
try {
if (typeof heartbeat.ping === "number") {
monitorResponseTime.set(this.monitorLabelValues, heartbeat.ping);
Prometheus.monitorResponseTime.set(this.monitorLabelValues, heartbeat.ping);
} else {
// Is it good?
monitorResponseTime.set(this.monitorLabelValues, -1);
Prometheus.monitorResponseTime.set(this.monitorLabelValues, -1);
}
} catch (e) {
log.error("prometheus", "Caught error");
@@ -108,10 +180,10 @@ class Prometheus {
*/
remove() {
try {
monitorCertDaysRemaining.remove(this.monitorLabelValues);
monitorCertIsValid.remove(this.monitorLabelValues);
monitorResponseTime.remove(this.monitorLabelValues);
monitorStatus.remove(this.monitorLabelValues);
Prometheus.monitorCertDaysRemaining?.remove(this.monitorLabelValues);
Prometheus.monitorCertIsValid?.remove(this.monitorLabelValues);
Prometheus.monitorResponseTime?.remove(this.monitorLabelValues);
Prometheus.monitorStatus?.remove(this.monitorLabelValues);
} catch (e) {
console.error(e);
}

View File

@@ -726,7 +726,7 @@ let needSetup = false;
await updateMonitorNotification(bean.id, notificationIDList);
await server.sendUpdateMonitorIntoList(socket, bean.id);
await server.sendMonitorList(socket);
if (monitor.active !== false) {
await startMonitor(socket.userID, bean.id);
@@ -879,11 +879,11 @@ let needSetup = false;
await updateMonitorNotification(bean.id, monitor.notificationIDList);
if (await Monitor.isActive(bean.id, bean.active)) {
if (await bean.isActive()) {
await restartMonitor(socket.userID, bean.id);
}
await server.sendUpdateMonitorIntoList(socket, bean.id);
await server.sendMonitorList(socket);
callback({
ok: true,
@@ -923,17 +923,14 @@ let needSetup = false;
log.info("monitor", `Get Monitor: ${monitorID} User ID: ${socket.userID}`);
let monitor = await R.findOne("monitor", " id = ? AND user_id = ? ", [
let bean = await R.findOne("monitor", " id = ? AND user_id = ? ", [
monitorID,
socket.userID,
]);
const monitorData = [{ id: monitor.id,
active: monitor.active
}];
const preloadData = await Monitor.preparePreloadData(monitorData);
callback({
ok: true,
monitor: monitor.toJSON(preloadData),
monitor: await bean.toJSON(),
});
} catch (e) {
@@ -984,7 +981,7 @@ let needSetup = false;
try {
checkLogin(socket);
await startMonitor(socket.userID, monitorID);
await server.sendUpdateMonitorIntoList(socket, monitorID);
await server.sendMonitorList(socket);
callback({
ok: true,
@@ -1004,7 +1001,7 @@ let needSetup = false;
try {
checkLogin(socket);
await pauseMonitor(socket.userID, monitorID);
await server.sendUpdateMonitorIntoList(socket, monitorID);
await server.sendMonitorList(socket);
callback({
ok: true,
@@ -1050,7 +1047,8 @@ let needSetup = false;
msg: "successDeleted",
msgi18n: true,
});
await server.sendDeleteMonitorFromList(socket, monitorID);
await server.sendMonitorList(socket);
} catch (e) {
callback({
@@ -1680,13 +1678,13 @@ async function afterLogin(socket, user) {
await StatusPage.sendStatusPageList(io, socket);
const monitorPromises = [];
for (let monitorID in monitorList) {
monitorPromises.push(sendHeartbeatList(socket, monitorID));
monitorPromises.push(Monitor.sendStats(io, monitorID, user.id));
await sendHeartbeatList(socket, monitorID);
}
await Promise.all(monitorPromises);
for (let monitorID in monitorList) {
await Monitor.sendStats(io, monitorID, user.id);
}
// Set server timezone from client browser if not set
// It should be run once only

View File

@@ -205,56 +205,24 @@ class UptimeKumaServer {
return list;
}
/**
* Update Monitor into list
* @param {Socket} socket Socket to send list on
* @param {number} monitorID update or deleted monitor id
* @returns {Promise<void>}
*/
async sendUpdateMonitorIntoList(socket, monitorID) {
let list = await this.getMonitorJSONList(socket.userID, monitorID);
this.io.to(socket.userID).emit("updateMonitorIntoList", list);
}
/**
* Delete Monitor from list
* @param {Socket} socket Socket to send list on
* @param {number} monitorID update or deleted monitor id
* @returns {Promise<void>}
*/
async sendDeleteMonitorFromList(socket, monitorID) {
this.io.to(socket.userID).emit("deleteMonitorFromList", monitorID);
}
/**
* Get a list of monitors for the given user.
* @param {string} userID - The ID of the user to get monitors for.
* @param {number} monitorID - The ID of monitor for.
* @returns {Promise<object>} A promise that resolves to an object with monitor IDs as keys and monitor objects as values.
*
* Generated by Trelent
*/
async getMonitorJSONList(userID, monitorID = null) {
async getMonitorJSONList(userID) {
let result = {};
let query = " user_id = ? ";
let queryParams = [ userID ];
let monitorList = await R.find("monitor", " user_id = ? ORDER BY weight DESC, name", [
userID,
]);
if (monitorID) {
query += "AND id = ? ";
queryParams.push(monitorID);
for (let monitor of monitorList) {
result[monitor.id] = await monitor.toJSON();
}
let monitorList = await R.find("monitor", query + "ORDER BY weight DESC, name", queryParams);
const monitorData = monitorList.map(monitor => ({
id: monitor.id,
active: monitor.active,
name: monitor.name,
}));
const preloadData = await Monitor.preparePreloadData(monitorData);
const result = {};
monitorList.forEach(monitor => result[monitor.id] = monitor.toJSON(preloadData));
return result;
}
@@ -552,4 +520,3 @@ const { DnsMonitorType } = require("./monitor-types/dns");
const { MqttMonitorType } = require("./monitor-types/mqtt");
const { SNMPMonitorType } = require("./monitor-types/snmp");
const { MongodbMonitorType } = require("./monitor-types/mongodb");
const Monitor = require("./model/monitor");

View File

@@ -9,12 +9,6 @@
<label for="slack-channel" class="form-label">{{ $t("Channel Name") }}</label>
<input id="slack-channel-name" v-model="$parent.notification.slackchannel" type="text" class="form-control">
<label class="form-label">{{ $t("Message format") }}</label>
<div class="form-check form-switch">
<input id="slack-text-message" v-model="$parent.notification.slackrichmessage" type="checkbox" class="form-check-input">
<label for="slack-text-message" class="form-label">{{ $t("Send rich messages") }}</label>
</div>
<div class="form-text">
<span style="color: red;"><sup>*</sup></span>{{ $t("Required") }}
<i18n-t tag="p" keypath="aboutWebhooks" style="margin-top: 8px;">

View File

@@ -82,7 +82,6 @@
"resendEveryXTimes": "Resend every {0} times",
"resendDisabled": "Resend disabled",
"retriesDescription": "Maximum retries before the service is marked as down and a notification is sent",
"ignoredTLSError": "TLS/SSL errors have been ignored",
"ignoreTLSError": "Ignore TLS/SSL errors for HTTPS websites",
"ignoreTLSErrorGeneral": "Ignore TLS/SSL error for connection",
"upsideDownModeDescription": "Flip the status upside down. If the service is reachable, it is DOWN.",
@@ -97,8 +96,6 @@
"pushOthers": "Others",
"programmingLanguages": "Programming Languages",
"Save": "Save",
"Debug": "Debug",
"Copy": "Copy",
"Notifications": "Notifications",
"Not available, please setup.": "Not available, please set up.",
"Setup Notification": "Set Up Notification",
@@ -251,14 +248,6 @@
"PushUrl": "Push URL",
"HeadersInvalidFormat": "The request headers are not valid JSON: ",
"BodyInvalidFormat": "The request body is not valid JSON: ",
"CopyToClipboardError": "Couldn't copy to clipbard: {error}",
"CopyToClipboardSuccess": "Copied!",
"CurlDebugInfo": "To debug the monitor, you can either paste this into your own machines terminal or into the machines terminal which uptime kuma is running on and see what you are requesting.{newiline}Please be aware of networking differences like {firewalls}, {dns_resolvers} or {docker_networks}.",
"firewalls": "firewalls",
"dns resolvers": "dns resolvers",
"docker networks": "docker networks",
"CurlDebugInfoOAuth2CCUnsupported": "Full oauth client credential flow is not supported in {curl}.{newline}Please get a bearer token and pass it via the {oauth2_bearer} option.",
"CurlDebugInfoProxiesUnsupported": "Proxy support in the above {curl} command is currently not implemented.",
"Monitor History": "Monitor History",
"clearDataOlderThan": "Keep monitor history data for {0} days.",
"PasswordsDoNotMatch": "Passwords do not match.",
@@ -895,8 +884,6 @@
"cacheBusterParamDescription": "Randomly generated parameter to skip caches.",
"gamedigGuessPort": "Gamedig: Guess Port",
"gamedigGuessPortDescription": "The port used by Valve Server Query Protocol may be different from the client port. Try this if the monitor cannot connect to your server.",
"Message format": "Message format",
"Send rich messages": "Send rich messages",
"Bitrix24 Webhook URL": "Bitrix24 Webhook URL",
"wayToGetBitrix24Webhook": "You can create a webhook by following the steps at {0}",
"bitrix24SupportUserID": "Enter your user ID in Bitrix24. You can find out the ID from the link by going to the user's profile.",

View File

@@ -141,21 +141,17 @@ export default {
});
socket.on("monitorList", (data) => {
this.assignMonitorUrlParser(data);
this.monitorList = data;
});
socket.on("updateMonitorIntoList", (data) => {
this.assignMonitorUrlParser(data);
Object.entries(data).forEach(([ monitorID, updatedMonitor ]) => {
this.monitorList[monitorID] = updatedMonitor;
// Add Helper function
Object.entries(data).forEach(([ monitorID, monitor ]) => {
monitor.getUrl = () => {
try {
return new URL(monitor.url);
} catch (_) {
return null;
}
};
});
});
socket.on("deleteMonitorFromList", (monitorID) => {
if (this.monitorList[monitorID]) {
delete this.monitorList[monitorID];
}
this.monitorList = data;
});
socket.on("monitorTypeList", (data) => {
@@ -293,23 +289,6 @@ export default {
location.reload();
});
},
/**
* parse all urls from list.
* @param {object} data Monitor data to modify
* @returns {object} list
*/
assignMonitorUrlParser(data) {
Object.entries(data).forEach(([ monitorID, monitor ]) => {
monitor.getUrl = () => {
try {
return new URL(monitor.url);
} catch (_) {
return null;
}
};
});
return data;
},
/**
* The storage currently in use

View File

@@ -565,8 +565,8 @@
<h2 v-if="monitor.type !== 'push'" class="mt-5 mb-2">{{ $t("Advanced") }}</h2>
<div v-if="monitor.type === 'http' || monitor.type === 'keyword' || monitor.type === 'json-query' " class="my-3 form-check" :title="monitor.ignoreTls ? $t('ignoredTLSError') : ''">
<input id="expiry-notification" v-model="monitor.expiryNotification" class="form-check-input" type="checkbox" :disabled="monitor.ignoreTls">
<div v-if="monitor.type === 'http' || monitor.type === 'keyword' || monitor.type === 'json-query' " class="my-3 form-check">
<input id="expiry-notification" v-model="monitor.expiryNotification" class="form-check-input" type="checkbox">
<label class="form-check-label" for="expiry-notification">
{{ $t("Certificate Expiry Notification") }}
</label>
@@ -982,23 +982,13 @@
<div class="fixed-bottom-bar p-3">
<button
id="monitor-submit-btn"
class="btn btn-primary me-2"
class="btn btn-primary"
type="submit"
:disabled="processing"
data-testid="save-button"
>
{{ $t("Save") }}
</button>
<button
v-if="monitor.type === 'http'"
id="monitor-debug-btn"
class="btn btn-outline-primary"
type="button"
:disabled="processing"
@click.stop="modal.show()"
>
{{ $t("Debug") }}
</button>
</div>
</div>
</form>
@@ -1010,58 +1000,9 @@
<RemoteBrowserDialog ref="remoteBrowserDialog" />
</div>
</transition>
<div ref="modal" class="modal fade" tabindex="-1">
<div class="modal-dialog modal-dialog-centered">
<div class="modal-content">
<div class="modal-body">
<textarea id="curl-debug" v-model="curlCommand" class="form-control mb-3" readonly wrap="off"></textarea>
<button id="debug-copy-btn" class="btn btn-outline-primary position-absolute top-0 end-0 mt-3 me-3 border-0" type="button" @click.stop="copyToClipboard">
<font-awesome-icon icon="copy" />
</button>
<i18n-t keypath="CurlDebugInfo" tag="p" class="form-text">
<template #newiline>
<br>
</template>
<template #firewalls>
<a href="https://xkcd.com/2259/" target="_blank">{{ $t('firewalls') }}</a>
</template>
<template #dns_resolvers>
<a href="https://www.reddit.com/r/sysadmin/comments/rxho93/thank_you_for_the_running_its_always_dns_joke_its/" target="_blank">{{ $t('dns resolvers') }}</a>
</template>
<template #docker_networks>
<a href="https://youtu.be/bKFMS5C4CG0" target="_blank">{{ $t('docker networks') }}</a>
</template>
</i18n-t>
<div v-if="monitor.authMethod === 'oauth2-cc'" class="alert alert-warning d-flex align-items-center gap-2" role="alert">
<div role="img" aria-label="Warning:"></div>
<i18n-t keypath="CurlDebugInfoOAuth2CCUnsupported" tag="div">
<template #curl>
<code>curl</code>
</template>
<template #newline>
<br>
</template>
<template #oauth2_bearer>
<code>--oauth2-bearer TOKEN</code>
</template>
</i18n-t>
</div>
<div v-if="monitor.proxyId" class="alert alert-warning d-flex align-items-center gap-2" role="alert">
<div role="img" aria-label="Warning:"></div>
<i18n-t keypath="CurlDebugInfoProxiesUnsupported" tag="div">
<template #curl>
<code>curl</code>
</template>
</i18n-t>
</div>
</div>
</div>
</div>
</div>
</template>
<script>
import { Modal } from "bootstrap";
import VueMultiselect from "vue-multiselect";
import { useToast } from "vue-toastification";
import ActionSelect from "../components/ActionSelect.vue";
@@ -1076,10 +1017,8 @@ import { genSecret, isDev, MAX_INTERVAL_SECOND, MIN_INTERVAL_SECOND, sleep } fro
import { hostNameRegexPattern } from "../util-frontend";
import HiddenInput from "../components/HiddenInput.vue";
import EditMonitorConditions from "../components/EditMonitorConditions.vue";
import { version } from "../../package.json";
const userAgent = `'Uptime-Kuma/${version}'`;
const toast = useToast();
const toast = useToast;
const pushTokenLength = 32;
@@ -1142,7 +1081,6 @@ export default {
data() {
return {
modal: null,
minInterval: MIN_INTERVAL_SECOND,
maxInterval: MAX_INTERVAL_SECOND,
processing: false,
@@ -1170,53 +1108,6 @@ export default {
computed: {
curlCommand() {
const command = [ "curl", "--verbose", "--head", "--request", this.monitor.method, "\\\n", "--user-agent", userAgent, "\\\n" ];
if (this.monitor.ignoreTls) {
command.push("--insecure", "\\\n");
}
if (this.monitor.headers) {
try {
// trying to parse the supplied data as json to trim whitespace
for (const [ key, value ] of Object.entries(JSON.parse(this.monitor.headers))) {
command.push("--header", `'${key}: ${value}'`, "\\\n");
}
} catch (e) {
command.push("--header", `'${this.monitor.headers}'`, "\\\n");
}
}
if (this.monitor.authMethod === "basic") {
command.push("--user", `${this.monitor.basic_auth_user}:${this.monitor.basic_auth_pass}`, "--basic", "\\\n");
} else if (this.monitor.authmethod === "mtls") {
command.push("--cacert", `'${this.monitor.tlsCa}'`, "\\\n", "--key", `'${this.monitor.tlsKey}'`, "\\\n", "--cert", `'${this.monitor.tlsCert}'`, "\\\n");
} else if (this.monitor.authMethod === "ntlm") {
command.push("--user", `'${this.monitor.authDomain ? `${this.monitor.authDomain}/` : ""}${this.monitor.basic_auth_user}:${this.monitor.basic_auth_pass}'`, "--ntlm", "\\\n");
}
if (this.monitor.body && this.monitor.httpBodyEncoding === "json") {
let json = "";
try {
// trying to parse the supplied data as json to trim whitespace
json = JSON.stringify(JSON.parse(this.monitor.body));
} catch (e) {
json = this.monitor.body;
}
command.push("--header", "'Content-Type: application/json'", "\\\n", "--data", `'${json}'`, "\\\n");
} else if (this.monitor.body && this.monitor.httpBodyEncoding === "xml") {
command.push("--headers", "'Content-Type: application/xml'", "\\\n", "--data", `'${this.monitor.body}'`, "\\\n");
}
if (this.monitor.maxredirects) {
command.push("--location", "--max-redirs", this.monitor.maxredirects, "\\\n");
}
if (this.monitor.timeout) {
command.push("--max-time", this.monitor.timeout, "\\\n");
}
if (this.monitor.maxretries) {
command.push("--retry", this.monitor.maxretries, "\\\n");
}
command.push("--url", this.monitor.url);
return command.join(" ");
},
ipRegex() {
// Allow to test with simple dns server with port (127.0.0.1:5300)
@@ -1565,15 +1456,8 @@ message HealthCheckResponse {
}
this.monitor.game = newGameObject.keys[0];
},
"monitor.ignoreTls"(newVal) {
if (newVal) {
this.monitor.expiryNotification = false;
}
},
},
mounted() {
this.modal = new Modal(this.$refs.modal);
this.init();
let acceptedStatusCodeOptions = [
@@ -1614,14 +1498,6 @@ message HealthCheckResponse {
this.kafkaSaslMechanismOptions = kafkaSaslMechanismOptions;
},
methods: {
async copyToClipboard() {
try {
await navigator.clipboard.writeText(this.curlCommand);
toast.success(this.$t("CopyToClipboardSuccess"));
} catch (err) {
toast.error(this.$t("CopyToClipboardError", { error: err.message }));
}
},
/**
* Initialize the edit monitor form
* @returns {void}
@@ -1813,6 +1689,7 @@ message HealthCheckResponse {
await this.startParentGroupMonitor();
}
this.processing = false;
this.$root.getMonitorList();
this.$router.push("/dashboard/" + res.monitorID);
} else {
this.processing = false;
@@ -1909,9 +1786,4 @@ message HealthCheckResponse {
textarea {
min-height: 200px;
}
#curl-debug {
font-family: monospace;
overflow: auto;
}
</style>

View File

@@ -68,17 +68,15 @@ const routes = [
},
],
},
],
},
{
path: "/add",
component: EditMonitor,
children: [
{
path: "/clone/:id",
component: EditMonitor,
},
]
{
path: "/add",
component: EditMonitor,
},
],
},
{
path: "/list",

View File

@@ -83,7 +83,6 @@ exports.CONSOLE_STYLE_BgMagenta = "\x1b[45m";
exports.CONSOLE_STYLE_BgCyan = "\x1b[46m";
exports.CONSOLE_STYLE_BgWhite = "\x1b[47m";
exports.CONSOLE_STYLE_BgGray = "\x1b[100m";
const consoleModuleColors = [
exports.CONSOLE_STYLE_FgCyan,
exports.CONSOLE_STYLE_FgGreen,

View File

@@ -0,0 +1,102 @@
const { describe, test } = require("node:test");
const assert = require("node:assert");
const { HiveMQContainer } = require("@testcontainers/hivemq");
const mqtt = require("mqtt");
const { MqttMonitorType } = require("../../server/monitor-types/mqtt");
const { UP, PENDING } = require("../../src/util");
/**
* Runs an MQTT test with the
* @param {string} mqttSuccessMessage the message that the monitor expects
* @param {null|"keyword"|"json-query"} mqttCheckType the type of check we perform
* @param {string} receivedMessage what message is recieved from the mqtt channel
* @returns {Promise<Heartbeat>} the heartbeat produced by the check
*/
async function testMqtt(mqttSuccessMessage, mqttCheckType, receivedMessage) {
const hiveMQContainer = await new HiveMQContainer().start();
const connectionString = hiveMQContainer.getConnectionString();
const mqttMonitorType = new MqttMonitorType();
const monitor = {
jsonPath: "firstProp", // always return firstProp for the json-query monitor
hostname: connectionString.split(":", 2).join(":"),
mqttTopic: "test",
port: connectionString.split(":")[2],
mqttUsername: null,
mqttPassword: null,
interval: 20, // controls the timeout
mqttSuccessMessage: mqttSuccessMessage, // for keywords
expectedValue: mqttSuccessMessage, // for json-query
mqttCheckType: mqttCheckType,
};
const heartbeat = {
msg: "",
status: PENDING,
};
const testMqttClient = mqtt.connect(hiveMQContainer.getConnectionString());
testMqttClient.on("connect", () => {
testMqttClient.subscribe("test", (error) => {
if (!error) {
testMqttClient.publish("test", receivedMessage);
}
});
});
try {
await mqttMonitorType.check(monitor, heartbeat, {});
} finally {
testMqttClient.end();
hiveMQContainer.stop();
}
return heartbeat;
}
describe("MqttMonitorType", {
concurrency: true,
skip: !!process.env.CI && (process.platform !== "linux" || process.arch !== "x64")
}, () => {
test("valid keywords (type=default)", async () => {
const heartbeat = await testMqtt("KEYWORD", null, "-> KEYWORD <-");
assert.strictEqual(heartbeat.status, UP);
assert.strictEqual(heartbeat.msg, "Topic: test; Message: -> KEYWORD <-");
});
test("valid keywords (type=keyword)", async () => {
const heartbeat = await testMqtt("KEYWORD", "keyword", "-> KEYWORD <-");
assert.strictEqual(heartbeat.status, UP);
assert.strictEqual(heartbeat.msg, "Topic: test; Message: -> KEYWORD <-");
});
test("invalid keywords (type=default)", async () => {
await assert.rejects(
testMqtt("NOT_PRESENT", null, "-> KEYWORD <-"),
new Error("Message Mismatch - Topic: test; Message: -> KEYWORD <-"),
);
});
test("invalid keyword (type=keyword)", async () => {
await assert.rejects(
testMqtt("NOT_PRESENT", "keyword", "-> KEYWORD <-"),
new Error("Message Mismatch - Topic: test; Message: -> KEYWORD <-"),
);
});
test("valid json-query", async () => {
// works because the monitors' jsonPath is hard-coded to "firstProp"
const heartbeat = await testMqtt("present", "json-query", "{\"firstProp\":\"present\"}");
assert.strictEqual(heartbeat.status, UP);
assert.strictEqual(heartbeat.msg, "Message received, expected value is found");
});
test("invalid (because query fails) json-query", async () => {
// works because the monitors' jsonPath is hard-coded to "firstProp"
await assert.rejects(
testMqtt("[not_relevant]", "json-query", "{}"),
new Error("Message received but value is not equal to expected value, value was: [undefined]"),
);
});
test("invalid (because successMessage fails) json-query", async () => {
// works because the monitors' jsonPath is hard-coded to "firstProp"
await assert.rejects(
testMqtt("[wrong_success_messsage]", "json-query", "{\"firstProp\":\"present\"}"),
new Error("Message received but value is not equal to expected value, value was: [present]")
);
});
});

View File

@@ -1,10 +1,10 @@
const fs = require("fs");
const rmSync = require("../extra/fs-rmSync.js");
const path = "./data/test";
if (fs.existsSync(path)) {
fs.rmSync(path, {
rmSync(path, {
recursive: true,
force: true,
});
}