postgresai 0.17.0-rc.2 → 0.17.0-rc.4
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 +32 -2
- package/dist/bin/postgres-ai.js +748 -76
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -271,6 +271,35 @@ postgresai mon check # System readiness check
|
|
|
271
271
|
postgresai mon shell <service> # Open shell to monitoring service
|
|
272
272
|
```
|
|
273
273
|
|
|
274
|
+
### PromQL queries (`promql`)
|
|
275
|
+
|
|
276
|
+
Ask a monitoring instance a PromQL question through the platform. The platform
|
|
277
|
+
never connects to the instance: it queues the query as a job, the instance picks
|
|
278
|
+
it up on its next poll, runs it against its own metric store, and posts the
|
|
279
|
+
answer back.
|
|
280
|
+
|
|
281
|
+
```bash
|
|
282
|
+
# instant query
|
|
283
|
+
pgai promql 'up' --instance <instance-uuid>
|
|
284
|
+
|
|
285
|
+
# range query
|
|
286
|
+
pgai promql 'sum(rate(pgwatch_db_stats_xact_commit[5m]))' \
|
|
287
|
+
--instance <instance-uuid> \
|
|
288
|
+
--range --start 2026-09-21T00:00:00Z --end 2026-09-21T01:00:00Z --step 60
|
|
289
|
+
|
|
290
|
+
# on the box itself, --instance is read from .pgwatch-config
|
|
291
|
+
pgai promql 'up'
|
|
292
|
+
```
|
|
293
|
+
|
|
294
|
+
`--at <time>` evaluates an instant query at a given moment (ignored with
|
|
295
|
+
`--range`). `--json` prints every point instead of the first and last per series.
|
|
296
|
+
The wait is sized from the instance's poll pacing; `--timeout` overrides it. A
|
|
297
|
+
result too large for the byte budget is trimmed and flagged `truncated`.
|
|
298
|
+
|
|
299
|
+
Requires platform-all !809
|
|
300
|
+
(https://gitlab.com/postgres-ai/platform-all/-/merge_requests/809); until it is
|
|
301
|
+
deployed the command returns a PGRST202 404.
|
|
302
|
+
|
|
274
303
|
### MCP server (`mcp` group)
|
|
275
304
|
|
|
276
305
|
```bash
|
|
@@ -299,7 +328,7 @@ required** — the server will not assume an organization. Call `orgs_list` (or
|
|
|
299
328
|
run `pgai orgs`) to discover the available ids.
|
|
300
329
|
|
|
301
330
|
Tools exposed:
|
|
302
|
-
- `list_issues`: returns the same JSON as `postgresai issues list` (args: `{ org_id, status?, hidden_only?, limit?, offset?, debug? }`).
|
|
331
|
+
- `list_issues`: returns the same JSON as `postgresai issues list` (args: `{ org_id, status?, hidden_only?, limit?, offset?, debug? }`). Unlike the CLI, omitting `status` returns open and closed issues; pass `status: "open"` to match the CLI default.
|
|
303
332
|
- `view_issue`: view a single issue with its comments (args: `{ issue_id, org_id, debug? }`).
|
|
304
333
|
- `create_issue`: create a new issue (args: `{ title, description?, org_id, project_id?, labels?, attachments?, is_hidden?, debug? }`).
|
|
305
334
|
- `update_issue`: update title/description/status/labels/is_hidden (args: `{ issue_id, org_id, title?, description?, status?, labels?, attachments?, is_hidden?, debug? }`).
|
|
@@ -350,7 +379,8 @@ sensitive.
|
|
|
350
379
|
### Issues management (`issues` group)
|
|
351
380
|
|
|
352
381
|
```bash
|
|
353
|
-
postgresai issues list # List issues (shows: id, title, status, created_at; is_hidden only when set)
|
|
382
|
+
postgresai issues list # List OPEN issues (shows: id, title, status, created_at; is_hidden only when set)
|
|
383
|
+
postgresai issues list --status closed # Only closed issues (--status all for both)
|
|
354
384
|
postgresai issues list --hidden-only # Only hidden issues (PostgresAI staff)
|
|
355
385
|
postgresai issues view <issueId> # View issue details and comments
|
|
356
386
|
postgresai issues create --org-id <id> <title> # Create a new issue
|
package/dist/bin/postgres-ai.js
CHANGED
|
@@ -13430,6 +13430,11 @@ var require_dist2 = __commonJS(function(exports, module) {
|
|
|
13430
13430
|
});
|
|
13431
13431
|
|
|
13432
13432
|
// lib/util.ts
|
|
13433
|
+
function requestTimeoutSignal2(timeoutMs) {
|
|
13434
|
+
const requested = typeof timeoutMs === "number" && Number.isFinite(timeoutMs) && timeoutMs > 0 ? timeoutMs : DEFAULT_HTTP_REQUEST_TIMEOUT_MS2;
|
|
13435
|
+
const bounded = Math.max(1, Math.min(DEFAULT_HTTP_REQUEST_TIMEOUT_MS2, Math.floor(requested)));
|
|
13436
|
+
return { signal: AbortSignal.timeout(bounded), timeoutMs: bounded };
|
|
13437
|
+
}
|
|
13433
13438
|
function maskSecret2(secret) {
|
|
13434
13439
|
if (!secret)
|
|
13435
13440
|
return "";
|
|
@@ -13461,6 +13466,7 @@ function resolveBaseUrls2(opts, cfg, defaults = {}) {
|
|
|
13461
13466
|
storageBaseUrl: normalizeBaseUrl2(storageCandidate)
|
|
13462
13467
|
};
|
|
13463
13468
|
}
|
|
13469
|
+
var DEFAULT_HTTP_REQUEST_TIMEOUT_MS2 = 25000;
|
|
13464
13470
|
var init_util = () => {};
|
|
13465
13471
|
|
|
13466
13472
|
// node_modules/commander/index.js
|
|
@@ -13524,7 +13530,7 @@ var {
|
|
|
13524
13530
|
// package.json
|
|
13525
13531
|
var package_default = {
|
|
13526
13532
|
name: "postgresai",
|
|
13527
|
-
version: "0.17.0-rc.
|
|
13533
|
+
version: "0.17.0-rc.4",
|
|
13528
13534
|
description: "postgres_ai CLI",
|
|
13529
13535
|
license: "Apache-2.0",
|
|
13530
13536
|
private: false,
|
|
@@ -16884,7 +16890,7 @@ var Result = import_lib.default.Result;
|
|
|
16884
16890
|
var TypeOverrides = import_lib.default.TypeOverrides;
|
|
16885
16891
|
var defaults = import_lib.default.defaults;
|
|
16886
16892
|
// package.json
|
|
16887
|
-
var version = "0.17.0-rc.
|
|
16893
|
+
var version = "0.17.0-rc.4";
|
|
16888
16894
|
var package_default2 = {
|
|
16889
16895
|
name: "postgresai",
|
|
16890
16896
|
version,
|
|
@@ -17217,6 +17223,22 @@ var TEXT_SENSITIVE_PAIR = /((?:password|passwd|db[-_]?pass|connstr|secret|token|
|
|
|
17217
17223
|
function redactTextSecrets(text) {
|
|
17218
17224
|
return text.replace(TEXT_URL_USERINFO, "$1:[REDACTED]@").replace(TEXT_SENSITIVE_PAIR, "$1[REDACTED]");
|
|
17219
17225
|
}
|
|
17226
|
+
function isControlCodePoint(cp) {
|
|
17227
|
+
return cp < 32 || cp === 127 || cp >= 128 && cp <= 159 || cp === 8232 || cp === 8233;
|
|
17228
|
+
}
|
|
17229
|
+
function stripControlsToSpace(text) {
|
|
17230
|
+
let out = "";
|
|
17231
|
+
for (const ch of text) {
|
|
17232
|
+
const cp = ch.codePointAt(0);
|
|
17233
|
+
out += cp !== undefined && isControlCodePoint(cp) ? " " : ch;
|
|
17234
|
+
}
|
|
17235
|
+
return out;
|
|
17236
|
+
}
|
|
17237
|
+
function whitespaceTolerantSecretPattern(secret) {
|
|
17238
|
+
const chars = Array.from(secret.replace(/\s+/g, ""));
|
|
17239
|
+
const body = chars.map((c) => c.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")).join("\\s*");
|
|
17240
|
+
return new RegExp(body, "g");
|
|
17241
|
+
}
|
|
17220
17242
|
function normalizeBaseUrl(value) {
|
|
17221
17243
|
const trimmed = (value || "").replace(/\/$/, "");
|
|
17222
17244
|
try {
|
|
@@ -17315,6 +17337,10 @@ function resolveMcpOrgScope(args, apiKey, cfg) {
|
|
|
17315
17337
|
}
|
|
17316
17338
|
|
|
17317
17339
|
// lib/issues.ts
|
|
17340
|
+
var IssueStatus = {
|
|
17341
|
+
OPEN: 0,
|
|
17342
|
+
CLOSED: 1
|
|
17343
|
+
};
|
|
17318
17344
|
function issueRequestHeaders(apiKey, orgScope) {
|
|
17319
17345
|
return buildAuthHeaders(apiKey, orgScope, {
|
|
17320
17346
|
Prefer: "return=representation",
|
|
@@ -17331,6 +17357,17 @@ function withVisibleHiddenFlag(issue) {
|
|
|
17331
17357
|
const { is_hidden: _dropped, ...rest } = issue;
|
|
17332
17358
|
return rest;
|
|
17333
17359
|
}
|
|
17360
|
+
function presentIssue(issue) {
|
|
17361
|
+
const visible = withVisibleHiddenFlag(issue);
|
|
17362
|
+
return { ...visible, status: issueStatusLabel(issue.status) };
|
|
17363
|
+
}
|
|
17364
|
+
function issueStatusLabel(status) {
|
|
17365
|
+
if (status === IssueStatus.OPEN)
|
|
17366
|
+
return "open";
|
|
17367
|
+
if (status === IssueStatus.CLOSED)
|
|
17368
|
+
return "closed";
|
|
17369
|
+
return status;
|
|
17370
|
+
}
|
|
17334
17371
|
async function fetchIssues(params) {
|
|
17335
17372
|
const { apiKey, apiBaseUrl, orgScope, orgId, status, limit = 20, offset = 0, hiddenOnly, debug } = params;
|
|
17336
17373
|
if (!apiKey) {
|
|
@@ -24994,7 +25031,7 @@ async function handleToolCall(req, rootOpts, extra) {
|
|
|
24994
25031
|
const offset = args.offset !== undefined ? Number(args.offset) : undefined;
|
|
24995
25032
|
const hiddenOnly = Boolean(args.hidden_only);
|
|
24996
25033
|
const issues = await fetchIssues({ apiKey, apiBaseUrl, orgId, orgScope: scope.orgScope, status, limit, offset, hiddenOnly, debug });
|
|
24997
|
-
const trimmed = issues.map((issue) =>
|
|
25034
|
+
const trimmed = issues.map((issue) => presentIssue(issue));
|
|
24998
25035
|
return { content: [{ type: "text", text: JSON.stringify(trimmed, null, 2) }] };
|
|
24999
25036
|
}
|
|
25000
25037
|
if (toolName === "view_issue") {
|
|
@@ -25010,7 +25047,7 @@ async function handleToolCall(req, rootOpts, extra) {
|
|
|
25010
25047
|
return { content: [{ type: "text", text: "Issue not found" }], isError: true };
|
|
25011
25048
|
}
|
|
25012
25049
|
const comments = await fetchIssueComments({ apiKey, apiBaseUrl, issueId, orgScope: scope.orgScope, debug });
|
|
25013
|
-
const combined = { issue:
|
|
25050
|
+
const combined = { issue: presentIssue(issue), comments };
|
|
25014
25051
|
return { content: [{ type: "text", text: JSON.stringify(combined, null, 2) }] };
|
|
25015
25052
|
}
|
|
25016
25053
|
if (toolName === "post_issue_comment") {
|
|
@@ -25314,7 +25351,7 @@ async function startMcpServer(rootOpts, extra) {
|
|
|
25314
25351
|
type: "object",
|
|
25315
25352
|
properties: {
|
|
25316
25353
|
org_id: { type: "number", description: "Organization ID. Optional with a per-organization token (falls back to config); REQUIRED with a global token, which can reach every organization the user belongs to and will not assume one. Use orgs_list / `pgai orgs` to discover valid values." },
|
|
25317
|
-
status: { type: "string", description: "Filter by status: 'open', 'closed', or omit for all" },
|
|
25354
|
+
status: { type: "string", description: "Filter by status: 'open', 'closed', or omit for all (note: the CLI defaults to open). Rows carry status as 'open'/'closed'." },
|
|
25318
25355
|
limit: { type: "number", description: "Max number of issues to return (default: 20)" },
|
|
25319
25356
|
offset: { type: "number", description: "Number of issues to skip (default: 0)" },
|
|
25320
25357
|
hidden_only: { type: "boolean", description: "List only hidden issues (PostgresAI staff; same as CLI --hidden-only)" },
|
|
@@ -25596,6 +25633,10 @@ async function startMcpServer(rootOpts, extra) {
|
|
|
25596
25633
|
}
|
|
25597
25634
|
|
|
25598
25635
|
// lib/issues.ts
|
|
25636
|
+
var IssueStatus2 = {
|
|
25637
|
+
OPEN: 0,
|
|
25638
|
+
CLOSED: 1
|
|
25639
|
+
};
|
|
25599
25640
|
function issueRequestHeaders2(apiKey, orgScope) {
|
|
25600
25641
|
return buildAuthHeaders(apiKey, orgScope, {
|
|
25601
25642
|
Prefer: "return=representation",
|
|
@@ -25612,6 +25653,17 @@ function withVisibleHiddenFlag2(issue) {
|
|
|
25612
25653
|
const { is_hidden: _dropped, ...rest } = issue;
|
|
25613
25654
|
return rest;
|
|
25614
25655
|
}
|
|
25656
|
+
function presentIssue2(issue) {
|
|
25657
|
+
const visible = withVisibleHiddenFlag2(issue);
|
|
25658
|
+
return { ...visible, status: issueStatusLabel2(issue.status) };
|
|
25659
|
+
}
|
|
25660
|
+
function issueStatusLabel2(status) {
|
|
25661
|
+
if (status === IssueStatus2.OPEN)
|
|
25662
|
+
return "open";
|
|
25663
|
+
if (status === IssueStatus2.CLOSED)
|
|
25664
|
+
return "closed";
|
|
25665
|
+
return status;
|
|
25666
|
+
}
|
|
25615
25667
|
async function fetchIssues2(params) {
|
|
25616
25668
|
const { apiKey, apiBaseUrl, orgScope, orgId, status, limit = 20, offset = 0, hiddenOnly, debug } = params;
|
|
25617
25669
|
if (!apiKey) {
|
|
@@ -27138,6 +27190,134 @@ async function destroySnapshot(params) {
|
|
|
27138
27190
|
// bin/postgres-ai.ts
|
|
27139
27191
|
init_util();
|
|
27140
27192
|
|
|
27193
|
+
// lib/promql.ts
|
|
27194
|
+
function describeRpcError(status, body) {
|
|
27195
|
+
try {
|
|
27196
|
+
const parsed = JSON.parse(body);
|
|
27197
|
+
const parts = [parsed.message, parsed.details, parsed.hint].filter(Boolean);
|
|
27198
|
+
if (parts.length)
|
|
27199
|
+
return parts.join(" — ");
|
|
27200
|
+
} catch {}
|
|
27201
|
+
return body.trim() || `HTTP ${status}`;
|
|
27202
|
+
}
|
|
27203
|
+
async function callRpc3(name, apiKey, body, opts) {
|
|
27204
|
+
const { apiBaseUrl } = resolveBaseUrls(opts);
|
|
27205
|
+
const res = await fetch(`${apiBaseUrl}/rpc/${name}`, {
|
|
27206
|
+
method: "POST",
|
|
27207
|
+
headers: {
|
|
27208
|
+
"Content-Type": "application/json",
|
|
27209
|
+
"access-token": apiKey,
|
|
27210
|
+
...orgScopeHeaders(getActiveOrgScope())
|
|
27211
|
+
},
|
|
27212
|
+
body: JSON.stringify(body),
|
|
27213
|
+
signal: AbortSignal.timeout(opts?.timeoutMs ?? 30000)
|
|
27214
|
+
});
|
|
27215
|
+
const text = await res.text().catch(() => "");
|
|
27216
|
+
if (!res.ok) {
|
|
27217
|
+
throw new Error(describeRpcError(res.status, text));
|
|
27218
|
+
}
|
|
27219
|
+
try {
|
|
27220
|
+
return text ? JSON.parse(text) : null;
|
|
27221
|
+
} catch {
|
|
27222
|
+
throw new Error(`${name} returned a body that is not JSON`);
|
|
27223
|
+
}
|
|
27224
|
+
}
|
|
27225
|
+
async function enqueueQuery(apiKey, args, opts) {
|
|
27226
|
+
const body = {
|
|
27227
|
+
p_instance_id: args.instanceId,
|
|
27228
|
+
p_kind: args.kind,
|
|
27229
|
+
p_query: args.query
|
|
27230
|
+
};
|
|
27231
|
+
if (args.kind === "promql_range") {
|
|
27232
|
+
body.p_start = args.start;
|
|
27233
|
+
body.p_end = args.end;
|
|
27234
|
+
body.p_step_s = args.stepS;
|
|
27235
|
+
} else if (args.at) {
|
|
27236
|
+
body.p_at = args.at;
|
|
27237
|
+
}
|
|
27238
|
+
const reply = await callRpc3("instance_query_enqueue", apiKey, body, opts);
|
|
27239
|
+
const jobId = reply?.job_id;
|
|
27240
|
+
if (!jobId) {
|
|
27241
|
+
throw new Error("instance_query_enqueue returned no job_id");
|
|
27242
|
+
}
|
|
27243
|
+
return { jobId, firstAnswerEstimateS: positiveSeconds(reply?.first_answer_estimate_s) };
|
|
27244
|
+
}
|
|
27245
|
+
function positiveSeconds(v) {
|
|
27246
|
+
return typeof v === "number" && Number.isFinite(v) && v > 0 ? v : null;
|
|
27247
|
+
}
|
|
27248
|
+
async function readQueryResult(apiKey, jobId, opts) {
|
|
27249
|
+
const reply = await callRpc3("instance_query_result", apiKey, { p_job_id: jobId }, opts);
|
|
27250
|
+
if (!reply || typeof reply.status !== "string") {
|
|
27251
|
+
throw new Error("instance_query_result returned an unexpected reply");
|
|
27252
|
+
}
|
|
27253
|
+
return reply;
|
|
27254
|
+
}
|
|
27255
|
+
var TERMINAL = new Set(["done", "failed", "expired"]);
|
|
27256
|
+
function isTerminal2(status) {
|
|
27257
|
+
return TERMINAL.has(status);
|
|
27258
|
+
}
|
|
27259
|
+
function pollDelay(attempt) {
|
|
27260
|
+
return Math.min(1000 * 2 ** attempt, 15000);
|
|
27261
|
+
}
|
|
27262
|
+
async function awaitQueryResult(apiKey, jobId, opts) {
|
|
27263
|
+
const now = opts.now ?? (() => Date.now());
|
|
27264
|
+
const sleep = opts.sleep ?? ((ms) => new Promise((r) => setTimeout(r, ms)));
|
|
27265
|
+
const deadline = now() + opts.timeoutMs;
|
|
27266
|
+
for (let attempt = 0;; attempt++) {
|
|
27267
|
+
const res = await readQueryResult(apiKey, jobId, opts);
|
|
27268
|
+
if (isTerminal2(res.status))
|
|
27269
|
+
return res;
|
|
27270
|
+
const delay = pollDelay(attempt);
|
|
27271
|
+
if (now() + delay >= deadline)
|
|
27272
|
+
return res;
|
|
27273
|
+
await sleep(delay);
|
|
27274
|
+
}
|
|
27275
|
+
}
|
|
27276
|
+
function formatMetric(metric) {
|
|
27277
|
+
const name = metric.__name__ ?? "";
|
|
27278
|
+
const labels = Object.entries(metric).filter(([k]) => k !== "__name__").sort(([a], [b]) => a.localeCompare(b)).map(([k, v]) => `${k}="${v}"`);
|
|
27279
|
+
if (!labels.length)
|
|
27280
|
+
return name || "{}";
|
|
27281
|
+
return `${name}{${labels.join(", ")}}`;
|
|
27282
|
+
}
|
|
27283
|
+
function pad(s, width) {
|
|
27284
|
+
return s.length >= width ? s : s + " ".repeat(width - s.length);
|
|
27285
|
+
}
|
|
27286
|
+
function renderPromQL(payload) {
|
|
27287
|
+
const lines = [];
|
|
27288
|
+
if (!payload.result.length) {
|
|
27289
|
+
lines.push("Empty result set.");
|
|
27290
|
+
} else if (payload.resultType === "vector") {
|
|
27291
|
+
const rows = payload.result.map((s) => [formatMetric(s.metric), s.value ? s.value[1] : ""]);
|
|
27292
|
+
const w = Math.max(6, ...rows.map((r) => r[0].length));
|
|
27293
|
+
lines.push(`${pad("SERIES", w)} VALUE`);
|
|
27294
|
+
for (const [m, v] of rows)
|
|
27295
|
+
lines.push(`${pad(m, w)} ${v}`);
|
|
27296
|
+
} else {
|
|
27297
|
+
const rows = payload.result.map((s) => {
|
|
27298
|
+
const vs = s.values ?? [];
|
|
27299
|
+
const first = vs.length ? vs[0][1] : "";
|
|
27300
|
+
const last = vs.length ? vs[vs.length - 1][1] : "";
|
|
27301
|
+
return [formatMetric(s.metric), String(vs.length), first, last];
|
|
27302
|
+
});
|
|
27303
|
+
const w = Math.max(6, ...rows.map((r) => r[0].length));
|
|
27304
|
+
lines.push(`${pad("SERIES", w)} POINTS FIRST LAST`);
|
|
27305
|
+
for (const [m, n, f, l] of rows)
|
|
27306
|
+
lines.push(`${pad(m, w)} ${pad(n, 6)} ${f} ${l}`);
|
|
27307
|
+
if (rows.length)
|
|
27308
|
+
lines.push("(first and last sample per series; use --json for every point)");
|
|
27309
|
+
}
|
|
27310
|
+
const series = payload.result.length;
|
|
27311
|
+
const points = payload.result.reduce((n, s) => n + (s.values?.length ?? 0) + (s.value ? 1 : 0), 0);
|
|
27312
|
+
lines.push(`
|
|
27313
|
+
${series} series, ${points} points`);
|
|
27314
|
+
if (payload.stats.truncated) {
|
|
27315
|
+
lines.push("⚠ TRUNCATED: too large to return whole. Raise --step, narrow the time range, " + "or select fewer series.");
|
|
27316
|
+
}
|
|
27317
|
+
return lines.join(`
|
|
27318
|
+
`);
|
|
27319
|
+
}
|
|
27320
|
+
|
|
27141
27321
|
// lib/instances.ts
|
|
27142
27322
|
import * as fs4 from "fs";
|
|
27143
27323
|
|
|
@@ -30402,6 +30582,38 @@ async function resolveDatasourceId(adminPassword, debug = false) {
|
|
|
30402
30582
|
return null;
|
|
30403
30583
|
}
|
|
30404
30584
|
}
|
|
30585
|
+
function aasSuccessMessage(r) {
|
|
30586
|
+
const armed = r.armedFor === "pull" ? " (armed for pull: the platform will query this instance's Grafana)" : r.armedFor === "jobs" ? " (armed for the job channel: this instance answers its own queries)" : "";
|
|
30587
|
+
const base = `AAS auto-collection registered${armed}`;
|
|
30588
|
+
if (r.vcpus === 0) {
|
|
30589
|
+
return `${base} — collection stays OFF until a source-DB vCPU count is known (the platform stamps it at provision time; --vcpus <n> sets it for a manual install)`;
|
|
30590
|
+
}
|
|
30591
|
+
if (r.urlPending) {
|
|
30592
|
+
return `${base} — collection starts once this deploy reports the Grafana url to the platform`;
|
|
30593
|
+
}
|
|
30594
|
+
return base;
|
|
30595
|
+
}
|
|
30596
|
+
var PLATFORM_ERROR_MAX = 300;
|
|
30597
|
+
function formatPlatformError(body, secrets = []) {
|
|
30598
|
+
if (!body || typeof body !== "object")
|
|
30599
|
+
return "";
|
|
30600
|
+
const b = body;
|
|
30601
|
+
const str = (v) => typeof v === "string" && v.trim() !== "" ? v.trim() : "";
|
|
30602
|
+
const text = str(b.details) || str(b.message);
|
|
30603
|
+
if (!text)
|
|
30604
|
+
return "";
|
|
30605
|
+
const code = str(b.code);
|
|
30606
|
+
let out = stripControlsToSpace(code ? `${code}: ${text}` : text).replace(/\s+/g, " ").trim();
|
|
30607
|
+
for (const secret of secrets) {
|
|
30608
|
+
const compact = secret.replace(/\s+/g, "");
|
|
30609
|
+
if (compact.length < 8)
|
|
30610
|
+
continue;
|
|
30611
|
+
out = out.replace(whitespaceTolerantSecretPattern(compact), "[redacted]");
|
|
30612
|
+
}
|
|
30613
|
+
out = redactTextSecrets(out);
|
|
30614
|
+
out = out.replace(/glsa_[A-Za-z0-9_-]+/g, "[redacted]");
|
|
30615
|
+
return out.length > PLATFORM_ERROR_MAX ? `${out.slice(0, PLATFORM_ERROR_MAX - 1).trimEnd()}…` : out;
|
|
30616
|
+
}
|
|
30405
30617
|
async function registerAasCollection(apiKey, instanceId, opts) {
|
|
30406
30618
|
const debug = !!opts.debug;
|
|
30407
30619
|
try {
|
|
@@ -30451,11 +30663,19 @@ async function registerAasCollection(apiKey, instanceId, opts) {
|
|
|
30451
30663
|
})
|
|
30452
30664
|
});
|
|
30453
30665
|
if (!res.ok) {
|
|
30666
|
+
const detail = formatPlatformError(await res.json().catch(() => null), [apiKey, saToken]);
|
|
30667
|
+
const suffix = detail ? ` — ${detail}` : "";
|
|
30454
30668
|
if (debug)
|
|
30455
|
-
console.error(`Debug: AAS register failed: HTTP ${res.status}`);
|
|
30456
|
-
return { ok: false, reason: `platform returned HTTP ${res.status}` };
|
|
30669
|
+
console.error(`Debug: AAS register failed: HTTP ${res.status}${suffix}`);
|
|
30670
|
+
return { ok: false, reason: `platform returned HTTP ${res.status}${suffix}` };
|
|
30457
30671
|
}
|
|
30458
|
-
|
|
30672
|
+
const reply = await res.json().catch(() => null);
|
|
30673
|
+
return {
|
|
30674
|
+
ok: true,
|
|
30675
|
+
vcpus: typeof reply?.vcpus === "number" ? reply.vcpus : undefined,
|
|
30676
|
+
urlPending: typeof reply?.url_pending === "boolean" ? reply.url_pending : undefined,
|
|
30677
|
+
armedFor: typeof reply?.armed_for === "string" ? reply.armed_for : undefined
|
|
30678
|
+
};
|
|
30459
30679
|
} catch (err) {
|
|
30460
30680
|
if (debug)
|
|
30461
30681
|
console.error(`Debug: AAS register error: ${err.message}`);
|
|
@@ -38784,9 +39004,62 @@ var LOCAL_INSTALL_MANAGED_ENV_KEYS = [
|
|
|
38784
39004
|
"VM_DELETE_AUTH_KEY",
|
|
38785
39005
|
"VM_SNAPSHOT_AUTH_KEY",
|
|
38786
39006
|
"VM_FORCE_MERGE_AUTH_KEY",
|
|
38787
|
-
"VM_PPROF_AUTH_KEY"
|
|
39007
|
+
"VM_PPROF_AUTH_KEY",
|
|
39008
|
+
"PGAI_INSTANCE_ID",
|
|
39009
|
+
"INSTANCE_JOBS_USER",
|
|
39010
|
+
"COMPOSE_PROFILES"
|
|
38788
39011
|
];
|
|
38789
|
-
|
|
39012
|
+
var INSTANCE_JOBS_PROFILE = "instance-jobs";
|
|
39013
|
+
function composeProfilesValue(existing, want) {
|
|
39014
|
+
const profiles = [];
|
|
39015
|
+
for (const raw of (existing ?? "").split(",")) {
|
|
39016
|
+
const token = raw.trim();
|
|
39017
|
+
if (token && !profiles.includes(token))
|
|
39018
|
+
profiles.push(token);
|
|
39019
|
+
}
|
|
39020
|
+
const at = profiles.indexOf(INSTANCE_JOBS_PROFILE);
|
|
39021
|
+
if (want === true && at === -1)
|
|
39022
|
+
profiles.push(INSTANCE_JOBS_PROFILE);
|
|
39023
|
+
if (want === false && at !== -1)
|
|
39024
|
+
profiles.splice(at, 1);
|
|
39025
|
+
return profiles.length > 0 ? profiles.join(",") : null;
|
|
39026
|
+
}
|
|
39027
|
+
function instanceJobsProfileEnabled(composeProfiles) {
|
|
39028
|
+
return (composeProfiles ?? "").split(",").map((p) => p.trim()).includes(INSTANCE_JOBS_PROFILE);
|
|
39029
|
+
}
|
|
39030
|
+
function resolveInstanceJobsRequest(flag, envValue) {
|
|
39031
|
+
if (flag !== undefined)
|
|
39032
|
+
return { enabled: flag };
|
|
39033
|
+
const value = (envValue ?? "").trim().toLowerCase();
|
|
39034
|
+
if (value === "")
|
|
39035
|
+
return {};
|
|
39036
|
+
if (["1", "true", "yes", "on"].includes(value))
|
|
39037
|
+
return { enabled: true };
|
|
39038
|
+
if (["0", "false", "no", "off"].includes(value))
|
|
39039
|
+
return { enabled: false };
|
|
39040
|
+
return {
|
|
39041
|
+
error: `PGAI_INSTANCE_JOBS=${envValue} is not a boolean ` + "(use true/false, 1/0, yes/no, on/off, or leave it empty to keep the current setting)"
|
|
39042
|
+
};
|
|
39043
|
+
}
|
|
39044
|
+
function statOwner(file) {
|
|
39045
|
+
if (!file)
|
|
39046
|
+
return;
|
|
39047
|
+
let st;
|
|
39048
|
+
try {
|
|
39049
|
+
st = fs9.statSync(file);
|
|
39050
|
+
} catch {
|
|
39051
|
+
return;
|
|
39052
|
+
}
|
|
39053
|
+
if (!st.isFile())
|
|
39054
|
+
return null;
|
|
39055
|
+
return `${st.uid}:${st.gid}`;
|
|
39056
|
+
}
|
|
39057
|
+
function processOwner() {
|
|
39058
|
+
if (typeof process.getuid !== "function" || typeof process.getgid !== "function")
|
|
39059
|
+
return null;
|
|
39060
|
+
return `${process.getuid()}:${process.getgid()}`;
|
|
39061
|
+
}
|
|
39062
|
+
function buildLocalInstallEnv(existingEnv, imageTag, instanceId, credentialFile, instanceJobs) {
|
|
38790
39063
|
const readRaw = (key) => {
|
|
38791
39064
|
const re = new RegExp(`^[ ]*(?:export[ ]+)?${key}=(.*)$`, "gm");
|
|
38792
39065
|
let value = null;
|
|
@@ -38810,6 +39083,18 @@ function buildLocalInstallEnv(existingEnv, imageTag) {
|
|
|
38810
39083
|
envLines.push(`VM_AUTH_USERNAME=${vmAuthUsername ? stripMatchingQuotes(vmAuthUsername) : "vmauth"}`);
|
|
38811
39084
|
const vmAuthPassword = readRaw("VM_AUTH_PASSWORD");
|
|
38812
39085
|
envLines.push(`VM_AUTH_PASSWORD=${vmAuthPassword ? stripMatchingQuotes(vmAuthPassword) : crypto2.randomBytes(18).toString("base64")}`);
|
|
39086
|
+
const instanceIdValue = instanceId?.trim() || readRaw("PGAI_INSTANCE_ID");
|
|
39087
|
+
if (instanceIdValue) {
|
|
39088
|
+
envLines.push(`PGAI_INSTANCE_ID=${stripMatchingQuotes(instanceIdValue)}`);
|
|
39089
|
+
}
|
|
39090
|
+
const owner = statOwner(credentialFile);
|
|
39091
|
+
const credentialOwner = owner === undefined ? processOwner() : owner;
|
|
39092
|
+
if (credentialOwner)
|
|
39093
|
+
envLines.push(`INSTANCE_JOBS_USER=${credentialOwner}`);
|
|
39094
|
+
const existingProfiles = readRaw("COMPOSE_PROFILES");
|
|
39095
|
+
const composeProfiles = composeProfilesValue(existingProfiles === null ? null : stripMatchingQuotes(existingProfiles), instanceJobs);
|
|
39096
|
+
if (composeProfiles)
|
|
39097
|
+
envLines.push(`COMPOSE_PROFILES=${composeProfiles}`);
|
|
38813
39098
|
for (const key of VM_ADMIN_AUTH_KEYS) {
|
|
38814
39099
|
const existing = readRaw(key);
|
|
38815
39100
|
envLines.push(`${key}=${existing ? stripMatchingQuotes(existing) : crypto2.randomBytes(32).toString("hex")}`);
|
|
@@ -40598,16 +40883,6 @@ function checkRunningContainers() {
|
|
|
40598
40883
|
return { running: false, containers: [] };
|
|
40599
40884
|
}
|
|
40600
40885
|
}
|
|
40601
|
-
function planMonitoringRegistration(args) {
|
|
40602
|
-
const projectName = args.project?.trim() || undefined;
|
|
40603
|
-
if (args.instanceId) {
|
|
40604
|
-
return { kind: "adopt", projectName };
|
|
40605
|
-
}
|
|
40606
|
-
if (!projectName) {
|
|
40607
|
-
return { kind: "error-missing-project", projectName: undefined };
|
|
40608
|
-
}
|
|
40609
|
-
return { kind: "self-register", projectName };
|
|
40610
|
-
}
|
|
40611
40886
|
async function registerMonitoringInstance(apiKey, projectName, opts) {
|
|
40612
40887
|
const { apiBaseUrl } = resolveBaseUrls2(opts);
|
|
40613
40888
|
const url = `${apiBaseUrl}/rpc/monitoring_instance_register`;
|
|
@@ -40642,7 +40917,8 @@ Debug: Registering monitoring instance...`);
|
|
|
40642
40917
|
"Content-Type": "application/json",
|
|
40643
40918
|
...orgScopeHeaders2(opts?.orgScope)
|
|
40644
40919
|
},
|
|
40645
|
-
body: JSON.stringify(requestBody)
|
|
40920
|
+
body: JSON.stringify(requestBody),
|
|
40921
|
+
signal: requestTimeoutSignal2(opts?.timeoutMs).signal
|
|
40646
40922
|
});
|
|
40647
40923
|
const body = await res.text().catch(() => "");
|
|
40648
40924
|
if (!res.ok) {
|
|
@@ -40677,6 +40953,25 @@ Debug: Registering monitoring instance...`);
|
|
|
40677
40953
|
}
|
|
40678
40954
|
return null;
|
|
40679
40955
|
}
|
|
40956
|
+
var INSTANCE_ID_RE = /^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$/;
|
|
40957
|
+
function instanceIdToPersist(reg, requestedId) {
|
|
40958
|
+
for (const candidate of [reg?.instanceId, requestedId]) {
|
|
40959
|
+
const value = candidate?.trim();
|
|
40960
|
+
if (value && INSTANCE_ID_RE.test(value))
|
|
40961
|
+
return value;
|
|
40962
|
+
}
|
|
40963
|
+
return null;
|
|
40964
|
+
}
|
|
40965
|
+
function planMonitoringRegistration(args) {
|
|
40966
|
+
const projectName = args.project?.trim() || undefined;
|
|
40967
|
+
if (args.instanceId) {
|
|
40968
|
+
return { kind: "adopt", projectName };
|
|
40969
|
+
}
|
|
40970
|
+
if (!projectName) {
|
|
40971
|
+
return { kind: "error-missing-project", projectName: undefined };
|
|
40972
|
+
}
|
|
40973
|
+
return { kind: "self-register", projectName };
|
|
40974
|
+
}
|
|
40680
40975
|
var PROJECT_NAME_RE = /^[A-Za-z0-9._-]+$/;
|
|
40681
40976
|
function resolveAdoptedProject(reg) {
|
|
40682
40977
|
if (!reg)
|
|
@@ -40693,8 +40988,11 @@ function updatePgwatchConfig(configPath, updates) {
|
|
|
40693
40988
|
let lines = [];
|
|
40694
40989
|
if (fs9.existsSync(configPath)) {
|
|
40695
40990
|
const stats = fs9.statSync(configPath);
|
|
40696
|
-
if (!stats.
|
|
40697
|
-
|
|
40991
|
+
if (!stats.isFile()) {
|
|
40992
|
+
throw new Error(`${configPath} exists but is not a regular file; refusing to write through it`);
|
|
40993
|
+
}
|
|
40994
|
+
{
|
|
40995
|
+
const content = fs9.readFileSync(configPath, "utf8").replace(/^\uFEFF+/, "");
|
|
40698
40996
|
lines = content.split(/\r?\n/).filter((l) => l.trim() !== "");
|
|
40699
40997
|
}
|
|
40700
40998
|
}
|
|
@@ -40702,6 +41000,7 @@ function updatePgwatchConfig(configPath, updates) {
|
|
|
40702
41000
|
const existingIndex = lines.findIndex((l) => l.startsWith(key + "="));
|
|
40703
41001
|
if (existingIndex >= 0) {
|
|
40704
41002
|
lines[existingIndex] = `${key}=${value}`;
|
|
41003
|
+
lines = lines.filter((l, i) => i === existingIndex || !l.startsWith(key + "="));
|
|
40705
41004
|
} else {
|
|
40706
41005
|
lines.push(`${key}=${value}`);
|
|
40707
41006
|
}
|
|
@@ -40716,6 +41015,129 @@ function updatePgwatchConfig(configPath, updates) {
|
|
|
40716
41015
|
console.error(`Warning: could not restrict permissions on ${configPath}: ${message}`);
|
|
40717
41016
|
}
|
|
40718
41017
|
}
|
|
41018
|
+
function applyApiKey(projectDir, apiKey) {
|
|
41019
|
+
const configPath = path7.resolve(projectDir, ".pgwatch-config");
|
|
41020
|
+
try {
|
|
41021
|
+
updatePgwatchConfig(configPath, { api_key: apiKey });
|
|
41022
|
+
} catch (err) {
|
|
41023
|
+
console.error(`\u2717 ${err instanceof Error ? err.message : String(err)}`);
|
|
41024
|
+
return false;
|
|
41025
|
+
}
|
|
41026
|
+
console.log(`\u2713 API key saved
|
|
41027
|
+
`);
|
|
41028
|
+
return true;
|
|
41029
|
+
}
|
|
41030
|
+
var DEFAULT_PROMQL_TIMEOUT_S = 11 * 60;
|
|
41031
|
+
function formatSeconds(total) {
|
|
41032
|
+
const s = Math.max(0, Math.round(total));
|
|
41033
|
+
if (s < 90)
|
|
41034
|
+
return `${s}s`;
|
|
41035
|
+
const m = Math.round(s / 60);
|
|
41036
|
+
if (m < 60)
|
|
41037
|
+
return `${m}m`;
|
|
41038
|
+
const h = Math.floor(m / 60);
|
|
41039
|
+
const rem = m % 60;
|
|
41040
|
+
return rem ? `${h}h${rem}m` : `${h}h`;
|
|
41041
|
+
}
|
|
41042
|
+
function parseDurationSeconds(v) {
|
|
41043
|
+
const m = /^(\d+)(s|m|h)?$/.exec(v.trim());
|
|
41044
|
+
if (!m)
|
|
41045
|
+
return null;
|
|
41046
|
+
const n = parseInt(m[1], 10);
|
|
41047
|
+
if (!Number.isFinite(n))
|
|
41048
|
+
return null;
|
|
41049
|
+
return n * (m[2] === "h" ? 3600 : m[2] === "m" ? 60 : 1);
|
|
41050
|
+
}
|
|
41051
|
+
function readInstanceIdFromProject() {
|
|
41052
|
+
try {
|
|
41053
|
+
const { projectDir } = resolvePaths();
|
|
41054
|
+
const configPath = path7.resolve(projectDir, ".pgwatch-config");
|
|
41055
|
+
if (!fs9.existsSync(configPath))
|
|
41056
|
+
return null;
|
|
41057
|
+
const line = fs9.readFileSync(configPath, "utf8").replace(/^\uFEFF+/, "").split(/\r?\n/).find((l) => l.startsWith("instance_id="));
|
|
41058
|
+
const value = line?.slice("instance_id=".length).trim();
|
|
41059
|
+
return value && INSTANCE_ID_RE.test(value) ? value : null;
|
|
41060
|
+
} catch {
|
|
41061
|
+
return null;
|
|
41062
|
+
}
|
|
41063
|
+
}
|
|
41064
|
+
function applyProjectName(projectDir, project) {
|
|
41065
|
+
const configPath = path7.resolve(projectDir, ".pgwatch-config");
|
|
41066
|
+
try {
|
|
41067
|
+
updatePgwatchConfig(configPath, { project_name: project });
|
|
41068
|
+
} catch (err) {
|
|
41069
|
+
console.error(`\u2717 ${err instanceof Error ? err.message : String(err)}`);
|
|
41070
|
+
return false;
|
|
41071
|
+
}
|
|
41072
|
+
console.log(`Using project name: ${project}
|
|
41073
|
+
`);
|
|
41074
|
+
return true;
|
|
41075
|
+
}
|
|
41076
|
+
function persistInstanceId(projectDir, reg, requestedId, alsoWrite = {}) {
|
|
41077
|
+
const instanceId = instanceIdToPersist(reg, requestedId);
|
|
41078
|
+
const updates = { ...alsoWrite, ...instanceId ? { instance_id: instanceId } : {} };
|
|
41079
|
+
if (Object.keys(updates).length === 0)
|
|
41080
|
+
return false;
|
|
41081
|
+
const configPath = path7.resolve(projectDir, ".pgwatch-config");
|
|
41082
|
+
try {
|
|
41083
|
+
updatePgwatchConfig(configPath, updates);
|
|
41084
|
+
return instanceId != null;
|
|
41085
|
+
} catch (err) {
|
|
41086
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
41087
|
+
console.error(`Warning: could not record the monitoring instance id in ${configPath}: ${message}`);
|
|
41088
|
+
return false;
|
|
41089
|
+
}
|
|
41090
|
+
}
|
|
41091
|
+
async function adoptAndPersist(projectDir, apiKey, projectName, instanceId, opts, register = registerMonitoringInstance) {
|
|
41092
|
+
const reg = await register(apiKey, projectName, {
|
|
41093
|
+
orgScope: opts.orgScope,
|
|
41094
|
+
apiBaseUrl: opts.apiBaseUrl,
|
|
41095
|
+
debug: opts.debug,
|
|
41096
|
+
instanceId
|
|
41097
|
+
});
|
|
41098
|
+
const adoptedProject = resolveAdoptedProject(reg);
|
|
41099
|
+
const persisted = persistInstanceId(projectDir, reg, instanceId, {
|
|
41100
|
+
...adoptedProject != null ? { project_name: adoptedProject } : {}
|
|
41101
|
+
});
|
|
41102
|
+
if (!persisted) {
|
|
41103
|
+
console.error(`\u26A0 Adopted instance ${instanceId} but could not record it in .pgwatch-config \u2014 ` + `instance jobs will idle until that file is writable and local-install is re-run
|
|
41104
|
+
`);
|
|
41105
|
+
}
|
|
41106
|
+
if (adoptedProject != null) {
|
|
41107
|
+
const verb = reg?.created ? "Registered" : "Adopted";
|
|
41108
|
+
console.log(`\u2713 ${verb} monitoring instance (project: ${adoptedProject})
|
|
41109
|
+
`);
|
|
41110
|
+
} else if (reg) {
|
|
41111
|
+
console.error(`\u26A0 Adopted provisioned instance ${instanceId} but the platform returned no project` + (projectName ? ` \u2014 reports will use project '${projectName}'` : ` \u2014 reports will have no project until 'postgresai mon local-install' is re-run with --project <name>`));
|
|
41112
|
+
} else {
|
|
41113
|
+
console.error(`\u26A0 Could not adopt provisioned instance ${instanceId}` + (projectName ? ` \u2014 reports will use project '${projectName}' until 'postgresai mon local-install' is re-run` : ` \u2014 reports will have no project until 'postgresai mon local-install' is re-run with --project <name>`));
|
|
41114
|
+
}
|
|
41115
|
+
return reg;
|
|
41116
|
+
}
|
|
41117
|
+
async function selfRegisterAndPersist(projectDir, apiKey, projectName, opts, register = registerMonitoringInstance) {
|
|
41118
|
+
const reg = await register(apiKey, projectName, {
|
|
41119
|
+
apiBaseUrl: opts.apiBaseUrl,
|
|
41120
|
+
debug: opts.debug,
|
|
41121
|
+
orgScope: opts.orgScope
|
|
41122
|
+
});
|
|
41123
|
+
const persisted = persistInstanceId(projectDir, reg, undefined);
|
|
41124
|
+
if (persisted) {
|
|
41125
|
+
console.log(`\u2713 Registered monitoring instance (project: ${projectName})
|
|
41126
|
+
`);
|
|
41127
|
+
return reg;
|
|
41128
|
+
}
|
|
41129
|
+
const minted = reg?.instanceId?.trim();
|
|
41130
|
+
if (minted) {
|
|
41131
|
+
console.error(`\u26A0 Registered monitoring instance ${minted} but could not record it in .pgwatch-config \u2014 ` + `instance jobs will idle. Add 'instance_id=${minted}' to that file, or re-run with ` + `--instance-id ${minted} (which adopts it rather than registering a second one)
|
|
41132
|
+
`);
|
|
41133
|
+
} else {
|
|
41134
|
+
const reRun = projectName ? `'postgresai mon local-install --project ${projectName}'` : "'postgresai mon local-install --project <name>'";
|
|
41135
|
+
console.error(`\u26A0 Could not register this monitoring instance \u2014 instance jobs will idle until ` + `${reRun} is re-run
|
|
41136
|
+
`);
|
|
41137
|
+
}
|
|
41138
|
+
process.exitCode = 1;
|
|
41139
|
+
return reg;
|
|
41140
|
+
}
|
|
40719
41141
|
async function applyMonitoringTargetsConfig() {
|
|
40720
41142
|
console.log("Applying monitoring target configuration...");
|
|
40721
41143
|
const generateCode = await runCompose(["run", "--rm", "sources-generator"]);
|
|
@@ -40724,7 +41146,7 @@ async function applyMonitoringTargetsConfig() {
|
|
|
40724
41146
|
console.log("Restarting pgwatch collectors to pick up target changes...");
|
|
40725
41147
|
return runCompose(["up", "-d", "--force-recreate", "pgwatch-prometheus", "pgwatch-postgres"]);
|
|
40726
41148
|
}
|
|
40727
|
-
async function runCompose(args, grafanaPassword) {
|
|
41149
|
+
async function runCompose(args, grafanaPassword, extraEnv) {
|
|
40728
41150
|
let composeFile;
|
|
40729
41151
|
let projectDir;
|
|
40730
41152
|
try {
|
|
@@ -40788,6 +41210,8 @@ async function runCompose(args, grafanaPassword) {
|
|
|
40788
41210
|
}
|
|
40789
41211
|
}
|
|
40790
41212
|
}
|
|
41213
|
+
if (extraEnv)
|
|
41214
|
+
Object.assign(env, extraEnv);
|
|
40791
41215
|
const finalArgs = [...args];
|
|
40792
41216
|
if (process.platform === "darwin" && shouldScaleOutNodeExporter(args)) {
|
|
40793
41217
|
finalArgs.push("--scale", "self-node-exporter=0");
|
|
@@ -40846,6 +41270,127 @@ Applying sink-prometheus config (VictoriaMetrics admin keys, #359)...`);
|
|
|
40846
41270
|
console.log("\u2713 sink-prometheus is running the current config");
|
|
40847
41271
|
return true;
|
|
40848
41272
|
}
|
|
41273
|
+
function readComposeProfiles(knownProjectDir) {
|
|
41274
|
+
const fromProcess = process.env.COMPOSE_PROFILES;
|
|
41275
|
+
if (fromProcess !== undefined && fromProcess.trim() !== "")
|
|
41276
|
+
return fromProcess;
|
|
41277
|
+
let projectDir;
|
|
41278
|
+
if (knownProjectDir) {
|
|
41279
|
+
projectDir = knownProjectDir;
|
|
41280
|
+
} else {
|
|
41281
|
+
try {
|
|
41282
|
+
({ projectDir } = resolvePaths());
|
|
41283
|
+
} catch {
|
|
41284
|
+
projectDir = getDefaultMonitoringProjectDir();
|
|
41285
|
+
}
|
|
41286
|
+
}
|
|
41287
|
+
const envFile = path7.resolve(projectDir, ".env");
|
|
41288
|
+
let content;
|
|
41289
|
+
try {
|
|
41290
|
+
content = fs9.readFileSync(envFile, "utf8");
|
|
41291
|
+
} catch (err) {
|
|
41292
|
+
if (err?.code !== "ENOENT") {
|
|
41293
|
+
console.error(`\u26A0 Could not read ${envFile}: ${err instanceof Error ? err.message : String(err)}`);
|
|
41294
|
+
console.error(" Treating the instance-jobs profile as disabled; if it is in fact enabled,");
|
|
41295
|
+
console.error(" an absent container will be reported as '- not enabled' rather than a fault.");
|
|
41296
|
+
}
|
|
41297
|
+
return null;
|
|
41298
|
+
}
|
|
41299
|
+
let value = null;
|
|
41300
|
+
for (const m of content.matchAll(/^[ \t]*(?:export[ \t]+)?COMPOSE_PROFILES=(.*)$/gm)) {
|
|
41301
|
+
value = stripMatchingQuotes(m[1].trim());
|
|
41302
|
+
}
|
|
41303
|
+
return value;
|
|
41304
|
+
}
|
|
41305
|
+
function instanceJobsRemovalOutcome(before, after, removeExitCode) {
|
|
41306
|
+
if (before === "unknown" || after === "unknown") {
|
|
41307
|
+
return {
|
|
41308
|
+
ok: false,
|
|
41309
|
+
message: `\u2717 Could not determine whether the instance-jobs container is present.
|
|
41310
|
+
` + " The profile is off in .env, but a container left behind keeps polling."
|
|
41311
|
+
};
|
|
41312
|
+
}
|
|
41313
|
+
if (before === "absent") {
|
|
41314
|
+
return { ok: true, message: "\u2713 instance-jobs is disabled (no container was present)" };
|
|
41315
|
+
}
|
|
41316
|
+
if (removeExitCode !== 0 || after !== "absent") {
|
|
41317
|
+
return {
|
|
41318
|
+
ok: false,
|
|
41319
|
+
message: `\u2717 Could not remove the instance-jobs container.
|
|
41320
|
+
` + ` The profile is off in .env, but the container is still there and still polling. To retry:
|
|
41321
|
+
` + ` COMPOSE_PROFILES=${INSTANCE_JOBS_PROFILE} docker compose rm -sf ${INSTANCE_JOBS_PROFILE}`
|
|
41322
|
+
};
|
|
41323
|
+
}
|
|
41324
|
+
return before === "running" ? {
|
|
41325
|
+
ok: true,
|
|
41326
|
+
message: "\u26A0 instance-jobs was RUNNING and has been removed \u2014 outbound collection on this machine is now OFF"
|
|
41327
|
+
} : { ok: true, message: "\u2713 instance-jobs container removed (it was not running)" };
|
|
41328
|
+
}
|
|
41329
|
+
function instanceJobsStartSkipReason(storeRunning) {
|
|
41330
|
+
if (storeRunning === true)
|
|
41331
|
+
return null;
|
|
41332
|
+
return storeRunning === false ? "the stack is not running, so instance-jobs was not started - it comes up with the rest on `mon start`" : "could not tell whether the stack is running, so instance-jobs was not started - run `mon start`, then `mon health`";
|
|
41333
|
+
}
|
|
41334
|
+
async function composeServiceRunning(projectDir, service) {
|
|
41335
|
+
const composeCmd = getComposeCmd();
|
|
41336
|
+
if (!composeCmd)
|
|
41337
|
+
return null;
|
|
41338
|
+
try {
|
|
41339
|
+
const { stdout } = await execFilePromise(composeCmd[0], [...composeCmd.slice(1), "-f", path7.resolve(projectDir, "docker-compose.yml"), "ps", "-q", service], { cwd: projectDir });
|
|
41340
|
+
return stdout.trim().length > 0;
|
|
41341
|
+
} catch {
|
|
41342
|
+
return null;
|
|
41343
|
+
}
|
|
41344
|
+
}
|
|
41345
|
+
async function applyInstanceJobsProfile(projectDir, enabled) {
|
|
41346
|
+
if (enabled === undefined)
|
|
41347
|
+
return true;
|
|
41348
|
+
if (enabled) {
|
|
41349
|
+
const skip = instanceJobsStartSkipReason(await composeServiceRunning(projectDir, "sink-prometheus"));
|
|
41350
|
+
if (skip) {
|
|
41351
|
+
console.log(`
|
|
41352
|
+
(${skip})`);
|
|
41353
|
+
return true;
|
|
41354
|
+
}
|
|
41355
|
+
console.log(`
|
|
41356
|
+
Starting the instance-jobs container (compose profile enabled)...`);
|
|
41357
|
+
const code = await runCompose(["up", "-d", "--no-deps", INSTANCE_JOBS_PROFILE]);
|
|
41358
|
+
if (code !== 0) {
|
|
41359
|
+
console.error("\u2717 Could not start the instance-jobs container.");
|
|
41360
|
+
console.error(" The channel is enabled in .env but nothing is polling. To retry:");
|
|
41361
|
+
console.error(` docker compose up -d --no-deps ${INSTANCE_JOBS_PROFILE}`);
|
|
41362
|
+
return false;
|
|
41363
|
+
}
|
|
41364
|
+
console.log("\u2713 instance-jobs is running");
|
|
41365
|
+
return true;
|
|
41366
|
+
}
|
|
41367
|
+
const before = await inspectInstanceJobsState();
|
|
41368
|
+
if (before === "absent" || before === "unknown") {
|
|
41369
|
+
const verdict = instanceJobsRemovalOutcome(before, before, 0);
|
|
41370
|
+
(verdict.ok ? console.log : console.error)(`
|
|
41371
|
+
${verdict.message}`);
|
|
41372
|
+
return verdict.ok;
|
|
41373
|
+
}
|
|
41374
|
+
const code = await runCompose(["rm", "-sf", INSTANCE_JOBS_PROFILE], undefined, { COMPOSE_PROFILES: INSTANCE_JOBS_PROFILE });
|
|
41375
|
+
const verdict = instanceJobsRemovalOutcome(before, await inspectInstanceJobsState(), code);
|
|
41376
|
+
(verdict.ok ? console.log : console.error)(`
|
|
41377
|
+
${verdict.message}`);
|
|
41378
|
+
return verdict.ok;
|
|
41379
|
+
}
|
|
41380
|
+
async function inspectInstanceJobsState() {
|
|
41381
|
+
try {
|
|
41382
|
+
const { stdout } = await execFilePromise("docker", [
|
|
41383
|
+
"inspect",
|
|
41384
|
+
"-f",
|
|
41385
|
+
"{{.State.Running}}",
|
|
41386
|
+
INSTANCE_JOBS_PROFILE
|
|
41387
|
+
]);
|
|
41388
|
+
return stdout.trim() === "true" ? "running" : "stopped";
|
|
41389
|
+
} catch (error) {
|
|
41390
|
+
const text = error instanceof Error ? error.message : String(error);
|
|
41391
|
+
return /no such (object|container)/i.test(text) ? "absent" : "unknown";
|
|
41392
|
+
}
|
|
41393
|
+
}
|
|
40849
41394
|
program2.command("help").description("show help").action(() => {
|
|
40850
41395
|
program2.outputHelp();
|
|
40851
41396
|
});
|
|
@@ -40859,7 +41404,7 @@ mon.command("local-install").description("install local monitoring stack (genera
|
|
|
40859
41404
|
" PGAI_ENABLE_IPV6=false (accepted: true|false|yes|no, lowercase)",
|
|
40860
41405
|
""
|
|
40861
41406
|
].join(`
|
|
40862
|
-
`)).option("--demo", "demo mode with sample database", false).option("--api-key <key>", "Postgres AI API key for automated report uploads").option("--org <alias>", `organization alias (or ${ORG_ENV}); required to register with a global token`).option("--org-id <id>", `organization id (or ${ORG_ID_ENV}); alternative to --org`).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("--vcpus <n>", "source DB vCPU count used for AAS zone thresholds (PGAI_VCPUS env also works). Omit or 0 = unknown: the platform keeps the value it stamped at provision time. An explicit non-zero value overwrites the stored one; changing it re-arms the AAS backfill.").option("-y, --yes", "accept all defaults and skip interactive prompts", false).action(async (opts) => {
|
|
41407
|
+
`)).option("--demo", "demo mode with sample database", false).option("--api-key <key>", "Postgres AI API key for automated report uploads").option("--org <alias>", `organization alias (or ${ORG_ENV}); required to register with a global token`).option("--org-id <id>", `organization id (or ${ORG_ID_ENV}); alternative to --org`).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("--vcpus <n>", "source DB vCPU count used for AAS zone thresholds (PGAI_VCPUS env also works). Omit or 0 = unknown: the platform keeps the value it stamped at provision time. An explicit non-zero value overwrites the stored one; changing it re-arms the AAS backfill.").option("--instance-jobs", "enable the instance-jobs compose profile (outbound collection channel; PGAI_INSTANCE_JOBS env also works). Writes COMPOSE_PROFILES into the stack .env, so every later mon command covers the container").option("--no-instance-jobs", "disable the instance-jobs compose profile and remove its container").option("-y, --yes", "accept all defaults and skip interactive prompts", false).action(async (opts) => {
|
|
40863
41408
|
const globalOpts = program2.opts();
|
|
40864
41409
|
let apiKey = opts.apiKey || globalOpts.apiKey;
|
|
40865
41410
|
console.log(`
|
|
@@ -40872,18 +41417,23 @@ mon.command("local-install").description("install local monitoring stack (genera
|
|
|
40872
41417
|
const { projectDir, instancesFile: instancesPath } = await resolveOrInitPaths();
|
|
40873
41418
|
console.log(`Project directory: ${projectDir}
|
|
40874
41419
|
`);
|
|
40875
|
-
if (opts.project) {
|
|
40876
|
-
|
|
40877
|
-
|
|
40878
|
-
console.log(`Using project name: ${opts.project}
|
|
40879
|
-
`);
|
|
41420
|
+
if (opts.project && !applyProjectName(projectDir, opts.project)) {
|
|
41421
|
+
process.exitCode = 1;
|
|
41422
|
+
return;
|
|
40880
41423
|
}
|
|
40881
41424
|
const envFile = path7.resolve(projectDir, ".env");
|
|
40882
41425
|
const existingEnv = fs9.existsSync(envFile) ? fs9.readFileSync(envFile, "utf8") : "";
|
|
40883
41426
|
const previousTagMatch = existingEnv.match(/^PGAI_TAG=(.+)$/m);
|
|
40884
41427
|
const previousTag = previousTagMatch ? previousTagMatch[1].trim() : null;
|
|
40885
41428
|
const imageTag = opts.tag || package_default.version;
|
|
40886
|
-
const
|
|
41429
|
+
const instanceJobsRequest = resolveInstanceJobsRequest(opts.instanceJobs, process.env.PGAI_INSTANCE_JOBS);
|
|
41430
|
+
if (instanceJobsRequest.error) {
|
|
41431
|
+
console.error(`\u2717 ${instanceJobsRequest.error}`);
|
|
41432
|
+
process.exitCode = 1;
|
|
41433
|
+
return;
|
|
41434
|
+
}
|
|
41435
|
+
const instanceJobs = instanceJobsRequest.enabled;
|
|
41436
|
+
const { content: envContent, preservedKeys } = buildLocalInstallEnv(existingEnv, imageTag, opts.instanceId || process.env.PGAI_INSTANCE_ID || null, path7.resolve(projectDir, ".pgwatch-config"), instanceJobs);
|
|
40887
41437
|
writeEnvFile(envFile, envContent);
|
|
40888
41438
|
if (preservedKeys.length > 0) {
|
|
40889
41439
|
console.log(`Preserved existing .env settings: ${preservedKeys.join(", ")}
|
|
@@ -40914,6 +41464,8 @@ Use demo mode without API key: postgres-ai mon local-install --demo`);
|
|
|
40914
41464
|
console.error(`\u26A0 Monitoring services are already running: ${containers.join(", ")}`);
|
|
40915
41465
|
console.log(`Use 'postgres-ai mon restart' to restart them
|
|
40916
41466
|
`);
|
|
41467
|
+
if (!await applyInstanceJobsProfile(projectDir, instanceJobs))
|
|
41468
|
+
process.exitCode = 1;
|
|
40917
41469
|
return;
|
|
40918
41470
|
}
|
|
40919
41471
|
if (!opts.demo) {
|
|
@@ -40923,9 +41475,10 @@ Use demo mode without API key: postgres-ai mon local-install --demo`);
|
|
|
40923
41475
|
if (apiKey) {
|
|
40924
41476
|
console.log("Using API key provided via --api-key parameter");
|
|
40925
41477
|
writeConfig({ apiKey });
|
|
40926
|
-
|
|
40927
|
-
|
|
40928
|
-
|
|
41478
|
+
if (!applyApiKey(projectDir, apiKey)) {
|
|
41479
|
+
process.exitCode = 1;
|
|
41480
|
+
return;
|
|
41481
|
+
}
|
|
40929
41482
|
} else if (opts.yes) {
|
|
40930
41483
|
console.log("Auto-yes mode: no API key provided, skipping API key setup");
|
|
40931
41484
|
console.error("\u26A0 Reports will be generated locally only");
|
|
@@ -40940,10 +41493,11 @@ Use demo mode without API key: postgres-ai mon local-install --demo`);
|
|
|
40940
41493
|
const trimmedKey = inputApiKey.trim();
|
|
40941
41494
|
if (trimmedKey) {
|
|
40942
41495
|
writeConfig({ apiKey: trimmedKey });
|
|
40943
|
-
|
|
41496
|
+
if (!applyApiKey(projectDir, trimmedKey)) {
|
|
41497
|
+
process.exitCode = 1;
|
|
41498
|
+
return;
|
|
41499
|
+
}
|
|
40944
41500
|
apiKey = trimmedKey;
|
|
40945
|
-
console.log(`\u2713 API key saved
|
|
40946
|
-
`);
|
|
40947
41501
|
break;
|
|
40948
41502
|
}
|
|
40949
41503
|
console.error("\u26A0 API key cannot be empty");
|
|
@@ -41201,6 +41755,9 @@ Searched: ${demoCandidates.join(", ")}
|
|
|
41201
41755
|
}
|
|
41202
41756
|
console.log(`\u2713 Services started
|
|
41203
41757
|
`);
|
|
41758
|
+
const instanceJobsApplied = await applyInstanceJobsProfile(projectDir, instanceJobs);
|
|
41759
|
+
if (!instanceJobsApplied)
|
|
41760
|
+
process.exitCode = 1;
|
|
41204
41761
|
if (apiKey && !opts.demo) {
|
|
41205
41762
|
let regOrgScope;
|
|
41206
41763
|
if (isGlobalTokenValue(apiKey)) {
|
|
@@ -41224,25 +41781,11 @@ Searched: ${demoCandidates.join(", ")}
|
|
|
41224
41781
|
const plan = planMonitoringRegistration({ project: opts.project, instanceId });
|
|
41225
41782
|
const projectName = plan.projectName;
|
|
41226
41783
|
if (instanceId) {
|
|
41227
|
-
const reg = await
|
|
41784
|
+
const reg = await adoptAndPersist(projectDir, apiKey, projectName, instanceId, {
|
|
41228
41785
|
orgScope: regOrgScope,
|
|
41229
41786
|
apiBaseUrl: globalOpts.apiBaseUrl,
|
|
41230
|
-
debug: !!process.env.DEBUG
|
|
41231
|
-
instanceId
|
|
41787
|
+
debug: !!process.env.DEBUG
|
|
41232
41788
|
});
|
|
41233
|
-
const adoptedProject = resolveAdoptedProject(reg);
|
|
41234
|
-
if (adoptedProject != null) {
|
|
41235
|
-
updatePgwatchConfig(path7.resolve(projectDir, ".pgwatch-config"), {
|
|
41236
|
-
project_name: adoptedProject
|
|
41237
|
-
});
|
|
41238
|
-
const verb = reg?.created ? "Registered" : "Adopted";
|
|
41239
|
-
console.log(`\u2713 ${verb} monitoring instance (project: ${adoptedProject})
|
|
41240
|
-
`);
|
|
41241
|
-
} else if (reg) {
|
|
41242
|
-
console.error(`\u26A0 Adopted provisioned instance ${instanceId} but the platform returned no project` + (projectName ? ` \u2014 reports will use project '${projectName}'` : ` \u2014 reports will have no project until 'postgresai mon local-install' is re-run with --project <name>`));
|
|
41243
|
-
} else {
|
|
41244
|
-
console.error(`\u26A0 Could not adopt provisioned instance ${instanceId}` + (projectName ? ` \u2014 reports will use project '${projectName}' until 'postgresai mon local-install' is re-run` : ` \u2014 reports will have no project until 'postgresai mon local-install' is re-run with --project <name>`));
|
|
41245
|
-
}
|
|
41246
41789
|
if (reg === null) {
|
|
41247
41790
|
console.error(`\u26A0 Skipping AAS auto-collection arming because instance adoption failed; it can be enabled later by re-running 'postgresai mon local-install'
|
|
41248
41791
|
`);
|
|
@@ -41256,7 +41799,7 @@ Searched: ${demoCandidates.join(", ")}
|
|
|
41256
41799
|
debug: !!process.env.DEBUG
|
|
41257
41800
|
});
|
|
41258
41801
|
if (aas.ok) {
|
|
41259
|
-
console.log(`\u2713
|
|
41802
|
+
console.log(`\u2713 ${aasSuccessMessage(aas)}
|
|
41260
41803
|
`);
|
|
41261
41804
|
} else {
|
|
41262
41805
|
console.error(`\u26A0 AAS auto-collection not registered (${aas.reason}); it can be enabled later by re-running 'postgresai mon local-install'
|
|
@@ -41267,7 +41810,7 @@ Searched: ${demoCandidates.join(", ")}
|
|
|
41267
41810
|
console.error("\u2717 A project name is required for self-registration (the 'postgres-ai-monitoring' default was removed). " + "Re-run with --project <name>, or adopt a console-provisioned instance with --instance-id <uuid>.");
|
|
41268
41811
|
process.exitCode = 1;
|
|
41269
41812
|
} else {
|
|
41270
|
-
|
|
41813
|
+
await selfRegisterAndPersist(projectDir, apiKey, projectName, {
|
|
41271
41814
|
apiBaseUrl: globalOpts.apiBaseUrl,
|
|
41272
41815
|
debug: !!process.env.DEBUG,
|
|
41273
41816
|
orgScope: regOrgScope
|
|
@@ -41275,9 +41818,15 @@ Searched: ${demoCandidates.join(", ")}
|
|
|
41275
41818
|
}
|
|
41276
41819
|
}
|
|
41277
41820
|
console.log("=================================");
|
|
41278
|
-
console.log(" Local install completed!");
|
|
41821
|
+
console.log(instanceJobsApplied ? " Local install completed!" : " Local install completed WITH ONE FAILURE");
|
|
41279
41822
|
console.log(`=================================
|
|
41280
41823
|
`);
|
|
41824
|
+
if (!instanceJobsApplied) {
|
|
41825
|
+
console.log(instanceJobs ? `\u2717 The instance-jobs container did not start. Everything below is running; the outbound
|
|
41826
|
+
` + " collection channel is not. See the error above, then `postgres-ai mon health`.\n" : `\u2717 The instance-jobs container could not be removed. Everything below is running, and so
|
|
41827
|
+
` + ` is a container that keeps polling. See the error above.
|
|
41828
|
+
`);
|
|
41829
|
+
}
|
|
41281
41830
|
console.log("What's running:");
|
|
41282
41831
|
if (opts.demo) {
|
|
41283
41832
|
console.log(" \u2705 Demo PostgreSQL database (monitoring target)");
|
|
@@ -41336,7 +41885,8 @@ var MONITORING_CONTAINERS = [
|
|
|
41336
41885
|
"self-postgres-exporter",
|
|
41337
41886
|
"flask-pgss-api",
|
|
41338
41887
|
"sources-generator",
|
|
41339
|
-
"postgres-reports"
|
|
41888
|
+
"postgres-reports",
|
|
41889
|
+
"instance-jobs"
|
|
41340
41890
|
];
|
|
41341
41891
|
var COMPOSE_PROJECT_NAME = "postgres_ai";
|
|
41342
41892
|
var DOCKER_NETWORK_NAME = `${COMPOSE_PROJECT_NAME}_default`;
|
|
@@ -41389,15 +41939,37 @@ mon.command("logs [service]").option("-f, --follow", "follow logs", false).optio
|
|
|
41389
41939
|
if (code !== 0)
|
|
41390
41940
|
process.exitCode = code;
|
|
41391
41941
|
});
|
|
41392
|
-
|
|
41393
|
-
|
|
41942
|
+
function judgeServiceHealth(service, present, status, health) {
|
|
41943
|
+
if (!present) {
|
|
41944
|
+
return service.optional ? { ok: true, mark: "-", label: "not enabled" } : { ok: false, mark: "\u2717", label: service.absentLabel ?? "unreachable" };
|
|
41945
|
+
}
|
|
41946
|
+
if (status !== "running") {
|
|
41947
|
+
return { ok: false, mark: "\u2717", label: `unhealthy (status: ${status})` };
|
|
41948
|
+
}
|
|
41949
|
+
if (service.readHealth && health && health !== "healthy" && health !== "starting") {
|
|
41950
|
+
return { ok: false, mark: "\u2717", label: `unhealthy (${health})` };
|
|
41951
|
+
}
|
|
41952
|
+
return { ok: true, mark: "\u2713", label: "healthy" };
|
|
41953
|
+
}
|
|
41954
|
+
function monitoringHealthServices(instanceJobsEnabled = false) {
|
|
41955
|
+
return [
|
|
41394
41956
|
{ name: "Grafana", container: "grafana-with-datasources" },
|
|
41395
41957
|
{ name: "Prometheus", container: "sink-prometheus" },
|
|
41396
41958
|
{ name: "PGWatch (Postgres)", container: "pgwatch-postgres" },
|
|
41397
41959
|
{ name: "PGWatch (Prometheus)", container: "pgwatch-prometheus" },
|
|
41398
41960
|
{ name: "Target DB", container: "target-db" },
|
|
41399
|
-
{ name: "Sink Postgres", container: "sink-postgres" }
|
|
41961
|
+
{ name: "Sink Postgres", container: "sink-postgres" },
|
|
41962
|
+
{
|
|
41963
|
+
name: "Instance jobs",
|
|
41964
|
+
container: "instance-jobs",
|
|
41965
|
+
optional: !instanceJobsEnabled,
|
|
41966
|
+
readHealth: true,
|
|
41967
|
+
absentLabel: "enabled but not running"
|
|
41968
|
+
}
|
|
41400
41969
|
];
|
|
41970
|
+
}
|
|
41971
|
+
mon.command("health").description("health check for monitoring services").option("--wait <seconds>", "wait time in seconds for services to become healthy", parseInt, 0).action(async (opts) => {
|
|
41972
|
+
const services = monitoringHealthServices(instanceJobsProfileEnabled(readComposeProfiles()));
|
|
41401
41973
|
const waitTime = opts.wait || 0;
|
|
41402
41974
|
const maxAttempts = waitTime > 0 ? Math.ceil(waitTime / 5) : 1;
|
|
41403
41975
|
console.log(`Checking service health...
|
|
@@ -41413,16 +41985,12 @@ mon.command("health").description("health check for monitoring services").option
|
|
|
41413
41985
|
for (const service of services) {
|
|
41414
41986
|
try {
|
|
41415
41987
|
const result = spawnSync2("docker", ["inspect", "-f", "{{.State.Status}}", service.container], { stdio: "pipe" });
|
|
41416
|
-
const
|
|
41417
|
-
if
|
|
41418
|
-
|
|
41419
|
-
}
|
|
41420
|
-
|
|
41988
|
+
const present = result.status === 0;
|
|
41989
|
+
const health = present && service.readHealth ? (spawnSync2("docker", ["inspect", "-f", "{{if .State.Health}}{{.State.Health.Status}}{{end}}", service.container], { stdio: "pipe" }).stdout || "").trim() : "";
|
|
41990
|
+
const verdict = judgeServiceHealth(service, present, (result.stdout || "").trim(), health);
|
|
41991
|
+
console.log(`${verdict.mark} ${service.name}: ${verdict.label}`);
|
|
41992
|
+
if (!verdict.ok)
|
|
41421
41993
|
allHealthy = false;
|
|
41422
|
-
} else {
|
|
41423
|
-
console.log(`\u2717 ${service.name}: unreachable`);
|
|
41424
|
-
allHealthy = false;
|
|
41425
|
-
}
|
|
41426
41994
|
} catch (error) {
|
|
41427
41995
|
console.log(`\u2717 ${service.name}: unreachable`);
|
|
41428
41996
|
allHealthy = false;
|
|
@@ -41535,6 +42103,12 @@ Updating Docker images...`);
|
|
|
41535
42103
|
process.exitCode = 1;
|
|
41536
42104
|
return;
|
|
41537
42105
|
}
|
|
42106
|
+
if (instanceJobsProfileEnabled(readComposeProfiles(projectDir))) {
|
|
42107
|
+
if (!await applyInstanceJobsProfile(projectDir, true)) {
|
|
42108
|
+
process.exitCode = 1;
|
|
42109
|
+
return;
|
|
42110
|
+
}
|
|
42111
|
+
}
|
|
41538
42112
|
console.log(`
|
|
41539
42113
|
\u2713 Update completed successfully`);
|
|
41540
42114
|
console.log(`
|
|
@@ -42236,8 +42810,87 @@ program2.command("orgs").description("list organizations this credential can rea
|
|
|
42236
42810
|
process.exitCode = 1;
|
|
42237
42811
|
}
|
|
42238
42812
|
});
|
|
42813
|
+
withOrgOptions(program2.command("promql")).description("run a PromQL query on a monitoring instance's own metric store").argument("<expr>", "PromQL expression").option("--instance <uuid>", "monitoring instance id (default: this box's .pgwatch-config)").option("--at <rfc3339>", "evaluate an instant query at this time (default: now; ignored with --range)").option("--range", "run a range query (requires --start, --end, --step)").option("--start <rfc3339>", "range start").option("--end <rfc3339>", "range end").option("--step <duration>", "range step, e.g. 60s or 5m").option("--timeout <duration>", "how long to wait for an answer (default: sized from the instance's poll pacing; 11m if the platform does not report it)").option("--json", "output the raw result payload").action(async (expr, opts) => {
|
|
42814
|
+
try {
|
|
42815
|
+
const rootOpts = program2.opts();
|
|
42816
|
+
const cfg = readConfig();
|
|
42817
|
+
const { apiKey } = getConfig(rootOpts);
|
|
42818
|
+
if (!apiKey) {
|
|
42819
|
+
console.error("API key is required. Run 'pgai auth' first or set --api-key.");
|
|
42820
|
+
process.exitCode = 1;
|
|
42821
|
+
return;
|
|
42822
|
+
}
|
|
42823
|
+
const { apiBaseUrl } = resolveBaseUrls2(rootOpts, cfg);
|
|
42824
|
+
const instanceId = opts.instance || readInstanceIdFromProject();
|
|
42825
|
+
if (!instanceId) {
|
|
42826
|
+
console.error("No monitoring instance. Pass --instance <uuid>, or run this on a box whose");
|
|
42827
|
+
console.error(".pgwatch-config has an instance_id (written by 'pgai mon local-install').");
|
|
42828
|
+
process.exitCode = 1;
|
|
42829
|
+
return;
|
|
42830
|
+
}
|
|
42831
|
+
const kind = opts.range ? "promql_range" : "promql_instant";
|
|
42832
|
+
const enqueue = { instanceId, kind, query: expr };
|
|
42833
|
+
if (opts.range) {
|
|
42834
|
+
if (!opts.start || !opts.end || !opts.step) {
|
|
42835
|
+
console.error("--range needs --start, --end and --step.");
|
|
42836
|
+
process.exitCode = 1;
|
|
42837
|
+
return;
|
|
42838
|
+
}
|
|
42839
|
+
const stepS = parseDurationSeconds(opts.step);
|
|
42840
|
+
if (stepS === null || stepS < 1) {
|
|
42841
|
+
console.error(`Invalid --step ${opts.step}: expected a duration like 60s, 5m or 1h.`);
|
|
42842
|
+
process.exitCode = 1;
|
|
42843
|
+
return;
|
|
42844
|
+
}
|
|
42845
|
+
enqueue.start = opts.start;
|
|
42846
|
+
enqueue.end = opts.end;
|
|
42847
|
+
enqueue.stepS = stepS;
|
|
42848
|
+
} else if (opts.at) {
|
|
42849
|
+
enqueue.at = opts.at;
|
|
42850
|
+
}
|
|
42851
|
+
const explicitTimeoutS = opts.timeout ? parseDurationSeconds(opts.timeout) : null;
|
|
42852
|
+
if (opts.timeout && (explicitTimeoutS === null || explicitTimeoutS < 1)) {
|
|
42853
|
+
console.error(`Invalid --timeout ${opts.timeout}: expected a duration like 60s or 2m.`);
|
|
42854
|
+
process.exitCode = 1;
|
|
42855
|
+
return;
|
|
42856
|
+
}
|
|
42857
|
+
const { jobId, firstAnswerEstimateS } = await enqueueQuery(apiKey, enqueue, { apiBaseUrl });
|
|
42858
|
+
const timeoutS = explicitTimeoutS ?? (firstAnswerEstimateS === null ? DEFAULT_PROMQL_TIMEOUT_S : Math.min(Math.max(Math.ceil(firstAnswerEstimateS * 1.5) + 30, 60), 1800));
|
|
42859
|
+
console.error(firstAnswerEstimateS === null ? `Queued as job ${jobId}; waiting up to ${formatSeconds(timeoutS)} for the instance's next poll.` : `Queued as job ${jobId}; this instance polls roughly every ${formatSeconds(firstAnswerEstimateS)} ` + `(jittered, so sometimes longer). Waiting up to ${formatSeconds(timeoutS)}.`);
|
|
42860
|
+
const res = await awaitQueryResult(apiKey, jobId, {
|
|
42861
|
+
apiBaseUrl,
|
|
42862
|
+
timeoutMs: timeoutS * 1000
|
|
42863
|
+
});
|
|
42864
|
+
if (!isTerminal2(res.status)) {
|
|
42865
|
+
console.error(`Timed out after ${timeoutS}s. Job ${jobId} is still ${res.status}.`);
|
|
42866
|
+
console.error(res.status === "queued" ? "Nothing has claimed it: check the instance-jobs container is running on that instance " + "(`postgresai mon health`; it is behind a compose profile, enabled with " + "`mon local-install --instance-jobs`), or wait \u2014 a first collection can queue " + "~20 periods ahead of it." : "The instance is working on it \u2014 wait, or re-run with a longer --timeout.");
|
|
42867
|
+
process.exitCode = 1;
|
|
42868
|
+
return;
|
|
42869
|
+
}
|
|
42870
|
+
if (res.status !== "done" || res.outcome !== "ok") {
|
|
42871
|
+
console.error(`Query ${res.status}${res.failure_class ? ` (${res.failure_class})` : ""}.`);
|
|
42872
|
+
if (res.error)
|
|
42873
|
+
console.error(res.error);
|
|
42874
|
+
process.exitCode = 1;
|
|
42875
|
+
return;
|
|
42876
|
+
}
|
|
42877
|
+
if (!res.result) {
|
|
42878
|
+
console.error("The job finished but carried no result.");
|
|
42879
|
+
process.exitCode = 1;
|
|
42880
|
+
return;
|
|
42881
|
+
}
|
|
42882
|
+
if (opts.json) {
|
|
42883
|
+
console.log(JSON.stringify(res.result, null, 2));
|
|
42884
|
+
return;
|
|
42885
|
+
}
|
|
42886
|
+
console.log(renderPromQL(res.result));
|
|
42887
|
+
} catch (err) {
|
|
42888
|
+
console.error(err instanceof Error ? err.message : String(err));
|
|
42889
|
+
process.exitCode = 1;
|
|
42890
|
+
}
|
|
42891
|
+
});
|
|
42239
42892
|
var issues = program2.command("issues").description("issues management");
|
|
42240
|
-
withOrgOptions(issues.command("list")).description("list issues").option("--status <status>", "filter by status: open, closed, or all
|
|
42893
|
+
withOrgOptions(issues.command("list")).description("list issues").option("--status <status>", "filter by status: open (default), closed, or all").option("--limit <n>", "max number of issues to return (default: 20)", parseInt).option("--offset <n>", "number of issues to skip (default: 0)", parseInt).option("--hidden-only", "list only hidden issues (PostgresAI staff)").option("--debug", "enable debug output").option("--json", "output raw JSON").action(async (opts) => {
|
|
42241
42894
|
const spinner = createTtySpinner(process.stdout.isTTY ?? false, "Fetching issues...");
|
|
42242
42895
|
try {
|
|
42243
42896
|
const rootOpts = program2.opts();
|
|
@@ -42252,11 +42905,15 @@ withOrgOptions(issues.command("list")).description("list issues").option("--stat
|
|
|
42252
42905
|
const orgScope = getActiveOrgScope2() ?? { source: "token" };
|
|
42253
42906
|
const orgId = configOrgIdForBody(apiKey, cfg.orgId);
|
|
42254
42907
|
const { apiBaseUrl } = resolveBaseUrls2(rootOpts, cfg);
|
|
42908
|
+
const statusArg = (opts.status ?? "open").trim().toLowerCase();
|
|
42255
42909
|
let statusFilter;
|
|
42256
|
-
if (
|
|
42257
|
-
statusFilter =
|
|
42258
|
-
} else if (
|
|
42259
|
-
|
|
42910
|
+
if (statusArg === "open" || statusArg === "closed") {
|
|
42911
|
+
statusFilter = statusArg;
|
|
42912
|
+
} else if (statusArg !== "all") {
|
|
42913
|
+
spinner.stop();
|
|
42914
|
+
console.error("--status must be open, closed, or all");
|
|
42915
|
+
process.exitCode = 1;
|
|
42916
|
+
return;
|
|
42260
42917
|
}
|
|
42261
42918
|
const result = await fetchIssues2({
|
|
42262
42919
|
apiKey,
|
|
@@ -42270,7 +42927,7 @@ withOrgOptions(issues.command("list")).description("list issues").option("--stat
|
|
|
42270
42927
|
debug: !!opts.debug
|
|
42271
42928
|
});
|
|
42272
42929
|
spinner.stop();
|
|
42273
|
-
const trimmed = Array.isArray(result) ? result.map((r) =>
|
|
42930
|
+
const trimmed = Array.isArray(result) ? result.map((r) => presentIssue2({
|
|
42274
42931
|
id: r.id,
|
|
42275
42932
|
title: r.title,
|
|
42276
42933
|
status: r.status,
|
|
@@ -42308,7 +42965,7 @@ withOrgOptions(issues.command("view <issueId>")).description("view issue details
|
|
|
42308
42965
|
spinner.update("Fetching comments...");
|
|
42309
42966
|
const comments = await fetchIssueComments2({ apiKey, apiBaseUrl, issueId, debug: !!opts.debug });
|
|
42310
42967
|
spinner.stop();
|
|
42311
|
-
const combined = { issue:
|
|
42968
|
+
const combined = { issue: presentIssue2(issue), comments };
|
|
42312
42969
|
printResult(combined, opts.json);
|
|
42313
42970
|
} catch (err) {
|
|
42314
42971
|
spinner.stop();
|
|
@@ -43512,14 +44169,29 @@ if (__require.main == __require.module) {
|
|
|
43512
44169
|
export {
|
|
43513
44170
|
LOCAL_INSTALL_MANAGED_ENV_KEYS,
|
|
43514
44171
|
ORG_SCOPED_COMMANDS,
|
|
44172
|
+
adoptAndPersist,
|
|
44173
|
+
applyApiKey,
|
|
44174
|
+
applyProjectName,
|
|
43515
44175
|
buildLocalInstallEnv,
|
|
44176
|
+
composeProfilesValue,
|
|
44177
|
+
instanceIdToPersist,
|
|
44178
|
+
instanceJobsProfileEnabled,
|
|
44179
|
+
instanceJobsRemovalOutcome,
|
|
44180
|
+
instanceJobsStartSkipReason,
|
|
43516
44181
|
isValidComposeYaml,
|
|
44182
|
+
judgeServiceHealth,
|
|
44183
|
+
monitoringHealthServices,
|
|
44184
|
+
persistInstanceId,
|
|
43517
44185
|
planMonitoringRegistration,
|
|
43518
44186
|
program2 as program,
|
|
44187
|
+
readComposeProfiles,
|
|
43519
44188
|
readDeployedTag,
|
|
43520
44189
|
refreshBundledComposeIfStale,
|
|
43521
44190
|
registerMonitoringInstance,
|
|
43522
44191
|
resolveAdoptedProject,
|
|
44192
|
+
resolveInstanceJobsRequest,
|
|
44193
|
+
selfRegisterAndPersist,
|
|
43523
44194
|
shouldScaleOutNodeExporter,
|
|
44195
|
+
statOwner,
|
|
43524
44196
|
updatePgwatchConfig
|
|
43525
44197
|
};
|