feat: added monitoring for postgres query result (#6736)

Co-authored-by: Dalton Pearson <dalton.pearson@praemo.com>
This commit is contained in:
Dalton Pearson
2026-01-14 23:48:48 +01:00
committed by GitHub
co-authored by Dalton Pearson
parent e022b5f976
commit d7296c6629
2 changed files with 308 additions and 6 deletions
+110 -6
View File
@@ -3,26 +3,61 @@ const { log, UP } = require("../../src/util");
const dayjs = require("dayjs");
const postgresConParse = require("pg-connection-string").parse;
const { Client } = require("pg");
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 PostgresMonitorType extends MonitorType {
name = "postgres";
supportsConditions = true;
conditionVariables = [new ConditionVariable("result", defaultStringOperators)];
/**
* @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";
}
await this.postgresQuery(monitor.databaseConnectionString, query);
heartbeat.msg = "";
heartbeat.status = UP;
heartbeat.ping = dayjs().valueOf() - startTime;
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.postgresQuerySingleValue(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.postgresQuery(monitor.databaseConnectionString, query);
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}`);
}
}
/**
@@ -76,6 +111,75 @@ class PostgresMonitorType extends MonitorType {
});
});
}
/**
* Run a query on Postgres
* @param {string} connectionString The database connection string
* @param {string} query The query to validate the database with
* @returns {Promise<(string[] | object[] | object)>} Response from
* server
*/
async postgresQuerySingleValue(connectionString, query) {
return new Promise((resolve, reject) => {
const config = postgresConParse(connectionString);
// Fix #3868, which true/false is not parsed to boolean
if (typeof config.ssl === "string") {
config.ssl = config.ssl === "true";
}
if (config.password === "") {
// See https://github.com/brianc/node-postgres/issues/1927
reject(new Error("Password is undefined."));
return;
}
const client = new Client(config);
client.on("error", (error) => {
log.debug(this.name, "Error caught in the error event handler.");
reject(error);
});
client.connect((err) => {
if (err) {
reject(err);
client.end();
} else {
// Connected here
try {
client.query(query, (err, res) => {
if (err) {
reject(err);
} else {
// Check if we have results
if (!res.rows || res.rows.length === 0) {
reject(new Error("Query returned no results"));
return;
}
// Check if we have multiple rows
if (res.rows.length > 1) {
reject(new Error("Multiple values were found, expected only one value"));
return;
}
const firstRow = res.rows[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;
}
resolve(firstRow[columnNames[0]]);
}
client.end();
});
} catch (e) {
reject(e);
client.end();
}
}
});
});
}
}
module.exports = {