postgresai 0.17.0-rc.2 → 0.17.0-rc.3
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 +489 -69
- 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.3",
|
|
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.3";
|
|
16888
16894
|
var package_default2 = {
|
|
16889
16895
|
name: "postgresai",
|
|
16890
16896
|
version,
|
|
@@ -17315,6 +17321,10 @@ function resolveMcpOrgScope(args, apiKey, cfg) {
|
|
|
17315
17321
|
}
|
|
17316
17322
|
|
|
17317
17323
|
// lib/issues.ts
|
|
17324
|
+
var IssueStatus = {
|
|
17325
|
+
OPEN: 0,
|
|
17326
|
+
CLOSED: 1
|
|
17327
|
+
};
|
|
17318
17328
|
function issueRequestHeaders(apiKey, orgScope) {
|
|
17319
17329
|
return buildAuthHeaders(apiKey, orgScope, {
|
|
17320
17330
|
Prefer: "return=representation",
|
|
@@ -17331,6 +17341,17 @@ function withVisibleHiddenFlag(issue) {
|
|
|
17331
17341
|
const { is_hidden: _dropped, ...rest } = issue;
|
|
17332
17342
|
return rest;
|
|
17333
17343
|
}
|
|
17344
|
+
function presentIssue(issue) {
|
|
17345
|
+
const visible = withVisibleHiddenFlag(issue);
|
|
17346
|
+
return { ...visible, status: issueStatusLabel(issue.status) };
|
|
17347
|
+
}
|
|
17348
|
+
function issueStatusLabel(status) {
|
|
17349
|
+
if (status === IssueStatus.OPEN)
|
|
17350
|
+
return "open";
|
|
17351
|
+
if (status === IssueStatus.CLOSED)
|
|
17352
|
+
return "closed";
|
|
17353
|
+
return status;
|
|
17354
|
+
}
|
|
17334
17355
|
async function fetchIssues(params) {
|
|
17335
17356
|
const { apiKey, apiBaseUrl, orgScope, orgId, status, limit = 20, offset = 0, hiddenOnly, debug } = params;
|
|
17336
17357
|
if (!apiKey) {
|
|
@@ -24994,7 +25015,7 @@ async function handleToolCall(req, rootOpts, extra) {
|
|
|
24994
25015
|
const offset = args.offset !== undefined ? Number(args.offset) : undefined;
|
|
24995
25016
|
const hiddenOnly = Boolean(args.hidden_only);
|
|
24996
25017
|
const issues = await fetchIssues({ apiKey, apiBaseUrl, orgId, orgScope: scope.orgScope, status, limit, offset, hiddenOnly, debug });
|
|
24997
|
-
const trimmed = issues.map((issue) =>
|
|
25018
|
+
const trimmed = issues.map((issue) => presentIssue(issue));
|
|
24998
25019
|
return { content: [{ type: "text", text: JSON.stringify(trimmed, null, 2) }] };
|
|
24999
25020
|
}
|
|
25000
25021
|
if (toolName === "view_issue") {
|
|
@@ -25010,7 +25031,7 @@ async function handleToolCall(req, rootOpts, extra) {
|
|
|
25010
25031
|
return { content: [{ type: "text", text: "Issue not found" }], isError: true };
|
|
25011
25032
|
}
|
|
25012
25033
|
const comments = await fetchIssueComments({ apiKey, apiBaseUrl, issueId, orgScope: scope.orgScope, debug });
|
|
25013
|
-
const combined = { issue:
|
|
25034
|
+
const combined = { issue: presentIssue(issue), comments };
|
|
25014
25035
|
return { content: [{ type: "text", text: JSON.stringify(combined, null, 2) }] };
|
|
25015
25036
|
}
|
|
25016
25037
|
if (toolName === "post_issue_comment") {
|
|
@@ -25314,7 +25335,7 @@ async function startMcpServer(rootOpts, extra) {
|
|
|
25314
25335
|
type: "object",
|
|
25315
25336
|
properties: {
|
|
25316
25337
|
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" },
|
|
25338
|
+
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
25339
|
limit: { type: "number", description: "Max number of issues to return (default: 20)" },
|
|
25319
25340
|
offset: { type: "number", description: "Number of issues to skip (default: 0)" },
|
|
25320
25341
|
hidden_only: { type: "boolean", description: "List only hidden issues (PostgresAI staff; same as CLI --hidden-only)" },
|
|
@@ -25596,6 +25617,10 @@ async function startMcpServer(rootOpts, extra) {
|
|
|
25596
25617
|
}
|
|
25597
25618
|
|
|
25598
25619
|
// lib/issues.ts
|
|
25620
|
+
var IssueStatus2 = {
|
|
25621
|
+
OPEN: 0,
|
|
25622
|
+
CLOSED: 1
|
|
25623
|
+
};
|
|
25599
25624
|
function issueRequestHeaders2(apiKey, orgScope) {
|
|
25600
25625
|
return buildAuthHeaders(apiKey, orgScope, {
|
|
25601
25626
|
Prefer: "return=representation",
|
|
@@ -25612,6 +25637,17 @@ function withVisibleHiddenFlag2(issue) {
|
|
|
25612
25637
|
const { is_hidden: _dropped, ...rest } = issue;
|
|
25613
25638
|
return rest;
|
|
25614
25639
|
}
|
|
25640
|
+
function presentIssue2(issue) {
|
|
25641
|
+
const visible = withVisibleHiddenFlag2(issue);
|
|
25642
|
+
return { ...visible, status: issueStatusLabel2(issue.status) };
|
|
25643
|
+
}
|
|
25644
|
+
function issueStatusLabel2(status) {
|
|
25645
|
+
if (status === IssueStatus2.OPEN)
|
|
25646
|
+
return "open";
|
|
25647
|
+
if (status === IssueStatus2.CLOSED)
|
|
25648
|
+
return "closed";
|
|
25649
|
+
return status;
|
|
25650
|
+
}
|
|
25615
25651
|
async function fetchIssues2(params) {
|
|
25616
25652
|
const { apiKey, apiBaseUrl, orgScope, orgId, status, limit = 20, offset = 0, hiddenOnly, debug } = params;
|
|
25617
25653
|
if (!apiKey) {
|
|
@@ -27138,6 +27174,134 @@ async function destroySnapshot(params) {
|
|
|
27138
27174
|
// bin/postgres-ai.ts
|
|
27139
27175
|
init_util();
|
|
27140
27176
|
|
|
27177
|
+
// lib/promql.ts
|
|
27178
|
+
function describeRpcError(status, body) {
|
|
27179
|
+
try {
|
|
27180
|
+
const parsed = JSON.parse(body);
|
|
27181
|
+
const parts = [parsed.message, parsed.details, parsed.hint].filter(Boolean);
|
|
27182
|
+
if (parts.length)
|
|
27183
|
+
return parts.join(" — ");
|
|
27184
|
+
} catch {}
|
|
27185
|
+
return body.trim() || `HTTP ${status}`;
|
|
27186
|
+
}
|
|
27187
|
+
async function callRpc3(name, apiKey, body, opts) {
|
|
27188
|
+
const { apiBaseUrl } = resolveBaseUrls(opts);
|
|
27189
|
+
const res = await fetch(`${apiBaseUrl}/rpc/${name}`, {
|
|
27190
|
+
method: "POST",
|
|
27191
|
+
headers: {
|
|
27192
|
+
"Content-Type": "application/json",
|
|
27193
|
+
"access-token": apiKey,
|
|
27194
|
+
...orgScopeHeaders(getActiveOrgScope())
|
|
27195
|
+
},
|
|
27196
|
+
body: JSON.stringify(body),
|
|
27197
|
+
signal: AbortSignal.timeout(opts?.timeoutMs ?? 30000)
|
|
27198
|
+
});
|
|
27199
|
+
const text = await res.text().catch(() => "");
|
|
27200
|
+
if (!res.ok) {
|
|
27201
|
+
throw new Error(describeRpcError(res.status, text));
|
|
27202
|
+
}
|
|
27203
|
+
try {
|
|
27204
|
+
return text ? JSON.parse(text) : null;
|
|
27205
|
+
} catch {
|
|
27206
|
+
throw new Error(`${name} returned a body that is not JSON`);
|
|
27207
|
+
}
|
|
27208
|
+
}
|
|
27209
|
+
async function enqueueQuery(apiKey, args, opts) {
|
|
27210
|
+
const body = {
|
|
27211
|
+
p_instance_id: args.instanceId,
|
|
27212
|
+
p_kind: args.kind,
|
|
27213
|
+
p_query: args.query
|
|
27214
|
+
};
|
|
27215
|
+
if (args.kind === "promql_range") {
|
|
27216
|
+
body.p_start = args.start;
|
|
27217
|
+
body.p_end = args.end;
|
|
27218
|
+
body.p_step_s = args.stepS;
|
|
27219
|
+
} else if (args.at) {
|
|
27220
|
+
body.p_at = args.at;
|
|
27221
|
+
}
|
|
27222
|
+
const reply = await callRpc3("instance_query_enqueue", apiKey, body, opts);
|
|
27223
|
+
const jobId = reply?.job_id;
|
|
27224
|
+
if (!jobId) {
|
|
27225
|
+
throw new Error("instance_query_enqueue returned no job_id");
|
|
27226
|
+
}
|
|
27227
|
+
return { jobId, firstAnswerEstimateS: positiveSeconds(reply?.first_answer_estimate_s) };
|
|
27228
|
+
}
|
|
27229
|
+
function positiveSeconds(v) {
|
|
27230
|
+
return typeof v === "number" && Number.isFinite(v) && v > 0 ? v : null;
|
|
27231
|
+
}
|
|
27232
|
+
async function readQueryResult(apiKey, jobId, opts) {
|
|
27233
|
+
const reply = await callRpc3("instance_query_result", apiKey, { p_job_id: jobId }, opts);
|
|
27234
|
+
if (!reply || typeof reply.status !== "string") {
|
|
27235
|
+
throw new Error("instance_query_result returned an unexpected reply");
|
|
27236
|
+
}
|
|
27237
|
+
return reply;
|
|
27238
|
+
}
|
|
27239
|
+
var TERMINAL = new Set(["done", "failed", "expired"]);
|
|
27240
|
+
function isTerminal2(status) {
|
|
27241
|
+
return TERMINAL.has(status);
|
|
27242
|
+
}
|
|
27243
|
+
function pollDelay(attempt) {
|
|
27244
|
+
return Math.min(1000 * 2 ** attempt, 15000);
|
|
27245
|
+
}
|
|
27246
|
+
async function awaitQueryResult(apiKey, jobId, opts) {
|
|
27247
|
+
const now = opts.now ?? (() => Date.now());
|
|
27248
|
+
const sleep = opts.sleep ?? ((ms) => new Promise((r) => setTimeout(r, ms)));
|
|
27249
|
+
const deadline = now() + opts.timeoutMs;
|
|
27250
|
+
for (let attempt = 0;; attempt++) {
|
|
27251
|
+
const res = await readQueryResult(apiKey, jobId, opts);
|
|
27252
|
+
if (isTerminal2(res.status))
|
|
27253
|
+
return res;
|
|
27254
|
+
const delay = pollDelay(attempt);
|
|
27255
|
+
if (now() + delay >= deadline)
|
|
27256
|
+
return res;
|
|
27257
|
+
await sleep(delay);
|
|
27258
|
+
}
|
|
27259
|
+
}
|
|
27260
|
+
function formatMetric(metric) {
|
|
27261
|
+
const name = metric.__name__ ?? "";
|
|
27262
|
+
const labels = Object.entries(metric).filter(([k]) => k !== "__name__").sort(([a], [b]) => a.localeCompare(b)).map(([k, v]) => `${k}="${v}"`);
|
|
27263
|
+
if (!labels.length)
|
|
27264
|
+
return name || "{}";
|
|
27265
|
+
return `${name}{${labels.join(", ")}}`;
|
|
27266
|
+
}
|
|
27267
|
+
function pad(s, width) {
|
|
27268
|
+
return s.length >= width ? s : s + " ".repeat(width - s.length);
|
|
27269
|
+
}
|
|
27270
|
+
function renderPromQL(payload) {
|
|
27271
|
+
const lines = [];
|
|
27272
|
+
if (!payload.result.length) {
|
|
27273
|
+
lines.push("Empty result set.");
|
|
27274
|
+
} else if (payload.resultType === "vector") {
|
|
27275
|
+
const rows = payload.result.map((s) => [formatMetric(s.metric), s.value ? s.value[1] : ""]);
|
|
27276
|
+
const w = Math.max(6, ...rows.map((r) => r[0].length));
|
|
27277
|
+
lines.push(`${pad("SERIES", w)} VALUE`);
|
|
27278
|
+
for (const [m, v] of rows)
|
|
27279
|
+
lines.push(`${pad(m, w)} ${v}`);
|
|
27280
|
+
} else {
|
|
27281
|
+
const rows = payload.result.map((s) => {
|
|
27282
|
+
const vs = s.values ?? [];
|
|
27283
|
+
const first = vs.length ? vs[0][1] : "";
|
|
27284
|
+
const last = vs.length ? vs[vs.length - 1][1] : "";
|
|
27285
|
+
return [formatMetric(s.metric), String(vs.length), first, last];
|
|
27286
|
+
});
|
|
27287
|
+
const w = Math.max(6, ...rows.map((r) => r[0].length));
|
|
27288
|
+
lines.push(`${pad("SERIES", w)} POINTS FIRST LAST`);
|
|
27289
|
+
for (const [m, n, f, l] of rows)
|
|
27290
|
+
lines.push(`${pad(m, w)} ${pad(n, 6)} ${f} ${l}`);
|
|
27291
|
+
if (rows.length)
|
|
27292
|
+
lines.push("(first and last sample per series; use --json for every point)");
|
|
27293
|
+
}
|
|
27294
|
+
const series = payload.result.length;
|
|
27295
|
+
const points = payload.result.reduce((n, s) => n + (s.values?.length ?? 0) + (s.value ? 1 : 0), 0);
|
|
27296
|
+
lines.push(`
|
|
27297
|
+
${series} series, ${points} points`);
|
|
27298
|
+
if (payload.stats.truncated) {
|
|
27299
|
+
lines.push("⚠ TRUNCATED: too large to return whole. Raise --step, narrow the time range, " + "or select fewer series.");
|
|
27300
|
+
}
|
|
27301
|
+
return lines.join(`
|
|
27302
|
+
`);
|
|
27303
|
+
}
|
|
27304
|
+
|
|
27141
27305
|
// lib/instances.ts
|
|
27142
27306
|
import * as fs4 from "fs";
|
|
27143
27307
|
|
|
@@ -38784,9 +38948,29 @@ var LOCAL_INSTALL_MANAGED_ENV_KEYS = [
|
|
|
38784
38948
|
"VM_DELETE_AUTH_KEY",
|
|
38785
38949
|
"VM_SNAPSHOT_AUTH_KEY",
|
|
38786
38950
|
"VM_FORCE_MERGE_AUTH_KEY",
|
|
38787
|
-
"VM_PPROF_AUTH_KEY"
|
|
38951
|
+
"VM_PPROF_AUTH_KEY",
|
|
38952
|
+
"PGAI_INSTANCE_ID",
|
|
38953
|
+
"INSTANCE_JOBS_USER"
|
|
38788
38954
|
];
|
|
38789
|
-
function
|
|
38955
|
+
function statOwner(file) {
|
|
38956
|
+
if (!file)
|
|
38957
|
+
return;
|
|
38958
|
+
let st;
|
|
38959
|
+
try {
|
|
38960
|
+
st = fs9.statSync(file);
|
|
38961
|
+
} catch {
|
|
38962
|
+
return;
|
|
38963
|
+
}
|
|
38964
|
+
if (!st.isFile())
|
|
38965
|
+
return null;
|
|
38966
|
+
return `${st.uid}:${st.gid}`;
|
|
38967
|
+
}
|
|
38968
|
+
function processOwner() {
|
|
38969
|
+
if (typeof process.getuid !== "function" || typeof process.getgid !== "function")
|
|
38970
|
+
return null;
|
|
38971
|
+
return `${process.getuid()}:${process.getgid()}`;
|
|
38972
|
+
}
|
|
38973
|
+
function buildLocalInstallEnv(existingEnv, imageTag, instanceId, credentialFile) {
|
|
38790
38974
|
const readRaw = (key) => {
|
|
38791
38975
|
const re = new RegExp(`^[ ]*(?:export[ ]+)?${key}=(.*)$`, "gm");
|
|
38792
38976
|
let value = null;
|
|
@@ -38810,6 +38994,14 @@ function buildLocalInstallEnv(existingEnv, imageTag) {
|
|
|
38810
38994
|
envLines.push(`VM_AUTH_USERNAME=${vmAuthUsername ? stripMatchingQuotes(vmAuthUsername) : "vmauth"}`);
|
|
38811
38995
|
const vmAuthPassword = readRaw("VM_AUTH_PASSWORD");
|
|
38812
38996
|
envLines.push(`VM_AUTH_PASSWORD=${vmAuthPassword ? stripMatchingQuotes(vmAuthPassword) : crypto2.randomBytes(18).toString("base64")}`);
|
|
38997
|
+
const instanceIdValue = instanceId?.trim() || readRaw("PGAI_INSTANCE_ID");
|
|
38998
|
+
if (instanceIdValue) {
|
|
38999
|
+
envLines.push(`PGAI_INSTANCE_ID=${stripMatchingQuotes(instanceIdValue)}`);
|
|
39000
|
+
}
|
|
39001
|
+
const owner = statOwner(credentialFile);
|
|
39002
|
+
const credentialOwner = owner === undefined ? processOwner() : owner;
|
|
39003
|
+
if (credentialOwner)
|
|
39004
|
+
envLines.push(`INSTANCE_JOBS_USER=${credentialOwner}`);
|
|
38813
39005
|
for (const key of VM_ADMIN_AUTH_KEYS) {
|
|
38814
39006
|
const existing = readRaw(key);
|
|
38815
39007
|
envLines.push(`${key}=${existing ? stripMatchingQuotes(existing) : crypto2.randomBytes(32).toString("hex")}`);
|
|
@@ -40598,16 +40790,6 @@ function checkRunningContainers() {
|
|
|
40598
40790
|
return { running: false, containers: [] };
|
|
40599
40791
|
}
|
|
40600
40792
|
}
|
|
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
40793
|
async function registerMonitoringInstance(apiKey, projectName, opts) {
|
|
40612
40794
|
const { apiBaseUrl } = resolveBaseUrls2(opts);
|
|
40613
40795
|
const url = `${apiBaseUrl}/rpc/monitoring_instance_register`;
|
|
@@ -40642,7 +40824,8 @@ Debug: Registering monitoring instance...`);
|
|
|
40642
40824
|
"Content-Type": "application/json",
|
|
40643
40825
|
...orgScopeHeaders2(opts?.orgScope)
|
|
40644
40826
|
},
|
|
40645
|
-
body: JSON.stringify(requestBody)
|
|
40827
|
+
body: JSON.stringify(requestBody),
|
|
40828
|
+
signal: requestTimeoutSignal2(opts?.timeoutMs).signal
|
|
40646
40829
|
});
|
|
40647
40830
|
const body = await res.text().catch(() => "");
|
|
40648
40831
|
if (!res.ok) {
|
|
@@ -40677,6 +40860,25 @@ Debug: Registering monitoring instance...`);
|
|
|
40677
40860
|
}
|
|
40678
40861
|
return null;
|
|
40679
40862
|
}
|
|
40863
|
+
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}$/;
|
|
40864
|
+
function instanceIdToPersist(reg, requestedId) {
|
|
40865
|
+
for (const candidate of [reg?.instanceId, requestedId]) {
|
|
40866
|
+
const value = candidate?.trim();
|
|
40867
|
+
if (value && INSTANCE_ID_RE.test(value))
|
|
40868
|
+
return value;
|
|
40869
|
+
}
|
|
40870
|
+
return null;
|
|
40871
|
+
}
|
|
40872
|
+
function planMonitoringRegistration(args) {
|
|
40873
|
+
const projectName = args.project?.trim() || undefined;
|
|
40874
|
+
if (args.instanceId) {
|
|
40875
|
+
return { kind: "adopt", projectName };
|
|
40876
|
+
}
|
|
40877
|
+
if (!projectName) {
|
|
40878
|
+
return { kind: "error-missing-project", projectName: undefined };
|
|
40879
|
+
}
|
|
40880
|
+
return { kind: "self-register", projectName };
|
|
40881
|
+
}
|
|
40680
40882
|
var PROJECT_NAME_RE = /^[A-Za-z0-9._-]+$/;
|
|
40681
40883
|
function resolveAdoptedProject(reg) {
|
|
40682
40884
|
if (!reg)
|
|
@@ -40693,8 +40895,11 @@ function updatePgwatchConfig(configPath, updates) {
|
|
|
40693
40895
|
let lines = [];
|
|
40694
40896
|
if (fs9.existsSync(configPath)) {
|
|
40695
40897
|
const stats = fs9.statSync(configPath);
|
|
40696
|
-
if (!stats.
|
|
40697
|
-
|
|
40898
|
+
if (!stats.isFile()) {
|
|
40899
|
+
throw new Error(`${configPath} exists but is not a regular file; refusing to write through it`);
|
|
40900
|
+
}
|
|
40901
|
+
{
|
|
40902
|
+
const content = fs9.readFileSync(configPath, "utf8").replace(/^\uFEFF+/, "");
|
|
40698
40903
|
lines = content.split(/\r?\n/).filter((l) => l.trim() !== "");
|
|
40699
40904
|
}
|
|
40700
40905
|
}
|
|
@@ -40702,6 +40907,7 @@ function updatePgwatchConfig(configPath, updates) {
|
|
|
40702
40907
|
const existingIndex = lines.findIndex((l) => l.startsWith(key + "="));
|
|
40703
40908
|
if (existingIndex >= 0) {
|
|
40704
40909
|
lines[existingIndex] = `${key}=${value}`;
|
|
40910
|
+
lines = lines.filter((l, i) => i === existingIndex || !l.startsWith(key + "="));
|
|
40705
40911
|
} else {
|
|
40706
40912
|
lines.push(`${key}=${value}`);
|
|
40707
40913
|
}
|
|
@@ -40716,6 +40922,129 @@ function updatePgwatchConfig(configPath, updates) {
|
|
|
40716
40922
|
console.error(`Warning: could not restrict permissions on ${configPath}: ${message}`);
|
|
40717
40923
|
}
|
|
40718
40924
|
}
|
|
40925
|
+
function applyApiKey(projectDir, apiKey) {
|
|
40926
|
+
const configPath = path7.resolve(projectDir, ".pgwatch-config");
|
|
40927
|
+
try {
|
|
40928
|
+
updatePgwatchConfig(configPath, { api_key: apiKey });
|
|
40929
|
+
} catch (err) {
|
|
40930
|
+
console.error(`\u2717 ${err instanceof Error ? err.message : String(err)}`);
|
|
40931
|
+
return false;
|
|
40932
|
+
}
|
|
40933
|
+
console.log(`\u2713 API key saved
|
|
40934
|
+
`);
|
|
40935
|
+
return true;
|
|
40936
|
+
}
|
|
40937
|
+
var DEFAULT_PROMQL_TIMEOUT_S = 11 * 60;
|
|
40938
|
+
function formatSeconds(total) {
|
|
40939
|
+
const s = Math.max(0, Math.round(total));
|
|
40940
|
+
if (s < 90)
|
|
40941
|
+
return `${s}s`;
|
|
40942
|
+
const m = Math.round(s / 60);
|
|
40943
|
+
if (m < 60)
|
|
40944
|
+
return `${m}m`;
|
|
40945
|
+
const h = Math.floor(m / 60);
|
|
40946
|
+
const rem = m % 60;
|
|
40947
|
+
return rem ? `${h}h${rem}m` : `${h}h`;
|
|
40948
|
+
}
|
|
40949
|
+
function parseDurationSeconds(v) {
|
|
40950
|
+
const m = /^(\d+)(s|m|h)?$/.exec(v.trim());
|
|
40951
|
+
if (!m)
|
|
40952
|
+
return null;
|
|
40953
|
+
const n = parseInt(m[1], 10);
|
|
40954
|
+
if (!Number.isFinite(n))
|
|
40955
|
+
return null;
|
|
40956
|
+
return n * (m[2] === "h" ? 3600 : m[2] === "m" ? 60 : 1);
|
|
40957
|
+
}
|
|
40958
|
+
function readInstanceIdFromProject() {
|
|
40959
|
+
try {
|
|
40960
|
+
const { projectDir } = resolvePaths();
|
|
40961
|
+
const configPath = path7.resolve(projectDir, ".pgwatch-config");
|
|
40962
|
+
if (!fs9.existsSync(configPath))
|
|
40963
|
+
return null;
|
|
40964
|
+
const line = fs9.readFileSync(configPath, "utf8").replace(/^\uFEFF+/, "").split(/\r?\n/).find((l) => l.startsWith("instance_id="));
|
|
40965
|
+
const value = line?.slice("instance_id=".length).trim();
|
|
40966
|
+
return value && INSTANCE_ID_RE.test(value) ? value : null;
|
|
40967
|
+
} catch {
|
|
40968
|
+
return null;
|
|
40969
|
+
}
|
|
40970
|
+
}
|
|
40971
|
+
function applyProjectName(projectDir, project) {
|
|
40972
|
+
const configPath = path7.resolve(projectDir, ".pgwatch-config");
|
|
40973
|
+
try {
|
|
40974
|
+
updatePgwatchConfig(configPath, { project_name: project });
|
|
40975
|
+
} catch (err) {
|
|
40976
|
+
console.error(`\u2717 ${err instanceof Error ? err.message : String(err)}`);
|
|
40977
|
+
return false;
|
|
40978
|
+
}
|
|
40979
|
+
console.log(`Using project name: ${project}
|
|
40980
|
+
`);
|
|
40981
|
+
return true;
|
|
40982
|
+
}
|
|
40983
|
+
function persistInstanceId(projectDir, reg, requestedId, alsoWrite = {}) {
|
|
40984
|
+
const instanceId = instanceIdToPersist(reg, requestedId);
|
|
40985
|
+
const updates = { ...alsoWrite, ...instanceId ? { instance_id: instanceId } : {} };
|
|
40986
|
+
if (Object.keys(updates).length === 0)
|
|
40987
|
+
return false;
|
|
40988
|
+
const configPath = path7.resolve(projectDir, ".pgwatch-config");
|
|
40989
|
+
try {
|
|
40990
|
+
updatePgwatchConfig(configPath, updates);
|
|
40991
|
+
return instanceId != null;
|
|
40992
|
+
} catch (err) {
|
|
40993
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
40994
|
+
console.error(`Warning: could not record the monitoring instance id in ${configPath}: ${message}`);
|
|
40995
|
+
return false;
|
|
40996
|
+
}
|
|
40997
|
+
}
|
|
40998
|
+
async function adoptAndPersist(projectDir, apiKey, projectName, instanceId, opts, register = registerMonitoringInstance) {
|
|
40999
|
+
const reg = await register(apiKey, projectName, {
|
|
41000
|
+
orgScope: opts.orgScope,
|
|
41001
|
+
apiBaseUrl: opts.apiBaseUrl,
|
|
41002
|
+
debug: opts.debug,
|
|
41003
|
+
instanceId
|
|
41004
|
+
});
|
|
41005
|
+
const adoptedProject = resolveAdoptedProject(reg);
|
|
41006
|
+
const persisted = persistInstanceId(projectDir, reg, instanceId, {
|
|
41007
|
+
...adoptedProject != null ? { project_name: adoptedProject } : {}
|
|
41008
|
+
});
|
|
41009
|
+
if (!persisted) {
|
|
41010
|
+
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
|
|
41011
|
+
`);
|
|
41012
|
+
}
|
|
41013
|
+
if (adoptedProject != null) {
|
|
41014
|
+
const verb = reg?.created ? "Registered" : "Adopted";
|
|
41015
|
+
console.log(`\u2713 ${verb} monitoring instance (project: ${adoptedProject})
|
|
41016
|
+
`);
|
|
41017
|
+
} else if (reg) {
|
|
41018
|
+
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>`));
|
|
41019
|
+
} else {
|
|
41020
|
+
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>`));
|
|
41021
|
+
}
|
|
41022
|
+
return reg;
|
|
41023
|
+
}
|
|
41024
|
+
async function selfRegisterAndPersist(projectDir, apiKey, projectName, opts, register = registerMonitoringInstance) {
|
|
41025
|
+
const reg = await register(apiKey, projectName, {
|
|
41026
|
+
apiBaseUrl: opts.apiBaseUrl,
|
|
41027
|
+
debug: opts.debug,
|
|
41028
|
+
orgScope: opts.orgScope
|
|
41029
|
+
});
|
|
41030
|
+
const persisted = persistInstanceId(projectDir, reg, undefined);
|
|
41031
|
+
if (persisted) {
|
|
41032
|
+
console.log(`\u2713 Registered monitoring instance (project: ${projectName})
|
|
41033
|
+
`);
|
|
41034
|
+
return reg;
|
|
41035
|
+
}
|
|
41036
|
+
const minted = reg?.instanceId?.trim();
|
|
41037
|
+
if (minted) {
|
|
41038
|
+
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)
|
|
41039
|
+
`);
|
|
41040
|
+
} else {
|
|
41041
|
+
const reRun = projectName ? `'postgresai mon local-install --project ${projectName}'` : "'postgresai mon local-install --project <name>'";
|
|
41042
|
+
console.error(`\u26A0 Could not register this monitoring instance \u2014 instance jobs will idle until ` + `${reRun} is re-run
|
|
41043
|
+
`);
|
|
41044
|
+
}
|
|
41045
|
+
process.exitCode = 1;
|
|
41046
|
+
return reg;
|
|
41047
|
+
}
|
|
40719
41048
|
async function applyMonitoringTargetsConfig() {
|
|
40720
41049
|
console.log("Applying monitoring target configuration...");
|
|
40721
41050
|
const generateCode = await runCompose(["run", "--rm", "sources-generator"]);
|
|
@@ -40872,18 +41201,16 @@ mon.command("local-install").description("install local monitoring stack (genera
|
|
|
40872
41201
|
const { projectDir, instancesFile: instancesPath } = await resolveOrInitPaths();
|
|
40873
41202
|
console.log(`Project directory: ${projectDir}
|
|
40874
41203
|
`);
|
|
40875
|
-
if (opts.project) {
|
|
40876
|
-
|
|
40877
|
-
|
|
40878
|
-
console.log(`Using project name: ${opts.project}
|
|
40879
|
-
`);
|
|
41204
|
+
if (opts.project && !applyProjectName(projectDir, opts.project)) {
|
|
41205
|
+
process.exitCode = 1;
|
|
41206
|
+
return;
|
|
40880
41207
|
}
|
|
40881
41208
|
const envFile = path7.resolve(projectDir, ".env");
|
|
40882
41209
|
const existingEnv = fs9.existsSync(envFile) ? fs9.readFileSync(envFile, "utf8") : "";
|
|
40883
41210
|
const previousTagMatch = existingEnv.match(/^PGAI_TAG=(.+)$/m);
|
|
40884
41211
|
const previousTag = previousTagMatch ? previousTagMatch[1].trim() : null;
|
|
40885
41212
|
const imageTag = opts.tag || package_default.version;
|
|
40886
|
-
const { content: envContent, preservedKeys } = buildLocalInstallEnv(existingEnv, imageTag);
|
|
41213
|
+
const { content: envContent, preservedKeys } = buildLocalInstallEnv(existingEnv, imageTag, opts.instanceId || process.env.PGAI_INSTANCE_ID || null, path7.resolve(projectDir, ".pgwatch-config"));
|
|
40887
41214
|
writeEnvFile(envFile, envContent);
|
|
40888
41215
|
if (preservedKeys.length > 0) {
|
|
40889
41216
|
console.log(`Preserved existing .env settings: ${preservedKeys.join(", ")}
|
|
@@ -40923,9 +41250,10 @@ Use demo mode without API key: postgres-ai mon local-install --demo`);
|
|
|
40923
41250
|
if (apiKey) {
|
|
40924
41251
|
console.log("Using API key provided via --api-key parameter");
|
|
40925
41252
|
writeConfig({ apiKey });
|
|
40926
|
-
|
|
40927
|
-
|
|
40928
|
-
|
|
41253
|
+
if (!applyApiKey(projectDir, apiKey)) {
|
|
41254
|
+
process.exitCode = 1;
|
|
41255
|
+
return;
|
|
41256
|
+
}
|
|
40929
41257
|
} else if (opts.yes) {
|
|
40930
41258
|
console.log("Auto-yes mode: no API key provided, skipping API key setup");
|
|
40931
41259
|
console.error("\u26A0 Reports will be generated locally only");
|
|
@@ -40940,10 +41268,11 @@ Use demo mode without API key: postgres-ai mon local-install --demo`);
|
|
|
40940
41268
|
const trimmedKey = inputApiKey.trim();
|
|
40941
41269
|
if (trimmedKey) {
|
|
40942
41270
|
writeConfig({ apiKey: trimmedKey });
|
|
40943
|
-
|
|
41271
|
+
if (!applyApiKey(projectDir, trimmedKey)) {
|
|
41272
|
+
process.exitCode = 1;
|
|
41273
|
+
return;
|
|
41274
|
+
}
|
|
40944
41275
|
apiKey = trimmedKey;
|
|
40945
|
-
console.log(`\u2713 API key saved
|
|
40946
|
-
`);
|
|
40947
41276
|
break;
|
|
40948
41277
|
}
|
|
40949
41278
|
console.error("\u26A0 API key cannot be empty");
|
|
@@ -41224,25 +41553,11 @@ Searched: ${demoCandidates.join(", ")}
|
|
|
41224
41553
|
const plan = planMonitoringRegistration({ project: opts.project, instanceId });
|
|
41225
41554
|
const projectName = plan.projectName;
|
|
41226
41555
|
if (instanceId) {
|
|
41227
|
-
const reg = await
|
|
41556
|
+
const reg = await adoptAndPersist(projectDir, apiKey, projectName, instanceId, {
|
|
41228
41557
|
orgScope: regOrgScope,
|
|
41229
41558
|
apiBaseUrl: globalOpts.apiBaseUrl,
|
|
41230
|
-
debug: !!process.env.DEBUG
|
|
41231
|
-
instanceId
|
|
41559
|
+
debug: !!process.env.DEBUG
|
|
41232
41560
|
});
|
|
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
41561
|
if (reg === null) {
|
|
41247
41562
|
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
41563
|
`);
|
|
@@ -41267,7 +41582,7 @@ Searched: ${demoCandidates.join(", ")}
|
|
|
41267
41582
|
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
41583
|
process.exitCode = 1;
|
|
41269
41584
|
} else {
|
|
41270
|
-
|
|
41585
|
+
await selfRegisterAndPersist(projectDir, apiKey, projectName, {
|
|
41271
41586
|
apiBaseUrl: globalOpts.apiBaseUrl,
|
|
41272
41587
|
debug: !!process.env.DEBUG,
|
|
41273
41588
|
orgScope: regOrgScope
|
|
@@ -41336,7 +41651,8 @@ var MONITORING_CONTAINERS = [
|
|
|
41336
41651
|
"self-postgres-exporter",
|
|
41337
41652
|
"flask-pgss-api",
|
|
41338
41653
|
"sources-generator",
|
|
41339
|
-
"postgres-reports"
|
|
41654
|
+
"postgres-reports",
|
|
41655
|
+
"instance-jobs"
|
|
41340
41656
|
];
|
|
41341
41657
|
var COMPOSE_PROJECT_NAME = "postgres_ai";
|
|
41342
41658
|
var DOCKER_NETWORK_NAME = `${COMPOSE_PROJECT_NAME}_default`;
|
|
@@ -41389,15 +41705,31 @@ mon.command("logs [service]").option("-f, --follow", "follow logs", false).optio
|
|
|
41389
41705
|
if (code !== 0)
|
|
41390
41706
|
process.exitCode = code;
|
|
41391
41707
|
});
|
|
41392
|
-
|
|
41393
|
-
|
|
41708
|
+
function judgeServiceHealth(service, present, status, health) {
|
|
41709
|
+
if (!present) {
|
|
41710
|
+
return service.optional ? { ok: true, mark: "-", label: "not enabled" } : { ok: false, mark: "\u2717", label: "unreachable" };
|
|
41711
|
+
}
|
|
41712
|
+
if (status !== "running") {
|
|
41713
|
+
return { ok: false, mark: "\u2717", label: `unhealthy (status: ${status})` };
|
|
41714
|
+
}
|
|
41715
|
+
if (service.readHealth && health && health !== "healthy" && health !== "starting") {
|
|
41716
|
+
return { ok: false, mark: "\u2717", label: `unhealthy (${health})` };
|
|
41717
|
+
}
|
|
41718
|
+
return { ok: true, mark: "\u2713", label: "healthy" };
|
|
41719
|
+
}
|
|
41720
|
+
function monitoringHealthServices() {
|
|
41721
|
+
return [
|
|
41394
41722
|
{ name: "Grafana", container: "grafana-with-datasources" },
|
|
41395
41723
|
{ name: "Prometheus", container: "sink-prometheus" },
|
|
41396
41724
|
{ name: "PGWatch (Postgres)", container: "pgwatch-postgres" },
|
|
41397
41725
|
{ name: "PGWatch (Prometheus)", container: "pgwatch-prometheus" },
|
|
41398
41726
|
{ name: "Target DB", container: "target-db" },
|
|
41399
|
-
{ name: "Sink Postgres", container: "sink-postgres" }
|
|
41727
|
+
{ name: "Sink Postgres", container: "sink-postgres" },
|
|
41728
|
+
{ name: "Instance jobs", container: "instance-jobs", optional: true, readHealth: true }
|
|
41400
41729
|
];
|
|
41730
|
+
}
|
|
41731
|
+
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) => {
|
|
41732
|
+
const services = monitoringHealthServices();
|
|
41401
41733
|
const waitTime = opts.wait || 0;
|
|
41402
41734
|
const maxAttempts = waitTime > 0 ? Math.ceil(waitTime / 5) : 1;
|
|
41403
41735
|
console.log(`Checking service health...
|
|
@@ -41413,16 +41745,12 @@ mon.command("health").description("health check for monitoring services").option
|
|
|
41413
41745
|
for (const service of services) {
|
|
41414
41746
|
try {
|
|
41415
41747
|
const result = spawnSync2("docker", ["inspect", "-f", "{{.State.Status}}", service.container], { stdio: "pipe" });
|
|
41416
|
-
const
|
|
41417
|
-
if
|
|
41418
|
-
|
|
41419
|
-
}
|
|
41420
|
-
|
|
41748
|
+
const present = result.status === 0;
|
|
41749
|
+
const health = present && service.readHealth ? (spawnSync2("docker", ["inspect", "-f", "{{if .State.Health}}{{.State.Health.Status}}{{end}}", service.container], { stdio: "pipe" }).stdout || "").trim() : "";
|
|
41750
|
+
const verdict = judgeServiceHealth(service, present, (result.stdout || "").trim(), health);
|
|
41751
|
+
console.log(`${verdict.mark} ${service.name}: ${verdict.label}`);
|
|
41752
|
+
if (!verdict.ok)
|
|
41421
41753
|
allHealthy = false;
|
|
41422
|
-
} else {
|
|
41423
|
-
console.log(`\u2717 ${service.name}: unreachable`);
|
|
41424
|
-
allHealthy = false;
|
|
41425
|
-
}
|
|
41426
41754
|
} catch (error) {
|
|
41427
41755
|
console.log(`\u2717 ${service.name}: unreachable`);
|
|
41428
41756
|
allHealthy = false;
|
|
@@ -42236,8 +42564,87 @@ program2.command("orgs").description("list organizations this credential can rea
|
|
|
42236
42564
|
process.exitCode = 1;
|
|
42237
42565
|
}
|
|
42238
42566
|
});
|
|
42567
|
+
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) => {
|
|
42568
|
+
try {
|
|
42569
|
+
const rootOpts = program2.opts();
|
|
42570
|
+
const cfg = readConfig();
|
|
42571
|
+
const { apiKey } = getConfig(rootOpts);
|
|
42572
|
+
if (!apiKey) {
|
|
42573
|
+
console.error("API key is required. Run 'pgai auth' first or set --api-key.");
|
|
42574
|
+
process.exitCode = 1;
|
|
42575
|
+
return;
|
|
42576
|
+
}
|
|
42577
|
+
const { apiBaseUrl } = resolveBaseUrls2(rootOpts, cfg);
|
|
42578
|
+
const instanceId = opts.instance || readInstanceIdFromProject();
|
|
42579
|
+
if (!instanceId) {
|
|
42580
|
+
console.error("No monitoring instance. Pass --instance <uuid>, or run this on a box whose");
|
|
42581
|
+
console.error(".pgwatch-config has an instance_id (written by 'pgai mon local-install').");
|
|
42582
|
+
process.exitCode = 1;
|
|
42583
|
+
return;
|
|
42584
|
+
}
|
|
42585
|
+
const kind = opts.range ? "promql_range" : "promql_instant";
|
|
42586
|
+
const enqueue = { instanceId, kind, query: expr };
|
|
42587
|
+
if (opts.range) {
|
|
42588
|
+
if (!opts.start || !opts.end || !opts.step) {
|
|
42589
|
+
console.error("--range needs --start, --end and --step.");
|
|
42590
|
+
process.exitCode = 1;
|
|
42591
|
+
return;
|
|
42592
|
+
}
|
|
42593
|
+
const stepS = parseDurationSeconds(opts.step);
|
|
42594
|
+
if (stepS === null || stepS < 1) {
|
|
42595
|
+
console.error(`Invalid --step ${opts.step}: expected a duration like 60s, 5m or 1h.`);
|
|
42596
|
+
process.exitCode = 1;
|
|
42597
|
+
return;
|
|
42598
|
+
}
|
|
42599
|
+
enqueue.start = opts.start;
|
|
42600
|
+
enqueue.end = opts.end;
|
|
42601
|
+
enqueue.stepS = stepS;
|
|
42602
|
+
} else if (opts.at) {
|
|
42603
|
+
enqueue.at = opts.at;
|
|
42604
|
+
}
|
|
42605
|
+
const explicitTimeoutS = opts.timeout ? parseDurationSeconds(opts.timeout) : null;
|
|
42606
|
+
if (opts.timeout && (explicitTimeoutS === null || explicitTimeoutS < 1)) {
|
|
42607
|
+
console.error(`Invalid --timeout ${opts.timeout}: expected a duration like 60s or 2m.`);
|
|
42608
|
+
process.exitCode = 1;
|
|
42609
|
+
return;
|
|
42610
|
+
}
|
|
42611
|
+
const { jobId, firstAnswerEstimateS } = await enqueueQuery(apiKey, enqueue, { apiBaseUrl });
|
|
42612
|
+
const timeoutS = explicitTimeoutS ?? (firstAnswerEstimateS === null ? DEFAULT_PROMQL_TIMEOUT_S : Math.min(Math.max(Math.ceil(firstAnswerEstimateS * 1.5) + 30, 60), 1800));
|
|
42613
|
+
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)}.`);
|
|
42614
|
+
const res = await awaitQueryResult(apiKey, jobId, {
|
|
42615
|
+
apiBaseUrl,
|
|
42616
|
+
timeoutMs: timeoutS * 1000
|
|
42617
|
+
});
|
|
42618
|
+
if (!isTerminal2(res.status)) {
|
|
42619
|
+
console.error(`Timed out after ${timeoutS}s. Job ${jobId} is still ${res.status}.`);
|
|
42620
|
+
console.error(res.status === "queued" ? "Nothing has claimed it: check the instance-jobs container is running (it is behind " + "a compose profile), 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.");
|
|
42621
|
+
process.exitCode = 1;
|
|
42622
|
+
return;
|
|
42623
|
+
}
|
|
42624
|
+
if (res.status !== "done" || res.outcome !== "ok") {
|
|
42625
|
+
console.error(`Query ${res.status}${res.failure_class ? ` (${res.failure_class})` : ""}.`);
|
|
42626
|
+
if (res.error)
|
|
42627
|
+
console.error(res.error);
|
|
42628
|
+
process.exitCode = 1;
|
|
42629
|
+
return;
|
|
42630
|
+
}
|
|
42631
|
+
if (!res.result) {
|
|
42632
|
+
console.error("The job finished but carried no result.");
|
|
42633
|
+
process.exitCode = 1;
|
|
42634
|
+
return;
|
|
42635
|
+
}
|
|
42636
|
+
if (opts.json) {
|
|
42637
|
+
console.log(JSON.stringify(res.result, null, 2));
|
|
42638
|
+
return;
|
|
42639
|
+
}
|
|
42640
|
+
console.log(renderPromQL(res.result));
|
|
42641
|
+
} catch (err) {
|
|
42642
|
+
console.error(err instanceof Error ? err.message : String(err));
|
|
42643
|
+
process.exitCode = 1;
|
|
42644
|
+
}
|
|
42645
|
+
});
|
|
42239
42646
|
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
|
|
42647
|
+
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
42648
|
const spinner = createTtySpinner(process.stdout.isTTY ?? false, "Fetching issues...");
|
|
42242
42649
|
try {
|
|
42243
42650
|
const rootOpts = program2.opts();
|
|
@@ -42252,11 +42659,15 @@ withOrgOptions(issues.command("list")).description("list issues").option("--stat
|
|
|
42252
42659
|
const orgScope = getActiveOrgScope2() ?? { source: "token" };
|
|
42253
42660
|
const orgId = configOrgIdForBody(apiKey, cfg.orgId);
|
|
42254
42661
|
const { apiBaseUrl } = resolveBaseUrls2(rootOpts, cfg);
|
|
42662
|
+
const statusArg = (opts.status ?? "open").trim().toLowerCase();
|
|
42255
42663
|
let statusFilter;
|
|
42256
|
-
if (
|
|
42257
|
-
statusFilter =
|
|
42258
|
-
} else if (
|
|
42259
|
-
|
|
42664
|
+
if (statusArg === "open" || statusArg === "closed") {
|
|
42665
|
+
statusFilter = statusArg;
|
|
42666
|
+
} else if (statusArg !== "all") {
|
|
42667
|
+
spinner.stop();
|
|
42668
|
+
console.error("--status must be open, closed, or all");
|
|
42669
|
+
process.exitCode = 1;
|
|
42670
|
+
return;
|
|
42260
42671
|
}
|
|
42261
42672
|
const result = await fetchIssues2({
|
|
42262
42673
|
apiKey,
|
|
@@ -42270,7 +42681,7 @@ withOrgOptions(issues.command("list")).description("list issues").option("--stat
|
|
|
42270
42681
|
debug: !!opts.debug
|
|
42271
42682
|
});
|
|
42272
42683
|
spinner.stop();
|
|
42273
|
-
const trimmed = Array.isArray(result) ? result.map((r) =>
|
|
42684
|
+
const trimmed = Array.isArray(result) ? result.map((r) => presentIssue2({
|
|
42274
42685
|
id: r.id,
|
|
42275
42686
|
title: r.title,
|
|
42276
42687
|
status: r.status,
|
|
@@ -42308,7 +42719,7 @@ withOrgOptions(issues.command("view <issueId>")).description("view issue details
|
|
|
42308
42719
|
spinner.update("Fetching comments...");
|
|
42309
42720
|
const comments = await fetchIssueComments2({ apiKey, apiBaseUrl, issueId, debug: !!opts.debug });
|
|
42310
42721
|
spinner.stop();
|
|
42311
|
-
const combined = { issue:
|
|
42722
|
+
const combined = { issue: presentIssue2(issue), comments };
|
|
42312
42723
|
printResult(combined, opts.json);
|
|
42313
42724
|
} catch (err) {
|
|
42314
42725
|
spinner.stop();
|
|
@@ -43512,14 +43923,23 @@ if (__require.main == __require.module) {
|
|
|
43512
43923
|
export {
|
|
43513
43924
|
LOCAL_INSTALL_MANAGED_ENV_KEYS,
|
|
43514
43925
|
ORG_SCOPED_COMMANDS,
|
|
43926
|
+
adoptAndPersist,
|
|
43927
|
+
applyApiKey,
|
|
43928
|
+
applyProjectName,
|
|
43515
43929
|
buildLocalInstallEnv,
|
|
43930
|
+
instanceIdToPersist,
|
|
43516
43931
|
isValidComposeYaml,
|
|
43932
|
+
judgeServiceHealth,
|
|
43933
|
+
monitoringHealthServices,
|
|
43934
|
+
persistInstanceId,
|
|
43517
43935
|
planMonitoringRegistration,
|
|
43518
43936
|
program2 as program,
|
|
43519
43937
|
readDeployedTag,
|
|
43520
43938
|
refreshBundledComposeIfStale,
|
|
43521
43939
|
registerMonitoringInstance,
|
|
43522
43940
|
resolveAdoptedProject,
|
|
43941
|
+
selfRegisterAndPersist,
|
|
43523
43942
|
shouldScaleOutNodeExporter,
|
|
43943
|
+
statOwner,
|
|
43524
43944
|
updatePgwatchConfig
|
|
43525
43945
|
};
|