mirror of
https://github.com/louislam/uptime-kuma.git
synced 2026-01-31 11:03:11 +08:00
Merge branch 'master' into my_dev
This commit is contained in:
10
package-lock.json
generated
10
package-lock.json
generated
@@ -101,6 +101,7 @@
|
||||
"@playwright/test": "~1.39.0",
|
||||
"@popperjs/core": "~2.10.2",
|
||||
"@testcontainers/hivemq": "^10.13.1",
|
||||
"@testcontainers/mariadb": "^10.13.0",
|
||||
"@testcontainers/mssqlserver": "^10.28.0",
|
||||
"@testcontainers/postgresql": "^11.9.0",
|
||||
"@testcontainers/rabbitmq": "^10.13.2",
|
||||
@@ -5645,6 +5646,15 @@
|
||||
"testcontainers": "^10.28.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@testcontainers/mariadb": {
|
||||
"version": "10.28.0",
|
||||
"resolved": "https://registry.npmjs.org/@testcontainers/mariadb/-/mariadb-10.28.0.tgz",
|
||||
"integrity": "sha512-+ETpRbHOWxEj6uwMfhTVvE6ap0U+olD+v8XbAE2+88YgsHzlmfWWi/EXsOfW1VZsWblYE5kR0k1O//a9Sei4Mg==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"testcontainers": "^10.28.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@testcontainers/mssqlserver": {
|
||||
"version": "10.28.0",
|
||||
"resolved": "https://registry.npmjs.org/@testcontainers/mssqlserver/-/mssqlserver-10.28.0.tgz",
|
||||
|
||||
@@ -162,6 +162,7 @@
|
||||
"@playwright/test": "~1.39.0",
|
||||
"@popperjs/core": "~2.10.2",
|
||||
"@testcontainers/hivemq": "^10.13.1",
|
||||
"@testcontainers/mariadb": "^10.13.0",
|
||||
"@testcontainers/mssqlserver": "^10.28.0",
|
||||
"@testcontainers/postgresql": "^11.9.0",
|
||||
"@testcontainers/rabbitmq": "^10.13.2",
|
||||
|
||||
@@ -8,7 +8,7 @@ const { log, UP, DOWN, PENDING, MAINTENANCE, flipStatus, MAX_INTERVAL_SECOND, MI
|
||||
PING_COUNT_MIN, PING_COUNT_MAX, PING_COUNT_DEFAULT,
|
||||
PING_PER_REQUEST_TIMEOUT_MIN, PING_PER_REQUEST_TIMEOUT_MAX, PING_PER_REQUEST_TIMEOUT_DEFAULT
|
||||
} = require("../../src/util");
|
||||
const { ping, checkCertificate, checkStatusCode, getTotalClientInRoom, setting, mysqlQuery, setSetting, httpNtlm, radius,
|
||||
const { ping, checkCertificate, checkStatusCode, getTotalClientInRoom, setting, setSetting, httpNtlm, radius,
|
||||
kafkaProducerAsync, getOidcTokenClientCredentials, rootCertificatesFingerprints, axiosAbortSignal, checkCertificateHostname
|
||||
} = require("../util-server");
|
||||
const { R } = require("redbean-node");
|
||||
@@ -781,16 +781,6 @@ class Monitor extends BeanModel {
|
||||
bean.status = UP;
|
||||
bean.msg = `Container has not reported health and is currently ${res.data.State.Status}. As it is running, it is considered UP. Consider adding a health check for better service visibility`;
|
||||
}
|
||||
} else if (this.type === "mysql") {
|
||||
let startTime = dayjs().valueOf();
|
||||
|
||||
// Use `radius_password` as `password` field, since there are too many unnecessary fields
|
||||
// TODO: rename `radius_password` to `password` later for general use
|
||||
let mysqlPassword = this.radiusPassword;
|
||||
|
||||
bean.msg = await mysqlQuery(this.databaseConnectionString, this.databaseQuery || "SELECT 1", mysqlPassword);
|
||||
bean.status = UP;
|
||||
bean.ping = dayjs().valueOf() - startTime;
|
||||
} else if (this.type === "radius") {
|
||||
let startTime = dayjs().valueOf();
|
||||
|
||||
|
||||
@@ -21,53 +21,92 @@ class MssqlMonitorType extends MonitorType {
|
||||
* @inheritdoc
|
||||
*/
|
||||
async check(monitor, heartbeat, _server) {
|
||||
let startTime = dayjs().valueOf();
|
||||
|
||||
let query = monitor.databaseQuery;
|
||||
// No query provided by user, use SELECT 1
|
||||
if (!query || (typeof query === "string" && query.trim() === "")) {
|
||||
query = "SELECT 1";
|
||||
}
|
||||
|
||||
let result;
|
||||
const conditions = monitor.conditions ? ConditionExpressionGroup.fromMonitor(monitor) : null;
|
||||
const hasConditions = conditions && conditions.children && conditions.children.length > 0;
|
||||
|
||||
const startTime = dayjs().valueOf();
|
||||
try {
|
||||
result = await this.mssqlQuery(
|
||||
monitor.databaseConnectionString,
|
||||
query
|
||||
);
|
||||
if (hasConditions) {
|
||||
// When conditions are enabled, expect a single value result
|
||||
const result = await this.mssqlQuerySingleValue(
|
||||
monitor.databaseConnectionString,
|
||||
query
|
||||
);
|
||||
heartbeat.ping = dayjs().valueOf() - startTime;
|
||||
|
||||
const conditionsResult = evaluateExpressionGroup(conditions, { result: String(result) });
|
||||
|
||||
if (!conditionsResult) {
|
||||
throw new Error(`Query result did not meet the specified conditions (${result})`);
|
||||
}
|
||||
|
||||
heartbeat.status = UP;
|
||||
heartbeat.msg = "Query did meet specified conditions";
|
||||
} else {
|
||||
// Backwards compatible: just check connection and return row count
|
||||
const result = await this.mssqlQuery(
|
||||
monitor.databaseConnectionString,
|
||||
query
|
||||
);
|
||||
heartbeat.ping = dayjs().valueOf() - startTime;
|
||||
heartbeat.status = UP;
|
||||
heartbeat.msg = result;
|
||||
}
|
||||
} catch (error) {
|
||||
log.error("sqlserver", "Database query failed:", error.message);
|
||||
throw new Error(
|
||||
`Database connection/query failed: ${error.message}`
|
||||
);
|
||||
} finally {
|
||||
heartbeat.ping = dayjs().valueOf() - startTime;
|
||||
// Re-throw condition errors as-is, wrap database errors
|
||||
if (error.message.includes("did not meet the specified conditions")) {
|
||||
throw error;
|
||||
}
|
||||
throw new Error(`Database connection/query failed: ${error.message}`);
|
||||
}
|
||||
|
||||
const conditions = ConditionExpressionGroup.fromMonitor(monitor);
|
||||
const handleConditions = (data) =>
|
||||
conditions ? evaluateExpressionGroup(conditions, data) : true;
|
||||
|
||||
// Since result is now a single value, pass it directly to conditions
|
||||
const conditionsResult = handleConditions({ result: String(result) });
|
||||
|
||||
if (!conditionsResult) {
|
||||
throw new Error(
|
||||
`Query result did not meet the specified conditions (${result})`
|
||||
);
|
||||
}
|
||||
|
||||
heartbeat.msg = "";
|
||||
heartbeat.status = UP;
|
||||
}
|
||||
|
||||
/**
|
||||
* Run a query on MSSQL server
|
||||
* Run a query on MSSQL server (backwards compatible - returns row count)
|
||||
* @param {string} connectionString The database connection string
|
||||
* @param {string} query The query to validate the database with
|
||||
* @returns {Promise<string>} Row count message
|
||||
*/
|
||||
async mssqlQuery(connectionString, query) {
|
||||
let pool;
|
||||
try {
|
||||
pool = new mssql.ConnectionPool(connectionString);
|
||||
await pool.connect();
|
||||
const result = await pool.request().query(query);
|
||||
|
||||
if (result.recordset) {
|
||||
return "Rows: " + result.recordset.length;
|
||||
} else {
|
||||
return "No Error, but the result is not an array. Type: " + typeof result.recordset;
|
||||
}
|
||||
} catch (err) {
|
||||
log.debug(
|
||||
"sqlserver",
|
||||
"Error caught in the query execution.",
|
||||
err.message
|
||||
);
|
||||
throw err;
|
||||
} finally {
|
||||
if (pool) {
|
||||
await pool.close();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Run a query on MSSQL server expecting a single value result
|
||||
* @param {string} connectionString The database connection string
|
||||
* @param {string} query The query to validate the database with
|
||||
* @returns {Promise<any>} Single value from the first column of the first row
|
||||
*/
|
||||
async mssqlQuery(connectionString, query) {
|
||||
async mssqlQuerySingleValue(connectionString, query) {
|
||||
let pool;
|
||||
try {
|
||||
pool = new mssql.ConnectionPool(connectionString);
|
||||
|
||||
165
server/monitor-types/mysql.js
Normal file
165
server/monitor-types/mysql.js
Normal file
@@ -0,0 +1,165 @@
|
||||
const { MonitorType } = require("./monitor-type");
|
||||
const { UP } = require("../../src/util");
|
||||
const dayjs = require("dayjs");
|
||||
const mysql = require("mysql2");
|
||||
const { ConditionVariable } = require("../monitor-conditions/variables");
|
||||
const { defaultStringOperators } = require("../monitor-conditions/operators");
|
||||
const { ConditionExpressionGroup } = require("../monitor-conditions/expression");
|
||||
const { evaluateExpressionGroup } = require("../monitor-conditions/evaluator");
|
||||
|
||||
class MysqlMonitorType extends MonitorType {
|
||||
name = "mysql";
|
||||
|
||||
supportsConditions = true;
|
||||
conditionVariables = [
|
||||
new ConditionVariable("result", defaultStringOperators),
|
||||
];
|
||||
|
||||
/**
|
||||
* @inheritdoc
|
||||
*/
|
||||
async check(monitor, heartbeat, _server) {
|
||||
let query = monitor.databaseQuery;
|
||||
if (!query || (typeof query === "string" && query.trim() === "")) {
|
||||
query = "SELECT 1";
|
||||
}
|
||||
|
||||
// Use `radius_password` as `password` field for backwards compatibility
|
||||
// TODO: rename `radius_password` to `password` later for general use
|
||||
const password = monitor.radiusPassword;
|
||||
|
||||
const conditions = monitor.conditions ? ConditionExpressionGroup.fromMonitor(monitor) : null;
|
||||
const hasConditions = conditions && conditions.children && conditions.children.length > 0;
|
||||
|
||||
const startTime = dayjs().valueOf();
|
||||
try {
|
||||
if (hasConditions) {
|
||||
// When conditions are enabled, expect a single value result
|
||||
const result = await this.mysqlQuerySingleValue(monitor.databaseConnectionString, query, password);
|
||||
heartbeat.ping = dayjs().valueOf() - startTime;
|
||||
|
||||
const conditionsResult = evaluateExpressionGroup(conditions, { result: String(result) });
|
||||
|
||||
if (!conditionsResult) {
|
||||
throw new Error(`Query result did not meet the specified conditions (${result})`);
|
||||
}
|
||||
|
||||
heartbeat.status = UP;
|
||||
heartbeat.msg = "Query did meet specified conditions";
|
||||
} else {
|
||||
// Backwards compatible: just check connection and return row count
|
||||
const result = await this.mysqlQuery(monitor.databaseConnectionString, query, password);
|
||||
heartbeat.ping = dayjs().valueOf() - startTime;
|
||||
heartbeat.status = UP;
|
||||
heartbeat.msg = result;
|
||||
}
|
||||
} catch (error) {
|
||||
heartbeat.ping = dayjs().valueOf() - startTime;
|
||||
// Re-throw condition errors as-is, wrap database errors
|
||||
if (error.message.includes("did not meet the specified conditions")) {
|
||||
throw error;
|
||||
}
|
||||
throw new Error(`Database connection/query failed: ${error.message}`);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Run a query on MySQL/MariaDB (backwards compatible - returns row count)
|
||||
* @param {string} connectionString The database connection string
|
||||
* @param {string} query The query to execute
|
||||
* @param {string} password Optional password override
|
||||
* @returns {Promise<string>} Row count message
|
||||
*/
|
||||
mysqlQuery(connectionString, query, password = undefined) {
|
||||
return new Promise((resolve, reject) => {
|
||||
const connection = mysql.createConnection({
|
||||
uri: connectionString,
|
||||
password
|
||||
});
|
||||
|
||||
connection.on("error", (err) => {
|
||||
reject(err);
|
||||
});
|
||||
|
||||
connection.query(query, (err, res) => {
|
||||
try {
|
||||
connection.end();
|
||||
} catch (_) {
|
||||
connection.destroy();
|
||||
}
|
||||
|
||||
if (err) {
|
||||
reject(err);
|
||||
return;
|
||||
}
|
||||
|
||||
if (Array.isArray(res)) {
|
||||
resolve("Rows: " + res.length);
|
||||
} else {
|
||||
resolve("No Error, but the result is not an array. Type: " + typeof res);
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Run a query on MySQL/MariaDB expecting a single value result
|
||||
* @param {string} connectionString The database connection string
|
||||
* @param {string} query The query to execute
|
||||
* @param {string} password Optional password override
|
||||
* @returns {Promise<any>} Single value from the first column of the first row
|
||||
*/
|
||||
mysqlQuerySingleValue(connectionString, query, password = undefined) {
|
||||
return new Promise((resolve, reject) => {
|
||||
const connection = mysql.createConnection({
|
||||
uri: connectionString,
|
||||
password
|
||||
});
|
||||
|
||||
connection.on("error", (err) => {
|
||||
reject(err);
|
||||
});
|
||||
|
||||
connection.query(query, (err, res) => {
|
||||
try {
|
||||
connection.end();
|
||||
} catch (_) {
|
||||
connection.destroy();
|
||||
}
|
||||
|
||||
if (err) {
|
||||
reject(err);
|
||||
return;
|
||||
}
|
||||
|
||||
// Check if we have results
|
||||
if (!Array.isArray(res) || res.length === 0) {
|
||||
reject(new Error("Query returned no results"));
|
||||
return;
|
||||
}
|
||||
|
||||
// Check if we have multiple rows
|
||||
if (res.length > 1) {
|
||||
reject(new Error("Multiple values were found, expected only one value"));
|
||||
return;
|
||||
}
|
||||
|
||||
const firstRow = res[0];
|
||||
const columnNames = Object.keys(firstRow);
|
||||
|
||||
// Check if we have multiple columns
|
||||
if (columnNames.length > 1) {
|
||||
reject(new Error("Multiple columns were found, expected only one value"));
|
||||
return;
|
||||
}
|
||||
|
||||
// Return the single value from the first (and only) column
|
||||
resolve(firstRow[columnNames[0]]);
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
MysqlMonitorType,
|
||||
};
|
||||
@@ -128,6 +128,7 @@ class UptimeKumaServer {
|
||||
UptimeKumaServer.monitorTypeList["redis"] = new RedisMonitorType();
|
||||
UptimeKumaServer.monitorTypeList["system-service"] = new SystemServiceMonitorType();
|
||||
UptimeKumaServer.monitorTypeList["sqlserver"] = new MssqlMonitorType();
|
||||
UptimeKumaServer.monitorTypeList["mysql"] = new MysqlMonitorType();
|
||||
|
||||
// Allow all CORS origins (polling) in development
|
||||
let cors = undefined;
|
||||
@@ -580,5 +581,6 @@ const { ManualMonitorType } = require("./monitor-types/manual");
|
||||
const { RedisMonitorType } = require("./monitor-types/redis");
|
||||
const { SystemServiceMonitorType } = require("./monitor-types/system-service");
|
||||
const { MssqlMonitorType } = require("./monitor-types/mssql");
|
||||
const { MysqlMonitorType } = require("./monitor-types/mysql");
|
||||
const Monitor = require("./model/monitor");
|
||||
|
||||
|
||||
@@ -10,7 +10,6 @@ const { Resolver } = require("dns");
|
||||
const iconv = require("iconv-lite");
|
||||
const chardet = require("chardet");
|
||||
const chroma = require("chroma-js");
|
||||
const mysql = require("mysql2");
|
||||
const { NtlmClient } = require("./modules/axios-ntlm/lib/ntlmClient.js");
|
||||
const { Settings } = require("./settings");
|
||||
const RadiusClient = require("./radius-client");
|
||||
@@ -321,44 +320,6 @@ exports.dnsResolve = function (hostname, resolverServer, resolverPort, rrtype) {
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Run a query on MySQL/MariaDB
|
||||
* @param {string} connectionString The database connection string
|
||||
* @param {string} query The query to validate the database with
|
||||
* @param {?string} password The password to use
|
||||
* @returns {Promise<(string)>} Response from server
|
||||
*/
|
||||
exports.mysqlQuery = function (connectionString, query, password = undefined) {
|
||||
return new Promise((resolve, reject) => {
|
||||
const connection = mysql.createConnection({
|
||||
uri: connectionString,
|
||||
password
|
||||
});
|
||||
|
||||
connection.on("error", (err) => {
|
||||
reject(err);
|
||||
});
|
||||
|
||||
connection.query(query, (err, res) => {
|
||||
if (err) {
|
||||
reject(err);
|
||||
} else {
|
||||
if (Array.isArray(res)) {
|
||||
resolve("Rows: " + res.length);
|
||||
} else {
|
||||
resolve("No Error, but the result is not an array. Type: " + typeof res);
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
connection.end();
|
||||
} catch (_) {
|
||||
connection.destroy();
|
||||
}
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Query radius server
|
||||
* @param {string} hostname Hostname of radius server
|
||||
|
||||
@@ -6,20 +6,25 @@ const { UP, PENDING } = require("../../../src/util");
|
||||
|
||||
/**
|
||||
* Helper function to create and start a MSSQL container
|
||||
* @returns {Promise<MSSQLServerContainer>} The started MSSQL container
|
||||
* @returns {Promise<{container: MSSQLServerContainer, connectionString: string}>} The started container and connection string
|
||||
*/
|
||||
async function createAndStartMSSQLContainer() {
|
||||
return await new MSSQLServerContainer(
|
||||
const container = await new MSSQLServerContainer(
|
||||
"mcr.microsoft.com/mssql/server:2022-latest"
|
||||
)
|
||||
.acceptLicense()
|
||||
// The default timeout of 30 seconds might not be enough for the container to start
|
||||
.withStartupTimeout(60000)
|
||||
.start();
|
||||
|
||||
return {
|
||||
container,
|
||||
connectionString: container.getConnectionUri(false)
|
||||
};
|
||||
}
|
||||
|
||||
describe(
|
||||
"MSSQL Single Node",
|
||||
"MSSQL Monitor",
|
||||
{
|
||||
skip:
|
||||
!!process.env.CI &&
|
||||
@@ -27,55 +32,63 @@ describe(
|
||||
},
|
||||
() => {
|
||||
test("check() sets status to UP when MSSQL server is reachable", async () => {
|
||||
let mssqlContainer;
|
||||
const { container, connectionString } = await createAndStartMSSQLContainer();
|
||||
|
||||
const mssqlMonitor = new MssqlMonitorType();
|
||||
const monitor = {
|
||||
databaseConnectionString: connectionString,
|
||||
conditions: "[]",
|
||||
};
|
||||
|
||||
const heartbeat = {
|
||||
msg: "",
|
||||
status: PENDING,
|
||||
};
|
||||
|
||||
try {
|
||||
mssqlContainer = await createAndStartMSSQLContainer();
|
||||
|
||||
const mssqlMonitor = new MssqlMonitorType();
|
||||
const monitor = {
|
||||
databaseConnectionString:
|
||||
mssqlContainer.getConnectionUri(false),
|
||||
conditions: "[]",
|
||||
};
|
||||
|
||||
const heartbeat = {
|
||||
msg: "",
|
||||
status: PENDING,
|
||||
};
|
||||
|
||||
await mssqlMonitor.check(monitor, heartbeat, {});
|
||||
assert.strictEqual(
|
||||
heartbeat.status,
|
||||
UP,
|
||||
`Expected status ${UP} but got ${heartbeat.status}`
|
||||
);
|
||||
} catch (error) {
|
||||
console.error("Test failed with error:", error.message);
|
||||
console.error("Error stack:", error.stack);
|
||||
if (mssqlContainer) {
|
||||
console.error("Container ID:", mssqlContainer.getId());
|
||||
console.error(
|
||||
"Container logs:",
|
||||
await mssqlContainer.logs()
|
||||
);
|
||||
}
|
||||
throw error;
|
||||
} finally {
|
||||
if (mssqlContainer) {
|
||||
console.log("Stopping MSSQL container...");
|
||||
await mssqlContainer.stop();
|
||||
}
|
||||
await container.stop();
|
||||
}
|
||||
});
|
||||
|
||||
test("check() sets status to UP when custom query returns single value", async () => {
|
||||
const mssqlContainer = await createAndStartMSSQLContainer();
|
||||
|
||||
test("check() rejects when MSSQL server is not reachable", async () => {
|
||||
const mssqlMonitor = new MssqlMonitorType();
|
||||
const monitor = {
|
||||
databaseConnectionString:
|
||||
mssqlContainer.getConnectionUri(false),
|
||||
"Server=localhost,15433;Database=master;User Id=Fail;Password=Fail;Encrypt=false",
|
||||
conditions: "[]",
|
||||
};
|
||||
|
||||
const heartbeat = {
|
||||
msg: "",
|
||||
status: PENDING,
|
||||
};
|
||||
|
||||
await assert.rejects(
|
||||
mssqlMonitor.check(monitor, heartbeat, {}),
|
||||
new Error(
|
||||
"Database connection/query failed: Failed to connect to localhost:15433 - Could not connect (sequence)"
|
||||
)
|
||||
);
|
||||
assert.notStrictEqual(
|
||||
heartbeat.status,
|
||||
UP,
|
||||
`Expected status should not be ${heartbeat.status}`
|
||||
);
|
||||
});
|
||||
|
||||
test("check() sets status to UP when custom query returns single value", async () => {
|
||||
const { container, connectionString } = await createAndStartMSSQLContainer();
|
||||
|
||||
const mssqlMonitor = new MssqlMonitorType();
|
||||
const monitor = {
|
||||
databaseConnectionString: connectionString,
|
||||
databaseQuery: "SELECT 42",
|
||||
conditions: "[]",
|
||||
};
|
||||
@@ -93,18 +106,17 @@ describe(
|
||||
`Expected status ${UP} but got ${heartbeat.status}`
|
||||
);
|
||||
} finally {
|
||||
await mssqlContainer.stop();
|
||||
await container.stop();
|
||||
}
|
||||
});
|
||||
|
||||
test("check() sets status to UP when custom query result meets condition", async () => {
|
||||
const mssqlContainer = await createAndStartMSSQLContainer();
|
||||
const { container, connectionString } = await createAndStartMSSQLContainer();
|
||||
|
||||
const mssqlMonitor = new MssqlMonitorType();
|
||||
const monitor = {
|
||||
databaseConnectionString:
|
||||
mssqlContainer.getConnectionUri(false),
|
||||
databaseQuery: "SELECT 42 as value",
|
||||
databaseConnectionString: connectionString,
|
||||
databaseQuery: "SELECT 42 AS value",
|
||||
conditions: JSON.stringify([
|
||||
{
|
||||
type: "expression",
|
||||
@@ -129,18 +141,17 @@ describe(
|
||||
`Expected status ${UP} but got ${heartbeat.status}`
|
||||
);
|
||||
} finally {
|
||||
await mssqlContainer.stop();
|
||||
await container.stop();
|
||||
}
|
||||
});
|
||||
|
||||
test("check() rejects when custom query result does not meet condition", async () => {
|
||||
const mssqlContainer = await createAndStartMSSQLContainer();
|
||||
const { container, connectionString } = await createAndStartMSSQLContainer();
|
||||
|
||||
const mssqlMonitor = new MssqlMonitorType();
|
||||
const monitor = {
|
||||
databaseConnectionString:
|
||||
mssqlContainer.getConnectionUri(false),
|
||||
databaseQuery: "SELECT 99 as value",
|
||||
databaseConnectionString: connectionString,
|
||||
databaseQuery: "SELECT 99 AS value",
|
||||
conditions: JSON.stringify([
|
||||
{
|
||||
type: "expression",
|
||||
@@ -170,19 +181,26 @@ describe(
|
||||
`Expected status should not be ${heartbeat.status}`
|
||||
);
|
||||
} finally {
|
||||
await mssqlContainer.stop();
|
||||
await container.stop();
|
||||
}
|
||||
});
|
||||
|
||||
test("check() rejects when query returns no results", async () => {
|
||||
const mssqlContainer = await createAndStartMSSQLContainer();
|
||||
test("check() rejects when query returns no results with conditions", async () => {
|
||||
const { container, connectionString } = await createAndStartMSSQLContainer();
|
||||
|
||||
const mssqlMonitor = new MssqlMonitorType();
|
||||
const monitor = {
|
||||
databaseConnectionString:
|
||||
mssqlContainer.getConnectionUri(false),
|
||||
databaseConnectionString: connectionString,
|
||||
databaseQuery: "SELECT 1 WHERE 1 = 0",
|
||||
conditions: "[]",
|
||||
conditions: JSON.stringify([
|
||||
{
|
||||
type: "expression",
|
||||
andOr: "and",
|
||||
variable: "result",
|
||||
operator: "equals",
|
||||
value: "1",
|
||||
},
|
||||
]),
|
||||
};
|
||||
|
||||
const heartbeat = {
|
||||
@@ -203,19 +221,26 @@ describe(
|
||||
`Expected status should not be ${heartbeat.status}`
|
||||
);
|
||||
} finally {
|
||||
await mssqlContainer.stop();
|
||||
await container.stop();
|
||||
}
|
||||
});
|
||||
|
||||
test("check() rejects when query returns multiple rows", async () => {
|
||||
const mssqlContainer = await createAndStartMSSQLContainer();
|
||||
test("check() rejects when query returns multiple rows with conditions", async () => {
|
||||
const { container, connectionString } = await createAndStartMSSQLContainer();
|
||||
|
||||
const mssqlMonitor = new MssqlMonitorType();
|
||||
const monitor = {
|
||||
databaseConnectionString:
|
||||
mssqlContainer.getConnectionUri(false),
|
||||
databaseConnectionString: connectionString,
|
||||
databaseQuery: "SELECT 1 UNION ALL SELECT 2",
|
||||
conditions: "[]",
|
||||
conditions: JSON.stringify([
|
||||
{
|
||||
type: "expression",
|
||||
andOr: "and",
|
||||
variable: "result",
|
||||
operator: "equals",
|
||||
value: "1",
|
||||
},
|
||||
]),
|
||||
};
|
||||
|
||||
const heartbeat = {
|
||||
@@ -236,19 +261,26 @@ describe(
|
||||
`Expected status should not be ${heartbeat.status}`
|
||||
);
|
||||
} finally {
|
||||
await mssqlContainer.stop();
|
||||
await container.stop();
|
||||
}
|
||||
});
|
||||
|
||||
test("check() rejects when query returns multiple columns", async () => {
|
||||
const mssqlContainer = await createAndStartMSSQLContainer();
|
||||
test("check() rejects when query returns multiple columns with conditions", async () => {
|
||||
const { container, connectionString } = await createAndStartMSSQLContainer();
|
||||
|
||||
const mssqlMonitor = new MssqlMonitorType();
|
||||
const monitor = {
|
||||
databaseConnectionString:
|
||||
mssqlContainer.getConnectionUri(false),
|
||||
databaseConnectionString: connectionString,
|
||||
databaseQuery: "SELECT 1 AS col1, 2 AS col2",
|
||||
conditions: "[]",
|
||||
conditions: JSON.stringify([
|
||||
{
|
||||
type: "expression",
|
||||
andOr: "and",
|
||||
variable: "result",
|
||||
operator: "equals",
|
||||
value: "1",
|
||||
},
|
||||
]),
|
||||
};
|
||||
|
||||
const heartbeat = {
|
||||
@@ -269,34 +301,8 @@ describe(
|
||||
`Expected status should not be ${heartbeat.status}`
|
||||
);
|
||||
} finally {
|
||||
await mssqlContainer.stop();
|
||||
await container.stop();
|
||||
}
|
||||
});
|
||||
|
||||
test("check() rejects when MSSQL server is not reachable", async () => {
|
||||
const mssqlMonitor = new MssqlMonitorType();
|
||||
const monitor = {
|
||||
databaseConnectionString:
|
||||
"Server=localhost,15433;Database=master;User Id=Fail;Password=Fail;Encrypt=false",
|
||||
conditions: "[]",
|
||||
};
|
||||
|
||||
const heartbeat = {
|
||||
msg: "",
|
||||
status: PENDING,
|
||||
};
|
||||
|
||||
await assert.rejects(
|
||||
mssqlMonitor.check(monitor, heartbeat, {}),
|
||||
new Error(
|
||||
"Database connection/query failed: Failed to connect to localhost:15433 - Could not connect (sequence)"
|
||||
)
|
||||
);
|
||||
assert.notStrictEqual(
|
||||
heartbeat.status,
|
||||
UP,
|
||||
`Expected status should not be ${heartbeat.status}`
|
||||
);
|
||||
});
|
||||
}
|
||||
);
|
||||
|
||||
163
test/backend-test/monitors/test-mysql.js
Normal file
163
test/backend-test/monitors/test-mysql.js
Normal file
@@ -0,0 +1,163 @@
|
||||
const { describe, test } = require("node:test");
|
||||
const assert = require("node:assert");
|
||||
const { MariaDbContainer } = require("@testcontainers/mariadb");
|
||||
const { MysqlMonitorType } = require("../../../server/monitor-types/mysql");
|
||||
const { UP, PENDING } = require("../../../src/util");
|
||||
|
||||
/**
|
||||
* Helper function to create and start a MariaDB container
|
||||
* @returns {Promise<{container: MariaDbContainer, connectionString: string}>} The started container and connection string
|
||||
*/
|
||||
async function createAndStartMariaDBContainer() {
|
||||
const container = await new MariaDbContainer("mariadb:10.11")
|
||||
.withStartupTimeout(90000)
|
||||
.start();
|
||||
|
||||
const connectionString = `mysql://${container.getUsername()}:${container.getUserPassword()}@${container.getHost()}:${container.getPort()}/${container.getDatabase()}`;
|
||||
|
||||
return {
|
||||
container,
|
||||
connectionString
|
||||
};
|
||||
}
|
||||
|
||||
describe(
|
||||
"MySQL/MariaDB Monitor",
|
||||
{
|
||||
skip:
|
||||
!!process.env.CI &&
|
||||
(process.platform !== "linux" || process.arch !== "x64"),
|
||||
},
|
||||
() => {
|
||||
test("check() sets status to UP when MariaDB server is reachable", async () => {
|
||||
const { container, connectionString } = await createAndStartMariaDBContainer();
|
||||
|
||||
const mysqlMonitor = new MysqlMonitorType();
|
||||
const monitor = {
|
||||
databaseConnectionString: connectionString,
|
||||
conditions: "[]",
|
||||
};
|
||||
|
||||
const heartbeat = {
|
||||
msg: "",
|
||||
status: PENDING,
|
||||
};
|
||||
|
||||
try {
|
||||
await mysqlMonitor.check(monitor, heartbeat, {});
|
||||
assert.strictEqual(
|
||||
heartbeat.status,
|
||||
UP,
|
||||
`Expected status ${UP} but got ${heartbeat.status}`
|
||||
);
|
||||
} finally {
|
||||
await container.stop();
|
||||
}
|
||||
});
|
||||
|
||||
test("check() rejects when MariaDB server is not reachable", async () => {
|
||||
const mysqlMonitor = new MysqlMonitorType();
|
||||
const monitor = {
|
||||
databaseConnectionString:
|
||||
"mysql://invalid:invalid@localhost:13306/test",
|
||||
conditions: "[]",
|
||||
};
|
||||
|
||||
const heartbeat = {
|
||||
msg: "",
|
||||
status: PENDING,
|
||||
};
|
||||
|
||||
await assert.rejects(
|
||||
mysqlMonitor.check(monitor, heartbeat, {}),
|
||||
(err) => {
|
||||
assert.ok(
|
||||
err.message.includes("Database connection/query failed"),
|
||||
`Expected error message to include "Database connection/query failed" but got: ${err.message}`
|
||||
);
|
||||
return true;
|
||||
}
|
||||
);
|
||||
assert.notStrictEqual(
|
||||
heartbeat.status,
|
||||
UP,
|
||||
`Expected status should not be ${UP}`
|
||||
);
|
||||
});
|
||||
|
||||
test("check() sets status to UP when custom query result meets condition", async () => {
|
||||
const { container, connectionString } = await createAndStartMariaDBContainer();
|
||||
|
||||
const mysqlMonitor = new MysqlMonitorType();
|
||||
const monitor = {
|
||||
databaseConnectionString: connectionString,
|
||||
databaseQuery: "SELECT 42 AS value",
|
||||
conditions: JSON.stringify([
|
||||
{
|
||||
type: "expression",
|
||||
andOr: "and",
|
||||
variable: "result",
|
||||
operator: "equals",
|
||||
value: "42",
|
||||
},
|
||||
]),
|
||||
};
|
||||
|
||||
const heartbeat = {
|
||||
msg: "",
|
||||
status: PENDING,
|
||||
};
|
||||
|
||||
try {
|
||||
await mysqlMonitor.check(monitor, heartbeat, {});
|
||||
assert.strictEqual(
|
||||
heartbeat.status,
|
||||
UP,
|
||||
`Expected status ${UP} but got ${heartbeat.status}`
|
||||
);
|
||||
} finally {
|
||||
await container.stop();
|
||||
}
|
||||
});
|
||||
|
||||
test("check() rejects when custom query result does not meet condition", async () => {
|
||||
const { container, connectionString } = await createAndStartMariaDBContainer();
|
||||
|
||||
const mysqlMonitor = new MysqlMonitorType();
|
||||
const monitor = {
|
||||
databaseConnectionString: connectionString,
|
||||
databaseQuery: "SELECT 99 AS value",
|
||||
conditions: JSON.stringify([
|
||||
{
|
||||
type: "expression",
|
||||
andOr: "and",
|
||||
variable: "result",
|
||||
operator: "equals",
|
||||
value: "42",
|
||||
},
|
||||
]),
|
||||
};
|
||||
|
||||
const heartbeat = {
|
||||
msg: "",
|
||||
status: PENDING,
|
||||
};
|
||||
|
||||
try {
|
||||
await assert.rejects(
|
||||
mysqlMonitor.check(monitor, heartbeat, {}),
|
||||
new Error(
|
||||
"Query result did not meet the specified conditions (99)"
|
||||
)
|
||||
);
|
||||
assert.strictEqual(
|
||||
heartbeat.status,
|
||||
PENDING,
|
||||
`Expected status should not be ${heartbeat.status}`
|
||||
);
|
||||
} finally {
|
||||
await container.stop();
|
||||
}
|
||||
});
|
||||
}
|
||||
);
|
||||
Reference in New Issue
Block a user