@wpmoo/toolkit 0.9.26 → 0.9.28

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.
@@ -0,0 +1,646 @@
1
+ export const POSTGRES_DIAGNOSTICS_CONTRACT_VERSION = 2;
2
+ export const POSTGRES_DIAGNOSTICS_QUERY = `
3
+ WITH
4
+ transaction_health AS (
5
+ SELECT
6
+ COALESCE(
7
+ COUNT(*) FILTER (
8
+ WHERE state = 'active'
9
+ AND xact_start IS NOT NULL
10
+ AND now() - xact_start > interval '5 minutes'
11
+ ),
12
+ 0
13
+ )::text AS long_transaction_count,
14
+ COALESCE(
15
+ MAX(
16
+ EXTRACT(EPOCH FROM now() - xact_start)
17
+ ) FILTER (
18
+ WHERE state = 'active'
19
+ AND xact_start IS NOT NULL
20
+ AND now() - xact_start > interval '5 minutes'
21
+ ),
22
+ 0
23
+ )::bigint::text AS oldest_long_transaction_age_seconds,
24
+ COALESCE(
25
+ COUNT(*) FILTER (
26
+ WHERE state = 'idle in transaction'
27
+ AND xact_start IS NOT NULL
28
+ AND now() - xact_start > interval '5 minutes'
29
+ ),
30
+ 0
31
+ )::text AS idle_in_transaction_count,
32
+ COALESCE(
33
+ MAX(
34
+ EXTRACT(EPOCH FROM now() - xact_start)
35
+ ) FILTER (
36
+ WHERE state = 'idle in transaction'
37
+ AND xact_start IS NOT NULL
38
+ AND now() - xact_start > interval '5 minutes'
39
+ ),
40
+ 0
41
+ )::bigint::text AS oldest_idle_in_transaction_age_seconds
42
+ FROM pg_stat_activity
43
+ WHERE datname IS NOT NULL
44
+ ),
45
+ table_health AS (
46
+ SELECT
47
+ COALESCE(
48
+ COUNT(*) FILTER (
49
+ WHERE n_live_tup > 0
50
+ AND n_dead_tup::numeric > 0
51
+ AND (n_dead_tup::numeric / NULLIF(n_live_tup::numeric, 0)) > 0.2
52
+ ),
53
+ 0
54
+ )::text AS table_health_risky_table_count,
55
+ COALESCE(
56
+ COUNT(*) FILTER (
57
+ WHERE n_dead_tup::numeric > 0
58
+ AND (n_dead_tup::numeric / NULLIF(n_live_tup::numeric, 0)) > 0.2
59
+ ),
60
+ 0
61
+ )::text AS table_health_requires_vacuum_count,
62
+ COALESCE(
63
+ COUNT(*) FILTER (WHERE n_mod_since_analyze > 1000),
64
+ 0
65
+ )::text AS table_health_requires_analyze_count
66
+ FROM pg_stat_user_tables
67
+ ),
68
+ table_health_ranked AS (
69
+ SELECT
70
+ schemaname,
71
+ relname,
72
+ CASE
73
+ WHEN n_live_tup > 0 THEN (n_dead_tup::numeric / NULLIF(n_live_tup::numeric, 0))
74
+ ELSE 0
75
+ END AS dead_tuple_ratio,
76
+ n_dead_tup AS dead_tup_count,
77
+ n_live_tup,
78
+ ROW_NUMBER() OVER (
79
+ ORDER BY
80
+ CASE WHEN n_live_tup > 0 THEN n_dead_tup::numeric / NULLIF(n_live_tup::numeric, 0) ELSE 0 END DESC,
81
+ n_dead_tup DESC,
82
+ n_live_tup DESC
83
+ ) AS rn
84
+ FROM pg_stat_user_tables
85
+ ),
86
+ top_table_health AS (
87
+ SELECT
88
+ COALESCE(
89
+ (SELECT format('%I.%I', schemaname, relname) FROM table_health_ranked WHERE rn = 1),
90
+ 'unavailable'
91
+ ) AS table_health_top_risky_table,
92
+ COALESCE(
93
+ (
94
+ SELECT dead_tuple_ratio
95
+ FROM table_health_ranked
96
+ WHERE rn = 1
97
+ )::text,
98
+ '0'
99
+ ) AS table_health_top_risky_dead_tuple_ratio,
100
+ COALESCE(
101
+ (
102
+ SELECT dead_tup_count
103
+ FROM table_health_ranked
104
+ WHERE rn = 1
105
+ )::text,
106
+ '0'
107
+ ) AS table_health_top_risky_dead_tuple_count
108
+ FROM (SELECT 1) AS _top_table_health_row
109
+ ),
110
+ index_health AS (
111
+ SELECT
112
+ COALESCE(
113
+ COUNT(*) FILTER (
114
+ WHERE NOT idx.indisunique
115
+ AND i.idx_scan = 0
116
+ AND i.idx_tup_fetch = 0
117
+ ),
118
+ 0
119
+ )::text AS unused_index_candidates_count
120
+ FROM pg_stat_user_indexes i
121
+ LEFT JOIN pg_index idx ON idx.indexrelid = i.indexrelid
122
+ WHERE i.schemaname NOT IN ('pg_catalog', 'information_schema')
123
+ ),
124
+ wal_health AS (
125
+ SELECT
126
+ COALESCE((SELECT setting FROM pg_settings WHERE name = 'wal_level'), 'unavailable')::text AS wal_level,
127
+ COALESCE((SELECT setting FROM pg_settings WHERE name = 'archive_mode'), 'unavailable')::text AS wal_archive_mode,
128
+ COALESCE((SELECT COUNT(*) FROM pg_ls_waldir()), 0)::text AS wal_file_count,
129
+ COALESCE((SELECT SUM(size) FROM pg_ls_waldir()), 0)::text AS wal_directory_size_bytes
130
+ ),
131
+ capacity_health AS (
132
+ SELECT
133
+ COALESCE((SELECT pg_tablespace_size('pg_default')), 0)::text AS default_tablespace_size_bytes,
134
+ COALESCE(
135
+ (SELECT SUM(n_tup_ins + n_tup_upd + n_tup_del) FROM pg_stat_database WHERE datname IS NOT NULL),
136
+ 0
137
+ )::text AS database_write_activity_rows
138
+ )
139
+ SELECT metric || '|' || value
140
+ FROM (
141
+ SELECT 'database_count'::text AS metric, count(*)::text AS value
142
+ FROM pg_database
143
+ WHERE datistemplate = false
144
+ UNION ALL
145
+ SELECT 'active_connections', COUNT(*)::text
146
+ FROM pg_stat_activity
147
+ WHERE datname IS NOT NULL
148
+ AND state = 'active'
149
+ UNION ALL
150
+ SELECT 'connection_count', COUNT(*)::text
151
+ FROM pg_stat_activity
152
+ WHERE datname IS NOT NULL
153
+ UNION ALL
154
+ SELECT 'max_connections', COALESCE(
155
+ (SELECT setting FROM pg_settings WHERE name = 'max_connections'),
156
+ 'unavailable'
157
+ )::text
158
+ UNION ALL
159
+ SELECT 'total_database_size_bytes', COALESCE(SUM(pg_database_size(datname)), 0)::text
160
+ FROM pg_database
161
+ WHERE datistemplate = false
162
+ UNION ALL
163
+ SELECT 'largest_database_name', COALESCE(
164
+ (
165
+ SELECT datname
166
+ FROM pg_database
167
+ WHERE datistemplate = false
168
+ ORDER BY pg_database_size(datname) DESC, datname
169
+ LIMIT 1
170
+ ),
171
+ 'unavailable'
172
+ )
173
+ UNION ALL
174
+ SELECT 'largest_database_size_bytes', COALESCE(
175
+ (
176
+ SELECT pg_database_size(datname)::text
177
+ FROM pg_database
178
+ WHERE datistemplate = false
179
+ ORDER BY pg_database_size(datname) DESC, datname
180
+ LIMIT 1
181
+ ),
182
+ '0'
183
+ )
184
+ UNION ALL
185
+ SELECT 'slow_query_logging', COALESCE(
186
+ (SELECT setting || COALESCE(unit, '') FROM pg_settings WHERE name = 'log_min_duration_statement'),
187
+ 'unavailable'
188
+ )
189
+ UNION ALL
190
+ SELECT 'pg_stat_statements',
191
+ CASE
192
+ WHEN EXISTS (SELECT 1 FROM pg_extension WHERE extname = 'pg_stat_statements') THEN 'installed'
193
+ WHEN EXISTS (SELECT 1 FROM pg_available_extensions WHERE name = 'pg_stat_statements') THEN 'available'
194
+ ELSE 'unavailable'
195
+ END
196
+ UNION ALL
197
+ SELECT 'pg_stat_statements_available_version', COALESCE(
198
+ (SELECT default_version FROM pg_available_extensions WHERE name = 'pg_stat_statements'),
199
+ 'unavailable'
200
+ )
201
+ UNION ALL
202
+ SELECT 'pg_stat_statements_installed_version', COALESCE(
203
+ (SELECT extversion FROM pg_extension WHERE extname = 'pg_stat_statements'),
204
+ ''
205
+ )
206
+ UNION ALL
207
+ SELECT 'shared_preload_libraries', COALESCE(
208
+ (SELECT setting FROM pg_settings WHERE name = 'shared_preload_libraries'),
209
+ 'unavailable'
210
+ )
211
+ UNION ALL
212
+ SELECT 'shared_buffers', COALESCE(
213
+ (SELECT setting FROM pg_settings WHERE name = 'shared_buffers'),
214
+ 'unavailable'
215
+ )
216
+ UNION ALL
217
+ SELECT 'long_transaction_count', long_transaction_count FROM transaction_health
218
+ UNION ALL
219
+ SELECT 'oldest_long_transaction_age_seconds', oldest_long_transaction_age_seconds FROM transaction_health
220
+ UNION ALL
221
+ SELECT 'idle_in_transaction_count', idle_in_transaction_count FROM transaction_health
222
+ UNION ALL
223
+ SELECT 'oldest_idle_in_transaction_age_seconds', oldest_idle_in_transaction_age_seconds FROM transaction_health
224
+ UNION ALL
225
+ SELECT 'table_health_risky_table_count', table_health_risky_table_count FROM table_health
226
+ UNION ALL
227
+ SELECT 'table_health_requires_vacuum_count', table_health_requires_vacuum_count FROM table_health
228
+ UNION ALL
229
+ SELECT 'table_health_requires_analyze_count', table_health_requires_analyze_count FROM table_health
230
+ UNION ALL
231
+ SELECT 'table_health_top_risky_table', table_health_top_risky_table FROM top_table_health
232
+ UNION ALL
233
+ SELECT 'table_health_top_risky_dead_tuple_ratio', table_health_top_risky_dead_tuple_ratio FROM top_table_health
234
+ UNION ALL
235
+ SELECT 'table_health_top_risky_dead_tuple_count', table_health_top_risky_dead_tuple_count FROM top_table_health
236
+ UNION ALL
237
+ SELECT 'unused_index_candidates_count', unused_index_candidates_count FROM index_health
238
+ UNION ALL
239
+ SELECT 'wal_level', wal_level FROM wal_health
240
+ UNION ALL
241
+ SELECT 'wal_archive_mode', wal_archive_mode FROM wal_health
242
+ UNION ALL
243
+ SELECT 'wal_file_count', wal_file_count FROM wal_health
244
+ UNION ALL
245
+ SELECT 'wal_directory_size_bytes', wal_directory_size_bytes FROM wal_health
246
+ UNION ALL
247
+ SELECT 'default_tablespace_size_bytes', default_tablespace_size_bytes FROM capacity_health
248
+ UNION ALL
249
+ SELECT 'database_write_activity_rows', database_write_activity_rows FROM capacity_health
250
+ ) metrics
251
+ ORDER BY metric;
252
+ `.trim();
253
+ export const POSTGRES_DIAGNOSTIC_KEYS = [
254
+ 'database_count',
255
+ 'active_connections',
256
+ 'connection_count',
257
+ 'max_connections',
258
+ 'total_database_size_bytes',
259
+ 'largest_database_name',
260
+ 'largest_database_size_bytes',
261
+ 'slow_query_logging',
262
+ 'pg_stat_statements',
263
+ 'pg_stat_statements_available_version',
264
+ 'pg_stat_statements_installed_version',
265
+ 'shared_preload_libraries',
266
+ 'shared_buffers',
267
+ 'long_transaction_count',
268
+ 'oldest_long_transaction_age_seconds',
269
+ 'idle_in_transaction_count',
270
+ 'oldest_idle_in_transaction_age_seconds',
271
+ 'table_health_risky_table_count',
272
+ 'table_health_requires_vacuum_count',
273
+ 'table_health_requires_analyze_count',
274
+ 'table_health_top_risky_table',
275
+ 'table_health_top_risky_dead_tuple_ratio',
276
+ 'table_health_top_risky_dead_tuple_count',
277
+ 'unused_index_candidates_count',
278
+ 'wal_level',
279
+ 'wal_archive_mode',
280
+ 'wal_file_count',
281
+ 'wal_directory_size_bytes',
282
+ 'default_tablespace_size_bytes',
283
+ 'database_write_activity_rows',
284
+ ];
285
+ export const REQUIRED_POSTGRES_DIAGNOSTIC_KEYS = [
286
+ 'database_count',
287
+ 'active_connections',
288
+ 'connection_count',
289
+ 'max_connections',
290
+ 'total_database_size_bytes',
291
+ 'slow_query_logging',
292
+ 'pg_stat_statements',
293
+ 'shared_buffers',
294
+ ];
295
+ const INTEGER_POSTGRES_DIAGNOSTIC_KEYS = [
296
+ 'database_count',
297
+ 'active_connections',
298
+ 'connection_count',
299
+ 'max_connections',
300
+ 'total_database_size_bytes',
301
+ 'largest_database_size_bytes',
302
+ 'long_transaction_count',
303
+ 'oldest_long_transaction_age_seconds',
304
+ 'idle_in_transaction_count',
305
+ 'oldest_idle_in_transaction_age_seconds',
306
+ 'table_health_risky_table_count',
307
+ 'table_health_requires_vacuum_count',
308
+ 'table_health_requires_analyze_count',
309
+ 'table_health_top_risky_dead_tuple_count',
310
+ 'unused_index_candidates_count',
311
+ 'wal_file_count',
312
+ 'wal_directory_size_bytes',
313
+ 'default_tablespace_size_bytes',
314
+ 'database_write_activity_rows',
315
+ ];
316
+ const DECIMAL_POSTGRES_DIAGNOSTIC_KEYS = [
317
+ 'table_health_top_risky_dead_tuple_ratio',
318
+ ];
319
+ export const LONG_TRANSACTION_AGE_WARNING_SECONDS = 5 * 60;
320
+ export const LONG_TRANSACTION_COUNT_WARNING_THRESHOLD = 3;
321
+ export const IDLE_IN_TRANSACTION_AGE_WARNING_SECONDS = 5 * 60;
322
+ export const IDLE_IN_TRANSACTION_COUNT_WARNING_THRESHOLD = 2;
323
+ export const TABLE_HEALTH_DEAD_TUPLE_RATIO_WARNING = 0.2;
324
+ export const TABLE_HEALTH_DEAD_TUPLE_COUNT_WARNING = 1000;
325
+ export const UNUSED_INDEX_CANDIDATES_WARNING = 20;
326
+ export const WAL_DIRECTORY_SIZE_WARNING_BYTES = 5 * 1024 * 1024 * 1024;
327
+ export const TOTAL_DATABASE_SIZE_WARNING_BYTES = 50 * 1024 * 1024 * 1024;
328
+ const allowedKeys = new Set(POSTGRES_DIAGNOSTIC_KEYS);
329
+ export function parsePostgresDiagnostics(output) {
330
+ const diagnostics = {};
331
+ for (const rawLine of output.split(/\r?\n/iu)) {
332
+ const line = rawLine.trim();
333
+ if (!line) {
334
+ continue;
335
+ }
336
+ const separatorIndex = line.indexOf('|');
337
+ if (separatorIndex === -1) {
338
+ continue;
339
+ }
340
+ const key = line.slice(0, separatorIndex).trim();
341
+ const value = line.slice(separatorIndex + 1).trim();
342
+ if (!value || !allowedKeys.has(key)) {
343
+ continue;
344
+ }
345
+ diagnostics[key] = value;
346
+ }
347
+ return diagnostics;
348
+ }
349
+ export function renderPostgresDiagnostics(diagnostics) {
350
+ const connectionUtilizationPct = postgresConnectionUtilizationPct(diagnostics);
351
+ const parts = POSTGRES_DIAGNOSTIC_KEYS.flatMap((key) => {
352
+ const value = diagnostics[key];
353
+ const rendered = value ? [`${key}=${value}`] : [];
354
+ if (key === 'max_connections' && connectionUtilizationPct !== undefined) {
355
+ rendered.push(`connection_utilization_pct=${connectionUtilizationPct}`);
356
+ }
357
+ return rendered;
358
+ });
359
+ return parts.length > 0 ? `OK PostgreSQL diagnostics ${parts.join(' ')}` : undefined;
360
+ }
361
+ export function missingPostgresDiagnosticKeys(diagnostics) {
362
+ return REQUIRED_POSTGRES_DIAGNOSTIC_KEYS.filter((key) => diagnostics[key] === undefined);
363
+ }
364
+ function integerDiagnostic(value) {
365
+ if (!value || !/^\d+$/u.test(value)) {
366
+ return undefined;
367
+ }
368
+ return Number.parseInt(value, 10);
369
+ }
370
+ function decimalDiagnostic(value) {
371
+ if (!value || !/^\d+(?:\.\d+)?$/u.test(value)) {
372
+ return undefined;
373
+ }
374
+ return Number.parseFloat(value);
375
+ }
376
+ export function malformedPostgresDiagnosticKeys(diagnostics) {
377
+ const malformed = [];
378
+ for (const key of INTEGER_POSTGRES_DIAGNOSTIC_KEYS) {
379
+ const value = diagnostics[key];
380
+ if (value !== undefined && integerDiagnostic(value) === undefined) {
381
+ malformed.push(key);
382
+ }
383
+ }
384
+ for (const key of DECIMAL_POSTGRES_DIAGNOSTIC_KEYS) {
385
+ const value = diagnostics[key];
386
+ if (value !== undefined && decimalDiagnostic(value) === undefined) {
387
+ malformed.push(key);
388
+ }
389
+ }
390
+ return malformed;
391
+ }
392
+ export function unavailablePostgresDiagnosticsWarning(diagnostics, missingKeys) {
393
+ return Object.keys(diagnostics).length === 0
394
+ ? 'no diagnostic rows returned'
395
+ : `incomplete diagnostic rows: missing ${missingKeys.join(', ')}`;
396
+ }
397
+ export function postgresConnectionUtilizationPct(diagnostics) {
398
+ const connectionCount = integerDiagnostic(diagnostics.connection_count);
399
+ const maxConnections = integerDiagnostic(diagnostics.max_connections);
400
+ if (connectionCount === undefined || maxConnections === undefined || maxConnections <= 0) {
401
+ return undefined;
402
+ }
403
+ return Math.round((connectionCount / maxConnections) * 100);
404
+ }
405
+ export function postgresConnectionUtilizationWarning(diagnostics) {
406
+ const connectionCount = integerDiagnostic(diagnostics.connection_count);
407
+ const maxConnections = integerDiagnostic(diagnostics.max_connections);
408
+ const utilizationPct = postgresConnectionUtilizationPct(diagnostics);
409
+ if (connectionCount === undefined ||
410
+ maxConnections === undefined ||
411
+ utilizationPct === undefined ||
412
+ utilizationPct < 80) {
413
+ return undefined;
414
+ }
415
+ return `PostgreSQL connection utilization is high: ${utilizationPct}% of max_connections used (${connectionCount}/${maxConnections}).`;
416
+ }
417
+ export function postgresSlowQueryLoggingWarning(diagnostics) {
418
+ const slowQueryLogging = diagnostics.slow_query_logging?.trim();
419
+ if (!slowQueryLogging || (!/^-1\s*(?:ms)?$/iu.test(slowQueryLogging) && !/^off$/iu.test(slowQueryLogging))) {
420
+ return undefined;
421
+ }
422
+ return `PostgreSQL slow-query logging is disabled (log_min_duration_statement=${slowQueryLogging}). Enable it before performance triage.`;
423
+ }
424
+ export function postgresExtensionVisibilityWarning(diagnostics) {
425
+ if (diagnostics.pg_stat_statements === 'available' &&
426
+ diagnostics.pg_stat_statements_available_version &&
427
+ !diagnostics.pg_stat_statements_installed_version) {
428
+ return 'PostgreSQL pg_stat_statements is available but not installed. Install it before query-level performance triage.';
429
+ }
430
+ return undefined;
431
+ }
432
+ export function postgresLongTransactionWarning(diagnostics) {
433
+ const longTransactionCount = integerDiagnostic(diagnostics.long_transaction_count);
434
+ const oldestAgeSeconds = integerDiagnostic(diagnostics.oldest_long_transaction_age_seconds);
435
+ if (longTransactionCount === undefined ||
436
+ oldestAgeSeconds === undefined ||
437
+ longTransactionCount < LONG_TRANSACTION_COUNT_WARNING_THRESHOLD ||
438
+ oldestAgeSeconds < LONG_TRANSACTION_AGE_WARNING_SECONDS) {
439
+ return undefined;
440
+ }
441
+ return `PostgreSQL has ${longTransactionCount} long transaction(s) running longer than ${LONG_TRANSACTION_AGE_WARNING_SECONDS / 60} minutes (oldest ${oldestAgeSeconds}s).`;
442
+ }
443
+ export function postgresIdleInTransactionWarning(diagnostics) {
444
+ const idleTransactionCount = integerDiagnostic(diagnostics.idle_in_transaction_count);
445
+ const oldestAgeSeconds = integerDiagnostic(diagnostics.oldest_idle_in_transaction_age_seconds);
446
+ if (idleTransactionCount === undefined ||
447
+ oldestAgeSeconds === undefined ||
448
+ idleTransactionCount < IDLE_IN_TRANSACTION_COUNT_WARNING_THRESHOLD ||
449
+ oldestAgeSeconds < IDLE_IN_TRANSACTION_AGE_WARNING_SECONDS) {
450
+ return undefined;
451
+ }
452
+ return `PostgreSQL has ${idleTransactionCount} transaction(s) idle in transaction for longer than ${IDLE_IN_TRANSACTION_AGE_WARNING_SECONDS / 60} minutes (oldest ${oldestAgeSeconds}s).`;
453
+ }
454
+ export function postgresTableHealthWarning(diagnostics) {
455
+ const riskyTables = integerDiagnostic(diagnostics.table_health_risky_table_count);
456
+ const vacuumNeeded = integerDiagnostic(diagnostics.table_health_requires_vacuum_count);
457
+ const analyzeNeeded = integerDiagnostic(diagnostics.table_health_requires_analyze_count);
458
+ const deadTupleRatio = decimalDiagnostic(diagnostics.table_health_top_risky_dead_tuple_ratio);
459
+ const deadTupleCount = integerDiagnostic(diagnostics.table_health_top_risky_dead_tuple_count);
460
+ if (riskyTables === undefined ||
461
+ vacuumNeeded === undefined ||
462
+ analyzeNeeded === undefined ||
463
+ deadTupleRatio === undefined ||
464
+ deadTupleCount === undefined) {
465
+ return undefined;
466
+ }
467
+ if (deadTupleRatio < TABLE_HEALTH_DEAD_TUPLE_RATIO_WARNING || deadTupleCount < TABLE_HEALTH_DEAD_TUPLE_COUNT_WARNING) {
468
+ return undefined;
469
+ }
470
+ const maxRiskyRatioPercent = Math.round(deadTupleRatio * 100);
471
+ return `PostgreSQL table health risk: ${riskyTables} table(s) have high dead-tuple ratio; ${vacuumNeeded} table(s) need vacuum and ${analyzeNeeded} need analyze. Top risk: ${diagnostics.table_health_top_risky_table ?? 'unavailable'} (${maxRiskyRatioPercent}% dead tuples, ${deadTupleCount} dead tuples).`;
472
+ }
473
+ export function postgresUnusedIndexWarning(diagnostics) {
474
+ const candidates = integerDiagnostic(diagnostics.unused_index_candidates_count);
475
+ if (candidates === undefined || candidates < UNUSED_INDEX_CANDIDATES_WARNING) {
476
+ return undefined;
477
+ }
478
+ return `PostgreSQL has ${candidates} unused index candidates with no index scans. Review indexing strategy before adding new indexes.`;
479
+ }
480
+ export function postgresWalWarning(diagnostics) {
481
+ const walSizeBytes = integerDiagnostic(diagnostics.wal_directory_size_bytes);
482
+ if (walSizeBytes !== undefined && walSizeBytes >= WAL_DIRECTORY_SIZE_WARNING_BYTES) {
483
+ return `PostgreSQL WAL visibility warning: WAL directory is ${walSizeBytes} bytes, which is high and can increase disk pressure.`;
484
+ }
485
+ return undefined;
486
+ }
487
+ export function postgresCapacityWarning(diagnostics) {
488
+ const totalDatabaseSizeBytes = integerDiagnostic(diagnostics.total_database_size_bytes);
489
+ if (totalDatabaseSizeBytes === undefined || totalDatabaseSizeBytes < TOTAL_DATABASE_SIZE_WARNING_BYTES) {
490
+ return undefined;
491
+ }
492
+ return `PostgreSQL capacity risk: total database size is ${totalDatabaseSizeBytes} bytes. Monitor growth and WAL retention settings before disk becomes constrained.`;
493
+ }
494
+ export function postgresPostgresWarnings(diagnostics) {
495
+ const warnings = [];
496
+ const connectionWarning = postgresConnectionUtilizationWarning(diagnostics);
497
+ if (connectionWarning) {
498
+ warnings.push(connectionWarning);
499
+ }
500
+ const slowQueryWarning = postgresSlowQueryLoggingWarning(diagnostics);
501
+ if (slowQueryWarning) {
502
+ warnings.push(slowQueryWarning);
503
+ }
504
+ const extensionWarning = postgresExtensionVisibilityWarning(diagnostics);
505
+ if (extensionWarning) {
506
+ warnings.push(extensionWarning);
507
+ }
508
+ const longTransactionWarning = postgresLongTransactionWarning(diagnostics);
509
+ if (longTransactionWarning) {
510
+ warnings.push(longTransactionWarning);
511
+ }
512
+ const idleTransactionWarning = postgresIdleInTransactionWarning(diagnostics);
513
+ if (idleTransactionWarning) {
514
+ warnings.push(idleTransactionWarning);
515
+ }
516
+ const tableHealthWarning = postgresTableHealthWarning(diagnostics);
517
+ if (tableHealthWarning) {
518
+ warnings.push(tableHealthWarning);
519
+ }
520
+ const indexWarning = postgresUnusedIndexWarning(diagnostics);
521
+ if (indexWarning) {
522
+ warnings.push(indexWarning);
523
+ }
524
+ const walWarning = postgresWalWarning(diagnostics);
525
+ if (walWarning) {
526
+ warnings.push(walWarning);
527
+ }
528
+ const capacityWarning = postgresCapacityWarning(diagnostics);
529
+ if (capacityWarning) {
530
+ warnings.push(capacityWarning);
531
+ }
532
+ return warnings;
533
+ }
534
+ export function structuredPostgresDiagnostics(diagnostics) {
535
+ const connectionUtilizationPct = postgresConnectionUtilizationPct(diagnostics);
536
+ const databaseCount = integerDiagnostic(diagnostics.database_count);
537
+ const activeConnections = integerDiagnostic(diagnostics.active_connections);
538
+ const connectionCount = integerDiagnostic(diagnostics.connection_count);
539
+ const maxConnections = integerDiagnostic(diagnostics.max_connections);
540
+ const totalDatabaseSizeBytes = integerDiagnostic(diagnostics.total_database_size_bytes);
541
+ const largestDatabaseSizeBytes = integerDiagnostic(diagnostics.largest_database_size_bytes);
542
+ const tableHealthRiskyTableCount = integerDiagnostic(diagnostics.table_health_risky_table_count);
543
+ const tableHealthRequiresVacuumCount = integerDiagnostic(diagnostics.table_health_requires_vacuum_count);
544
+ const tableHealthRequiresAnalyzeCount = integerDiagnostic(diagnostics.table_health_requires_analyze_count);
545
+ const tableHealthTopRiskyDeadTupleRatio = decimalDiagnostic(diagnostics.table_health_top_risky_dead_tuple_ratio);
546
+ const tableHealthTopRiskyDeadTupleCount = integerDiagnostic(diagnostics.table_health_top_risky_dead_tuple_count);
547
+ const longTransactionCount = integerDiagnostic(diagnostics.long_transaction_count);
548
+ const oldestLongTransactionAgeSeconds = integerDiagnostic(diagnostics.oldest_long_transaction_age_seconds);
549
+ const idleInTransactionCount = integerDiagnostic(diagnostics.idle_in_transaction_count);
550
+ const oldestIdleInTransactionAgeSeconds = integerDiagnostic(diagnostics.oldest_idle_in_transaction_age_seconds);
551
+ const unusedIndexCandidatesCount = integerDiagnostic(diagnostics.unused_index_candidates_count);
552
+ const walFileCount = integerDiagnostic(diagnostics.wal_file_count);
553
+ const walDirectorySizeBytes = integerDiagnostic(diagnostics.wal_directory_size_bytes);
554
+ const defaultTablespaceSizeBytes = integerDiagnostic(diagnostics.default_tablespace_size_bytes);
555
+ const databaseWriteActivityRows = integerDiagnostic(diagnostics.database_write_activity_rows);
556
+ const structured = {
557
+ schemaVersion: POSTGRES_DIAGNOSTICS_CONTRACT_VERSION,
558
+ };
559
+ if (databaseCount !== undefined)
560
+ structured.databaseCount = databaseCount;
561
+ if (activeConnections !== undefined)
562
+ structured.activeConnections = activeConnections;
563
+ if (connectionCount !== undefined)
564
+ structured.connectionCount = connectionCount;
565
+ if (maxConnections !== undefined)
566
+ structured.maxConnections = maxConnections;
567
+ if (connectionUtilizationPct !== undefined)
568
+ structured.connectionUtilizationPct = connectionUtilizationPct;
569
+ if (totalDatabaseSizeBytes !== undefined)
570
+ structured.totalDatabaseSizeBytes = totalDatabaseSizeBytes;
571
+ if (diagnostics.largest_database_name) {
572
+ structured.largestDatabaseName = diagnostics.largest_database_name;
573
+ }
574
+ if (largestDatabaseSizeBytes !== undefined)
575
+ structured.largestDatabaseSizeBytes = largestDatabaseSizeBytes;
576
+ if (diagnostics.slow_query_logging) {
577
+ structured.slowQueryLogging = diagnostics.slow_query_logging;
578
+ }
579
+ if (diagnostics.pg_stat_statements) {
580
+ structured.pgStatStatements = diagnostics.pg_stat_statements;
581
+ }
582
+ if (diagnostics.pg_stat_statements_available_version) {
583
+ structured.pgStatStatementsAvailableVersion = diagnostics.pg_stat_statements_available_version;
584
+ }
585
+ if (diagnostics.pg_stat_statements_installed_version) {
586
+ structured.pgStatStatementsInstalledVersion = diagnostics.pg_stat_statements_installed_version;
587
+ }
588
+ if (diagnostics.shared_preload_libraries) {
589
+ structured.sharedPreloadLibraries = diagnostics.shared_preload_libraries;
590
+ }
591
+ if (diagnostics.shared_buffers) {
592
+ structured.sharedBuffers = diagnostics.shared_buffers;
593
+ }
594
+ if (longTransactionCount !== undefined) {
595
+ structured.longTransactionCount = longTransactionCount;
596
+ }
597
+ if (oldestLongTransactionAgeSeconds !== undefined) {
598
+ structured.oldestLongTransactionAgeSeconds = oldestLongTransactionAgeSeconds;
599
+ }
600
+ if (idleInTransactionCount !== undefined) {
601
+ structured.idleInTransactionCount = idleInTransactionCount;
602
+ }
603
+ if (oldestIdleInTransactionAgeSeconds !== undefined) {
604
+ structured.oldestIdleInTransactionAgeSeconds = oldestIdleInTransactionAgeSeconds;
605
+ }
606
+ if (tableHealthRiskyTableCount !== undefined) {
607
+ structured.tableHealthRiskyTableCount = tableHealthRiskyTableCount;
608
+ }
609
+ if (tableHealthRequiresVacuumCount !== undefined) {
610
+ structured.tableHealthRequiresVacuumCount = tableHealthRequiresVacuumCount;
611
+ }
612
+ if (tableHealthRequiresAnalyzeCount !== undefined) {
613
+ structured.tableHealthRequiresAnalyzeCount = tableHealthRequiresAnalyzeCount;
614
+ }
615
+ if (diagnostics.table_health_top_risky_table) {
616
+ structured.tableHealthTopRiskyTable = diagnostics.table_health_top_risky_table;
617
+ }
618
+ if (tableHealthTopRiskyDeadTupleRatio !== undefined) {
619
+ structured.tableHealthTopRiskyDeadTupleRatio = tableHealthTopRiskyDeadTupleRatio;
620
+ }
621
+ if (tableHealthTopRiskyDeadTupleCount !== undefined) {
622
+ structured.tableHealthTopRiskyDeadTupleCount = tableHealthTopRiskyDeadTupleCount;
623
+ }
624
+ if (unusedIndexCandidatesCount !== undefined) {
625
+ structured.unusedIndexCandidatesCount = unusedIndexCandidatesCount;
626
+ }
627
+ if (diagnostics.wal_level) {
628
+ structured.walLevel = diagnostics.wal_level;
629
+ }
630
+ if (diagnostics.wal_archive_mode) {
631
+ structured.walArchiveMode = diagnostics.wal_archive_mode;
632
+ }
633
+ if (walFileCount !== undefined) {
634
+ structured.walFileCount = walFileCount;
635
+ }
636
+ if (walDirectorySizeBytes !== undefined) {
637
+ structured.walDirectorySizeBytes = walDirectorySizeBytes;
638
+ }
639
+ if (defaultTablespaceSizeBytes !== undefined) {
640
+ structured.defaultTablespaceSizeBytes = defaultTablespaceSizeBytes;
641
+ }
642
+ if (databaseWriteActivityRows !== undefined) {
643
+ structured.databaseWriteActivityRows = databaseWriteActivityRows;
644
+ }
645
+ return structured;
646
+ }