@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.
- package/README.md +28 -5
- package/dist/doctor.js +6 -266
- package/dist/postgres-diagnostics.js +646 -0
- package/docs/1-0-readiness.md +129 -0
- package/docs/command-reference.md +110 -0
- package/docs/generated-environment-verification.md +11 -11
- package/docs/handoff.md +11 -0
- package/docs/lifecycle-recipes.md +190 -0
- package/docs/troubleshooting.md +225 -0
- package/package.json +1 -1
|
@@ -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
|
+
}
|