postgresai 0.14.0-dev.36 → 0.14.0-dev.38

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/lib/checkup.ts ADDED
@@ -0,0 +1,622 @@
1
+ /**
2
+ * Express checkup module - generates JSON reports directly from PostgreSQL
3
+ * without going through Prometheus.
4
+ *
5
+ * This module reuses the same SQL queries from metrics.yml but runs them
6
+ * directly against the target database.
7
+ */
8
+
9
+ import { Client } from "pg";
10
+ import * as fs from "fs";
11
+ import * as path from "path";
12
+ import * as pkg from "../package.json";
13
+
14
+ /**
15
+ * PostgreSQL version information
16
+ */
17
+ export interface PostgresVersion {
18
+ version: string;
19
+ server_version_num: string;
20
+ server_major_ver: string;
21
+ server_minor_ver: string;
22
+ }
23
+
24
+ /**
25
+ * Setting information from pg_settings
26
+ */
27
+ export interface SettingInfo {
28
+ setting: string;
29
+ unit: string;
30
+ category: string;
31
+ context: string;
32
+ vartype: string;
33
+ pretty_value: string;
34
+ }
35
+
36
+ /**
37
+ * Altered setting (A007) - subset of SettingInfo
38
+ */
39
+ export interface AlteredSetting {
40
+ value: string;
41
+ unit: string;
42
+ category: string;
43
+ pretty_value: string;
44
+ }
45
+
46
+ /**
47
+ * Cluster metric (A004)
48
+ */
49
+ export interface ClusterMetric {
50
+ value: string;
51
+ unit: string;
52
+ description: string;
53
+ }
54
+
55
+ /**
56
+ * Node result for reports
57
+ */
58
+ export interface NodeResult {
59
+ data: Record<string, any>;
60
+ postgres_version?: PostgresVersion;
61
+ }
62
+
63
+ /**
64
+ * Report structure matching JSON schemas
65
+ */
66
+ export interface Report {
67
+ version: string | null;
68
+ build_ts: string | null;
69
+ checkId: string;
70
+ checkTitle: string;
71
+ timestamptz: string;
72
+ nodes: {
73
+ primary: string;
74
+ standbys: string[];
75
+ };
76
+ results: Record<string, NodeResult>;
77
+ }
78
+
79
+ /**
80
+ * SQL queries derived from metrics.yml
81
+ * These are the same queries used by pgwatch to export metrics to Prometheus
82
+ */
83
+ export const METRICS_SQL = {
84
+ // From metrics.yml: settings metric
85
+ // Queries pg_settings for all configuration parameters
86
+ settings: `
87
+ SELECT
88
+ name,
89
+ setting,
90
+ COALESCE(unit, '') as unit,
91
+ category,
92
+ context,
93
+ vartype,
94
+ CASE
95
+ WHEN unit = '8kB' THEN pg_size_pretty(setting::bigint * 8192)
96
+ WHEN unit = 'kB' THEN pg_size_pretty(setting::bigint * 1024)
97
+ WHEN unit = 'MB' THEN pg_size_pretty(setting::bigint * 1024 * 1024)
98
+ WHEN unit = 'B' THEN pg_size_pretty(setting::bigint)
99
+ WHEN unit = 'ms' THEN setting || ' ms'
100
+ WHEN unit = 's' THEN setting || ' s'
101
+ WHEN unit = 'min' THEN setting || ' min'
102
+ ELSE setting
103
+ END as pretty_value,
104
+ source,
105
+ CASE WHEN source <> 'default' THEN 0 ELSE 1 END as is_default
106
+ FROM pg_settings
107
+ ORDER BY name
108
+ `,
109
+
110
+ // Altered settings - non-default values only (A007)
111
+ alteredSettings: `
112
+ SELECT
113
+ name,
114
+ setting,
115
+ COALESCE(unit, '') as unit,
116
+ category,
117
+ CASE
118
+ WHEN unit = '8kB' THEN pg_size_pretty(setting::bigint * 8192)
119
+ WHEN unit = 'kB' THEN pg_size_pretty(setting::bigint * 1024)
120
+ WHEN unit = 'MB' THEN pg_size_pretty(setting::bigint * 1024 * 1024)
121
+ WHEN unit = 'B' THEN pg_size_pretty(setting::bigint)
122
+ WHEN unit = 'ms' THEN setting || ' ms'
123
+ WHEN unit = 's' THEN setting || ' s'
124
+ WHEN unit = 'min' THEN setting || ' min'
125
+ ELSE setting
126
+ END as pretty_value
127
+ FROM pg_settings
128
+ WHERE source <> 'default'
129
+ ORDER BY name
130
+ `,
131
+
132
+ // Version info - extracts server_version and server_version_num
133
+ version: `
134
+ SELECT
135
+ name,
136
+ setting
137
+ FROM pg_settings
138
+ WHERE name IN ('server_version', 'server_version_num')
139
+ `,
140
+
141
+ // Database sizes (A004)
142
+ databaseSizes: `
143
+ SELECT
144
+ datname,
145
+ pg_database_size(datname) as size_bytes
146
+ FROM pg_database
147
+ WHERE datistemplate = false
148
+ ORDER BY size_bytes DESC
149
+ `,
150
+
151
+ // Cluster statistics (A004)
152
+ clusterStats: `
153
+ SELECT
154
+ sum(numbackends) as total_connections,
155
+ sum(xact_commit) as total_commits,
156
+ sum(xact_rollback) as total_rollbacks,
157
+ sum(blks_read) as blocks_read,
158
+ sum(blks_hit) as blocks_hit,
159
+ sum(tup_returned) as tuples_returned,
160
+ sum(tup_fetched) as tuples_fetched,
161
+ sum(tup_inserted) as tuples_inserted,
162
+ sum(tup_updated) as tuples_updated,
163
+ sum(tup_deleted) as tuples_deleted,
164
+ sum(deadlocks) as total_deadlocks,
165
+ sum(temp_files) as temp_files_created,
166
+ sum(temp_bytes) as temp_bytes_written
167
+ FROM pg_stat_database
168
+ WHERE datname IS NOT NULL
169
+ `,
170
+
171
+ // Connection states (A004)
172
+ connectionStates: `
173
+ SELECT
174
+ COALESCE(state, 'null') as state,
175
+ count(*) as count
176
+ FROM pg_stat_activity
177
+ GROUP BY state
178
+ `,
179
+
180
+ // Uptime info (A004)
181
+ uptimeInfo: `
182
+ SELECT
183
+ pg_postmaster_start_time() as start_time,
184
+ current_timestamp - pg_postmaster_start_time() as uptime
185
+ `,
186
+ };
187
+
188
+ /**
189
+ * Parse PostgreSQL version number into major and minor components
190
+ */
191
+ export function parseVersionNum(versionNum: string): { major: string; minor: string } {
192
+ if (!versionNum || versionNum.length < 6) {
193
+ return { major: "", minor: "" };
194
+ }
195
+ try {
196
+ const num = parseInt(versionNum, 10);
197
+ return {
198
+ major: Math.floor(num / 10000).toString(),
199
+ minor: (num % 10000).toString(),
200
+ };
201
+ } catch {
202
+ return { major: "", minor: "" };
203
+ }
204
+ }
205
+
206
+ /**
207
+ * Format bytes to human readable string
208
+ */
209
+ export function formatBytes(bytes: number): string {
210
+ if (bytes === 0) return "0 B";
211
+ const units = ["B", "kB", "MB", "GB", "TB", "PB"];
212
+ const i = Math.floor(Math.log(bytes) / Math.log(1024));
213
+ return `${(bytes / Math.pow(1024, i)).toFixed(2)} ${units[i]}`;
214
+ }
215
+
216
+ /**
217
+ * Get PostgreSQL version information
218
+ */
219
+ export async function getPostgresVersion(client: Client): Promise<PostgresVersion> {
220
+ const result = await client.query(METRICS_SQL.version);
221
+
222
+ let version = "";
223
+ let serverVersionNum = "";
224
+
225
+ for (const row of result.rows) {
226
+ if (row.name === "server_version") {
227
+ version = row.setting;
228
+ } else if (row.name === "server_version_num") {
229
+ serverVersionNum = row.setting;
230
+ }
231
+ }
232
+
233
+ const { major, minor } = parseVersionNum(serverVersionNum);
234
+
235
+ return {
236
+ version,
237
+ server_version_num: serverVersionNum,
238
+ server_major_ver: major,
239
+ server_minor_ver: minor,
240
+ };
241
+ }
242
+
243
+ /**
244
+ * Get all PostgreSQL settings
245
+ */
246
+ export async function getSettings(client: Client): Promise<Record<string, SettingInfo>> {
247
+ const result = await client.query(METRICS_SQL.settings);
248
+ const settings: Record<string, SettingInfo> = {};
249
+
250
+ for (const row of result.rows) {
251
+ settings[row.name] = {
252
+ setting: row.setting,
253
+ unit: row.unit,
254
+ category: row.category,
255
+ context: row.context,
256
+ vartype: row.vartype,
257
+ pretty_value: row.pretty_value,
258
+ };
259
+ }
260
+
261
+ return settings;
262
+ }
263
+
264
+ /**
265
+ * Get altered (non-default) PostgreSQL settings
266
+ */
267
+ export async function getAlteredSettings(client: Client): Promise<Record<string, AlteredSetting>> {
268
+ const result = await client.query(METRICS_SQL.alteredSettings);
269
+ const settings: Record<string, AlteredSetting> = {};
270
+
271
+ for (const row of result.rows) {
272
+ settings[row.name] = {
273
+ value: row.setting,
274
+ unit: row.unit,
275
+ category: row.category,
276
+ pretty_value: row.pretty_value,
277
+ };
278
+ }
279
+
280
+ return settings;
281
+ }
282
+
283
+ /**
284
+ * Get database sizes
285
+ */
286
+ export async function getDatabaseSizes(client: Client): Promise<Record<string, number>> {
287
+ const result = await client.query(METRICS_SQL.databaseSizes);
288
+ const sizes: Record<string, number> = {};
289
+
290
+ for (const row of result.rows) {
291
+ sizes[row.datname] = parseInt(row.size_bytes, 10);
292
+ }
293
+
294
+ return sizes;
295
+ }
296
+
297
+ /**
298
+ * Get cluster general info metrics
299
+ */
300
+ export async function getClusterInfo(client: Client): Promise<Record<string, ClusterMetric>> {
301
+ const info: Record<string, ClusterMetric> = {};
302
+
303
+ // Get cluster statistics
304
+ const statsResult = await client.query(METRICS_SQL.clusterStats);
305
+ if (statsResult.rows.length > 0) {
306
+ const stats = statsResult.rows[0];
307
+
308
+ info.total_connections = {
309
+ value: String(stats.total_connections || 0),
310
+ unit: "connections",
311
+ description: "Total active database connections",
312
+ };
313
+
314
+ info.total_commits = {
315
+ value: String(stats.total_commits || 0),
316
+ unit: "transactions",
317
+ description: "Total committed transactions",
318
+ };
319
+
320
+ info.total_rollbacks = {
321
+ value: String(stats.total_rollbacks || 0),
322
+ unit: "transactions",
323
+ description: "Total rolled back transactions",
324
+ };
325
+
326
+ const blocksHit = parseInt(stats.blocks_hit || "0", 10);
327
+ const blocksRead = parseInt(stats.blocks_read || "0", 10);
328
+ const totalBlocks = blocksHit + blocksRead;
329
+ const cacheHitRatio = totalBlocks > 0 ? ((blocksHit / totalBlocks) * 100).toFixed(2) : "0.00";
330
+
331
+ info.cache_hit_ratio = {
332
+ value: cacheHitRatio,
333
+ unit: "%",
334
+ description: "Buffer cache hit ratio",
335
+ };
336
+
337
+ info.blocks_read = {
338
+ value: String(blocksRead),
339
+ unit: "blocks",
340
+ description: "Total disk blocks read",
341
+ };
342
+
343
+ info.blocks_hit = {
344
+ value: String(blocksHit),
345
+ unit: "blocks",
346
+ description: "Total buffer cache hits",
347
+ };
348
+
349
+ info.tuples_returned = {
350
+ value: String(stats.tuples_returned || 0),
351
+ unit: "rows",
352
+ description: "Total rows returned by queries",
353
+ };
354
+
355
+ info.tuples_fetched = {
356
+ value: String(stats.tuples_fetched || 0),
357
+ unit: "rows",
358
+ description: "Total rows fetched by queries",
359
+ };
360
+
361
+ info.tuples_inserted = {
362
+ value: String(stats.tuples_inserted || 0),
363
+ unit: "rows",
364
+ description: "Total rows inserted",
365
+ };
366
+
367
+ info.tuples_updated = {
368
+ value: String(stats.tuples_updated || 0),
369
+ unit: "rows",
370
+ description: "Total rows updated",
371
+ };
372
+
373
+ info.tuples_deleted = {
374
+ value: String(stats.tuples_deleted || 0),
375
+ unit: "rows",
376
+ description: "Total rows deleted",
377
+ };
378
+
379
+ info.total_deadlocks = {
380
+ value: String(stats.total_deadlocks || 0),
381
+ unit: "deadlocks",
382
+ description: "Total deadlocks detected",
383
+ };
384
+
385
+ info.temp_files_created = {
386
+ value: String(stats.temp_files_created || 0),
387
+ unit: "files",
388
+ description: "Total temporary files created",
389
+ };
390
+
391
+ const tempBytes = parseInt(stats.temp_bytes_written || "0", 10);
392
+ info.temp_bytes_written = {
393
+ value: formatBytes(tempBytes),
394
+ unit: "bytes",
395
+ description: "Total temporary file bytes written",
396
+ };
397
+ }
398
+
399
+ // Get connection states
400
+ const connResult = await client.query(METRICS_SQL.connectionStates);
401
+ for (const row of connResult.rows) {
402
+ const stateKey = `connections_${row.state.replace(/\s+/g, "_")}`;
403
+ info[stateKey] = {
404
+ value: String(row.count),
405
+ unit: "connections",
406
+ description: `Connections in '${row.state}' state`,
407
+ };
408
+ }
409
+
410
+ // Get uptime info
411
+ const uptimeResult = await client.query(METRICS_SQL.uptimeInfo);
412
+ if (uptimeResult.rows.length > 0) {
413
+ const uptime = uptimeResult.rows[0];
414
+ info.start_time = {
415
+ value: uptime.start_time.toISOString(),
416
+ unit: "timestamp",
417
+ description: "PostgreSQL server start time",
418
+ };
419
+ info.uptime = {
420
+ value: uptime.uptime,
421
+ unit: "interval",
422
+ description: "Server uptime",
423
+ };
424
+ }
425
+
426
+ return info;
427
+ }
428
+
429
+ /**
430
+ * Create base report structure
431
+ */
432
+ export function createBaseReport(
433
+ checkId: string,
434
+ checkTitle: string,
435
+ nodeName: string
436
+ ): Report {
437
+ const buildTs = resolveBuildTs();
438
+ return {
439
+ version: pkg.version || null,
440
+ build_ts: buildTs,
441
+ checkId,
442
+ checkTitle,
443
+ timestamptz: new Date().toISOString(),
444
+ nodes: {
445
+ primary: nodeName,
446
+ standbys: [],
447
+ },
448
+ results: {},
449
+ };
450
+ }
451
+
452
+ function readTextFileSafe(p: string): string | null {
453
+ try {
454
+ const value = fs.readFileSync(p, "utf8").trim();
455
+ return value || null;
456
+ } catch {
457
+ return null;
458
+ }
459
+ }
460
+
461
+ function resolveBuildTs(): string | null {
462
+ // Follow reporter.py approach: read BUILD_TS from filesystem, with env override.
463
+ // Default: /BUILD_TS (useful in container images).
464
+ const envPath = process.env.PGAI_BUILD_TS_FILE;
465
+ const p = (envPath && envPath.trim()) ? envPath.trim() : "/BUILD_TS";
466
+
467
+ const fromFile = readTextFileSafe(p);
468
+ if (fromFile) return fromFile;
469
+
470
+ // Fallback for packaged CLI: allow placing BUILD_TS next to dist/ (package root).
471
+ // dist/lib/checkup.js => package root: dist/..
472
+ try {
473
+ const pkgRoot = path.resolve(__dirname, "..");
474
+ const fromPkgFile = readTextFileSafe(path.join(pkgRoot, "BUILD_TS"));
475
+ if (fromPkgFile) return fromPkgFile;
476
+ } catch {
477
+ // ignore
478
+ }
479
+
480
+ // Last resort: use package.json mtime as an approximation (non-null, stable-ish).
481
+ try {
482
+ const pkgJsonPath = path.resolve(__dirname, "..", "package.json");
483
+ const st = fs.statSync(pkgJsonPath);
484
+ return st.mtime.toISOString();
485
+ } catch {
486
+ return new Date().toISOString();
487
+ }
488
+ }
489
+
490
+ /**
491
+ * Generate A002 report - Postgres major version
492
+ */
493
+ export async function generateA002(client: Client, nodeName: string = "node-01"): Promise<Report> {
494
+ const report = createBaseReport("A002", "Postgres major version", nodeName);
495
+ const postgresVersion = await getPostgresVersion(client);
496
+
497
+ report.results[nodeName] = {
498
+ data: {
499
+ version: postgresVersion,
500
+ },
501
+ };
502
+
503
+ return report;
504
+ }
505
+
506
+ /**
507
+ * Generate A003 report - Postgres settings
508
+ */
509
+ export async function generateA003(client: Client, nodeName: string = "node-01"): Promise<Report> {
510
+ const report = createBaseReport("A003", "Postgres settings", nodeName);
511
+ const settings = await getSettings(client);
512
+ const postgresVersion = await getPostgresVersion(client);
513
+
514
+ report.results[nodeName] = {
515
+ data: settings,
516
+ postgres_version: postgresVersion,
517
+ };
518
+
519
+ return report;
520
+ }
521
+
522
+ /**
523
+ * Generate A004 report - Cluster information
524
+ */
525
+ export async function generateA004(client: Client, nodeName: string = "node-01"): Promise<Report> {
526
+ const report = createBaseReport("A004", "Cluster information", nodeName);
527
+ const generalInfo = await getClusterInfo(client);
528
+ const databaseSizes = await getDatabaseSizes(client);
529
+ const postgresVersion = await getPostgresVersion(client);
530
+
531
+ report.results[nodeName] = {
532
+ data: {
533
+ general_info: generalInfo,
534
+ database_sizes: databaseSizes,
535
+ },
536
+ postgres_version: postgresVersion,
537
+ };
538
+
539
+ return report;
540
+ }
541
+
542
+ /**
543
+ * Generate A007 report - Altered settings
544
+ */
545
+ export async function generateA007(client: Client, nodeName: string = "node-01"): Promise<Report> {
546
+ const report = createBaseReport("A007", "Altered settings", nodeName);
547
+ const alteredSettings = await getAlteredSettings(client);
548
+ const postgresVersion = await getPostgresVersion(client);
549
+
550
+ report.results[nodeName] = {
551
+ data: alteredSettings,
552
+ postgres_version: postgresVersion,
553
+ };
554
+
555
+ return report;
556
+ }
557
+
558
+ /**
559
+ * Generate A013 report - Postgres minor version
560
+ */
561
+ export async function generateA013(client: Client, nodeName: string = "node-01"): Promise<Report> {
562
+ const report = createBaseReport("A013", "Postgres minor version", nodeName);
563
+ const postgresVersion = await getPostgresVersion(client);
564
+
565
+ report.results[nodeName] = {
566
+ data: {
567
+ version: postgresVersion,
568
+ },
569
+ };
570
+
571
+ return report;
572
+ }
573
+
574
+ /**
575
+ * Available report generators
576
+ */
577
+ export const REPORT_GENERATORS: Record<string, (client: Client, nodeName: string) => Promise<Report>> = {
578
+ A002: generateA002,
579
+ A003: generateA003,
580
+ A004: generateA004,
581
+ A007: generateA007,
582
+ A013: generateA013,
583
+ };
584
+
585
+ /**
586
+ * Check IDs and titles
587
+ */
588
+ export const CHECK_INFO: Record<string, string> = {
589
+ A002: "Postgres major version",
590
+ A003: "Postgres settings",
591
+ A004: "Cluster information",
592
+ A007: "Altered settings",
593
+ A013: "Postgres minor version",
594
+ };
595
+
596
+ /**
597
+ * Generate all available reports
598
+ */
599
+ export async function generateAllReports(
600
+ client: Client,
601
+ nodeName: string = "node-01",
602
+ onProgress?: (info: { checkId: string; checkTitle: string; index: number; total: number }) => void
603
+ ): Promise<Record<string, Report>> {
604
+ const reports: Record<string, Report> = {};
605
+
606
+ const entries = Object.entries(REPORT_GENERATORS);
607
+ const total = entries.length;
608
+ let index = 0;
609
+
610
+ for (const [checkId, generator] of entries) {
611
+ index += 1;
612
+ onProgress?.({
613
+ checkId,
614
+ checkTitle: CHECK_INFO[checkId] || checkId,
615
+ index,
616
+ total,
617
+ });
618
+ reports[checkId] = await generator(client, nodeName);
619
+ }
620
+
621
+ return reports;
622
+ }
package/lib/config.ts CHANGED
@@ -9,6 +9,7 @@ export interface Config {
9
9
  apiKey: string | null;
10
10
  baseUrl: string | null;
11
11
  orgId: number | null;
12
+ defaultProject: string | null;
12
13
  }
13
14
 
14
15
  /**
@@ -46,6 +47,7 @@ export function readConfig(): Config {
46
47
  apiKey: null,
47
48
  baseUrl: null,
48
49
  orgId: null,
50
+ defaultProject: null,
49
51
  };
50
52
 
51
53
  // Try user-level config first
@@ -57,6 +59,7 @@ export function readConfig(): Config {
57
59
  config.apiKey = parsed.apiKey || null;
58
60
  config.baseUrl = parsed.baseUrl || null;
59
61
  config.orgId = parsed.orgId || null;
62
+ config.defaultProject = parsed.defaultProject || null;
60
63
  return config;
61
64
  } catch (err) {
62
65
  const message = err instanceof Error ? err.message : String(err);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "postgresai",
3
- "version": "0.14.0-dev.36",
3
+ "version": "0.14.0-dev.38",
4
4
  "description": "postgres_ai CLI (Node.js)",
5
5
  "license": "Apache-2.0",
6
6
  "private": false,
@@ -0,0 +1,23 @@
1
+ {
2
+ "version": "0.0.0-dev.0",
3
+ "build_ts": "2025-12-25T10:09:54.142Z",
4
+ "checkId": "A002",
5
+ "checkTitle": "Postgres major version",
6
+ "timestamptz": "2025-12-25T10:11:11.158Z",
7
+ "nodes": {
8
+ "primary": "node-01",
9
+ "standbys": []
10
+ },
11
+ "results": {
12
+ "node-01": {
13
+ "data": {
14
+ "version": {
15
+ "version": "17.6 (Ubuntu 17.6-2.pgdg22.04+1)",
16
+ "server_version_num": "170006",
17
+ "server_major_ver": "17",
18
+ "server_minor_ver": "6"
19
+ }
20
+ }
21
+ }
22
+ }
23
+ }