diff --git a/server/monitor-types/mysql.js b/server/monitor-types/mysql.js index 5b2c2c050..549d2db02 100644 --- a/server/monitor-types/mysql.js +++ b/server/monitor-types/mysql.js @@ -30,39 +30,84 @@ class MysqlMonitorType extends MonitorType { // TODO: rename `radius_password` to `password` later for general use const password = monitor.radiusPassword; - let result; + const conditions = ConditionExpressionGroup.fromMonitor(monitor); + const hasConditions = conditions && conditions.length > 0; + try { - result = await this.mysqlQuery(monitor.databaseConnectionString, query, password); + 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.msg = ""; + } 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.msg = result; + } } catch (error) { + heartbeat.ping = dayjs().valueOf() - startTime; log.error("mysql", "Database query failed:", error.message); throw new Error(`Database connection/query failed: ${error.message}`); - } finally { - heartbeat.ping = dayjs().valueOf() - startTime; } - 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 MySQL/MariaDB + * 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} 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} Single value from the first column of the first row */ - mysqlQuery(connectionString, query, password = undefined) { + mysqlQuerySingleValue(connectionString, query, password = undefined) { return new Promise((resolve, reject) => { const connection = mysql.createConnection({ uri: connectionString, diff --git a/test/backend-test/monitors/test-mysql.js b/test/backend-test/monitors/test-mysql.js index 898b79e95..d14a6a1a6 100644 --- a/test/backend-test/monitors/test-mysql.js +++ b/test/backend-test/monitors/test-mysql.js @@ -6,12 +6,19 @@ const { UP, PENDING } = require("../../../src/util"); /** * Helper function to create and start a MariaDB container - * @returns {Promise} The started MariaDB container + * @returns {Promise<{container: MariaDbContainer, connectionString: string}>} The started container and connection string */ async function createAndStartMariaDBContainer() { - return await new MariaDbContainer("mariadb:10.11") + 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( @@ -23,11 +30,11 @@ describe( }, () => { test("check() sets status to UP when MariaDB server is reachable", async () => { - const mariadbContainer = await createAndStartMariaDBContainer(); + const { container, connectionString } = await createAndStartMariaDBContainer(); const mysqlMonitor = new MysqlMonitorType(); const monitor = { - databaseConnectionString: `mysql://${mariadbContainer.getUsername()}:${mariadbContainer.getUserPassword()}@${mariadbContainer.getHost()}:${mariadbContainer.getPort()}/${mariadbContainer.getDatabase()}`, + databaseConnectionString: connectionString, conditions: "[]", }; @@ -44,7 +51,7 @@ describe( `Expected status ${UP} but got ${heartbeat.status}` ); } finally { - await mariadbContainer.stop(); + await container.stop(); } }); @@ -79,11 +86,11 @@ describe( }); test("check() sets status to UP when custom query result meets condition", async () => { - const mariadbContainer = await createAndStartMariaDBContainer(); + const { container, connectionString } = await createAndStartMariaDBContainer(); const mysqlMonitor = new MysqlMonitorType(); const monitor = { - databaseConnectionString: `mysql://${mariadbContainer.getUsername()}:${mariadbContainer.getUserPassword()}@${mariadbContainer.getHost()}:${mariadbContainer.getPort()}/${mariadbContainer.getDatabase()}`, + databaseConnectionString: connectionString, databaseQuery: "SELECT 42 AS value", conditions: JSON.stringify([ { @@ -109,16 +116,16 @@ describe( `Expected status ${UP} but got ${heartbeat.status}` ); } finally { - await mariadbContainer.stop(); + await container.stop(); } }); test("check() rejects when custom query result does not meet condition", async () => { - const mariadbContainer = await createAndStartMariaDBContainer(); + const { container, connectionString } = await createAndStartMariaDBContainer(); const mysqlMonitor = new MysqlMonitorType(); const monitor = { - databaseConnectionString: `mysql://${mariadbContainer.getUsername()}:${mariadbContainer.getUserPassword()}@${mariadbContainer.getHost()}:${mariadbContainer.getPort()}/${mariadbContainer.getDatabase()}`, + databaseConnectionString: connectionString, databaseQuery: "SELECT 99 AS value", conditions: JSON.stringify([ { @@ -149,7 +156,7 @@ describe( `Expected status should not be ${heartbeat.status}` ); } finally { - await mariadbContainer.stop(); + await container.stop(); } }); }