Extracted the postgresql monitor to its own monitor-type (#6443)

Co-authored-by: Dalton Pearson <dalton.pearson@praemo.com>
Co-authored-by: Frank Elsinga <frank@elsinga.de>
This commit is contained in:
Dalton Pearson
2025-12-04 09:16:06 -05:00
committed by GitHub
parent 48bc6cd029
commit 91edeaeb9c
7 changed files with 209 additions and 73 deletions

64
package-lock.json generated
View File

@@ -98,6 +98,7 @@
"@playwright/test": "~1.39.0", "@playwright/test": "~1.39.0",
"@popperjs/core": "~2.10.2", "@popperjs/core": "~2.10.2",
"@testcontainers/hivemq": "^10.13.1", "@testcontainers/hivemq": "^10.13.1",
"@testcontainers/postgresql": "^11.9.0",
"@testcontainers/rabbitmq": "^10.13.2", "@testcontainers/rabbitmq": "^10.13.2",
"@types/bootstrap": "~5.1.9", "@types/bootstrap": "~5.1.9",
"@types/node": "^20.8.6", "@types/node": "^20.8.6",
@@ -5680,6 +5681,63 @@
"testcontainers": "^10.28.0" "testcontainers": "^10.28.0"
} }
}, },
"node_modules/@testcontainers/postgresql": {
"version": "11.9.0",
"resolved": "https://registry.npmjs.org/@testcontainers/postgresql/-/postgresql-11.9.0.tgz",
"integrity": "sha512-beLyLdLygFllktviM132Xd6tQ4i5FnuyZP+4BQEjUb5sJYHYnIrV/ZBzRRflIlF8gugt1GXgudkmr/HxM9vtKw==",
"dev": true,
"license": "MIT",
"dependencies": {
"testcontainers": "^11.9.0"
}
},
"node_modules/@testcontainers/postgresql/node_modules/docker-compose": {
"version": "1.3.0",
"resolved": "https://registry.npmjs.org/docker-compose/-/docker-compose-1.3.0.tgz",
"integrity": "sha512-7Gevk/5eGD50+eMD+XDnFnOrruFkL0kSd7jEG4cjmqweDSUhB7i0g8is/nBdVpl+Bx338SqIB2GLKm32M+Vs6g==",
"dev": true,
"license": "MIT",
"dependencies": {
"yaml": "^2.2.2"
},
"engines": {
"node": ">= 6.0.0"
}
},
"node_modules/@testcontainers/postgresql/node_modules/testcontainers": {
"version": "11.9.0",
"resolved": "https://registry.npmjs.org/testcontainers/-/testcontainers-11.9.0.tgz",
"integrity": "sha512-SQ6OqQUig7HcGVF72i+ZVIMvxPSpEz8cgC/B63ekqMzgf98DnveoBbOmqux/Wa5wQAQCt4mEPNMa/Jz7vMg9fQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"@balena/dockerignore": "^1.0.2",
"@types/dockerode": "^3.3.47",
"archiver": "^7.0.1",
"async-lock": "^1.4.1",
"byline": "^5.0.0",
"debug": "^4.4.3",
"docker-compose": "^1.3.0",
"dockerode": "^4.0.9",
"get-port": "^7.1.0",
"proper-lockfile": "^4.1.2",
"properties-reader": "^2.3.0",
"ssh-remote-port-forward": "^1.0.4",
"tar-fs": "^3.1.1",
"tmp": "^0.2.5",
"undici": "^7.16.0"
}
},
"node_modules/@testcontainers/postgresql/node_modules/undici": {
"version": "7.16.0",
"resolved": "https://registry.npmjs.org/undici/-/undici-7.16.0.tgz",
"integrity": "sha512-QEg3HPMll0o3t2ourKwOeUAZ159Kn9mx5pnzHRQO8+Wixmh88YdZRiIwat0iNzNNXn0yoEtXJqFpyW7eM8BV7g==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=20.18.1"
}
},
"node_modules/@testcontainers/rabbitmq": { "node_modules/@testcontainers/rabbitmq": {
"version": "10.28.0", "version": "10.28.0",
"resolved": "https://registry.npmjs.org/@testcontainers/rabbitmq/-/rabbitmq-10.28.0.tgz", "resolved": "https://registry.npmjs.org/@testcontainers/rabbitmq/-/rabbitmq-10.28.0.tgz",
@@ -5789,9 +5847,9 @@
} }
}, },
"node_modules/@types/dockerode": { "node_modules/@types/dockerode": {
"version": "3.3.44", "version": "3.3.47",
"resolved": "https://registry.npmjs.org/@types/dockerode/-/dockerode-3.3.44.tgz", "resolved": "https://registry.npmjs.org/@types/dockerode/-/dockerode-3.3.47.tgz",
"integrity": "sha512-fUpIHlsbYpxAJb285xx3vp7q5wf5mjqSn3cYwl/MhiM+DB99OdO5sOCPlO0PjO+TyOtphPs7tMVLU/RtOo/JjA==", "integrity": "sha512-ShM1mz7rCjdssXt7Xz0u1/R2BJC7piWa3SJpUBiVjCf2A3XNn4cP6pUVaD8bLanpPVVn4IKzJuw3dOvkJ8IbYw==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {

View File

@@ -121,6 +121,7 @@
"nanoid": "~3.3.4", "nanoid": "~3.3.4",
"net-snmp": "^3.11.2", "net-snmp": "^3.11.2",
"node-cloudflared-tunnel": "~1.0.9", "node-cloudflared-tunnel": "~1.0.9",
"node-radius-utils": "~1.2.0",
"nodemailer": "~6.9.13", "nodemailer": "~6.9.13",
"nostr-tools": "^2.10.4", "nostr-tools": "^2.10.4",
"notp": "~2.0.3", "notp": "~2.0.3",
@@ -135,7 +136,6 @@
"protobufjs": "~7.2.4", "protobufjs": "~7.2.4",
"qs": "~6.10.4", "qs": "~6.10.4",
"radius": "~1.1.4", "radius": "~1.1.4",
"node-radius-utils": "~1.2.0",
"redbean-node": "~0.3.0", "redbean-node": "~0.3.0",
"redis": "~5.9.0", "redis": "~5.9.0",
"semver": "~7.5.4", "semver": "~7.5.4",
@@ -159,6 +159,7 @@
"@playwright/test": "~1.39.0", "@playwright/test": "~1.39.0",
"@popperjs/core": "~2.10.2", "@popperjs/core": "~2.10.2",
"@testcontainers/hivemq": "^10.13.1", "@testcontainers/hivemq": "^10.13.1",
"@testcontainers/postgresql": "^11.9.0",
"@testcontainers/rabbitmq": "^10.13.2", "@testcontainers/rabbitmq": "^10.13.2",
"@types/bootstrap": "~5.1.9", "@types/bootstrap": "~5.1.9",
"@types/node": "^20.8.6", "@types/node": "^20.8.6",

View File

@@ -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_COUNT_MIN, PING_COUNT_MAX, PING_COUNT_DEFAULT,
PING_PER_REQUEST_TIMEOUT_MIN, PING_PER_REQUEST_TIMEOUT_MAX, PING_PER_REQUEST_TIMEOUT_DEFAULT PING_PER_REQUEST_TIMEOUT_MIN, PING_PER_REQUEST_TIMEOUT_MAX, PING_PER_REQUEST_TIMEOUT_DEFAULT
} = require("../../src/util"); } = require("../../src/util");
const { ping, checkCertificate, checkStatusCode, getTotalClientInRoom, setting, mssqlQuery, postgresQuery, mysqlQuery, setSetting, httpNtlm, radius, const { ping, checkCertificate, checkStatusCode, getTotalClientInRoom, setting, mssqlQuery, mysqlQuery, setSetting, httpNtlm, radius,
kafkaProducerAsync, getOidcTokenClientCredentials, rootCertificatesFingerprints, axiosAbortSignal, checkCertificateHostname kafkaProducerAsync, getOidcTokenClientCredentials, rootCertificatesFingerprints, axiosAbortSignal, checkCertificateHostname
} = require("../util-server"); } = require("../util-server");
const { R } = require("redbean-node"); const { R } = require("redbean-node");
@@ -783,14 +783,6 @@ class Monitor extends BeanModel {
await mssqlQuery(this.databaseConnectionString, this.databaseQuery || "SELECT 1"); await mssqlQuery(this.databaseConnectionString, this.databaseQuery || "SELECT 1");
bean.msg = "";
bean.status = UP;
bean.ping = dayjs().valueOf() - startTime;
} else if (this.type === "postgres") {
let startTime = dayjs().valueOf();
await postgresQuery(this.databaseConnectionString, this.databaseQuery || "SELECT 1");
bean.msg = ""; bean.msg = "";
bean.status = UP; bean.status = UP;
bean.ping = dayjs().valueOf() - startTime; bean.ping = dayjs().valueOf() - startTime;

View File

@@ -0,0 +1,83 @@
const { MonitorType } = require("./monitor-type");
const { log, UP } = require("../../src/util");
const dayjs = require("dayjs");
const postgresConParse = require("pg-connection-string").parse;
const { Client } = require("pg");
class PostgresMonitorType extends MonitorType {
name = "postgres";
/**
* @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;
}
/**
* 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 postgresQuery(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("postgres", "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 {
resolve(res);
}
client.end();
});
} catch (e) {
reject(e);
client.end();
}
}
});
});
}
}
module.exports = {
PostgresMonitorType,
};

View File

@@ -113,6 +113,7 @@ class UptimeKumaServer {
UptimeKumaServer.monitorTypeList["tailscale-ping"] = new TailscalePing(); UptimeKumaServer.monitorTypeList["tailscale-ping"] = new TailscalePing();
UptimeKumaServer.monitorTypeList["websocket-upgrade"] = new WebSocketMonitorType(); UptimeKumaServer.monitorTypeList["websocket-upgrade"] = new WebSocketMonitorType();
UptimeKumaServer.monitorTypeList["dns"] = new DnsMonitorType(); UptimeKumaServer.monitorTypeList["dns"] = new DnsMonitorType();
UptimeKumaServer.monitorTypeList["postgres"] = new PostgresMonitorType();
UptimeKumaServer.monitorTypeList["mqtt"] = new MqttMonitorType(); UptimeKumaServer.monitorTypeList["mqtt"] = new MqttMonitorType();
UptimeKumaServer.monitorTypeList["smtp"] = new SMTPMonitorType(); UptimeKumaServer.monitorTypeList["smtp"] = new SMTPMonitorType();
UptimeKumaServer.monitorTypeList["group"] = new GroupMonitorType(); UptimeKumaServer.monitorTypeList["group"] = new GroupMonitorType();
@@ -558,6 +559,7 @@ const { RealBrowserMonitorType } = require("./monitor-types/real-browser-monitor
const { TailscalePing } = require("./monitor-types/tailscale-ping"); const { TailscalePing } = require("./monitor-types/tailscale-ping");
const { WebSocketMonitorType } = require("./monitor-types/websocket-upgrade"); const { WebSocketMonitorType } = require("./monitor-types/websocket-upgrade");
const { DnsMonitorType } = require("./monitor-types/dns"); const { DnsMonitorType } = require("./monitor-types/dns");
const { PostgresMonitorType } = require("./monitor-types/postgres");
const { MqttMonitorType } = require("./monitor-types/mqtt"); const { MqttMonitorType } = require("./monitor-types/mqtt");
const { SMTPMonitorType } = require("./monitor-types/smtp"); const { SMTPMonitorType } = require("./monitor-types/smtp");
const { GroupMonitorType } = require("./monitor-types/group"); const { GroupMonitorType } = require("./monitor-types/group");

View File

@@ -11,8 +11,6 @@ const iconv = require("iconv-lite");
const chardet = require("chardet"); const chardet = require("chardet");
const chroma = require("chroma-js"); const chroma = require("chroma-js");
const mssql = require("mssql"); const mssql = require("mssql");
const { Client } = require("pg");
const postgresConParse = require("pg-connection-string").parse;
const mysql = require("mysql2"); const mysql = require("mysql2");
const { NtlmClient } = require("./modules/axios-ntlm/lib/ntlmClient.js"); const { NtlmClient } = require("./modules/axios-ntlm/lib/ntlmClient.js");
const { Settings } = require("./settings"); const { Settings } = require("./settings");
@@ -349,64 +347,6 @@ exports.mssqlQuery = async function (connectionString, query) {
} }
}; };
/**
* 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
*/
exports.postgresQuery = function (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("postgres", "Error caught in the error event handler.");
reject(error);
});
client.connect((err) => {
if (err) {
reject(err);
client.end();
} else {
// Connected here
try {
// No query provided by user, use SELECT 1
if (!query || (typeof query === "string" && query.trim() === "")) {
query = "SELECT 1";
}
client.query(query, (err, res) => {
if (err) {
reject(err);
} else {
resolve(res);
}
client.end();
});
} catch (e) {
reject(e);
client.end();
}
}
});
});
};
/** /**
* Run a query on MySQL/MariaDB * Run a query on MySQL/MariaDB
* @param {string} connectionString The database connection string * @param {string} connectionString The database connection string

View File

@@ -0,0 +1,60 @@
const { describe, test } = require("node:test");
const assert = require("node:assert");
const { PostgreSqlContainer } = require("@testcontainers/postgresql");
const { PostgresMonitorType } = require("../../server/monitor-types/postgres");
const { UP, PENDING } = require("../../src/util");
describe(
"Postgres Single Node",
{
skip:
!!process.env.CI &&
(process.platform !== "linux" || process.arch !== "x64"),
},
() => {
test("Postgres is running", async () => {
// The default timeout of 30 seconds might not be enough for the container to start
const postgresContainer = await new PostgreSqlContainer(
"postgres:latest"
)
.withStartupTimeout(60000)
.start();
const postgresMonitor = new PostgresMonitorType();
const monitor = {
databaseConnectionString: postgresContainer.getConnectionUri(),
};
const heartbeat = {
msg: "",
status: PENDING,
};
try {
await postgresMonitor.check(monitor, heartbeat, {});
assert.strictEqual(heartbeat.status, UP);
} finally {
postgresContainer.stop();
}
});
test("Postgres is not running", async () => {
const postgresMonitor = new PostgresMonitorType();
const monitor = {
databaseConnectionString: "http://localhost:15432",
};
const heartbeat = {
msg: "",
status: PENDING,
};
// regex match any string
const regex = /.+/;
await assert.rejects(
postgresMonitor.check(monitor, heartbeat, {}),
regex
);
});
}
);