mirror of
https://github.com/louislam/uptime-kuma.git
synced 2025-08-09 10:07:42 +08:00
Merge remote-tracking branch 'origin/master' into skaempfe#2593
# Conflicts: # server/model/monitor.js # src/pages/Details.vue
This commit is contained in:
@@ -2,8 +2,12 @@ const https = require("https");
|
||||
const dayjs = require("dayjs");
|
||||
const axios = require("axios");
|
||||
const { Prometheus } = require("../prometheus");
|
||||
const { log, UP, DOWN, PENDING, MAINTENANCE, flipStatus, TimeLogger, MAX_INTERVAL_SECOND, MIN_INTERVAL_SECOND } = require("../../src/util");
|
||||
const { tcping, ping, dnsResolve, checkCertificate, checkStatusCode, getTotalClientInRoom, setting, mssqlQuery, postgresQuery, mysqlQuery, mqttAsync, setSetting, httpNtlm, radius, grpcQuery } = require("../util-server");
|
||||
const { log, UP, DOWN, PENDING, MAINTENANCE, flipStatus, TimeLogger, MAX_INTERVAL_SECOND, MIN_INTERVAL_SECOND,
|
||||
SQL_DATETIME_FORMAT
|
||||
} = require("../../src/util");
|
||||
const { tcping, ping, dnsResolve, checkCertificate, checkStatusCode, getTotalClientInRoom, setting, mssqlQuery, postgresQuery, mysqlQuery, mqttAsync, setSetting, httpNtlm, radius, grpcQuery,
|
||||
redisPingAsync, mongodbPing,
|
||||
} = require("../util-server");
|
||||
const { R } = require("redbean-node");
|
||||
const { BeanModel } = require("redbean-node/dist/bean-model");
|
||||
const { Notification } = require("../notification");
|
||||
@@ -14,8 +18,8 @@ const apicache = require("../modules/apicache");
|
||||
const { UptimeKumaServer } = require("../uptime-kuma-server");
|
||||
const { CacheableDnsHttpAgent } = require("../cacheable-dns-http-agent");
|
||||
const { DockerHost } = require("../docker");
|
||||
const Maintenance = require("./maintenance");
|
||||
const { UptimeCacheList } = require("../uptime-cache-list");
|
||||
const Gamedig = require("gamedig");
|
||||
|
||||
/**
|
||||
* status:
|
||||
@@ -36,7 +40,6 @@ class Monitor extends BeanModel {
|
||||
id: this.id,
|
||||
name: this.name,
|
||||
sendUrl: this.sendUrl,
|
||||
maintenance: await Monitor.isUnderMaintenance(this.id),
|
||||
};
|
||||
|
||||
if (this.sendUrl) {
|
||||
@@ -70,6 +73,7 @@ class Monitor extends BeanModel {
|
||||
let data = {
|
||||
id: this.id,
|
||||
name: this.name,
|
||||
description: this.description,
|
||||
url: this.url,
|
||||
method: this.method,
|
||||
hostname: this.hostname,
|
||||
@@ -85,6 +89,7 @@ class Monitor extends BeanModel {
|
||||
expiryNotification: this.isEnabledExpiryNotification(),
|
||||
ignoreTls: this.getIgnoreTls(),
|
||||
upsideDown: this.isUpsideDown(),
|
||||
packetSize: this.packetSize,
|
||||
maxredirects: this.maxredirects,
|
||||
accepted_statuscodes: this.getAcceptedStatuscodes(),
|
||||
dns_resolve_type: this.dns_resolve_type,
|
||||
@@ -107,6 +112,8 @@ class Monitor extends BeanModel {
|
||||
grpcEnableTls: this.getGrpcEnableTls(),
|
||||
radiusCalledStationId: this.radiusCalledStationId,
|
||||
radiusCallingStationId: this.radiusCallingStationId,
|
||||
game: this.game,
|
||||
httpBodyEncoding: this.httpBodyEncoding
|
||||
};
|
||||
|
||||
if (includeSensitiveData) {
|
||||
@@ -127,6 +134,9 @@ class Monitor extends BeanModel {
|
||||
mqttPassword: this.mqttPassword,
|
||||
authWorkstation: this.authWorkstation,
|
||||
authDomain: this.authDomain,
|
||||
tlsCa: this.tlsCa,
|
||||
tlsCert: this.tlsCert,
|
||||
tlsKey: this.tlsKey,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -139,7 +149,7 @@ class Monitor extends BeanModel {
|
||||
* @returns {Promise<LooseObject<any>[]>}
|
||||
*/
|
||||
async getTags() {
|
||||
return await R.getAll("SELECT mt.*, tag.name, tag.color FROM monitor_tag mt JOIN tag ON mt.tag_id = tag.id WHERE mt.monitor_id = ?", [ this.id ]);
|
||||
return await R.getAll("SELECT mt.*, tag.name, tag.color FROM monitor_tag mt JOIN tag ON mt.tag_id = tag.id WHERE mt.monitor_id = ? ORDER BY tag.name", [ this.id ]);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -199,7 +209,7 @@ class Monitor extends BeanModel {
|
||||
let previousBeat = null;
|
||||
let retries = 0;
|
||||
|
||||
let prometheus = new Prometheus(this);
|
||||
this.prometheus = new Prometheus(this);
|
||||
|
||||
const beat = async () => {
|
||||
|
||||
@@ -268,17 +278,34 @@ class Monitor extends BeanModel {
|
||||
|
||||
log.debug("monitor", `[${this.name}] Prepare Options for axios`);
|
||||
|
||||
let contentType = null;
|
||||
let bodyValue = null;
|
||||
|
||||
if (this.body && (typeof this.body === "string" && this.body.trim().length > 0)) {
|
||||
if (!this.httpBodyEncoding || this.httpBodyEncoding === "json") {
|
||||
try {
|
||||
bodyValue = JSON.parse(this.body);
|
||||
contentType = "application/json";
|
||||
} catch (e) {
|
||||
throw new Error("Your JSON body is invalid. " + e.message);
|
||||
}
|
||||
} else if (this.httpBodyEncoding === "xml") {
|
||||
bodyValue = this.body;
|
||||
contentType = "text/xml; charset=utf-8";
|
||||
}
|
||||
}
|
||||
|
||||
// Axios Options
|
||||
const options = {
|
||||
url: this.url,
|
||||
method: (this.method || "get").toLowerCase(),
|
||||
...(this.body ? { data: JSON.parse(this.body) } : {}),
|
||||
timeout: this.interval * 1000 * 0.8,
|
||||
headers: {
|
||||
"Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.9",
|
||||
"User-Agent": "Uptime-Kuma/" + version,
|
||||
...(this.headers ? JSON.parse(this.headers) : {}),
|
||||
...(contentType ? { "Content-Type": contentType } : {}),
|
||||
...(basicAuthHeader),
|
||||
...(this.headers ? JSON.parse(this.headers) : {})
|
||||
},
|
||||
maxRedirects: this.maxredirects,
|
||||
validateStatus: (status) => {
|
||||
@@ -286,6 +313,10 @@ class Monitor extends BeanModel {
|
||||
},
|
||||
};
|
||||
|
||||
if (bodyValue) {
|
||||
options.data = bodyValue;
|
||||
}
|
||||
|
||||
if (this.proxy_id) {
|
||||
const proxy = await R.load("proxy", this.proxy_id);
|
||||
|
||||
@@ -304,6 +335,18 @@ class Monitor extends BeanModel {
|
||||
options.httpsAgent = new https.Agent(httpsAgentOptions);
|
||||
}
|
||||
|
||||
if (this.auth_method === "mtls") {
|
||||
if (this.tlsCert !== null && this.tlsCert !== "") {
|
||||
options.httpsAgent.options.cert = Buffer.from(this.tlsCert);
|
||||
}
|
||||
if (this.tlsCa !== null && this.tlsCa !== "") {
|
||||
options.httpsAgent.options.ca = Buffer.from(this.tlsCa);
|
||||
}
|
||||
if (this.tlsKey !== null && this.tlsKey !== "") {
|
||||
options.httpsAgent.options.key = Buffer.from(this.tlsKey);
|
||||
}
|
||||
}
|
||||
|
||||
log.debug("monitor", `[${this.name}] Axios Options: ${JSON.stringify(options)}`);
|
||||
log.debug("monitor", `[${this.name}] Axios Request`);
|
||||
|
||||
@@ -357,7 +400,7 @@ class Monitor extends BeanModel {
|
||||
bean.msg += ", keyword is found";
|
||||
bean.status = UP;
|
||||
} else {
|
||||
data = data.replace(/<[^>]*>?|[\n\r]|\s+/gm, " ");
|
||||
data = data.replace(/<[^>]*>?|[\n\r]|\s+/gm, " ").trim();
|
||||
if (data.length > 50) {
|
||||
data = data.substring(0, 47) + "...";
|
||||
}
|
||||
@@ -372,7 +415,7 @@ class Monitor extends BeanModel {
|
||||
bean.status = UP;
|
||||
|
||||
} else if (this.type === "ping") {
|
||||
bean.ping = await ping(this.hostname);
|
||||
bean.ping = await ping(this.hostname, this.packetSize);
|
||||
bean.msg = "";
|
||||
bean.status = UP;
|
||||
} else if (this.type === "dns") {
|
||||
@@ -482,25 +525,44 @@ class Monitor extends BeanModel {
|
||||
bean.msg = res.data.response.servers[0].name;
|
||||
|
||||
try {
|
||||
bean.ping = await ping(this.hostname);
|
||||
bean.ping = await ping(this.hostname, this.packetSize);
|
||||
} catch (_) { }
|
||||
} else {
|
||||
throw new Error("Server not found on Steam");
|
||||
}
|
||||
} else if (this.type === "gamedig") {
|
||||
try {
|
||||
const state = await Gamedig.query({
|
||||
type: this.game,
|
||||
host: this.hostname,
|
||||
port: this.port,
|
||||
givenPortOnly: true,
|
||||
});
|
||||
|
||||
bean.msg = state.name;
|
||||
bean.status = UP;
|
||||
bean.ping = state.ping;
|
||||
} catch (e) {
|
||||
throw new Error(e.message);
|
||||
}
|
||||
} else if (this.type === "docker") {
|
||||
log.debug(`[${this.name}] Prepare Options for Axios`);
|
||||
log.debug("monitor", `[${this.name}] Prepare Options for Axios`);
|
||||
|
||||
const dockerHost = await R.load("docker_host", this.docker_host);
|
||||
|
||||
const options = {
|
||||
url: `/containers/${this.docker_container}/json`,
|
||||
timeout: this.interval * 1000 * 0.8,
|
||||
headers: {
|
||||
"Accept": "*/*",
|
||||
"User-Agent": "Uptime-Kuma/" + version,
|
||||
},
|
||||
httpsAgent: new https.Agent({
|
||||
httpsAgent: CacheableDnsHttpAgent.getHttpsAgent({
|
||||
maxCachedSessions: 0, // Use Custom agent to disable session reuse (https://github.com/nodejs/node/issues/3940)
|
||||
rejectUnauthorized: ! this.getIgnoreTls(),
|
||||
rejectUnauthorized: !this.getIgnoreTls(),
|
||||
}),
|
||||
httpAgent: CacheableDnsHttpAgent.getHttpAgent({
|
||||
maxCachedSessions: 0,
|
||||
}),
|
||||
};
|
||||
|
||||
@@ -510,7 +572,7 @@ class Monitor extends BeanModel {
|
||||
options.baseURL = DockerHost.patchDockerURL(dockerHost._dockerDaemon);
|
||||
}
|
||||
|
||||
log.debug(`[${this.name}] Axios Request`);
|
||||
log.debug("monitor", `[${this.name}] Axios Request`);
|
||||
let res = await axios.request(options);
|
||||
if (res.data.State.Running) {
|
||||
bean.status = UP;
|
||||
@@ -576,11 +638,18 @@ class Monitor extends BeanModel {
|
||||
} else if (this.type === "mysql") {
|
||||
let startTime = dayjs().valueOf();
|
||||
|
||||
await mysqlQuery(this.databaseConnectionString, this.databaseQuery);
|
||||
bean.msg = await mysqlQuery(this.databaseConnectionString, this.databaseQuery);
|
||||
bean.status = UP;
|
||||
bean.ping = dayjs().valueOf() - startTime;
|
||||
} else if (this.type === "mongodb") {
|
||||
let startTime = dayjs().valueOf();
|
||||
|
||||
await mongodbPing(this.databaseConnectionString);
|
||||
|
||||
bean.msg = "";
|
||||
bean.status = UP;
|
||||
bean.ping = dayjs().valueOf() - startTime;
|
||||
|
||||
} else if (this.type === "radius") {
|
||||
let startTime = dayjs().valueOf();
|
||||
|
||||
@@ -617,9 +686,23 @@ class Monitor extends BeanModel {
|
||||
}
|
||||
}
|
||||
bean.ping = dayjs().valueOf() - startTime;
|
||||
} else if (this.type === "redis") {
|
||||
let startTime = dayjs().valueOf();
|
||||
|
||||
bean.msg = await redisPingAsync(this.databaseConnectionString);
|
||||
bean.status = UP;
|
||||
bean.ping = dayjs().valueOf() - startTime;
|
||||
|
||||
} else if (this.type in UptimeKumaServer.monitorTypeList) {
|
||||
let startTime = dayjs().valueOf();
|
||||
const monitorType = UptimeKumaServer.monitorTypeList[this.type];
|
||||
await monitorType.check(this, bean);
|
||||
if (!bean.ping) {
|
||||
bean.ping = dayjs().valueOf() - startTime;
|
||||
}
|
||||
|
||||
} else {
|
||||
bean.msg = "Unknown Monitor Type";
|
||||
bean.status = PENDING;
|
||||
throw new Error("Unknown Monitor Type");
|
||||
}
|
||||
|
||||
if (this.isUpsideDown()) {
|
||||
@@ -709,7 +792,7 @@ class Monitor extends BeanModel {
|
||||
await R.store(bean);
|
||||
|
||||
log.debug("monitor", `[${this.name}] prometheus.update`);
|
||||
prometheus.update(bean, tlsInfo);
|
||||
this.prometheus?.update(bean, tlsInfo);
|
||||
|
||||
previousBeat = bean;
|
||||
|
||||
@@ -748,6 +831,13 @@ class Monitor extends BeanModel {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Make a request using axios
|
||||
* @param {Object} options Options for Axios
|
||||
* @param {boolean} finalCall Should this be the final call i.e
|
||||
* don't retry on faliure
|
||||
* @returns {Object} Axios response
|
||||
*/
|
||||
async makeAxiosRequest(options, finalCall = false) {
|
||||
try {
|
||||
let res;
|
||||
@@ -760,7 +850,6 @@ class Monitor extends BeanModel {
|
||||
domain: this.authDomain,
|
||||
workstation: this.authWorkstation ? this.authWorkstation : undefined
|
||||
});
|
||||
|
||||
} else {
|
||||
res = await axios.request(options);
|
||||
}
|
||||
@@ -787,15 +876,15 @@ class Monitor extends BeanModel {
|
||||
clearTimeout(this.heartbeatInterval);
|
||||
this.isStop = true;
|
||||
|
||||
this.prometheus().remove();
|
||||
this.prometheus?.remove();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a new prometheus instance
|
||||
* @returns {Prometheus}
|
||||
* Get prometheus instance
|
||||
* @returns {Prometheus|undefined}
|
||||
*/
|
||||
prometheus() {
|
||||
return new Prometheus(this);
|
||||
getPrometheus() {
|
||||
return this.prometheus;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -1089,12 +1178,18 @@ class Monitor extends BeanModel {
|
||||
|
||||
for (let notification of notificationList) {
|
||||
try {
|
||||
// Prevent if the msg is undefined, notifications such as Discord cannot send out.
|
||||
const heartbeatJSON = bean.toJSON();
|
||||
|
||||
// Prevent if the msg is undefined, notifications such as Discord cannot send out.
|
||||
if (!heartbeatJSON["msg"]) {
|
||||
heartbeatJSON["msg"] = "N/A";
|
||||
}
|
||||
|
||||
// Also provide the time in server timezone
|
||||
heartbeatJSON["timezone"] = await UptimeKumaServer.getInstance().getTimezone();
|
||||
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, await monitor.toJSON(false), heartbeatJSON);
|
||||
} catch (e) {
|
||||
log.error("monitor", "Cannot send notification to " + notification.name);
|
||||
@@ -1167,11 +1262,11 @@ class Monitor extends BeanModel {
|
||||
*/
|
||||
async sendCertNotificationByTargetDays(certCN, certType, daysRemaining, targetDays, notificationList) {
|
||||
|
||||
let row = await R.getRow("SELECT * FROM notification_sent_history WHERE type = ? AND monitor_id = ? AND days = ?", [
|
||||
"certificate",
|
||||
this.id,
|
||||
targetDays,
|
||||
]);
|
||||
let row = await R.getRow("SELECT * FROM notification_sent_history WHERE type = ? AND monitor_id = ? AND days <= ?", [
|
||||
"certificate",
|
||||
this.id,
|
||||
targetDays,
|
||||
]);
|
||||
|
||||
// Sent already, no need to send again
|
||||
if (row) {
|
||||
@@ -1209,7 +1304,7 @@ class Monitor extends BeanModel {
|
||||
*/
|
||||
static async getPreviousHeartbeat(monitorID) {
|
||||
return await R.getRow(`
|
||||
SELECT status, time FROM heartbeat
|
||||
SELECT ping, status, time FROM heartbeat
|
||||
WHERE id = (select MAX(id) from heartbeat where monitor_id = ?)
|
||||
`, [
|
||||
monitorID
|
||||
@@ -1222,20 +1317,22 @@ class Monitor extends BeanModel {
|
||||
* @returns {Promise<boolean>}
|
||||
*/
|
||||
static async isUnderMaintenance(monitorID) {
|
||||
let activeCondition = Maintenance.getActiveMaintenanceSQLCondition();
|
||||
const maintenance = await R.getRow(`
|
||||
SELECT COUNT(*) AS count
|
||||
FROM monitor_maintenance mm
|
||||
JOIN maintenance
|
||||
ON mm.maintenance_id = maintenance.id
|
||||
AND mm.monitor_id = ?
|
||||
LEFT JOIN maintenance_timeslot
|
||||
ON maintenance_timeslot.maintenance_id = maintenance.id
|
||||
WHERE ${activeCondition}
|
||||
LIMIT 1`, [ monitorID ]);
|
||||
return maintenance.count !== 0;
|
||||
const maintenanceIDList = await R.getCol(`
|
||||
SELECT maintenance_id FROM monitor_maintenance
|
||||
WHERE monitor_id = ?
|
||||
`, [ monitorID ]);
|
||||
|
||||
for (const maintenanceID of maintenanceIDList) {
|
||||
const maintenance = await UptimeKumaServer.getInstance().getMaintenance(maintenanceID);
|
||||
if (maintenance && await maintenance.isUnderMaintenance()) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/** Make sure monitor interval is between bounds */
|
||||
validate() {
|
||||
if (this.interval > MAX_INTERVAL_SECOND) {
|
||||
throw new Error(`Interval cannot be more than ${MAX_INTERVAL_SECOND} seconds`);
|
||||
|
Reference in New Issue
Block a user