pgserve 2.3.0 → 2.5.0

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.
Files changed (44) hide show
  1. package/bin/pgserve-wrapper.cjs +9 -4
  2. package/bin/postgres-server.js +170 -631
  3. package/config/logrotate.d/pgserve +47 -0
  4. package/config/pgaudit.conf +31 -0
  5. package/package.json +3 -2
  6. package/scripts/audit-redaction-lint.js +349 -0
  7. package/scripts/test-npx.sh +32 -10
  8. package/src/audit/audit.js +134 -0
  9. package/src/cli-install.cjs +340 -100
  10. package/src/commands/uninstall.js +241 -0
  11. package/src/commands/verify.js +360 -0
  12. package/src/cosign/cache-token.js +328 -0
  13. package/src/cosign/schema.js +97 -0
  14. package/src/cosign/trust-list.js +81 -0
  15. package/src/cosign/verify-binary.js +277 -0
  16. package/src/index.js +11 -44
  17. package/src/lib/admin-json.js +202 -0
  18. package/src/lib/pm2-args.js +119 -0
  19. package/src/lib/runtime-json.js +181 -0
  20. package/src/lib/socket-dir.js +69 -0
  21. package/src/postgres.js +64 -5
  22. package/src/upgrade/index.js +5 -0
  23. package/src/upgrade/steps/cosign-meta-migration.js +123 -0
  24. package/src/admin-client.js +0 -223
  25. package/src/audit.js +0 -168
  26. package/src/cluster.js +0 -654
  27. package/src/control-db.js +0 -330
  28. package/src/daemon-control.js +0 -468
  29. package/src/daemon-shared.js +0 -18
  30. package/src/daemon-tcp.js +0 -297
  31. package/src/daemon.js +0 -709
  32. package/src/dashboard.js +0 -217
  33. package/src/fingerprint.js +0 -479
  34. package/src/gc.js +0 -351
  35. package/src/pg-wire.js +0 -869
  36. package/src/protocol.js +0 -389
  37. package/src/restore.js +0 -574
  38. package/src/router.js +0 -546
  39. package/src/sdk.js +0 -137
  40. package/src/stats-collector.js +0 -453
  41. package/src/stats-dashboard.js +0 -401
  42. package/src/sync.js +0 -335
  43. package/src/tenancy.js +0 -75
  44. package/src/tokens.js +0 -102
@@ -1,453 +0,0 @@
1
- /**
2
- * Stats Collector - Centralized statistics gathering for pgserve
3
- *
4
- * Aggregates stats from:
5
- * - Router (active connections)
6
- * - PostgreSQL Manager (databases, storage)
7
- * - PostgreSQL internals (pg_stat_activity, pg_stat_database)
8
- * - Process (memory, uptime)
9
- */
10
-
11
- // CPU sampling threshold - avoid noise from too-frequent calls
12
- const CPU_SAMPLE_MIN_INTERVAL_MS = 100;
13
-
14
- // /proc/diskstats minimum fields per device line
15
- // Fields: major minor device reads rd_merged rd_sectors rd_ms writes wr_merged wr_sectors wr_ms io_in_progress io_ms weighted_io_ms
16
- const PROC_DISKSTATS_MIN_FIELDS = 14;
17
-
18
- export class StatsCollector {
19
- constructor(options = {}) {
20
- this.pgManager = options.pgManager;
21
- this.router = options.router;
22
- this.clusterStats = options.clusterStats; // Function that returns cluster stats
23
- this.logger = options.logger;
24
-
25
- // Override values for cluster mode where router is null
26
- this.serverPort = options.port;
27
- this.serverHost = options.host;
28
-
29
- // Cache to avoid over-querying (configurable for different monitoring needs)
30
- this.cache = null;
31
- this.cacheTime = 0;
32
- this.cacheTTL = options.cacheTTL || 1000; // Default 1s cache
33
-
34
- // CPU tracking
35
- this.lastCpuUsage = process.cpuUsage();
36
- this.lastCpuTime = Date.now();
37
-
38
- // Disk I/O tracking (Linux)
39
- this.lastDiskStats = null;
40
- this.lastDiskTime = 0;
41
-
42
- // pgvector stats cache (longer TTL since it requires cross-DB queries)
43
- this.pgvectorCache = null;
44
- this.pgvectorCacheTime = 0;
45
- this.pgvectorCacheTTL = 5000; // 5s cache for pgvector stats
46
-
47
- // Connection pool reuse for pgvector stats (avoids TCP handshake overhead per collection)
48
- this.pgvectorDbPools = new Map(); // dbName -> SQL pool
49
- this.pgvectorExtCache = new Map(); // dbName -> boolean (vector extension exists)
50
- }
51
-
52
- /**
53
- * Collect all available stats
54
- * @returns {Promise<StatsSnapshot>}
55
- */
56
- async collect() {
57
- // Return cached if recent
58
- if (this.cache && Date.now() - this.cacheTime < this.cacheTTL) {
59
- return this.cache;
60
- }
61
-
62
- const pgStats = this.pgManager?.getStats?.() || {};
63
- const routerStats = this.router?.getStats?.() || {};
64
- // Prefer clusterStats (cluster mode aggregates from all workers)
65
- // Fall back to routerStats (single-process mode)
66
- const clusterStats = this.clusterStats?.() || null;
67
-
68
- const snapshot = {
69
- timestamp: Date.now(),
70
- uptime: process.uptime(),
71
-
72
- // Connection stats - cluster mode has aggregated stats, single-process uses router directly
73
- connections: {
74
- active: clusterStats?.connections?.active ?? routerStats.activeConnections ?? 0,
75
- totalConnected: clusterStats?.connections?.totalConnected ?? 0,
76
- totalDisconnected: clusterStats?.connections?.totalDisconnected ?? 0,
77
- max: this.router?.maxConnections || 1000
78
- },
79
-
80
- // Server config
81
- server: {
82
- port: this.serverPort || routerStats.port || this.router?.port || 8432,
83
- host: this.serverHost || routerStats.host || this.router?.host || '127.0.0.1',
84
- pgPort: routerStats.pgPort || pgStats.port || 0,
85
- memoryMode: this.router?.memoryMode ?? !pgStats.persistent,
86
- useRam: this.pgManager?.useRam || false
87
- },
88
-
89
- // PostgreSQL manager stats
90
- postgres: {
91
- port: pgStats.port,
92
- databases: pgStats.databases || [],
93
- databaseDir: pgStats.databaseDir,
94
- socketDir: pgStats.socketDir,
95
- socketPath: pgStats.socketPath,
96
- persistent: pgStats.persistent
97
- },
98
-
99
- // Cluster stats (if in cluster mode)
100
- cluster: clusterStats ? {
101
- workers: clusterStats.workers,
102
- pids: clusterStats.pids,
103
- workerStats: clusterStats.workerStats || {}
104
- } : null,
105
-
106
- // PostgreSQL internals (pg_stat_*)
107
- internals: await this.collectPgStats(),
108
-
109
- // pgvector stats (if enabled)
110
- pgvector: await this.collectPgvectorStats(),
111
-
112
- // Process stats
113
- process: {
114
- pid: process.pid,
115
- memory: process.memoryUsage(),
116
- cpu: this.getCpuUsage()
117
- },
118
-
119
- // System stats (Linux)
120
- system: await this.getSystemStats()
121
- };
122
-
123
- this.cache = snapshot;
124
- this.cacheTime = Date.now();
125
- return snapshot;
126
- }
127
-
128
- /**
129
- * Get CPU usage percentage
130
- */
131
- getCpuUsage() {
132
- const now = Date.now();
133
- const elapsed = now - this.lastCpuTime;
134
- if (elapsed < CPU_SAMPLE_MIN_INTERVAL_MS) return this.lastCpuPercent || 0;
135
-
136
- const cpuUsage = process.cpuUsage(this.lastCpuUsage);
137
- const totalMicros = cpuUsage.user + cpuUsage.system;
138
- const elapsedMicros = elapsed * 1000; // Convert ms to microseconds
139
-
140
- // CPU percentage (can be > 100% on multi-core)
141
- const percent = (totalMicros / elapsedMicros) * 100;
142
-
143
- this.lastCpuUsage = process.cpuUsage();
144
- this.lastCpuTime = now;
145
- this.lastCpuPercent = percent;
146
-
147
- // Return raw percentage - can exceed 100% on multi-core systems
148
- // Consumers (dashboard, etc.) can format/cap as needed for display
149
- return percent;
150
- }
151
-
152
- /**
153
- * Get system stats (Linux-specific)
154
- */
155
- async getSystemStats() {
156
- const stats = {
157
- loadAvg: null,
158
- diskIO: null
159
- };
160
-
161
- try {
162
- // Load average (works on Linux/macOS)
163
- const os = await import('os');
164
- const loadAvg = os.loadavg();
165
- stats.loadAvg = {
166
- '1m': loadAvg[0],
167
- '5m': loadAvg[1],
168
- '15m': loadAvg[2]
169
- };
170
-
171
- // Disk I/O stats (Linux only via /proc/diskstats)
172
- if (process.platform === 'linux') {
173
- const fs = await import('fs/promises');
174
- try {
175
- const diskstats = await fs.readFile('/proc/diskstats', 'utf8');
176
- const now = Date.now();
177
-
178
- // Parse diskstats - find main disk (sda, nvme0n1, vda, etc.)
179
- let readSectors = 0;
180
- let writeSectors = 0;
181
- let readOps = 0;
182
- let writeOps = 0;
183
-
184
- for (const line of diskstats.split('\n')) {
185
- const parts = line.trim().split(/\s+/);
186
- if (parts.length < PROC_DISKSTATS_MIN_FIELDS) continue;
187
-
188
- const device = parts[2];
189
- // Match main disks (sda, sdb, nvme0n1, vda, etc.) but not partitions
190
- if (/^(sd[a-z]|nvme\d+n\d+|vd[a-z])$/.test(device)) {
191
- readOps += parseInt(parts[3]) || 0;
192
- readSectors += parseInt(parts[5]) || 0;
193
- writeOps += parseInt(parts[7]) || 0;
194
- writeSectors += parseInt(parts[9]) || 0;
195
- }
196
- }
197
-
198
- if (this.lastDiskStats && this.lastDiskTime) {
199
- const elapsed = (now - this.lastDiskTime) / 1000; // seconds
200
- if (elapsed > 0) {
201
- const readDiff = readSectors - this.lastDiskStats.readSectors;
202
- const writeDiff = writeSectors - this.lastDiskStats.writeSectors;
203
- const readOpsDiff = readOps - this.lastDiskStats.readOps;
204
- const writeOpsDiff = writeOps - this.lastDiskStats.writeOps;
205
-
206
- // Sectors are typically 512 bytes
207
- stats.diskIO = {
208
- readMBps: ((readDiff * 512) / (1024 * 1024)) / elapsed,
209
- writeMBps: ((writeDiff * 512) / (1024 * 1024)) / elapsed,
210
- readIOPS: readOpsDiff / elapsed,
211
- writeIOPS: writeOpsDiff / elapsed
212
- };
213
- }
214
- }
215
-
216
- this.lastDiskStats = { readSectors, writeSectors, readOps, writeOps };
217
- this.lastDiskTime = now;
218
- } catch (err) {
219
- // /proc/diskstats not available (normal on non-Linux or restricted environments)
220
- this.logger?.debug?.({ err: err.message }, 'Could not read /proc/diskstats');
221
- }
222
- }
223
- } catch (err) {
224
- // OS module or stats not available (normal on some platforms)
225
- this.logger?.debug?.({ err: err.message }, 'Could not collect system stats');
226
- }
227
-
228
- return stats;
229
- }
230
-
231
- /**
232
- * Query PostgreSQL internal statistics
233
- */
234
- async collectPgStats() {
235
- // Get admin pool from pgManager
236
- const adminPool = this.pgManager?.adminPool;
237
- if (!adminPool) return null;
238
-
239
- try {
240
- // Query pg_stat_activity for connection details
241
- const activity = await adminPool`
242
- SELECT
243
- count(*) FILTER (WHERE state = 'active') as active_queries,
244
- count(*) FILTER (WHERE state = 'idle') as idle_connections,
245
- count(*) as total_connections
246
- FROM pg_stat_activity
247
- WHERE datname IS NOT NULL
248
- `;
249
-
250
- // Query pg_stat_database for DB-level stats
251
- const dbStats = await adminPool`
252
- SELECT
253
- datname,
254
- numbackends,
255
- xact_commit,
256
- xact_rollback,
257
- blks_read,
258
- blks_hit,
259
- tup_returned,
260
- tup_fetched,
261
- tup_inserted,
262
- tup_updated,
263
- tup_deleted
264
- FROM pg_stat_database
265
- WHERE datname NOT IN ('template0', 'template1')
266
- ORDER BY numbackends DESC
267
- LIMIT 10
268
- `;
269
-
270
- return {
271
- activity: activity[0] || {},
272
- databases: dbStats || []
273
- };
274
- } catch (err) {
275
- this.logger?.debug?.({ err: err.message }, 'Failed to collect pg_stat_*');
276
- return null;
277
- }
278
- }
279
-
280
- /**
281
- * Collect pgvector statistics across all databases
282
- * Uses separate cache with longer TTL due to cross-DB query overhead
283
- */
284
- async collectPgvectorStats() {
285
- // Return cached if recent
286
- if (this.pgvectorCache && Date.now() - this.pgvectorCacheTime < this.pgvectorCacheTTL) {
287
- return this.pgvectorCache;
288
- }
289
-
290
- const adminPool = this.pgManager?.adminPool;
291
- if (!adminPool) return null;
292
-
293
- // Query PostgreSQL directly for databases (pgManager.getStats().databases may be empty in cluster mode)
294
- let databases = [];
295
- try {
296
- const dbResult = await adminPool`
297
- SELECT datname FROM pg_database
298
- WHERE datname NOT IN ('template0', 'template1', 'postgres')
299
- `;
300
- databases = dbResult.map(row => row.datname);
301
- } catch (err) {
302
- this.logger?.debug?.({ err: err.message }, 'Failed to query databases for pgvector stats');
303
- return null;
304
- }
305
-
306
- if (databases.length === 0) return null;
307
-
308
- try {
309
- let totalTables = 0;
310
- let totalRows = 0;
311
- const dimensions = new Set();
312
- let dbsWithVectors = 0;
313
-
314
- // Query each database for vector columns (parallel for performance)
315
- const allDbStats = await Promise.all(
316
- databases.map(dbName => this.queryDatabaseVectorStats(dbName))
317
- );
318
-
319
- for (const dbStats of allDbStats) {
320
- if (dbStats && dbStats.tableCount > 0) {
321
- dbsWithVectors++;
322
- totalTables += dbStats.tableCount;
323
- totalRows += dbStats.rowCount;
324
- dbStats.dimensions.forEach(d => dimensions.add(d));
325
- }
326
- }
327
-
328
- // Only return stats if vectors exist
329
- if (totalTables === 0) {
330
- this.pgvectorCache = null;
331
- this.pgvectorCacheTime = Date.now();
332
- return null;
333
- }
334
-
335
- const stats = {
336
- enabled: true,
337
- databases: dbsWithVectors,
338
- tableCount: totalTables,
339
- totalRows: totalRows,
340
- dimensions: [...dimensions].sort((a, b) => b - a).join(', ') || null
341
- };
342
-
343
- this.pgvectorCache = stats;
344
- this.pgvectorCacheTime = Date.now();
345
- return stats;
346
- } catch (err) {
347
- this.logger?.debug?.({ err: err.message }, 'Failed to collect pgvector stats');
348
- return null;
349
- }
350
- }
351
-
352
- /**
353
- * Query vector column stats for a specific database
354
- * @param {string} dbName - Database name to query
355
- * @returns {Promise<{tableCount: number, rowCount: number, dimensions: number[]}>}
356
- */
357
- async queryDatabaseVectorStats(dbName) {
358
- try {
359
- // Reuse connection pool (avoids TCP handshake + auth overhead per collection)
360
- let dbPool = this.pgvectorDbPools.get(dbName);
361
- if (!dbPool) {
362
- const { SQL } = await import('bun');
363
- dbPool = new SQL({
364
- hostname: '127.0.0.1',
365
- port: this.pgManager?.port || 5433,
366
- database: dbName,
367
- username: 'postgres',
368
- password: 'postgres',
369
- max: 2, // Allow some concurrency
370
- idleTimeout: 30, // Keep alive longer for reuse
371
- connectionTimeout: 3,
372
- });
373
- this.pgvectorDbPools.set(dbName, dbPool);
374
- }
375
-
376
- // Check cached extension status (only query once per DB)
377
- if (!this.pgvectorExtCache.has(dbName)) {
378
- const extCheck = await dbPool`
379
- SELECT 1 FROM pg_extension WHERE extname = 'vector'
380
- `;
381
- this.pgvectorExtCache.set(dbName, extCheck.length > 0);
382
- }
383
-
384
- if (!this.pgvectorExtCache.get(dbName)) {
385
- return { tableCount: 0, rowCount: 0, dimensions: [] };
386
- }
387
-
388
- // Single query: get tables, dimensions, and row counts via pg_class.reltuples
389
- // Note: reltuples is an estimate (updated by ANALYZE/VACUUM), not exact COUNT(*)
390
- // This is acceptable for dashboard stats with 5s cache - avoids expensive full table scans
391
- // Trade-off: ~50-100x faster, but counts may be stale after bulk INSERT/DELETE until ANALYZE
392
- const vectorInfo = await dbPool`
393
- SELECT
394
- c.relname as table_name,
395
- a.atttypmod as dimensions,
396
- GREATEST(c.reltuples, 0)::bigint AS row_count
397
- FROM pg_attribute a
398
- JOIN pg_class c ON a.attrelid = c.oid
399
- JOIN pg_type t ON a.atttypid = t.oid
400
- WHERE t.typname = 'vector'
401
- AND c.relkind = 'r'
402
- AND a.attnum > 0
403
- AND NOT a.attisdropped
404
- `;
405
-
406
- if (vectorInfo.length === 0) {
407
- return { tableCount: 0, rowCount: 0, dimensions: [] };
408
- }
409
-
410
- // Aggregate results from single query
411
- const tableRows = new Map();
412
- const dims = [];
413
-
414
- for (const row of vectorInfo) {
415
- if (!tableRows.has(row.table_name)) {
416
- tableRows.set(row.table_name, Number(row.row_count || 0));
417
- }
418
- if (row.dimensions > 0) {
419
- dims.push(row.dimensions);
420
- }
421
- }
422
-
423
- const totalRows = Array.from(tableRows.values()).reduce((sum, count) => sum + count, 0);
424
-
425
- return {
426
- tableCount: tableRows.size,
427
- rowCount: totalRows,
428
- dimensions: dims
429
- };
430
- } catch (err) {
431
- this.logger?.debug?.({ dbName, err: err.message }, 'Failed to query database vector stats');
432
- // Invalidate caches on error (DB might have been dropped)
433
- this.pgvectorExtCache.delete(dbName);
434
- const pool = this.pgvectorDbPools.get(dbName);
435
- if (pool) {
436
- this.pgvectorDbPools.delete(dbName);
437
- await pool.close().catch(() => {});
438
- }
439
- return { tableCount: 0, rowCount: 0, dimensions: [] };
440
- }
441
- }
442
-
443
- /**
444
- * Close all pgvector database pools (called on shutdown)
445
- */
446
- async closePgvectorPools() {
447
- for (const [_dbName, pool] of this.pgvectorDbPools) {
448
- await pool.close().catch(() => {});
449
- }
450
- this.pgvectorDbPools.clear();
451
- this.pgvectorExtCache.clear();
452
- }
453
- }