postgresai 0.15.0 → 0.16.0-rc.1
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 +3 -0
- package/bin/postgres-ai.ts +184 -31
- package/dist/bin/postgres-ai.js +386 -90
- package/lib/checkup-api.ts +75 -0
- package/lib/checkup-summary.ts +30 -0
- package/lib/checkup.ts +227 -21
- package/lib/metrics-loader.ts +10 -8
- package/lib/util.ts +10 -3
- package/package.json +1 -1
- package/scripts/embed-metrics.ts +7 -6
- package/test/checkup.integration.test.ts +55 -0
- package/test/checkup.test.ts +471 -1
- package/test/mcp-server.test.ts +4 -0
- package/test/monitoring.test.ts +128 -49
- package/test/schema-validation.test.ts +29 -0
- package/test/test-utils.ts +8 -0
- package/test/util.test.ts +44 -0
package/dist/bin/postgres-ai.js
CHANGED
|
@@ -13334,9 +13334,11 @@ function isHtmlContent2(text) {
|
|
|
13334
13334
|
function formatHttpError2(operation, status, responseBody) {
|
|
13335
13335
|
const statusMessage = HTTP_STATUS_MESSAGES2[status] || "Request failed";
|
|
13336
13336
|
let errMsg = `${operation}: HTTP ${status} - ${statusMessage}`;
|
|
13337
|
+
const remediation = status === 401 ? `
|
|
13338
|
+
${AUTH_REMEDIATION_HINT2}` : "";
|
|
13337
13339
|
if (responseBody) {
|
|
13338
13340
|
if (isHtmlContent2(responseBody)) {
|
|
13339
|
-
return errMsg;
|
|
13341
|
+
return errMsg + remediation;
|
|
13340
13342
|
}
|
|
13341
13343
|
try {
|
|
13342
13344
|
const errObj = JSON.parse(responseBody);
|
|
@@ -13356,7 +13358,7 @@ ${trimmed}`;
|
|
|
13356
13358
|
}
|
|
13357
13359
|
}
|
|
13358
13360
|
}
|
|
13359
|
-
return errMsg;
|
|
13361
|
+
return errMsg + remediation;
|
|
13360
13362
|
}
|
|
13361
13363
|
function maskSecret2(secret) {
|
|
13362
13364
|
if (!secret)
|
|
@@ -13389,7 +13391,7 @@ function resolveBaseUrls2(opts, cfg, defaults2 = {}) {
|
|
|
13389
13391
|
storageBaseUrl: normalizeBaseUrl2(storageCandidate)
|
|
13390
13392
|
};
|
|
13391
13393
|
}
|
|
13392
|
-
var HTTP_STATUS_MESSAGES2;
|
|
13394
|
+
var HTTP_STATUS_MESSAGES2, AUTH_REMEDIATION_HINT2 = "Run 'postgresai auth' to (re)authenticate, or set/update PGAI_API_KEY.";
|
|
13393
13395
|
var init_util = __esm(() => {
|
|
13394
13396
|
HTTP_STATUS_MESSAGES2 = {
|
|
13395
13397
|
400: "Bad Request",
|
|
@@ -13423,7 +13425,7 @@ var {
|
|
|
13423
13425
|
// package.json
|
|
13424
13426
|
var package_default = {
|
|
13425
13427
|
name: "postgresai",
|
|
13426
|
-
version: "0.
|
|
13428
|
+
version: "0.16.0-rc.1",
|
|
13427
13429
|
description: "postgres_ai CLI",
|
|
13428
13430
|
license: "Apache-2.0",
|
|
13429
13431
|
private: false,
|
|
@@ -16254,7 +16256,7 @@ var Result = import_lib.default.Result;
|
|
|
16254
16256
|
var TypeOverrides = import_lib.default.TypeOverrides;
|
|
16255
16257
|
var defaults = import_lib.default.defaults;
|
|
16256
16258
|
// package.json
|
|
16257
|
-
var version = "0.
|
|
16259
|
+
var version = "0.16.0-rc.1";
|
|
16258
16260
|
var package_default2 = {
|
|
16259
16261
|
name: "postgresai",
|
|
16260
16262
|
version,
|
|
@@ -16395,12 +16397,15 @@ function isHtmlContent(text) {
|
|
|
16395
16397
|
const trimmed = text.trim();
|
|
16396
16398
|
return trimmed.startsWith("<!DOCTYPE") || trimmed.startsWith("<html") || trimmed.startsWith("<HTML");
|
|
16397
16399
|
}
|
|
16400
|
+
var AUTH_REMEDIATION_HINT = "Run 'postgresai auth' to (re)authenticate, or set/update PGAI_API_KEY.";
|
|
16398
16401
|
function formatHttpError(operation, status, responseBody) {
|
|
16399
16402
|
const statusMessage = HTTP_STATUS_MESSAGES[status] || "Request failed";
|
|
16400
16403
|
let errMsg = `${operation}: HTTP ${status} - ${statusMessage}`;
|
|
16404
|
+
const remediation = status === 401 ? `
|
|
16405
|
+
${AUTH_REMEDIATION_HINT}` : "";
|
|
16401
16406
|
if (responseBody) {
|
|
16402
16407
|
if (isHtmlContent(responseBody)) {
|
|
16403
|
-
return errMsg;
|
|
16408
|
+
return errMsg + remediation;
|
|
16404
16409
|
}
|
|
16405
16410
|
try {
|
|
16406
16411
|
const errObj = JSON.parse(responseBody);
|
|
@@ -16420,7 +16425,7 @@ ${trimmed}`;
|
|
|
16420
16425
|
}
|
|
16421
16426
|
}
|
|
16422
16427
|
}
|
|
16423
|
-
return errMsg;
|
|
16428
|
+
return errMsg + remediation;
|
|
16424
16429
|
}
|
|
16425
16430
|
function maskSecret(secret) {
|
|
16426
16431
|
if (!secret)
|
|
@@ -27689,7 +27694,7 @@ where
|
|
|
27689
27694
|
pg_invalid_indexes: {
|
|
27690
27695
|
description: "This metric identifies invalid indexes in the database with decision tree data for remediation. It provides insights into whether to DROP (if duplicate exists), RECREATE (if backs constraint), or flag as UNCERTAIN (if additional RCA is needed to check query plans). Decision tree: 1) Valid duplicate exists -> DROP, 2) Backs PK/UNIQUE constraint -> RECREATE, 3) Table < 10K rows -> RECREATE (small tables rebuild quickly, typically under 1 second), 4) Otherwise -> UNCERTAIN (need query plan analysis to assess impact). Adapts the top-N + `'$other$'` bucket pattern from !262 to this metric: ranks invalid indexes by `index_size_bytes desc` (ties broken by schema, table, then index name for stability), keeps the top 100, and folds the tail into a single `'$other$'` row whose `index_size_bytes` / `table_row_estimate` are summed and whose tag columns carry the literal `'$other$'` sentinel. The `'$other$'` row is omitted entirely (via `HAVING count(*) > 0`) when all invalid indexes fit within the top-100 cap, so its absence on healthy clusters is normal.",
|
|
27691
27696
|
sqls: {
|
|
27692
|
-
11: `with fk_indexes as ( /* pgwatch_generated */
|
|
27697
|
+
11: `with fk_indexes as materialized ( /* pgwatch_generated */
|
|
27693
27698
|
select
|
|
27694
27699
|
schemaname as schema_name,
|
|
27695
27700
|
indexrelid,
|
|
@@ -27815,7 +27820,7 @@ having count(*) > 0;
|
|
|
27815
27820
|
unused_indexes: {
|
|
27816
27821
|
description: "This metric identifies unused indexes in the database. It provides insights into the number of unused indexes and their details. This metric helps administrators identify and fix unused indexes to improve database performance. Adapts the top-N + `'$other$'` bucket pattern from !262 to this metric: within the `idx_scan = 0 AND idx_is_btree` filter, ranks indexes by `index_size_bytes desc` (ties broken by schema, table, index name), keeps the top 100, and folds the tail into a single `'$other$'` row. Counter columns (`idx_scan`, `all_scans`, `writes`, `index_size_bytes`, `table_size_bytes`, `relpages`) are summed across the tail; ratio columns (`index_scan_pct`, `scans_per_write`) and the `supports_fk` boolean are deliberately zeroed/false on the aggregate row because the tail-level average would mislead and the per-row FK relationship has no meaningful aggregate. Tag columns carry the literal `'$other$'` sentinel. The `'$other$'` row is omitted entirely (via `HAVING count(*) > 0`) when ≤100 indexes match the unused filter.",
|
|
27817
27822
|
sqls: {
|
|
27818
|
-
11: `with fk_indexes as ( /* pgwatch_generated */
|
|
27823
|
+
11: `with fk_indexes as materialized ( /* pgwatch_generated */
|
|
27819
27824
|
select
|
|
27820
27825
|
n.nspname as schema_name,
|
|
27821
27826
|
ci.relname as index_name,
|
|
@@ -27956,7 +27961,7 @@ having count(*) > 0;
|
|
|
27956
27961
|
redundant_indexes: {
|
|
27957
27962
|
description: "This metric identifies redundant indexes that can potentially be dropped to save storage space and improve write performance. It analyzes index relationships and finds indexes that are covered by other indexes, considering column order, operator classes, and foreign key constraints. Uses the exact logic from tmp.sql with JSON aggregation and proper thresholds. Adapts the top-N + `'$other$'` bucket pattern from !262 to this metric: ranks redundant indexes by `index_size_bytes desc` (ties broken by `table_name`), keeps the top 100, and folds the tail into a single `'$other$'` row whose `table_size_bytes`, `index_size_bytes` and `index_usage` columns are summed and whose tag columns carry the literal `'$other$'` sentinel. The `redundant_indexes_grouped` CTE intentionally preserves duplicate column aliases (`tag_schema_name` / `tag_index_name` appear twice — once from the raw name and once from the `formated_*` variant) because the dashboards rely on both spellings; the duplication is preserved on the `'$other$'` row for consistency. The `'$other$'` row is omitted entirely (via `HAVING count(*) > 0`) when there are ≤100 redundant pairs, so its absence on healthy clusters is normal.",
|
|
27958
27963
|
sqls: {
|
|
27959
|
-
11: `with fk_indexes as ( /* pgwatch_generated */
|
|
27964
|
+
11: `with fk_indexes as materialized ( /* pgwatch_generated */
|
|
27960
27965
|
select
|
|
27961
27966
|
n.nspname as schema_name,
|
|
27962
27967
|
ci.relname as index_name,
|
|
@@ -28163,6 +28168,64 @@ where datname = current_database()
|
|
|
28163
28168
|
gauges: ["stats_reset_epoch", "seconds_since_reset"],
|
|
28164
28169
|
statement_timeout_seconds: 15
|
|
28165
28170
|
},
|
|
28171
|
+
pg_dead_tuples: {
|
|
28172
|
+
description: "Per-table dead-tuple accumulation and per-table autovacuum overrides for the F003 express check (\"Autovacuum: dead tuples\"). Reads the live counters in pg_stat_user_tables joined to pg_class for reloptions, so dead tuples that have NEVER been vacuumed are visible — unlike the statistical bloat estimators (F004/F005), which completely miss a table where autovacuum is disabled and dead tuples pile up unvacuumed. Returns relations that either carry dead tuples or have autovacuum disabled per-table, bounded to the top 100 by n_dead_tup so pathological schemas can't blow up the report. autovacuum_disabled recognises every boolean spelling Postgres accepts and stores verbatim in reloptions (false/off/no/0 and their unique prefixes; confirmed against PG13 and PG16 that `alter table .. set (autovacuum_enabled = false|off|0)` stores the literal text, so matching only 'autovacuum_enabled=off' misses the common '=false' spelling). Timestamps are exported as epoch seconds (0 = never) because Prometheus gauges must be numeric. The AccessExclusiveLock guard mirrors table_stats: pg_table_size() takes a lock and would block behind a fully locked relation. Compatible with all supported Postgres versions.",
|
|
28173
|
+
sqls: {
|
|
28174
|
+
11: `with dead_tuples as ( /* pgwatch_generated */
|
|
28175
|
+
select
|
|
28176
|
+
s.schemaname,
|
|
28177
|
+
s.relname,
|
|
28178
|
+
s.n_live_tup,
|
|
28179
|
+
s.n_dead_tup,
|
|
28180
|
+
coalesce(
|
|
28181
|
+
round(100 * s.n_dead_tup::numeric / nullif(s.n_live_tup + s.n_dead_tup, 0), 2),
|
|
28182
|
+
0
|
|
28183
|
+
)::float as dead_pct,
|
|
28184
|
+
extract(epoch from greatest(s.last_autovacuum, '1970-01-01Z'))::int8 as last_autovacuum,
|
|
28185
|
+
extract(epoch from greatest(s.last_vacuum, '1970-01-01Z'))::int8 as last_vacuum,
|
|
28186
|
+
s.autovacuum_count,
|
|
28187
|
+
s.vacuum_count,
|
|
28188
|
+
case when exists (
|
|
28189
|
+
select 1
|
|
28190
|
+
from unnest(coalesce(c.reloptions, '{}'::text[])) as opt
|
|
28191
|
+
where lower(opt) ~ '^autovacuum_enabled=(false|fals|fal|fa|f|no|n|off|of|0)$'
|
|
28192
|
+
) then 1 else 0 end as autovacuum_disabled,
|
|
28193
|
+
pg_table_size(c.oid) as table_size_b
|
|
28194
|
+
from pg_stat_user_tables s
|
|
28195
|
+
join pg_class c on c.oid = s.relid
|
|
28196
|
+
where not exists (
|
|
28197
|
+
select 1 from pg_locks
|
|
28198
|
+
where relation = s.relid and mode = 'AccessExclusiveLock'
|
|
28199
|
+
)
|
|
28200
|
+
), ranked as (
|
|
28201
|
+
select
|
|
28202
|
+
row_number() over (
|
|
28203
|
+
order by n_dead_tup desc, schemaname, relname
|
|
28204
|
+
) as rownum,
|
|
28205
|
+
*
|
|
28206
|
+
from dead_tuples
|
|
28207
|
+
where n_dead_tup > 0 or autovacuum_disabled = 1
|
|
28208
|
+
)
|
|
28209
|
+
select
|
|
28210
|
+
current_database() as tag_datname,
|
|
28211
|
+
schemaname as tag_schemaname,
|
|
28212
|
+
relname as tag_relname,
|
|
28213
|
+
n_live_tup,
|
|
28214
|
+
n_dead_tup,
|
|
28215
|
+
dead_pct,
|
|
28216
|
+
last_autovacuum,
|
|
28217
|
+
last_vacuum,
|
|
28218
|
+
autovacuum_count,
|
|
28219
|
+
vacuum_count,
|
|
28220
|
+
autovacuum_disabled,
|
|
28221
|
+
table_size_b
|
|
28222
|
+
from ranked
|
|
28223
|
+
where rownum <= 100
|
|
28224
|
+
`
|
|
28225
|
+
},
|
|
28226
|
+
gauges: ["n_live_tup", "n_dead_tup", "dead_pct", "last_autovacuum", "last_vacuum", "autovacuum_count", "vacuum_count", "autovacuum_disabled", "table_size_b"],
|
|
28227
|
+
statement_timeout_seconds: 15
|
|
28228
|
+
},
|
|
28166
28229
|
pg_table_bloat: {
|
|
28167
28230
|
description: "Estimated per-table bloat (heap pages allocated vs heap pages needed at perfect packing), bounded to the top 100 per database. Adapts the top-N + `'$other$'` bucket pattern from !262: everything below the cap is summed into a single `'$other$'` row so dashboard \"total bloat across the DB\" stays correct even when the tail is large. Ranks by `bloat_pct` descending (most-bloated tables first), with `is_na = 0` preferred (don't crowd top-N with tables whose estimate is unreliable) and stable schemaname/tblname tiebreakers. Preserves the existing >1 MiB filter (zero-byte and tiny tables aren't interesting for bloat). Aggregate semantics on the `'$other$'` row: sum for real_size_mib / extra_size / bloat_size (total wasted bytes in the tail); recompute extra_pct and bloat_pct from the summed numerator/denominator (weighted-avg effectively); avg(fillfactor); max(is_na) (any tail row with bad stats taints the aggregate). The `'$other$'` sentinel cannot collide with a real Postgres identifier.",
|
|
28168
28231
|
sqls: {
|
|
@@ -28477,8 +28540,8 @@ group by
|
|
|
28477
28540
|
(sum(coalesce(write_bytes, 0)) / 1048576.0)::int8 as write_bytes_mb,
|
|
28478
28541
|
sum(coalesce(write_time, 0))::int8 as write_time_ms,
|
|
28479
28542
|
sum(coalesce(writebacks, 0))::int8 as writebacks,
|
|
28480
|
-
-- PostgreSQL 18
|
|
28481
|
-
|
|
28543
|
+
-- PostgreSQL 18 removed the per-operation byte size column and exposes no writeback byte counts, so this is always 0.
|
|
28544
|
+
0::int8 as writeback_bytes_mb,
|
|
28482
28545
|
sum(coalesce(writeback_time, 0))::int8 as writeback_time_ms,
|
|
28483
28546
|
sum(coalesce(fsyncs, 0))::int8 as fsyncs,
|
|
28484
28547
|
sum(coalesce(fsync_time, 0))::int8 as fsync_time_ms,
|
|
@@ -28515,6 +28578,7 @@ var METRIC_NAMES = {
|
|
|
28515
28578
|
H001: "pg_invalid_indexes",
|
|
28516
28579
|
H002: "unused_indexes",
|
|
28517
28580
|
H004: "redundant_indexes",
|
|
28581
|
+
F003: "pg_dead_tuples",
|
|
28518
28582
|
F004: "pg_table_bloat",
|
|
28519
28583
|
F005: "pg_btree_bloat",
|
|
28520
28584
|
settings: "settings",
|
|
@@ -28809,46 +28873,6 @@ var CHECKUP_DICTIONARY_DATA = [
|
|
|
28809
28873
|
sort_order: null,
|
|
28810
28874
|
is_system_report: false
|
|
28811
28875
|
},
|
|
28812
|
-
{
|
|
28813
|
-
code: "F001",
|
|
28814
|
-
title: "Autovacuum: current settings",
|
|
28815
|
-
description: "Autovacuum configuration",
|
|
28816
|
-
category: "vacuum",
|
|
28817
|
-
sort_order: null,
|
|
28818
|
-
is_system_report: false
|
|
28819
|
-
},
|
|
28820
|
-
{
|
|
28821
|
-
code: "F002",
|
|
28822
|
-
title: "Autovacuum: transaction ID wraparound",
|
|
28823
|
-
description: "XID age and wraparound risk",
|
|
28824
|
-
category: "vacuum",
|
|
28825
|
-
sort_order: null,
|
|
28826
|
-
is_system_report: false
|
|
28827
|
-
},
|
|
28828
|
-
{
|
|
28829
|
-
code: "F003",
|
|
28830
|
-
title: "Autovacuum: dead tuples",
|
|
28831
|
-
description: "Dead tuple counts and cleanup status",
|
|
28832
|
-
category: "vacuum",
|
|
28833
|
-
sort_order: null,
|
|
28834
|
-
is_system_report: false
|
|
28835
|
-
},
|
|
28836
|
-
{
|
|
28837
|
-
code: "F004",
|
|
28838
|
-
title: "Autovacuum: heap bloat estimate",
|
|
28839
|
-
description: "Estimated table bloat",
|
|
28840
|
-
category: "vacuum",
|
|
28841
|
-
sort_order: null,
|
|
28842
|
-
is_system_report: false
|
|
28843
|
-
},
|
|
28844
|
-
{
|
|
28845
|
-
code: "F005",
|
|
28846
|
-
title: "Autovacuum: index bloat estimate",
|
|
28847
|
-
description: "Estimated index bloat",
|
|
28848
|
-
category: "vacuum",
|
|
28849
|
-
sort_order: null,
|
|
28850
|
-
is_system_report: false
|
|
28851
|
-
},
|
|
28852
28876
|
{
|
|
28853
28877
|
code: "F006",
|
|
28854
28878
|
title: "Precise heap bloat analysis",
|
|
@@ -28865,14 +28889,6 @@ var CHECKUP_DICTIONARY_DATA = [
|
|
|
28865
28889
|
sort_order: null,
|
|
28866
28890
|
is_system_report: false
|
|
28867
28891
|
},
|
|
28868
|
-
{
|
|
28869
|
-
code: "F008",
|
|
28870
|
-
title: "Autovacuum: resource usage",
|
|
28871
|
-
description: "Autovacuum I/O and CPU impact",
|
|
28872
|
-
category: "vacuum",
|
|
28873
|
-
sort_order: null,
|
|
28874
|
-
is_system_report: false
|
|
28875
|
-
},
|
|
28876
28892
|
{
|
|
28877
28893
|
code: "G001",
|
|
28878
28894
|
title: "Memory-related settings",
|
|
@@ -29088,6 +29104,54 @@ var CHECKUP_DICTIONARY_DATA = [
|
|
|
29088
29104
|
category: "waits",
|
|
29089
29105
|
sort_order: null,
|
|
29090
29106
|
is_system_report: false
|
|
29107
|
+
},
|
|
29108
|
+
{
|
|
29109
|
+
code: "F001",
|
|
29110
|
+
title: "Autovacuum settings",
|
|
29111
|
+
description: "Autovacuum configuration",
|
|
29112
|
+
category: "vacuum",
|
|
29113
|
+
sort_order: null,
|
|
29114
|
+
is_system_report: false
|
|
29115
|
+
},
|
|
29116
|
+
{
|
|
29117
|
+
code: "F002",
|
|
29118
|
+
title: "Autovacuum transaction ID wraparound",
|
|
29119
|
+
description: "XID age and wraparound risk",
|
|
29120
|
+
category: "vacuum",
|
|
29121
|
+
sort_order: null,
|
|
29122
|
+
is_system_report: false
|
|
29123
|
+
},
|
|
29124
|
+
{
|
|
29125
|
+
code: "F003",
|
|
29126
|
+
title: "Autovacuum dead tuples",
|
|
29127
|
+
description: "Dead tuple counts and cleanup status",
|
|
29128
|
+
category: "vacuum",
|
|
29129
|
+
sort_order: null,
|
|
29130
|
+
is_system_report: false
|
|
29131
|
+
},
|
|
29132
|
+
{
|
|
29133
|
+
code: "F004",
|
|
29134
|
+
title: "Autovacuum heap bloat estimate",
|
|
29135
|
+
description: "Estimated table bloat",
|
|
29136
|
+
category: "vacuum",
|
|
29137
|
+
sort_order: null,
|
|
29138
|
+
is_system_report: false
|
|
29139
|
+
},
|
|
29140
|
+
{
|
|
29141
|
+
code: "F005",
|
|
29142
|
+
title: "Autovacuum index bloat estimate",
|
|
29143
|
+
description: "Estimated index bloat",
|
|
29144
|
+
category: "vacuum",
|
|
29145
|
+
sort_order: null,
|
|
29146
|
+
is_system_report: false
|
|
29147
|
+
},
|
|
29148
|
+
{
|
|
29149
|
+
code: "F008",
|
|
29150
|
+
title: "Autovacuum resource usage",
|
|
29151
|
+
description: "Autovacuum I/O and CPU impact",
|
|
29152
|
+
category: "vacuum",
|
|
29153
|
+
sort_order: null,
|
|
29154
|
+
is_system_report: false
|
|
29091
29155
|
}
|
|
29092
29156
|
];
|
|
29093
29157
|
|
|
@@ -29109,6 +29173,9 @@ var SECONDS_PER_MINUTE = 60;
|
|
|
29109
29173
|
function toBool(val) {
|
|
29110
29174
|
return val === true || val === 1 || val === "t" || val === "true";
|
|
29111
29175
|
}
|
|
29176
|
+
var F003_DEAD_TUPLES_MIN = 1e5;
|
|
29177
|
+
var F003_DEAD_PCT_MIN = 20;
|
|
29178
|
+
var F003_AUTOVACUUM_DISABLED_MIN_ROWS = 1e4;
|
|
29112
29179
|
function parseVersionNum(versionNum) {
|
|
29113
29180
|
if (!versionNum || versionNum.length < 6) {
|
|
29114
29181
|
return { major: "", minor: "" };
|
|
@@ -29515,6 +29582,58 @@ async function getRedundantIndexes(client, pgMajorVersion = 16) {
|
|
|
29515
29582
|
return result2;
|
|
29516
29583
|
});
|
|
29517
29584
|
}
|
|
29585
|
+
async function getDeadTuples(client, pgMajorVersion = 16) {
|
|
29586
|
+
const sql = getMetricSql(METRIC_NAMES.F003, pgMajorVersion);
|
|
29587
|
+
const result = await client.query(sql);
|
|
29588
|
+
return result.rows.map((row) => {
|
|
29589
|
+
const t = transformMetricRow(row);
|
|
29590
|
+
const nLive = parseInt(String(t.n_live_tup || 0), 10);
|
|
29591
|
+
const nDead = parseInt(String(t.n_dead_tup || 0), 10);
|
|
29592
|
+
const deadPct = parseFloat(String(t.dead_pct)) || 0;
|
|
29593
|
+
const lastAutovacuumEpoch = parseInt(String(t.last_autovacuum || 0), 10);
|
|
29594
|
+
const lastVacuumEpoch = parseInt(String(t.last_vacuum || 0), 10);
|
|
29595
|
+
const autovacuumDisabled = parseInt(String(t.autovacuum_disabled || 0), 10) === 1 || toBool(t.autovacuum_disabled);
|
|
29596
|
+
const tableSizeBytes = parseInt(String(t.table_size_b || 0), 10);
|
|
29597
|
+
return {
|
|
29598
|
+
schema_name: String(t.schemaname || ""),
|
|
29599
|
+
table_name: String(t.relname || ""),
|
|
29600
|
+
n_live_tup: nLive,
|
|
29601
|
+
n_dead_tup: nDead,
|
|
29602
|
+
dead_pct: deadPct,
|
|
29603
|
+
last_autovacuum: lastAutovacuumEpoch > 0 ? new Date(lastAutovacuumEpoch * 1000).toISOString() : null,
|
|
29604
|
+
last_autovacuum_epoch: lastAutovacuumEpoch,
|
|
29605
|
+
last_vacuum: lastVacuumEpoch > 0 ? new Date(lastVacuumEpoch * 1000).toISOString() : null,
|
|
29606
|
+
last_vacuum_epoch: lastVacuumEpoch,
|
|
29607
|
+
autovacuum_count: parseInt(String(t.autovacuum_count || 0), 10),
|
|
29608
|
+
vacuum_count: parseInt(String(t.vacuum_count || 0), 10),
|
|
29609
|
+
autovacuum_disabled: autovacuumDisabled,
|
|
29610
|
+
table_size_bytes: tableSizeBytes,
|
|
29611
|
+
table_size_pretty: formatBytes(tableSizeBytes),
|
|
29612
|
+
exceeds_dead_tuple_thresholds: nDead >= F003_DEAD_TUPLES_MIN && deadPct >= F003_DEAD_PCT_MIN,
|
|
29613
|
+
autovacuum_disabled_flagged: autovacuumDisabled && nLive + nDead >= F003_AUTOVACUUM_DISABLED_MIN_ROWS
|
|
29614
|
+
};
|
|
29615
|
+
});
|
|
29616
|
+
}
|
|
29617
|
+
function buildDeadTuplesConclusions(tables) {
|
|
29618
|
+
const conclusions = [];
|
|
29619
|
+
const recommendations = [];
|
|
29620
|
+
const fmt = (n) => n.toLocaleString("en-US");
|
|
29621
|
+
for (const t of tables) {
|
|
29622
|
+
const rel = `"${t.schema_name}"."${t.table_name}"`;
|
|
29623
|
+
const lastAv = t.last_autovacuum ? `last autovacuum: ${t.last_autovacuum}` : "autovacuum has never vacuumed it";
|
|
29624
|
+
if (t.exceeds_dead_tuple_thresholds && t.autovacuum_disabled) {
|
|
29625
|
+
conclusions.push(`Table ${rel} has ${fmt(t.n_dead_tup)} dead tuples (${t.dead_pct}% of all tuples) ` + `and autovacuum is disabled on it via reloptions (${lastAv}).`);
|
|
29626
|
+
recommendations.push(`Re-enable autovacuum on ${rel}: alter table ${rel} reset (autovacuum_enabled); ` + `then run: vacuum (analyze) ${rel}; to clean up the accumulated dead tuples.`);
|
|
29627
|
+
} else if (t.exceeds_dead_tuple_thresholds) {
|
|
29628
|
+
conclusions.push(`Table ${rel} has ${fmt(t.n_dead_tup)} dead tuples (${t.dead_pct}% of all tuples; ${lastAv}).`);
|
|
29629
|
+
recommendations.push(`Run: vacuum (analyze) ${rel}; and review autovacuum settings ` + `(autovacuum_vacuum_scale_factor, autovacuum_vacuum_cost_delay, autovacuum_max_workers) ` + `if dead tuples keep accumulating on ${rel}.`);
|
|
29630
|
+
} else if (t.autovacuum_disabled_flagged) {
|
|
29631
|
+
conclusions.push(`Autovacuum is disabled via reloptions on table ${rel} ` + `(~${fmt(t.n_live_tup + t.n_dead_tup)} tuples); dead tuples and transaction ID age ` + `will accumulate unchecked.`);
|
|
29632
|
+
recommendations.push(`Re-enable autovacuum on ${rel}: alter table ${rel} reset (autovacuum_enabled); ` + `unless this table is managed by a carefully scheduled manual vacuum job.`);
|
|
29633
|
+
}
|
|
29634
|
+
}
|
|
29635
|
+
return { conclusions, recommendations };
|
|
29636
|
+
}
|
|
29518
29637
|
function createBaseReport(checkId, checkTitle, nodeName) {
|
|
29519
29638
|
const buildTs = resolveBuildTs();
|
|
29520
29639
|
return {
|
|
@@ -29778,6 +29897,40 @@ async function generateF001(client, nodeName) {
|
|
|
29778
29897
|
};
|
|
29779
29898
|
return report;
|
|
29780
29899
|
}
|
|
29900
|
+
async function generateF003(client, nodeName) {
|
|
29901
|
+
const report = createBaseReport("F003", "Autovacuum: dead tuples", nodeName);
|
|
29902
|
+
const postgresVersion = await getPostgresVersion(client);
|
|
29903
|
+
const pgMajorVersion = parseInt(postgresVersion.server_major_ver, 10) || 16;
|
|
29904
|
+
const tables = await getDeadTuples(client, pgMajorVersion);
|
|
29905
|
+
const { datname: dbName, size_bytes: dbSizeBytes } = await getCurrentDatabaseInfo(client, pgMajorVersion);
|
|
29906
|
+
const flaggedCount = tables.filter((t) => t.exceeds_dead_tuple_thresholds).length;
|
|
29907
|
+
const autovacuumDisabledCount = tables.filter((t) => t.autovacuum_disabled).length;
|
|
29908
|
+
const autovacuumDisabledFlaggedCount = tables.filter((t) => t.autovacuum_disabled_flagged).length;
|
|
29909
|
+
const totalDeadTuples = tables.reduce((sum, t) => sum + t.n_dead_tup, 0);
|
|
29910
|
+
const { conclusions, recommendations } = buildDeadTuplesConclusions(tables);
|
|
29911
|
+
const dbEntry = {
|
|
29912
|
+
dead_tuples_tables: tables,
|
|
29913
|
+
total_count: tables.length,
|
|
29914
|
+
flagged_count: flaggedCount,
|
|
29915
|
+
autovacuum_disabled_count: autovacuumDisabledCount,
|
|
29916
|
+
autovacuum_disabled_flagged_count: autovacuumDisabledFlaggedCount,
|
|
29917
|
+
total_dead_tuples: totalDeadTuples,
|
|
29918
|
+
thresholds: {
|
|
29919
|
+
dead_tuples_min: F003_DEAD_TUPLES_MIN,
|
|
29920
|
+
dead_pct_min: F003_DEAD_PCT_MIN,
|
|
29921
|
+
autovacuum_disabled_min_rows: F003_AUTOVACUUM_DISABLED_MIN_ROWS
|
|
29922
|
+
},
|
|
29923
|
+
conclusions,
|
|
29924
|
+
recommendations,
|
|
29925
|
+
database_size_bytes: dbSizeBytes,
|
|
29926
|
+
database_size_pretty: formatBytes(dbSizeBytes)
|
|
29927
|
+
};
|
|
29928
|
+
report.results[nodeName] = {
|
|
29929
|
+
data: { [dbName]: dbEntry },
|
|
29930
|
+
postgres_version: postgresVersion
|
|
29931
|
+
};
|
|
29932
|
+
return report;
|
|
29933
|
+
}
|
|
29781
29934
|
async function generateF004(client, nodeName) {
|
|
29782
29935
|
const report = createBaseReport("F004", "Autovacuum: heap bloat (estimated)", nodeName);
|
|
29783
29936
|
const postgresVersion = await getPostgresVersion(client);
|
|
@@ -30211,6 +30364,7 @@ var REPORT_GENERATORS = {
|
|
|
30211
30364
|
D001: generateD001,
|
|
30212
30365
|
D004: generateD004,
|
|
30213
30366
|
F001: generateF001,
|
|
30367
|
+
F003: generateF003,
|
|
30214
30368
|
F004: generateF004,
|
|
30215
30369
|
F005: generateF005,
|
|
30216
30370
|
G001: generateG001,
|
|
@@ -30452,6 +30606,45 @@ async function postRpc(params) {
|
|
|
30452
30606
|
req.end();
|
|
30453
30607
|
});
|
|
30454
30608
|
}
|
|
30609
|
+
var VERIFY_API_KEY_TIMEOUT_MS = 1e4;
|
|
30610
|
+
async function verifyApiKey(params) {
|
|
30611
|
+
const { apiKey, apiBaseUrl, timeoutMs = VERIFY_API_KEY_TIMEOUT_MS } = params;
|
|
30612
|
+
const base = normalizeBaseUrl(apiBaseUrl);
|
|
30613
|
+
const url = new URL3(`${base}/checkup_reports`);
|
|
30614
|
+
url.searchParams.set("limit", "1");
|
|
30615
|
+
if (url.protocol === "http:") {
|
|
30616
|
+
const hostname = url.hostname.replace(/^\[|\]$/g, "");
|
|
30617
|
+
const isLoopback = ["localhost", "127.0.0.1", "::1"].includes(hostname);
|
|
30618
|
+
if (!isLoopback && process.env.CHECKUP_ALLOW_HTTP !== "1") {
|
|
30619
|
+
return {
|
|
30620
|
+
status: "unknown",
|
|
30621
|
+
detail: `refusing to send API key over plaintext HTTP to '${url.host}'`
|
|
30622
|
+
};
|
|
30623
|
+
}
|
|
30624
|
+
}
|
|
30625
|
+
const controller = new AbortController;
|
|
30626
|
+
const timer = setTimeout(() => controller.abort(), timeoutMs);
|
|
30627
|
+
try {
|
|
30628
|
+
const response = await fetch(url.toString(), {
|
|
30629
|
+
method: "GET",
|
|
30630
|
+
headers: { "access-token": apiKey },
|
|
30631
|
+
signal: controller.signal
|
|
30632
|
+
});
|
|
30633
|
+
await response.text().catch(() => "");
|
|
30634
|
+
if (response.status === 401 || response.status === 403) {
|
|
30635
|
+
return { status: "invalid", statusCode: response.status };
|
|
30636
|
+
}
|
|
30637
|
+
if (response.ok) {
|
|
30638
|
+
return { status: "valid" };
|
|
30639
|
+
}
|
|
30640
|
+
return { status: "unknown", detail: `HTTP ${response.status}` };
|
|
30641
|
+
} catch (err) {
|
|
30642
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
30643
|
+
return { status: "unknown", detail: message };
|
|
30644
|
+
} finally {
|
|
30645
|
+
clearTimeout(timer);
|
|
30646
|
+
}
|
|
30647
|
+
}
|
|
30455
30648
|
async function createCheckupReport(params) {
|
|
30456
30649
|
const { apiKey, apiBaseUrl, project, status } = params;
|
|
30457
30650
|
const bodyObj = {
|
|
@@ -30544,6 +30737,8 @@ function generateCheckSummary(checkId, report) {
|
|
|
30544
30737
|
return summarizeD004(nodeData);
|
|
30545
30738
|
case "F001":
|
|
30546
30739
|
return summarizeF001(nodeData);
|
|
30740
|
+
case "F003":
|
|
30741
|
+
return summarizeF003(nodeData);
|
|
30547
30742
|
case "G001":
|
|
30548
30743
|
return summarizeG001(nodeData);
|
|
30549
30744
|
case "G003":
|
|
@@ -30708,6 +30903,27 @@ function summarizeF001(nodeData) {
|
|
|
30708
30903
|
message: `${settingsCount} autovacuum setting${settingsCount > 1 ? "s" : ""} collected`
|
|
30709
30904
|
};
|
|
30710
30905
|
}
|
|
30906
|
+
function summarizeF003(nodeData) {
|
|
30907
|
+
const data = nodeData?.data || {};
|
|
30908
|
+
let flaggedCount = 0;
|
|
30909
|
+
let disabledCount = 0;
|
|
30910
|
+
for (const dbData of Object.values(data)) {
|
|
30911
|
+
const dbEntry = dbData;
|
|
30912
|
+
flaggedCount += dbEntry.flagged_count || 0;
|
|
30913
|
+
disabledCount += dbEntry.autovacuum_disabled_flagged_count || 0;
|
|
30914
|
+
}
|
|
30915
|
+
if (flaggedCount === 0 && disabledCount === 0) {
|
|
30916
|
+
return { status: "ok", message: "No significant dead tuple accumulation" };
|
|
30917
|
+
}
|
|
30918
|
+
const parts = [];
|
|
30919
|
+
if (flaggedCount > 0) {
|
|
30920
|
+
parts.push(`${flaggedCount} table${flaggedCount > 1 ? "s" : ""} with excessive dead tuples`);
|
|
30921
|
+
}
|
|
30922
|
+
if (disabledCount > 0) {
|
|
30923
|
+
parts.push(`${disabledCount} table${disabledCount > 1 ? "s" : ""} with autovacuum disabled`);
|
|
30924
|
+
}
|
|
30925
|
+
return { status: "warning", message: parts.join(", ") };
|
|
30926
|
+
}
|
|
30711
30927
|
function summarizeG001(nodeData) {
|
|
30712
30928
|
const data = nodeData?.data || {};
|
|
30713
30929
|
const settingsCount = Object.keys(data).length;
|
|
@@ -33720,6 +33936,9 @@ function prepareUploadConfig(opts, rootOpts, shouldUpload, uploadExplicitlyReque
|
|
|
33720
33936
|
console.error("Tip: run 'postgresai auth' or pass --api-key / set PGAI_API_KEY");
|
|
33721
33937
|
return null;
|
|
33722
33938
|
}
|
|
33939
|
+
console.error("Notice: no API key configured \u2014 results will NOT be uploaded to PostgresAI.");
|
|
33940
|
+
console.error(" To upload: run 'postgresai auth login' or pass --api-key / set PGAI_API_KEY.");
|
|
33941
|
+
console.error(" To run locally without this notice, pass --no-upload.");
|
|
33723
33942
|
return;
|
|
33724
33943
|
}
|
|
33725
33944
|
const cfg = readConfig();
|
|
@@ -35010,6 +35229,22 @@ Usage: postgresai checkup ${checkId} postgresql://user@host:5432/dbname
|
|
|
35010
35229
|
const uploadCfg = uploadResult?.config;
|
|
35011
35230
|
const projectWasGenerated = uploadResult?.projectWasGenerated ?? false;
|
|
35012
35231
|
shouldUpload = !!uploadCfg;
|
|
35232
|
+
if (uploadCfg) {
|
|
35233
|
+
const verification = await verifyApiKey({
|
|
35234
|
+
apiKey: uploadCfg.apiKey,
|
|
35235
|
+
apiBaseUrl: uploadCfg.apiBaseUrl
|
|
35236
|
+
});
|
|
35237
|
+
if (verification.status === "invalid") {
|
|
35238
|
+
console.error(`Error: the configured API key was rejected by the PostgresAI API (HTTP ${verification.statusCode})`);
|
|
35239
|
+
console.error("Tip: run 'postgresai auth login' to re-authenticate, or pass a valid --api-key / set PGAI_API_KEY");
|
|
35240
|
+
console.error("Tip: pass --no-upload to run checks locally without uploading");
|
|
35241
|
+
process.exitCode = 1;
|
|
35242
|
+
return;
|
|
35243
|
+
}
|
|
35244
|
+
if (verification.status === "unknown") {
|
|
35245
|
+
console.error(`Warning: could not verify API key before running checks (${verification.detail}); continuing \u2014 upload will still be attempted`);
|
|
35246
|
+
}
|
|
35247
|
+
}
|
|
35013
35248
|
const adminConn = resolveAdminConnection({
|
|
35014
35249
|
conn,
|
|
35015
35250
|
envPassword: process.env.PGPASSWORD
|
|
@@ -35233,42 +35468,79 @@ function checkRunningContainers() {
|
|
|
35233
35468
|
return { running: false, containers: [] };
|
|
35234
35469
|
}
|
|
35235
35470
|
}
|
|
35236
|
-
function registerMonitoringInstance(apiKey, projectName, opts) {
|
|
35471
|
+
async function registerMonitoringInstance(apiKey, projectName, opts) {
|
|
35237
35472
|
const { apiBaseUrl } = resolveBaseUrls2(opts);
|
|
35238
35473
|
const url = `${apiBaseUrl}/rpc/monitoring_instance_register`;
|
|
35239
35474
|
const debug = opts?.debug;
|
|
35475
|
+
const instanceId = opts?.instanceId;
|
|
35476
|
+
const retries = opts?.retries ?? (instanceId ? 1 : 0);
|
|
35477
|
+
const retryDelayMs = opts?.retryDelayMs ?? 400;
|
|
35240
35478
|
if (debug) {
|
|
35241
35479
|
console.error(`
|
|
35242
35480
|
Debug: Registering monitoring instance...`);
|
|
35243
35481
|
console.error(`Debug: POST ${url}`);
|
|
35244
|
-
console.error(`Debug: project_name=${projectName}`);
|
|
35482
|
+
console.error(`Debug: project_name=${projectName}${instanceId ? ` instance_id=${instanceId}` : ""}`);
|
|
35245
35483
|
}
|
|
35246
|
-
|
|
35247
|
-
|
|
35248
|
-
|
|
35249
|
-
|
|
35250
|
-
|
|
35251
|
-
|
|
35252
|
-
|
|
35253
|
-
|
|
35254
|
-
|
|
35255
|
-
|
|
35256
|
-
|
|
35257
|
-
|
|
35484
|
+
const requestBody = {
|
|
35485
|
+
api_token: apiKey,
|
|
35486
|
+
project_name: projectName
|
|
35487
|
+
};
|
|
35488
|
+
if (instanceId) {
|
|
35489
|
+
requestBody.instance_id = instanceId;
|
|
35490
|
+
}
|
|
35491
|
+
for (let attempt = 0;attempt <= retries; attempt++) {
|
|
35492
|
+
if (attempt > 0 && retryDelayMs > 0) {
|
|
35493
|
+
await new Promise((resolve8) => setTimeout(resolve8, retryDelayMs));
|
|
35494
|
+
}
|
|
35495
|
+
try {
|
|
35496
|
+
const res = await fetch(url, {
|
|
35497
|
+
method: "POST",
|
|
35498
|
+
headers: {
|
|
35499
|
+
"Content-Type": "application/json"
|
|
35500
|
+
},
|
|
35501
|
+
body: JSON.stringify(requestBody)
|
|
35502
|
+
});
|
|
35503
|
+
const body = await res.text().catch(() => "");
|
|
35504
|
+
if (!res.ok) {
|
|
35505
|
+
if (debug) {
|
|
35506
|
+
console.error(`Debug: Monitoring registration failed: HTTP ${res.status}`);
|
|
35507
|
+
console.error(`Debug: Response: ${body}`);
|
|
35508
|
+
}
|
|
35509
|
+
continue;
|
|
35510
|
+
}
|
|
35258
35511
|
if (debug) {
|
|
35259
|
-
console.error(`Debug: Monitoring registration
|
|
35260
|
-
|
|
35512
|
+
console.error(`Debug: Monitoring registration response: ${body}`);
|
|
35513
|
+
}
|
|
35514
|
+
try {
|
|
35515
|
+
const parsed = JSON.parse(body);
|
|
35516
|
+
return {
|
|
35517
|
+
instanceId: typeof parsed.instance_id === "string" ? parsed.instance_id : undefined,
|
|
35518
|
+
projectId: typeof parsed.project_id === "number" ? parsed.project_id : undefined,
|
|
35519
|
+
projectName: typeof parsed.project_name === "string" ? parsed.project_name : undefined,
|
|
35520
|
+
created: typeof parsed.created === "boolean" ? parsed.created : undefined
|
|
35521
|
+
};
|
|
35522
|
+
} catch {
|
|
35523
|
+
return {};
|
|
35524
|
+
}
|
|
35525
|
+
} catch (err) {
|
|
35526
|
+
if (debug) {
|
|
35527
|
+
console.error(`Debug: Monitoring registration error: ${err.message}`);
|
|
35261
35528
|
}
|
|
35262
|
-
return;
|
|
35263
|
-
}
|
|
35264
|
-
if (debug) {
|
|
35265
|
-
console.error(`Debug: Monitoring registration response: ${body}`);
|
|
35266
|
-
}
|
|
35267
|
-
}).catch((err) => {
|
|
35268
|
-
if (debug) {
|
|
35269
|
-
console.error(`Debug: Monitoring registration error: ${err.message}`);
|
|
35270
35529
|
}
|
|
35271
|
-
}
|
|
35530
|
+
}
|
|
35531
|
+
return null;
|
|
35532
|
+
}
|
|
35533
|
+
var PROJECT_NAME_RE = /^[A-Za-z0-9._-]+$/;
|
|
35534
|
+
function resolveAdoptedProject(reg) {
|
|
35535
|
+
if (!reg)
|
|
35536
|
+
return null;
|
|
35537
|
+
if (typeof reg.projectId === "number" && Number.isFinite(reg.projectId)) {
|
|
35538
|
+
return String(reg.projectId);
|
|
35539
|
+
}
|
|
35540
|
+
if (typeof reg.projectName === "string" && PROJECT_NAME_RE.test(reg.projectName)) {
|
|
35541
|
+
return reg.projectName;
|
|
35542
|
+
}
|
|
35543
|
+
return null;
|
|
35272
35544
|
}
|
|
35273
35545
|
function updatePgwatchConfig(configPath, updates) {
|
|
35274
35546
|
let lines = [];
|
|
@@ -35389,7 +35661,7 @@ mon.command("local-install").description("install local monitoring stack (genera
|
|
|
35389
35661
|
" PGAI_ENABLE_IPV6=false (accepted: true|false|yes|no, lowercase)",
|
|
35390
35662
|
""
|
|
35391
35663
|
].join(`
|
|
35392
|
-
`)).option("--demo", "demo mode with sample database", false).option("--api-key <key>", "Postgres AI API key for automated report uploads").option("--db-url <url>", "PostgreSQL connection URL to monitor").option("--tag <tag>", "Docker image tag to use (e.g., 0.14.0, 0.14.0-dev.33)").option("--project <name>", "Docker Compose project name (default: postgres_ai)").option("-y, --yes", "accept all defaults and skip interactive prompts", false).action(async (opts) => {
|
|
35664
|
+
`)).option("--demo", "demo mode with sample database", false).option("--api-key <key>", "Postgres AI API key for automated report uploads").option("--db-url <url>", "PostgreSQL connection URL to monitor").option("--tag <tag>", "Docker image tag to use (e.g., 0.14.0, 0.14.0-dev.33)").option("--project <name>", "Docker Compose project name (default: postgres_ai)").option("--instance-id <uuid>", "adopt a console-provisioned monitoring instance instead of self-registering a new one (set automatically by the provisioning flow; PGAI_INSTANCE_ID env also works)").option("-y, --yes", "accept all defaults and skip interactive prompts", false).action(async (opts) => {
|
|
35393
35665
|
const globalOpts = program2.opts();
|
|
35394
35666
|
let apiKey = opts.apiKey || globalOpts.apiKey;
|
|
35395
35667
|
console.log(`
|
|
@@ -35756,10 +36028,32 @@ Searched: ${demoCandidates.join(", ")}
|
|
|
35756
36028
|
`);
|
|
35757
36029
|
if (apiKey && !opts.demo) {
|
|
35758
36030
|
const projectName = opts.project || "postgres-ai-monitoring";
|
|
35759
|
-
|
|
35760
|
-
|
|
35761
|
-
|
|
35762
|
-
|
|
36031
|
+
const instanceId = opts.instanceId || process.env.PGAI_INSTANCE_ID;
|
|
36032
|
+
if (instanceId) {
|
|
36033
|
+
const reg = await registerMonitoringInstance(apiKey, projectName, {
|
|
36034
|
+
apiBaseUrl: globalOpts.apiBaseUrl,
|
|
36035
|
+
debug: !!process.env.DEBUG,
|
|
36036
|
+
instanceId
|
|
36037
|
+
});
|
|
36038
|
+
const adoptedProject = resolveAdoptedProject(reg);
|
|
36039
|
+
if (adoptedProject != null) {
|
|
36040
|
+
updatePgwatchConfig(path7.resolve(projectDir, ".pgwatch-config"), {
|
|
36041
|
+
project_name: adoptedProject
|
|
36042
|
+
});
|
|
36043
|
+
const verb = reg?.created ? "Registered" : "Adopted";
|
|
36044
|
+
console.log(`\u2713 ${verb} monitoring instance (project: ${adoptedProject})
|
|
36045
|
+
`);
|
|
36046
|
+
} else if (reg) {
|
|
36047
|
+
console.error(`\u26A0 Adopted provisioned instance ${instanceId} but the platform returned no project \u2014 reports will use project '${projectName}'`);
|
|
36048
|
+
} else {
|
|
36049
|
+
console.error(`\u26A0 Could not adopt provisioned instance ${instanceId} \u2014 reports will use project '${projectName}' until 'postgresai mon local-install' is re-run`);
|
|
36050
|
+
}
|
|
36051
|
+
} else {
|
|
36052
|
+
registerMonitoringInstance(apiKey, projectName, {
|
|
36053
|
+
apiBaseUrl: globalOpts.apiBaseUrl,
|
|
36054
|
+
debug: !!process.env.DEBUG
|
|
36055
|
+
});
|
|
36056
|
+
}
|
|
35763
36057
|
}
|
|
35764
36058
|
console.log("=================================");
|
|
35765
36059
|
console.log(" Local install completed!");
|
|
@@ -37515,6 +37809,8 @@ if (__require.main == __require.module) {
|
|
|
37515
37809
|
});
|
|
37516
37810
|
}
|
|
37517
37811
|
export {
|
|
37812
|
+
resolveAdoptedProject,
|
|
37813
|
+
registerMonitoringInstance,
|
|
37518
37814
|
refreshBundledComposeIfStale,
|
|
37519
37815
|
readDeployedTag,
|
|
37520
37816
|
isValidComposeYaml
|