Skip to content

Commit ca6368e

Browse files
committed
feat: add express checkup CLI command
Add `postgresai checkup` command for direct PostgreSQL health checks without requiring Prometheus. Generates JSON reports that comply with defined schemas. Features: - A002: Postgres major version - A003: Postgres settings (reuses query from metrics.yml) - A013: Postgres minor version (new schema added) Usage: postgresai checkup postgresql://user:pass@host/db postgresai checkup --check-id A003 --json The express mode runs SQL queries directly against PostgreSQL, useful for quick health checks without full monitoring setup.
1 parent 8ca45b1 commit ca6368e

File tree

3 files changed

+484
-0
lines changed

3 files changed

+484
-0
lines changed

cli/bin/postgres-ai.ts

Lines changed: 160 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ import { startMcpServer } from "../lib/mcp-server";
1717
import { fetchIssues, fetchIssueComments, createIssueComment, fetchIssue } from "../lib/issues";
1818
import { resolveBaseUrls } from "../lib/util";
1919
import { applyInitPlan, buildInitPlan, connectWithSslFallback, DEFAULT_MONITORING_USER, redactPasswordsInSql, resolveAdminConnection, resolveMonitoringPassword, verifyInitSetup } from "../lib/init";
20+
import { REPORT_GENERATORS, CHECK_INFO, generateAllReports } from "../lib/checkup";
2021

2122
const execPromise = promisify(exec);
2223
const execFilePromise = promisify(execFile);
@@ -2251,5 +2252,164 @@ mcp
22512252
}
22522253
});
22532254

2255+
// Express checkup - direct database analysis
2256+
program
2257+
.command("checkup [conn]")
2258+
.description("generate health check reports directly from PostgreSQL (express mode)")
2259+
.option("--db-url <url>", "PostgreSQL connection URL (deprecated; pass as positional arg)")
2260+
.option("-h, --host <host>", "PostgreSQL host")
2261+
.option("-p, --port <port>", "PostgreSQL port")
2262+
.option("-U, --username <username>", "PostgreSQL user")
2263+
.option("-d, --dbname <dbname>", "PostgreSQL database name")
2264+
.option("--password <password>", "PostgreSQL password (otherwise uses PGPASSWORD)")
2265+
.option("--check-id <id>", `specific check to run: ${Object.keys(CHECK_INFO).join(", ")}, or ALL`, "ALL")
2266+
.option("--node-name <name>", "node name for reports", "node-01")
2267+
.option("--output <path>", "output directory for JSON files (default: current directory)")
2268+
.option("--json", "output to stdout as JSON instead of files")
2269+
.addHelpText(
2270+
"after",
2271+
[
2272+
"",
2273+
"Examples:",
2274+
" postgresai checkup postgresql://user:pass@host:5432/dbname",
2275+
" postgresai checkup -h localhost -p 5432 -U postgres -d mydb",
2276+
" postgresai checkup --db-url postgresql://... --check-id A003",
2277+
" postgresai checkup postgresql://... --json",
2278+
"",
2279+
"Available checks:",
2280+
...Object.entries(CHECK_INFO).map(([id, title]) => ` ${id}: ${title}`),
2281+
"",
2282+
"Express mode runs SQL queries directly against PostgreSQL,",
2283+
"without requiring Prometheus. Useful for quick health checks.",
2284+
].join("\n")
2285+
)
2286+
.action(async (conn: string | undefined, opts: {
2287+
dbUrl?: string;
2288+
host?: string;
2289+
port?: string;
2290+
username?: string;
2291+
dbname?: string;
2292+
password?: string;
2293+
checkId: string;
2294+
nodeName: string;
2295+
output?: string;
2296+
json?: boolean;
2297+
}) => {
2298+
// Build connection config
2299+
let connectionString: string | undefined;
2300+
let connectionConfig: {
2301+
host?: string;
2302+
port?: number;
2303+
user?: string;
2304+
database?: string;
2305+
password?: string;
2306+
} | undefined;
2307+
2308+
if (conn) {
2309+
connectionString = conn;
2310+
} else if (opts.dbUrl) {
2311+
connectionString = opts.dbUrl;
2312+
} else if (opts.host || opts.username || opts.dbname) {
2313+
connectionConfig = {
2314+
host: opts.host || process.env.PGHOST || "localhost",
2315+
port: parseInt(opts.port || process.env.PGPORT || "5432", 10),
2316+
user: opts.username || process.env.PGUSER || "postgres",
2317+
database: opts.dbname || process.env.PGDATABASE || "postgres",
2318+
password: opts.password || process.env.PGPASSWORD,
2319+
};
2320+
} else {
2321+
// Try environment variables
2322+
if (process.env.PGHOST || process.env.DATABASE_URL) {
2323+
connectionString = process.env.DATABASE_URL;
2324+
if (!connectionString) {
2325+
connectionConfig = {
2326+
host: process.env.PGHOST,
2327+
port: parseInt(process.env.PGPORT || "5432", 10),
2328+
user: process.env.PGUSER || "postgres",
2329+
database: process.env.PGDATABASE || "postgres",
2330+
password: process.env.PGPASSWORD,
2331+
};
2332+
}
2333+
} else {
2334+
console.error("Error: Connection details required");
2335+
console.error("");
2336+
console.error("Provide connection via:");
2337+
console.error(" positional argument: postgresai checkup postgresql://...");
2338+
console.error(" flags: postgresai checkup -h host -p port -U user -d database");
2339+
console.error(" environment: PGHOST, PGPORT, PGUSER, PGDATABASE, PGPASSWORD");
2340+
process.exitCode = 1;
2341+
return;
2342+
}
2343+
}
2344+
2345+
// Validate check ID
2346+
const checkId = opts.checkId.toUpperCase();
2347+
if (checkId !== "ALL" && !REPORT_GENERATORS[checkId]) {
2348+
console.error(`Error: Unknown check ID: ${opts.checkId}`);
2349+
console.error(`Available checks: ${Object.keys(CHECK_INFO).join(", ")}, ALL`);
2350+
process.exitCode = 1;
2351+
return;
2352+
}
2353+
2354+
// Connect to database
2355+
const client = new Client(connectionString ? { connectionString } : connectionConfig);
2356+
2357+
try {
2358+
console.error("Connecting to PostgreSQL...");
2359+
await client.connect();
2360+
2361+
const dbResult = await client.query("SELECT current_database() as db, version() as ver");
2362+
const dbName = dbResult.rows[0]?.db;
2363+
const dbVersion = dbResult.rows[0]?.ver;
2364+
console.error(`Connected to: ${dbName}`);
2365+
console.error(`Version: ${dbVersion}`);
2366+
console.error("");
2367+
2368+
if (checkId === "ALL") {
2369+
// Generate all reports
2370+
console.error("Generating all reports...");
2371+
const reports = await generateAllReports(client, opts.nodeName);
2372+
2373+
if (opts.json) {
2374+
// Output all reports as JSON to stdout
2375+
console.log(JSON.stringify(reports, null, 2));
2376+
} else {
2377+
// Write each report to a file
2378+
const outputDir = opts.output || process.cwd();
2379+
for (const [id, report] of Object.entries(reports)) {
2380+
const filePath = path.join(outputDir, `${id}.json`);
2381+
fs.writeFileSync(filePath, JSON.stringify(report, null, 2), "utf8");
2382+
console.error(`Generated: ${filePath}`);
2383+
}
2384+
console.error("");
2385+
console.error(`All reports written to: ${outputDir}`);
2386+
}
2387+
} else {
2388+
// Generate specific report
2389+
console.error(`Generating ${checkId} report...`);
2390+
const generator = REPORT_GENERATORS[checkId];
2391+
const report = await generator(client, opts.nodeName);
2392+
2393+
if (opts.json) {
2394+
// Output to stdout
2395+
console.log(JSON.stringify(report, null, 2));
2396+
} else {
2397+
// Write to file
2398+
const outputDir = opts.output || process.cwd();
2399+
const filePath = path.join(outputDir, `${checkId}.json`);
2400+
fs.writeFileSync(filePath, JSON.stringify(report, null, 2), "utf8");
2401+
console.error(`Generated: ${filePath}`);
2402+
}
2403+
}
2404+
2405+
} catch (error) {
2406+
const message = error instanceof Error ? error.message : String(error);
2407+
console.error(`Error: ${message}`);
2408+
process.exitCode = 1;
2409+
} finally {
2410+
await client.end();
2411+
}
2412+
});
2413+
22542414
program.parseAsync(process.argv);
22552415

0 commit comments

Comments
 (0)