@mason-data/cli 0.7.0 → 0.8.0
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 +90 -71
- package/dist/src/api/error.js +3 -0
- package/dist/src/api/postgres/catalog.js +222 -13
- package/dist/src/api/postgres/encode.js +3 -1
- package/dist/src/api/postgres/explain.js +3 -2
- package/dist/src/api/postgres/protocol.js +39 -23
- package/dist/src/api/postgres/server.js +1173 -207
- package/dist/src/api/postgres/translate.js +81 -9
- package/dist/src/api/request-log.js +18 -5
- package/dist/src/cdc/poll.js +5 -5
- package/dist/src/cli/background.js +29 -6
- package/dist/src/cli/cancel.js +75 -0
- package/dist/src/cli/command.js +1 -0
- package/dist/src/cli/commands.js +622 -183
- package/dist/src/cli/control.js +59 -0
- package/dist/src/cli/demo.js +19 -10
- package/dist/src/cli/dev.js +351 -111
- package/dist/src/cli/doctor.js +20 -10
- package/dist/src/cli/guides.js +79 -31
- package/dist/src/cli/init.js +119 -58
- package/dist/src/cli/machine.js +12 -11
- package/dist/src/cli/main.js +3 -3
- package/dist/src/cli/mcp.js +29 -20
- package/dist/src/cli/metrics.js +79 -0
- package/dist/src/cli/pg-tls.js +13 -3
- package/dist/src/cli/pg-url.js +4 -0
- package/dist/src/cli/pin.js +13 -12
- package/dist/src/cli/preflight.js +8 -7
- package/dist/src/cli/project.js +43 -4
- package/dist/src/cli/readiness.js +38 -4
- package/dist/src/cli/registry.js +53 -34
- package/dist/src/cli/replication-checks.js +7 -5
- package/dist/src/cli/replication-mode.js +105 -26
- package/dist/src/cli/request-filter.js +134 -0
- package/dist/src/cli/roles.js +584 -0
- package/dist/src/cli/run-server.js +483 -41
- package/dist/src/cli/runs.js +1 -1
- package/dist/src/cli/security.js +10 -8
- package/dist/src/cli/serve.js +144 -42
- package/dist/src/cli/share-run.js +286 -0
- package/dist/src/cli/share.js +286 -0
- package/dist/src/cli/source-edit.js +65 -13
- package/dist/src/cli/sources.js +287 -38
- package/dist/src/cli/supervise.js +12 -1
- package/dist/src/cli/sync.js +55 -25
- package/dist/src/cli/tunnel.js +200 -0
- package/dist/src/cli/ui.js +9 -6
- package/dist/src/cli/usage.js +128 -0
- package/dist/src/cli/user.js +496 -40
- package/dist/src/cli/version.js +9 -7
- package/dist/src/demo/seed.js +3 -0
- package/dist/src/duck/cancel.js +10 -1
- package/dist/src/duck/events.js +134 -0
- package/dist/src/duck/executor-worker.js +18 -9
- package/dist/src/duck/executor.js +43 -30
- package/dist/src/duck/explain.js +56 -46
- package/dist/src/duck/extensions.js +160 -0
- package/dist/src/duck/functions.js +13 -14
- package/dist/src/duck/gaps.js +17 -8
- package/dist/src/duck/grants.js +176 -0
- package/dist/src/duck/history.js +123 -0
- package/dist/src/duck/kept.js +6 -6
- package/dist/src/duck/plan.js +102 -25
- package/dist/src/duck/publication.js +6 -6
- package/dist/src/duck/requests.js +265 -93
- package/dist/src/duck/rewrite.js +25 -5
- package/dist/src/duck/row-filter.js +649 -0
- package/dist/src/duck/store.js +156 -120
- package/dist/src/duck/stream.js +10 -5
- package/dist/src/duck/sync.js +835 -268
- package/dist/src/duck/types.js +6 -5
- package/dist/src/duck/usage.js +324 -0
- package/dist/src/duck/users.js +64 -15
- package/dist/src/duck/verified.js +3 -0
- package/dist/src/duck/views.js +2 -3
- package/dist/src/lib/database-name.js +62 -0
- package/dist/src/lib/dotenv.js +17 -4
- package/dist/src/lib/log-buffer.js +68 -0
- package/dist/src/lib/pg-numeric.js +5 -2
- package/dist/src/lib/project-version.js +23 -23
- package/dist/src/lib/table-sync.js +267 -0
- package/dist/src/web/app.css +706 -66
- package/dist/src/web/app.js +327 -410
- package/dist/src/web/charts.js +229 -0
- package/dist/src/web/index.html +82 -18
- package/dist/src/web/pages/queries.js +192 -0
- package/dist/src/web/pages/status.js +446 -0
- package/dist/src/web/pages/syncs.js +155 -0
- package/dist/src/web/pages/tables.js +434 -0
- package/dist/src/web/pages/usage.js +397 -0
- package/dist/src/web/pages/users.js +218 -0
- package/dist/src/web/shared.js +568 -0
- package/dist/src/web/vendor/README.md +25 -0
- package/dist/src/web/vendor/geist-1.7.2/Geist-Variable.woff2 +0 -0
- package/dist/src/web/vendor/geist-1.7.2/GeistMono-Variable.woff2 +0 -0
- package/dist/src/web/vendor/geist-1.7.2/LICENSE.txt +92 -0
- package/dist/src/web/vendor/lucide-1.56.0/LICENSE.txt +43 -0
- package/dist/src/web/vendor/lucide-1.56.0/chart-column.mjs +29 -0
- package/dist/src/web/vendor/lucide-1.56.0/layout-dashboard.mjs +29 -0
- package/dist/src/web/vendor/lucide-1.56.0/logs.mjs +34 -0
- package/dist/src/web/vendor/lucide-1.56.0/menu.mjs +28 -0
- package/dist/src/web/vendor/lucide-1.56.0/square-terminal.mjs +28 -0
- package/dist/src/web/vendor/lucide-1.56.0/table.mjs +29 -0
- package/dist/src/web/vendor/lucide-1.56.0/users.mjs +29 -0
- package/dist/src/web/vendor/lucide-1.56.0/x.mjs +27 -0
- package/dist/src/web/vendor/uplot-1.6.32/LICENSE.txt +21 -0
- package/dist/src/web/vendor/uplot-1.6.32/uPlot.esm.js +6140 -0
- package/dist/src/web/vendor/uplot-1.6.32/uPlot.min.css +1 -0
- package/dist/src/web/vendor/zilla-slab-5.3.0/LICENSE.txt +93 -0
- package/dist/src/web/vendor/zilla-slab-5.3.0/zilla-slab-latin-700-normal.woff2 +0 -0
- package/package.json +10 -3
package/dist/src/cli/commands.js
CHANGED
|
@@ -8,8 +8,8 @@
|
|
|
8
8
|
* mason tables [<table>…] [--json] the database's tables, their columns, keys and foreign keys
|
|
9
9
|
* mason add <table>… [--columns …] copy tables into Mason: every column, or the ones --columns names
|
|
10
10
|
* mason remove <table>… take tables out of the sources file; their copies are dropped
|
|
11
|
-
* mason wait [--timeout <s>] [--json] until every added table is
|
|
12
|
-
* mason logs [-n <lines>] [-f] [--start] [--requests] the run's log; --start, the start's terminal; --requests, its request log
|
|
11
|
+
* mason wait [--timeout <s>] [--json] until every added table is synced: 0 ready, 1 a copy failed (said), 124 timed out
|
|
12
|
+
* mason logs [-n <lines>] [-f] [--start] [--requests] [--events] the run's log; --start, the start's terminal; --requests, its request log; --events, the sync's events
|
|
13
13
|
* mason stop stop Mason for this folder's project (the copy stays)
|
|
14
14
|
*/
|
|
15
15
|
import * as fs from "node:fs";
|
|
@@ -18,20 +18,21 @@ import { asFailure, endText, errorJson, failed, jsonText, nextBlock, parseComman
|
|
|
18
18
|
import { err as styleErr, out as styleOut, stepRenderer, wrap } from "./style.js";
|
|
19
19
|
import { alive, endRun, END_GRACE_MS, notAnswering, runState, theRun } from "./runs.js";
|
|
20
20
|
import { version } from "./version.js";
|
|
21
|
-
import {
|
|
22
|
-
import { DEMO_DIR, DUCK_FILE, STATE_DIR, isProject, projectSource, readProject, theProject, writeConfig } from "./project.js";
|
|
21
|
+
import { restartsFact, signalSupervisor, stopSupervisor, supervisorGone } from "./supervise.js";
|
|
22
|
+
import { DEMO_DIR, DUCK_FILE, STATE_DIR, databaseMoved, isProject, projectDatabase, projectSource, readProject, theProject, writeConfig } from "./project.js";
|
|
23
23
|
import { callRun, RunUnreachable } from "./control.js";
|
|
24
|
-
import { withPlaceholders, postgresLink } from "./pg-url.js";
|
|
24
|
+
import { withPlaceholders, postgresFacts, postgresLink } from "./pg-url.js";
|
|
25
25
|
import { listenerText } from "./pg-tls.js";
|
|
26
|
-
import {
|
|
26
|
+
import { logEntries, logLineText } from "../lib/log-buffer.js";
|
|
27
27
|
import { catalogTable, entryName, looksSecret, tableNameProblem } from "./sources.js";
|
|
28
28
|
import { addEntry, Refusal, removeEntries, sourceToEdit } from "./source-edit.js";
|
|
29
|
-
import { changesOf } from "./replication-mode.js";
|
|
30
29
|
import { connectSource, maskedUrl, parseSource, readSourceFacts } from "./preflight.js";
|
|
31
30
|
import { reach } from "./state.js";
|
|
32
|
-
import { copyingText, copyProgress, duration, readiness } from "./readiness.js";
|
|
31
|
+
import { copyingText, copyProgress, duration, msText, readiness, readinessWord } from "./readiness.js";
|
|
33
32
|
import { pathText } from "../duck/kept-route.js";
|
|
33
|
+
import { DEFAULT_REQUEST_LIMIT, eventLimit, MAX_SEARCH, SLOW_MS, requestFilter, requestFilterParams } from "./request-filter.js";
|
|
34
34
|
import { countsText } from "../duck/stream.js";
|
|
35
|
+
import { agoText, BIG_TABLE_ROWS, parseSync, syncText } from "../lib/table-sync.js";
|
|
35
36
|
const out = (m) => { process.stdout.write(`${m}\n`); };
|
|
36
37
|
const err = (m) => { process.stderr.write(`${m}\n`); };
|
|
37
38
|
// ------------------------------------------------------------------ shared
|
|
@@ -40,8 +41,9 @@ async function runOr(spec, json) {
|
|
|
40
41
|
const w = await reach(spec, json);
|
|
41
42
|
return w !== null && "run" in w ? w.run : null;
|
|
42
43
|
}
|
|
44
|
+
/** The run's state with each table's columns (`?columns=1`: `mason status --json` carries them; the page polls it lean). */
|
|
43
45
|
export async function readState(run) {
|
|
44
|
-
const r = await callRun(run, "GET", "/api/state");
|
|
46
|
+
const r = await callRun(run, "GET", "/api/state?columns=1");
|
|
45
47
|
if (r.status !== 200)
|
|
46
48
|
throw new RunUnreachable(String(r.body.error ?? `HTTP ${r.status}`));
|
|
47
49
|
return r.body;
|
|
@@ -81,7 +83,7 @@ export const tableArg = entryName;
|
|
|
81
83
|
/** What to run once every added table is copied: a user for the tool that will query it (src/cli/user.ts). */
|
|
82
84
|
export const READY_STEPS = [
|
|
83
85
|
["mason user add <name>", "a user for your BI tool or psql: it prints the password and the URL to paste"],
|
|
84
|
-
["mason status", "each table
|
|
86
|
+
["mason status", "each synced table, and the statements the Postgres API answered slowly or refused"],
|
|
85
87
|
];
|
|
86
88
|
/** What to run next over a database's tables: a table's columns, then adding it — named after a real table not yet added.
|
|
87
89
|
* `from: "tables"` is mason tables itself asking: it never points at itself. */
|
|
@@ -94,14 +96,15 @@ export function addSteps(tables, from = null) {
|
|
|
94
96
|
return [
|
|
95
97
|
...(from === "tables" ? [] : [["mason tables", "the database's tables: rows, key, whether added"]]),
|
|
96
98
|
[`mason tables ${name}`, "one table's columns"],
|
|
97
|
-
[`mason add ${name}`, "
|
|
99
|
+
[`mason add ${name}`, "sync it into Mason: it is added to the sources file (--columns a,b,c: those only)"],
|
|
98
100
|
];
|
|
99
101
|
}
|
|
100
102
|
// ------------------------------------------------------------------ readiness (mason wait, mason status): src/cli/readiness.ts
|
|
101
|
-
export { copyingText, copyProgress, duration, readiness };
|
|
102
|
-
/** A table line's bar in its state's colour (on a terminal): green copied, red failed, yellow served but not kept current, cyan at work
|
|
103
|
+
export { copyingText, copyProgress, duration, msText, readiness, readinessWord };
|
|
104
|
+
/** A table line's bar in its state's colour (on a terminal): green copied, red failed, yellow served but not kept current, cyan at work
|
|
105
|
+
* (a table whose sync is off is copied as it asked: green). */
|
|
103
106
|
export function paintBar(line, s) {
|
|
104
|
-
const colour = / its
|
|
107
|
+
const colour = / its first sync failed: /.test(line) ? s.red : / NOT kept current /.test(line) ? s.yellow : / 100% [\d,]+ rows · synced( · |$)/.test(line) ? s.green : s.cyan;
|
|
105
108
|
return line.replace(/(█+)(░*)|(░+)/, (_m, full, empty, none) => full !== undefined ? `${colour(full)}${s.dim(empty ?? "")}` : s.dim(none ?? ""));
|
|
106
109
|
}
|
|
107
110
|
/** The width of a table's progress bar, in characters. */
|
|
@@ -112,37 +115,34 @@ const bar = (fraction) => { const full = Math.round(Math.max(0, Math.min(1, frac
|
|
|
112
115
|
* b; it shows a line for each table, 100% for those up to date and a progress bar for those still at work, in order of
|
|
113
116
|
* last processed, so the ones at work are at the bottom): its bar, its percent and what it is doing. The finished
|
|
114
117
|
* first, in the order they finished (`copy.updatedAt`: a finished copy's state is written once, when it finishes), then
|
|
115
|
-
* the failed, the copying and the waiting.
|
|
118
|
+
* the failed, the copying and the waiting. A copied table says how it is kept current and how old its copy is
|
|
119
|
+
* (`keptAge`): a copy's age is never hidden.
|
|
116
120
|
*/
|
|
117
|
-
export function tableLines(s) {
|
|
121
|
+
export function tableLines(s, now = Date.now()) {
|
|
118
122
|
const picked = s.tables.filter((t) => t.added);
|
|
119
123
|
// 0 done, 1 failed, 2 copying, 3 waiting for its copy
|
|
120
124
|
const rank = (t) => t.error ? 1 : t.copy?.phase === "copying" ? 2 : !t.copy ? 3 : 0;
|
|
121
125
|
const rows = picked.map((t) => {
|
|
122
126
|
const r = rank(t);
|
|
123
127
|
let fraction, what;
|
|
124
|
-
|
|
125
|
-
if (r === 0 && t.unsynced) {
|
|
128
|
+
if (r === 0) {
|
|
126
129
|
fraction = 1;
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
else if (r === 0) {
|
|
130
|
-
fraction = 1;
|
|
131
|
-
what = `${n(t.copy?.rowsCopied ?? 0)} rows · copied${t.copy?.retired ? ` · ${t.copy.retired}` : ""}`;
|
|
130
|
+
const age = keptAge(t, now);
|
|
131
|
+
what = `${n(t.copy?.rowsCopied ?? 0)} rows · synced${t.copy?.retired ? ` · ${t.copy.retired}` : ""}${age ? ` · ${age}` : ""}`;
|
|
132
132
|
}
|
|
133
133
|
else if (r === 1) {
|
|
134
134
|
fraction = 1;
|
|
135
|
-
what = `its
|
|
135
|
+
what = `its first sync failed: ${firstLine(t.error)}`;
|
|
136
136
|
}
|
|
137
137
|
else if (r === 2) {
|
|
138
138
|
const c = copyingText(t.copy.rowsCopied, t.rows, t.copy.progress);
|
|
139
139
|
fraction = c.fraction ?? 0;
|
|
140
140
|
// a count says it is copying; "reading the first chunk" says so itself
|
|
141
|
-
what = /^\d/.test(c.text) ? `
|
|
141
|
+
what = /^\d/.test(c.text) ? `syncing ${c.text}` : c.text;
|
|
142
142
|
}
|
|
143
143
|
else {
|
|
144
144
|
fraction = 0;
|
|
145
|
-
what = "waiting for its
|
|
145
|
+
what = "waiting for its first sync to start";
|
|
146
146
|
}
|
|
147
147
|
return { t, r, at: t.copy?.updatedAt ?? "", line: `${bar(fraction)} ${`${Math.floor(fraction * 100)}%`.padStart(4)} ${what}` };
|
|
148
148
|
});
|
|
@@ -150,8 +150,33 @@ export function tableLines(s) {
|
|
|
150
150
|
const w = Math.max(0, ...rows.map((x) => x.t.table.length));
|
|
151
151
|
return rows.map((x) => `${x.t.table.padEnd(w)} ${x.line}`);
|
|
152
152
|
}
|
|
153
|
+
/**
|
|
154
|
+
* HOW A COPIED TABLE IS KEPT CURRENT, AND HOW OLD ITS COPY IS, on its line (issue 33: a copy days old answers today's
|
|
155
|
+
* count(*) wrongly, and nothing else tells): `sync: off, last synced 3 days ago — mason sync orders`, `polled every
|
|
156
|
+
* 30s, last 4 s ago`, `replicated, at 0/1A2B3C4D, 2 s ago` (the stream's position, and the last moment it had
|
|
157
|
+
* everything the database wrote); a table being caught up, copied again or not streamed says why it is NOT kept current.
|
|
158
|
+
* The Stream section says the rest of the stream.
|
|
159
|
+
*/
|
|
160
|
+
export function keptAge(t, now = Date.now()) {
|
|
161
|
+
const k = t.kept;
|
|
162
|
+
if (k?.by === "replication") {
|
|
163
|
+
if (k.state !== "streamed")
|
|
164
|
+
return `NOT kept current (${k.syncedAt === null ? "never synced" : `last synced ${agoText(k.syncedAt, now)}`}): ${k.why ?? "it is not streamed yet"}`;
|
|
165
|
+
const at = `replicated, at ${k.position}, ${k.syncedAt === null ? "not synced yet" : agoText(k.syncedAt, now)}`;
|
|
166
|
+
return k.notRunning === null ? at : `${at} — NOT kept current now, the stream is not running: ${k.notRunning}`;
|
|
167
|
+
}
|
|
168
|
+
if (k?.by !== "poll")
|
|
169
|
+
return null;
|
|
170
|
+
const synced = k.syncedAt === null ? "never synced" : `last synced ${agoText(k.syncedAt, now)}`;
|
|
171
|
+
if (k.pending !== undefined)
|
|
172
|
+
return `NOT kept current (${synced}): ${k.pending}`;
|
|
173
|
+
// the line to write as well as the command: a project an older Mason polled is off after the upgrade
|
|
174
|
+
if (k.sync.mode === "off")
|
|
175
|
+
return `sync: off, ${synced} — mason sync ${tableArg(t.table)} syncs it now; sync: poll 5m on its entry polls it`;
|
|
176
|
+
return `polled every ${k.sync.every}, ${k.syncedAt === null ? "not polled yet" : `last ${agoText(k.syncedAt, now)}`}`;
|
|
177
|
+
}
|
|
153
178
|
/** A span of seconds as the stream's lines say it: `0.3 s` under ten seconds, then as a wait reads (`duration`). */
|
|
154
|
-
const spanText = (seconds) => seconds < 10 ? `${Math.max(0, seconds).toFixed(1)} s` : duration(seconds);
|
|
179
|
+
export const spanText = (seconds) => seconds < 10 ? `${Math.max(0, seconds).toFixed(1)} s` : duration(seconds);
|
|
155
180
|
/**
|
|
156
181
|
* WHERE THE STREAM IS, a thing a line (the first replication run on a server showed nothing of it in 35
|
|
157
182
|
* minutes): its position against the database's and the bytes between, the newest change applied and how long after
|
|
@@ -170,6 +195,7 @@ export function streamLines(st, now = Date.now()) {
|
|
|
170
195
|
? `the database is at ${st.database.lsn}: ${n(st.lag.bytes ?? 0)} bytes behind (read ${spanText(since(st.database.at))} ago)`
|
|
171
196
|
: `the database's position could not be read: ${st.database.error.split("\n")[0]}`;
|
|
172
197
|
rows.push(["position", `${st.position} · ${db}`]);
|
|
198
|
+
rows.push(["slot", slotText(st.slotWal, since)]);
|
|
173
199
|
rows.push(["last change", st.lastBatch === null ? "none applied this run"
|
|
174
200
|
: `committed ${spanText(since(st.lastBatch.committedAt))} ago, applied ${spanText((Date.parse(st.lastBatch.appliedAt) - Date.parse(st.lastBatch.committedAt)) / 1000)} after its commit`]);
|
|
175
201
|
const total = (t) => t.applied.inserts + t.applied.updates + t.applied.deletes + t.applied.truncates;
|
|
@@ -187,12 +213,32 @@ export function streamLines(st, now = Date.now()) {
|
|
|
187
213
|
const w = Math.max(...rows.map(([k]) => k.length));
|
|
188
214
|
return rows.map(([k, v]) => `${k.padEnd(w)} ${v}`);
|
|
189
215
|
}
|
|
216
|
+
/**
|
|
217
|
+
* THE WAL THE SLOT HOLDS ON THE DATABASE, in a line (`StreamStatus.slotWal`): how much, Postgres's status of it, and the cap —
|
|
218
|
+
* or, with `max_slot_wal_keep_size` unset, that nothing caps it: while nothing reads the slot it holds WAL for ever, until
|
|
219
|
+
* the database's disk fills (CLAUDE.md "Hard-won gotchas"). A read that failed is said, never a 0.
|
|
220
|
+
*/
|
|
221
|
+
export function slotText(w, since) {
|
|
222
|
+
if ("error" in w)
|
|
223
|
+
return `the WAL it holds could not be read: ${w.error.split("\n")[0]}`;
|
|
224
|
+
const read = `(read ${spanText(since(w.at))} ago)`;
|
|
225
|
+
if (w.walStatus === "lost")
|
|
226
|
+
return `lost: Postgres invalidated it (it held more WAL than max_slot_wal_keep_size), so Mason makes it again and catches the synced tables up by one poll ${read}`;
|
|
227
|
+
const cap = w.safeWalSizeBytes === null
|
|
228
|
+
? "max_slot_wal_keep_size is unset, so nothing caps it: while nothing reads the slot it holds WAL for ever, until the database's disk fills (set max_slot_wal_keep_size on the database)"
|
|
229
|
+
: w.safeWalSizeBytes <= 0 ? "past max_slot_wal_keep_size: Postgres invalidates it at its next checkpoint, and Mason makes it again"
|
|
230
|
+
: `${fileSize(w.safeWalSizeBytes)} more before Postgres invalidates it (max_slot_wal_keep_size)`;
|
|
231
|
+
return `holds ${fileSize(w.walHeldBytes)} of the database's WAL${w.walStatus === null ? "" : ` (${w.walStatus})`} · ${cap} ${read}`;
|
|
232
|
+
}
|
|
190
233
|
// ------------------------------------------------------------------ mason status
|
|
191
234
|
const STATUS_USAGE = `usage: mason status [--json]
|
|
192
235
|
|
|
193
236
|
Status: running, not running, or hung — then this project's facts, one a line: its folder, its database and how
|
|
194
|
-
its changes are read, the run and
|
|
195
|
-
|
|
237
|
+
its changes are read, the run and the synced data's file and the synced tables in all (synced, rows, the last sync), the Postgres API
|
|
238
|
+
and how it listens. Then each added table as mason wait shows it, with how it is kept current and when it was last synced
|
|
239
|
+
(sync: off, last synced 3 days ago; polled every 30s, last 4 s ago); the sync's latest failures (mason logs --events
|
|
240
|
+
lists every event) and the latest statements refused, failed or cancelled (mason logs --requests --failed); the Postgres API's statements of the last day and the last week (each outcome, the completed
|
|
241
|
+
ones' average, p50 and p95 time, the rows and bytes sent); the ones it answered slowly in the last day; and the ones it refused or
|
|
196
242
|
failed, each with the reason (mason logs --requests lists every one). With Mason stopped, the same rows say "not
|
|
197
243
|
running", how the last run ended, and the tables are the ones the sources file names. Hung is a run that is alive and does
|
|
198
244
|
not answer its commands (a statement may be holding its main thread): the same rows, and mason stop ends it (exit 1).
|
|
@@ -200,24 +246,46 @@ const STATUS_USAGE = `usage: mason status [--json]
|
|
|
200
246
|
mason status
|
|
201
247
|
mason status --json
|
|
202
248
|
|
|
203
|
-
--json as data: { running, pid, postgres: { url, host, port, database, sslmode (require when it is TLS),
|
|
249
|
+
--json as data: { running, pid, postgres: { url, host, port, database, sslmode (require when it is TLS), shown (the URL with
|
|
250
|
+
<user>:<password> in the credentials' place), psql (its psql line), listener:
|
|
204
251
|
{ host (what it is bound to), tls (certificate, self-signed or null), fingerprint } }, source: { name (the
|
|
205
|
-
source's: its sources file's), file (the sources file), url, database, remote, version, standby, changes,
|
|
206
|
-
replication, demo (the demo database this run hosts)
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
252
|
+
source's: its sources file's), file (the sources file), shownFile (it from the project's root), url, database, remote, version, standby, changes,
|
|
253
|
+
replication, demo (the demo database this run hosts), sync: { default (every table's sync unless its entry
|
|
254
|
+
says its own: off, poll 30s), written (the file's top sync: says it; false: the default) } }, warnings, folder, project, tables: [{ table, rows,
|
|
255
|
+
bytes, key, added, addedColumns, copy, error, kept, sync: { state (synced, syncing, failed, waiting), syncedAt (when
|
|
256
|
+
it was last brought current), syncSeconds (how long that took), syncedBy (copy, poll, catch-up, stream),
|
|
257
|
+
ageSeconds (how long ago that was) }, … }], totals: { added, copied, failed, rows (the synced tables' rows),
|
|
258
|
+
syncedAt (the latest sync of any) }, engine: { file, bytes, keptDays (MASON_HISTORY_DAYS), version (the run's), startedAt,
|
|
259
|
+
uptimeSeconds, restarts (the times its supervisor started the run again: { count, last: { at, why, upSeconds }
|
|
260
|
+
or null }, or { error }) }, disk: { path (where the synced data's file is), freeBytes, copyBytes (the file),
|
|
261
|
+
projectedBytes (what the first syncs running are projected to add; null: none, or too early to say), error },
|
|
262
|
+
sessions: { open (the Postgres API's sessions logged in), byClient: [{ user ('' a token's), client (its
|
|
263
|
+
application_name), open, running }] }, share: (null, or what mason share has open) { port, fingerprint, sessions,
|
|
264
|
+
refusedLogins, droppedConnections, shares: [{ user, role, reads, openedAt, via, host, port, endsAt, state }] }, queries: { keptDays, day, week } (the last day's and the last 7
|
|
265
|
+
days' statements, each { days, covered (the days of it the request log keeps), statements, completed, refused,
|
|
266
|
+
failed, cancelled, rows, bytesSent, durationMs: { avg, p50, p95 } (the completed ones') or null }; { error }
|
|
267
|
+
when it could not be read), slowQueries, refusedQueries, refusedTotal (how many in all), unsupported: [{ gap
|
|
268
|
+
(its entry of docs/SQL-SUPPORT.md), title (what it is; null for an entry this Mason no longer has), runs
|
|
269
|
+
(the statements it refused in the last day), users, clients (application_name), last, example (the latest,
|
|
270
|
+
cut at 1,000 characters) }] (the most refused first: GET /api/unsupported) or { error },
|
|
271
|
+
recentErrors: { sync: [{ at, kind, failed, table, message }] (the latest failures of the sync and restarts,
|
|
272
|
+
newest last: mason logs --events), error (why they could not be read, or null), queries: [{ requestId,
|
|
273
|
+
startedAt, outcome, sqlstate, message, query, … }] (the latest statements refused, failed or cancelled, newest
|
|
274
|
+
last, as mason logs --requests --json shows each: mason logs --requests --failed), queriesError (or null) },
|
|
275
|
+
readiness (what mason wait waits on), ui (whether this run serves the web page: mason ui), next }.
|
|
276
|
+
Each table's kept is how it is kept current now (absent for a table the sync does not hold):
|
|
277
|
+
{ by: poll, sync: { mode: off } or { mode: poll, every (30s), seconds } (its entry's sync:, else the file's,
|
|
278
|
+
else off), syncedAt (its last whole read or poll), nextAt (when its next poll is due; null: off, or not polled
|
|
279
|
+
now), pending (only when it applies: why it is not kept current now, its phase included: it waits for
|
|
280
|
+
its first sync, it is being read whole, its first sync failed, it is read again whole, caught up first) }
|
|
215
281
|
or { by: recopy, copiedAt, pending (only when it applies, as the poll's) }
|
|
216
282
|
or { by: replication, slot, state (streamed, catching-up, waiting, left-out), why, position, lastCommit,
|
|
217
283
|
notRunning (why the stream is not running, null when it is) }; with Mason stopped: { running: false, project, source: { name,
|
|
218
284
|
file, url, error (the sources file's problem, or null) }, engine, tables: [{ table }], lastRun, lastStop:
|
|
219
285
|
{ at, how } or null, next }; hung: the same with hung: { pid, problem, why },
|
|
220
|
-
exit 1. In replication mode stream: { slot,
|
|
286
|
+
exit 1. In replication mode stream: { slot, slotWal: { walHeldBytes (the WAL the slot holds on the
|
|
287
|
+
database), walStatus (reserved, extended, unreserved, lost), safeWalSizeBytes (the WAL left before Postgres
|
|
288
|
+
invalidates it; null: max_slot_wal_keep_size unset, nothing caps it), at } or { error }, publication, running, failure, position,
|
|
221
289
|
applied, database: { lsn, at } or { error }, lag: { bytes, seconds }, lastBatch: { appliedAt, committedAt },
|
|
222
290
|
tables: [{ table, state (streamed, catching-up, waiting, left-out), why, catchUp: { phase (queued, polling,
|
|
223
291
|
applying), from, startedAt, held, retryAt }, applied: { inserts, updates, deletes, truncates } }] }.
|
|
@@ -226,14 +294,17 @@ const STATUS_USAGE = `usage: mason status [--json]
|
|
|
226
294
|
fill: { rows, bytesPerRow, seconds, basis (measured: from the last fill of its column; disk or text: modelled from
|
|
227
295
|
the column's bytes a row on disk, or its text's before it is on disk), rowsPerSecond } (the fill's estimate it
|
|
228
296
|
was decided on), filled: { rows, seconds } (what its own fill measured), queued (a backfill waiting for the fill
|
|
229
|
-
under way), keptAt, readyAt, why }]
|
|
297
|
+
under way), keptAt, readyAt, why }]. roles (a run with a roles file, mason/roles.yml: what a user added
|
|
298
|
+
with --role may read): { file, roles: [name], problem (what is wrong with the file now, every statement of a
|
|
299
|
+
user with a role refused while there is; null), default (the role of a user added without --role; null: the
|
|
300
|
+
file names none, and such a user reads nothing), all (the roles of tables: all) }
|
|
230
301
|
|
|
231
302
|
In replication mode a Stream section says where the stream is: its position against the database's (bytes behind),
|
|
232
303
|
when the newest change applied was committed and how long after it was applied, and each added table — streamed,
|
|
233
304
|
with the changes applied this run; being caught up by one poll; not streamed yet; or left out — and why.
|
|
234
305
|
|
|
235
306
|
Stored JSON paths: a JSON value statements keep reading (payload ->> 'customer_id' read by several statements, or by
|
|
236
|
-
one slow one) is stored as a column of the
|
|
307
|
+
one slow one) is stored as a column of the synced table and read from there, computed once when its row is written instead
|
|
237
308
|
of for every row of every statement; on a table whose fill would take a minute or more, only once the statements
|
|
238
309
|
reading it took a tenth of the fill's estimate in all. The estimate is the rate the last fill of the same JSON
|
|
239
310
|
column measured, once one has run, else modelled from the column's bytes on disk; the line says which. Each path is
|
|
@@ -244,11 +315,12 @@ const STATUS_USAGE = `usage: mason status [--json]
|
|
|
244
315
|
export const STATUS = {
|
|
245
316
|
needs: "project",
|
|
246
317
|
name: "status",
|
|
247
|
-
summary: "what Mason is doing: the database, each table
|
|
318
|
+
summary: "what Mason is doing: the database, each synced table, the Postgres API, its statements and errors",
|
|
248
319
|
usage: STATUS_USAGE,
|
|
249
320
|
options: { json: { type: "boolean" } },
|
|
250
|
-
json: ["running", "pid", "postgres", "source", "warnings", "folder", "project", "tables", "engine", "
|
|
251
|
-
"
|
|
321
|
+
json: ["running", "pid", "postgres", "source", "warnings", "folder", "project", "tables", "totals", "engine", "disk", "sessions", "stream", "jsonPaths", "queries", "slowQueries", "refusedQueries",
|
|
322
|
+
"refusedTotal", "unsupported", "recentErrors", "readiness", "ui",
|
|
323
|
+
"roles", "share", "lastRun", "lastStop", "hung", "next"],
|
|
252
324
|
};
|
|
253
325
|
export async function status(argv) {
|
|
254
326
|
const f = parseCommand(STATUS, argv);
|
|
@@ -266,7 +338,12 @@ export async function status(argv) {
|
|
|
266
338
|
const run = r0.run;
|
|
267
339
|
return guarded("status", json, async () => {
|
|
268
340
|
const st = styleOut();
|
|
269
|
-
const
|
|
341
|
+
const read = await readState(run);
|
|
342
|
+
// the name is read at the start, so an edit of the sources file's database: is said here until the run takes it
|
|
343
|
+
const answersAs = postgresLink(run.postgresUrl).database;
|
|
344
|
+
const fileSays = databaseMoved(r0.project, answersAs);
|
|
345
|
+
const s = fileSays !== null
|
|
346
|
+
? { ...read, warnings: [...read.warnings, `the sources file now says tools connect with ${fileSays}, and this run answers as ${answersAs}: mason stop && mason start takes it`] } : read;
|
|
270
347
|
const r = readiness(s);
|
|
271
348
|
// the statements the Postgres API answered slowly, and the ones it refused, with the reason (src/duck/requests.ts)
|
|
272
349
|
const slowReply = await callRun(run, "GET", "/api/slow");
|
|
@@ -274,30 +351,56 @@ export async function status(argv) {
|
|
|
274
351
|
const refusedReply = await callRun(run, "GET", "/api/refused");
|
|
275
352
|
const refused = refusedReply.status === 200 ? refusedReply.body.refused : [];
|
|
276
353
|
const refusedTotal = typeof refusedReply.body.total === "number" ? refusedReply.body.total : null;
|
|
354
|
+
// the same refusals grouped by the unsupported SQL they used (src/duck/gaps.ts): what to rewrite
|
|
355
|
+
const unsupportedReply = await callRun(run, "GET", `/api/unsupported?limit=${STATUS_UNSUPPORTED}`);
|
|
356
|
+
const unsupported = unsupportedReply.status === 200 ? unsupportedReply.body.unsupported
|
|
357
|
+
: { error: String(unsupportedReply.body.error ?? `the run answered ${unsupportedReply.status}`) };
|
|
358
|
+
// the sync's latest failures (src/duck/events.ts), and the run's restarts among them
|
|
359
|
+
const eventsReply = await callRun(run, "GET", `/api/events?failed=1&limit=${RECENT_ERRORS}`);
|
|
360
|
+
// and the latest statements refused, failed or cancelled: the request log's own page, the shape mason logs --requests --json prints
|
|
361
|
+
const failedReply = await callRun(run, "GET", `/api/requests?failed=1&limit=${RECENT_ERRORS}`);
|
|
362
|
+
const recentErrors = {
|
|
363
|
+
sync: eventsReply.status === 200 ? eventsReply.body.events : [],
|
|
364
|
+
error: eventsReply.status === 200 ? null : String(eventsReply.body.error ?? `the run answered ${eventsReply.status}`),
|
|
365
|
+
queries: failedReply.status === 200 ? failedReply.body.requests : [],
|
|
366
|
+
queriesError: failedReply.status === 200 ? null : String(failedReply.body.error ?? `the run answered ${failedReply.status}`),
|
|
367
|
+
};
|
|
368
|
+
// the last day's and week's statements (src/duck/requests.ts summary): asked here, never with the state
|
|
369
|
+
const queriesReply = await callRun(run, "GET", "/api/queries");
|
|
370
|
+
const queries = queriesReply.status === 200 ? queriesReply.body.queries
|
|
371
|
+
: { error: String(queriesReply.body.error ?? `the run answered ${queriesReply.status}`) };
|
|
277
372
|
const picked = s.tables.filter((t) => t.added);
|
|
278
373
|
// what to run next, the same for a person and under --json
|
|
279
374
|
const next = !picked.length ? addSteps(s.tables)
|
|
280
375
|
: r.ready ? [...READY_STEPS]
|
|
281
376
|
: r.failed.length ? [["mason logs", "the detail of each failure"], ["mason wait", "each failure, with the way out"]]
|
|
282
377
|
: [["mason wait", "until it is done"]];
|
|
283
|
-
// the times its supervisor started it again (src/cli/supervise.ts): a run that crashed says so, never only in a log
|
|
284
|
-
const restarts = restartsOf(run.home);
|
|
285
378
|
if (json) {
|
|
286
|
-
out(jsonText(STATUS, { running: true, pid: run.pid,
|
|
287
|
-
...s,
|
|
379
|
+
out(jsonText(STATUS, { running: true, pid: run.pid,
|
|
380
|
+
...s, postgres: { ...postgresFacts(run.postgresUrl), listener: run.postgresListener }, queries, slowQueries: slow, refusedQueries: refused, refusedTotal, unsupported, recentErrors, readiness: r }, next));
|
|
288
381
|
return 0;
|
|
289
382
|
}
|
|
290
383
|
const rows = [
|
|
291
384
|
["Project", `${run.name}, files in ${s.folder ?? "(none chosen)"}`],
|
|
292
385
|
...sourceRows(s),
|
|
293
|
-
|
|
294
|
-
["
|
|
295
|
-
...
|
|
386
|
+
masonRow(s.engine, run.pid),
|
|
387
|
+
["Synced data", `${s.engine.file}${s.engine.bytes === null ? "" : ` · ${fileSize(s.engine.bytes)}`}`],
|
|
388
|
+
...diskRows(s.disk),
|
|
389
|
+
// the copies in all (src/cli/readiness.ts syncTotals): a run from before them has none
|
|
390
|
+
...(s.totals ? [["Synced tables", totalsText(s.totals)]] : []),
|
|
391
|
+
// the times its supervisor started it again (src/cli/supervise.ts): a run that crashed says so, never only in a log
|
|
392
|
+
...restartsRows(s.engine.restarts),
|
|
296
393
|
["Postgres API", `${withPlaceholders(run.postgresUrl)} (mason user add <name> makes a user)`],
|
|
394
|
+
...sessionsRows(s.sessions),
|
|
297
395
|
// what it serves: its address, TLS or plain (src/cli/pg-tls.ts)
|
|
298
396
|
["Listener", listenerText(run.postgresListener, postgresLink(run.postgresUrl).port)],
|
|
397
|
+
...shareRows(s.share),
|
|
299
398
|
// the web page (mason ui), without its one-time token: mason ui prints the URL that carries it, and opens it
|
|
300
399
|
...(s.ui === true ? [["Web UI", `${run.control.url}/ (mason ui opens it)`]] : []),
|
|
400
|
+
// the roles file as the listener reads it now (mason user add --role): its roles, or what is wrong with it
|
|
401
|
+
...(s.roles && s.roles.file !== null
|
|
402
|
+
? [["Roles", s.roles.problem !== null ? st.red(`${s.roles.problem} (every statement of a user with a role is refused until it is fixed)`)
|
|
403
|
+
: `${s.roles.roles.join(", ")} (${s.roles.file}; ${s.roles.default != null ? `default ${s.roles.default} for a user added without --role` : "no default: a user with no role reads nothing"})`]] : []),
|
|
301
404
|
...s.warnings.map((w) => ["Warning", w]),
|
|
302
405
|
];
|
|
303
406
|
out(reportText({ headline: `Status: ${st.green("running")}`, details: rows }, st));
|
|
@@ -315,19 +418,29 @@ export async function status(argv) {
|
|
|
315
418
|
out(st.bold(`Tables (${picked.length} of ${s.tables.length} added):`));
|
|
316
419
|
for (const l of tableLines(s))
|
|
317
420
|
out(` ${paintBar(l, st)}`);
|
|
421
|
+
for (const l of syncErrorLines(recentErrors, st))
|
|
422
|
+
out(l);
|
|
423
|
+
for (const l of queryErrorLines(recentErrors, st))
|
|
424
|
+
out(l);
|
|
318
425
|
for (const l of jsonPathLines(s.jsonPaths ?? [], st))
|
|
319
426
|
out(l);
|
|
427
|
+
for (const l of queryLines(queries, st))
|
|
428
|
+
out(l);
|
|
320
429
|
for (const l of slowLines(slowReply.status === 200 ? slow : { error: String(slowReply.body.error ?? slowReply.status) }, st))
|
|
321
430
|
out(l);
|
|
322
431
|
for (const l of refusedLines(refusedReply.status === 200 ? { refused, total: refusedTotal } : { error: String(refusedReply.body.error ?? refusedReply.status) }, st))
|
|
323
432
|
out(l);
|
|
433
|
+
for (const l of unsupportedLines(unsupported, st))
|
|
434
|
+
out(l);
|
|
324
435
|
out("");
|
|
436
|
+
// the word the web page's Status says too (readinessWord)
|
|
437
|
+
const word = readinessWord(r);
|
|
325
438
|
if (r.ready)
|
|
326
|
-
out(withEnd(st.green(
|
|
439
|
+
out(withEnd(st.green(`${word}: every added table is synced and served.`), { next }));
|
|
327
440
|
else if (r.failed.length)
|
|
328
|
-
out(withEnd(`${st.red(
|
|
441
|
+
out(withEnd(`${st.red(`${word}:`)}\n${bullets(r.failed)}`, { next }));
|
|
329
442
|
else
|
|
330
|
-
out(withEnd(
|
|
443
|
+
out(withEnd(`${word}:\n${r.waiting.map((x) => ` - ${x}`).join("\n")}`, { next }));
|
|
331
444
|
return 0;
|
|
332
445
|
});
|
|
333
446
|
}
|
|
@@ -337,12 +450,15 @@ export async function status(argv) {
|
|
|
337
450
|
*/
|
|
338
451
|
export function sourceRows(s) {
|
|
339
452
|
return [
|
|
340
|
-
...(s.source.file ? [["Source", `${s.source.file} (the tables it names are added)`]] : []),
|
|
453
|
+
...(s.source.file ? [["Source", `${s.source.shownFile ?? s.source.file} (the tables it names are added)`]] : []),
|
|
341
454
|
["Database", s.source.demo ? `the demo database Mason hosts (${s.source.url} · Postgres ${s.source.version})`
|
|
342
455
|
: `${s.source.url} · Postgres ${s.source.version}${s.source.standby ? " · a read replica" : ""}`],
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
456
|
+
// in a web developer's words first (each table as its sync: says, streamed as they happen); the mechanism an operator needs in parentheses
|
|
457
|
+
["Changes", `each table as its sync: says${s.source.replication ? "" : " (a SELECT of its changed rows)"}${s.source.sync ? ` · sync: ${s.source.sync.default} ${s.source.sync.written ? "for every table without its own" : "(the default: the file writes none)"}` : ""} · ` +
|
|
458
|
+
(s.source.replication ? `sync: replication tables streamed as they happen (logical replication: slot ${s.source.replication.slot}, publication ${s.source.replication.publication}, ` +
|
|
459
|
+
`${s.source.replication.owned ? "Mason's own, carrying them alone" : "named: read as it is, never altered"}); the others by a SELECT of their changed rows`
|
|
460
|
+
: s.source.demo ? "the demo database is checked this way only"
|
|
461
|
+
: "sync: replication on an entry streams that table as it happens instead")],
|
|
346
462
|
];
|
|
347
463
|
}
|
|
348
464
|
/** An SQL statement on one line, cut at `max` characters (the whole of it is mason logs --requests --json's). */
|
|
@@ -359,7 +475,7 @@ export function jsonPathLines(paths, st) {
|
|
|
359
475
|
const dropped = paths.length - live.length;
|
|
360
476
|
if (!live.length && !dropped)
|
|
361
477
|
return [];
|
|
362
|
-
const lines = [`\n${st.bold("Stored JSON paths")} ${st.dim("(read from a column of the
|
|
478
|
+
const lines = [`\n${st.bold("Stored JSON paths")} ${st.dim("(read from a column of the synced table, computed once per row written; MASON_STORE_JSON_PATHS=off turns it off)")}:`];
|
|
363
479
|
for (const p of live) {
|
|
364
480
|
const pct = p.rowsTotal > 0 ? Math.min(99, Math.floor((p.rowsDone / p.rowsTotal) * 100)) : 0;
|
|
365
481
|
// a path its fill's cost holds back says so: its why is the decision, with the estimate
|
|
@@ -371,6 +487,51 @@ export function jsonPathLines(paths, st) {
|
|
|
371
487
|
lines.push(` ${st.dim(`${dropped} dropped (mason status --json says why)`)}`);
|
|
372
488
|
return lines;
|
|
373
489
|
}
|
|
490
|
+
/** The copies in all, as mason status's `Copies` row says them: `3 of 3 added synced, 1,234 rows, the last synced 5 s ago`. */
|
|
491
|
+
export function totalsText(t, now = Date.now()) {
|
|
492
|
+
const last = t.syncedAt === null ? "" : `, the last synced ${duration((now - Date.parse(t.syncedAt)) / 1000)} ago`;
|
|
493
|
+
return `${n(t.copied)} of ${n(t.added)} added synced, ${n(t.rows)} row${t.rows === 1 ? "" : "s"}${t.failed ? `, ${n(t.failed)} failed` : ""}${last}`;
|
|
494
|
+
}
|
|
495
|
+
/** How many of the latest sync failures and failed statements mason status shows (`recentErrors`); mason logs --events and --requests --failed list each. */
|
|
496
|
+
export const RECENT_ERRORS = 5;
|
|
497
|
+
/** One event of the sync on one line (src/duck/events.ts): when, what (a failure in red), its table, its reason's first line. */
|
|
498
|
+
export function eventLine(e, st) {
|
|
499
|
+
return `${e.at.slice(0, 19).replace("T", " ")} ${(e.failed ? st.red : st.green)(e.kind.padEnd(18))} ${e.table ?? "(the sync)"} ${firstLine(e.message)}`;
|
|
500
|
+
}
|
|
501
|
+
/** mason status's sync errors: the latest few, a line each, newest last; none, no section. */
|
|
502
|
+
export function syncErrorLines(r, st) {
|
|
503
|
+
if (r.error !== null)
|
|
504
|
+
return [`\n${st.bold("Sync errors")}: could not read them (${firstLine(r.error)}; mason logs has the detail)`];
|
|
505
|
+
if (!r.sync.length)
|
|
506
|
+
return [];
|
|
507
|
+
return [`\n${st.bold("Sync errors")} ${st.dim("(the latest, newest last; mason logs --events lists every event)")}:`, ...r.sync.map((e) => ` ${eventLine(e, st)}`)];
|
|
508
|
+
}
|
|
509
|
+
/** mason status's statement errors: the latest few refused, failed or cancelled, a line each (`requestLine`), newest last; none, no section. */
|
|
510
|
+
export function queryErrorLines(r, st) {
|
|
511
|
+
if (r.queriesError !== null)
|
|
512
|
+
return [`\n${st.bold("Query errors")}: could not read them (${firstLine(r.queriesError)}; mason logs has the detail)`];
|
|
513
|
+
if (!r.queries.length)
|
|
514
|
+
return [];
|
|
515
|
+
return [`\n${st.bold("Query errors")} ${st.dim("(the latest refused, failed or cancelled, newest last; mason logs --requests --failed lists each)")}:`, ...r.queries.map((q) => ` ${requestLine(q, st)}`)];
|
|
516
|
+
}
|
|
517
|
+
/** Bytes as a person reads them: B, KiB, MiB, GiB (the web page's statements say the same, src/web/shared.js). */
|
|
518
|
+
export function bytesText(b) { return b < 1024 ? `${n(b)} B` : fileSize(b); }
|
|
519
|
+
/**
|
|
520
|
+
* mason status's queries (src/duck/requests.ts summary): the last day's and the last week's statements, a line each —
|
|
521
|
+
* how many, each outcome, the completed ones' times, the rows and bytes sent — and, when the request log keeps fewer
|
|
522
|
+
* days than the week, that the week is only what it keeps.
|
|
523
|
+
*/
|
|
524
|
+
export function queryLines(q, st) {
|
|
525
|
+
if ("error" in q)
|
|
526
|
+
return [`\n${st.bold("Queries")}: could not read them (${firstLine(q.error)}; mason logs has the detail)`];
|
|
527
|
+
const line = (label, w) => {
|
|
528
|
+
const counts = `${n(w.statements)} statement${w.statements === 1 ? "" : "s"}: ${n(w.completed)} completed, ${n(w.refused)} refused, ${n(w.failed)} failed, ${n(w.cancelled)} cancelled`;
|
|
529
|
+
const times = w.durationMs === null ? "" : ` · ${msText(w.durationMs.avg)} / ${msText(w.durationMs.p50)} / ${msText(w.durationMs.p95)}`;
|
|
530
|
+
const short = w.covered < w.days ? st.dim(` (the request log keeps ${w.covered} day${w.covered === 1 ? "" : "s"}: MASON_HISTORY_DAYS)`) : "";
|
|
531
|
+
return ` ${label.padEnd(10)} ${counts}${times} · ${n(w.rows)} rows, ${bytesText(w.bytesSent)} sent${short}`;
|
|
532
|
+
};
|
|
533
|
+
return [`\n${st.bold("Queries")} ${st.dim("(the completed ones' time: average / p50 / p95; mason logs --requests lists each)")}:`, line("last day", q.day), line("last week", q.week)];
|
|
534
|
+
}
|
|
374
535
|
/** mason status's slow statements (src/duck/requests.ts slow), each on one line with its times. */
|
|
375
536
|
export function slowLines(slow, st) {
|
|
376
537
|
if (!Array.isArray(slow))
|
|
@@ -379,7 +540,7 @@ export function slowLines(slow, st) {
|
|
|
379
540
|
return [];
|
|
380
541
|
const lines = [`\n${st.bold("Slow queries")} ${st.dim(`(the last day, still ${(SLOW_MS / 1000).toLocaleString("en-US")} s or more; slowest first)`)}:`];
|
|
381
542
|
for (const q of slow) {
|
|
382
|
-
const times = `${
|
|
543
|
+
const times = `${msText(q.latestMs)} last time (${q.runs} run${q.runs === 1 ? "" : "s"}, median ${msText(q.medianMs)})`;
|
|
383
544
|
lines.push(` ${times}, last ${q.last.slice(0, 16).replace("T", " ")} UTC`, ` ${oneLine(q.payload)}`);
|
|
384
545
|
}
|
|
385
546
|
return lines;
|
|
@@ -400,27 +561,80 @@ export function refusedLines(r, st) {
|
|
|
400
561
|
}
|
|
401
562
|
return lines;
|
|
402
563
|
}
|
|
403
|
-
/**
|
|
404
|
-
|
|
564
|
+
/** how many entries of the unsupported SQL mason status shows (and its --json carries) */
|
|
565
|
+
const STATUS_UNSUPPORTED = 5;
|
|
566
|
+
/**
|
|
567
|
+
* `Unsupported SQL` in mason status: the last day's refused statements grouped by the registry entry that refused them
|
|
568
|
+
* (src/duck/requests.ts unsupported), the most refused first — how many, what they used, who sent them, and the latest
|
|
569
|
+
* — so a person knows what to rewrite; docs/SQL-SUPPORT.md has each entry's way out. None: no section.
|
|
570
|
+
*/
|
|
571
|
+
export function unsupportedLines(u, st) {
|
|
572
|
+
if ("error" in u)
|
|
573
|
+
return [`\n${st.bold("Unsupported SQL")}: could not read it (${firstLine(u.error)}; mason logs has the detail)`];
|
|
574
|
+
if (!u.length)
|
|
575
|
+
return [];
|
|
576
|
+
const lines = [`\n${st.bold("Unsupported SQL")} ${st.dim("(the last day's refused statements by what they used, the most first)")}:`];
|
|
577
|
+
for (const g of u) {
|
|
578
|
+
const title = g.title ?? "SQL this Mason no longer lists";
|
|
579
|
+
const who = [...(g.users.length ? [`as ${g.users.join(", ")}`] : []), ...(g.clients.length ? [`from ${g.clients.join(", ")}`] : [])];
|
|
580
|
+
lines.push(` ${title}: ${n(g.runs)} statement${g.runs === 1 ? "" : "s"}${who.length ? ` ${who.join(" ")}` : ""}`);
|
|
581
|
+
lines.push(` last ${g.last.slice(0, 16).replace("T", " ")} — ${oneLine(g.example)}`);
|
|
582
|
+
lines.push(` How to rewrite it: see docs/SQL-SUPPORT.md › ${title}`);
|
|
583
|
+
}
|
|
584
|
+
return lines;
|
|
585
|
+
}
|
|
586
|
+
/** A file's size, as a person reads one (mason status's copy, mason usage's) */
|
|
587
|
+
export function fileSize(b) {
|
|
405
588
|
return b >= 1024 ** 3 ? `${(b / 1024 ** 3).toFixed(1)} GiB` : b >= 1024 ** 2 ? `${(b / 1024 ** 2).toFixed(1)} MiB` : `${(b / 1024).toFixed(1)} KiB`;
|
|
406
589
|
}
|
|
407
|
-
/**
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
590
|
+
/** `Mason` in mason status: the run's own version (a CLI of another version says the run's), its pid, and how long it
|
|
591
|
+
* has been up (a run from before it: the CLI's version, no uptime) */
|
|
592
|
+
export function masonRow(e, pid) {
|
|
593
|
+
return ["Mason", `${e.version ?? version()}, pid ${pid} · one process${e.startedAt ? ` · up ${spanText(e.uptimeSeconds ?? 0)}, since ${e.startedAt.slice(0, 19).replace("T", " ")} UTC` : ""}`];
|
|
594
|
+
}
|
|
595
|
+
/** `Sessions` in mason status: how many are logged in to the Postgres API, how many run a statement, and by whom (the
|
|
596
|
+
* three with the most, then how many others) */
|
|
597
|
+
export function sessionsRows(s) {
|
|
598
|
+
if (s === undefined)
|
|
599
|
+
return [];
|
|
600
|
+
if (s.open === 0)
|
|
601
|
+
return [["Sessions", "none open"]];
|
|
602
|
+
const running = s.byClient.reduce((n, c) => n + c.running, 0);
|
|
603
|
+
const who = s.byClient.slice(0, 3).map((c) => `${c.open} ${c.user === "" ? "by a token" : `as ${c.user}`}${c.client === "" ? "" : ` (${c.client})`}`);
|
|
604
|
+
const others = s.byClient.length - who.length;
|
|
605
|
+
return [["Sessions", `${s.open} open, ${running} running a statement · ${who.join(", ")}${others > 0 ? `, and ${others} other client${others === 1 ? "" : "s"}` : ""} (mason logs --running lists the statements)`]];
|
|
606
|
+
}
|
|
607
|
+
/** `Share` in mason status: each share open now (mason share) and what its socket met; no row when none is. */
|
|
608
|
+
export function shareRows(s, now = Date.now()) {
|
|
609
|
+
if (s === undefined || s === null)
|
|
610
|
+
return [];
|
|
611
|
+
const each = s.shares.map((h) => {
|
|
612
|
+
const where = h.host === null ? "its tunnel is opening" : `${h.host}:${h.port} via ${h.via ?? "a tunnel"}${h.state === "reconnecting" ? " (reconnecting)" : ""}`;
|
|
613
|
+
const ends = h.endsAt === null ? "" : `, ends in about ${Math.max(0, Math.round((Date.parse(h.endsAt) - now) / 60_000))} min`;
|
|
614
|
+
return ["Share", `${where}, as ${h.user}, reads ${h.role === null ? h.reads : `as the role ${h.role}`}${ends}`];
|
|
615
|
+
});
|
|
616
|
+
return [...each, ["Share socket", `127.0.0.1:${s.port}, TLS required, SHA-256 ${s.fingerprint} · ${s.sessions} session${s.sessions === 1 ? "" : "s"}, ${s.refusedLogins} login${s.refusedLogins === 1 ? "" : "s"} refused, ${s.droppedConnections} dropped before a login`]];
|
|
415
617
|
}
|
|
416
|
-
/** `
|
|
417
|
-
|
|
418
|
-
|
|
618
|
+
/** `Disk` in mason status: the bytes free where the synced data is, and what the first syncs running are projected to add (none: no
|
|
619
|
+
* row, for a run from before it) */
|
|
620
|
+
export function diskRows(d) {
|
|
621
|
+
if (d === undefined)
|
|
622
|
+
return [];
|
|
623
|
+
const adds = d.projectedBytes === null ? "" : ` · the first syncs running are projected to add about ${fileSize(d.projectedBytes)} more`;
|
|
624
|
+
if (d.freeBytes === null)
|
|
625
|
+
return [["Disk", `${d.error ?? "the free space could not be read"}${adds}`]];
|
|
626
|
+
return [["Disk", `${fileSize(d.freeBytes)} free in ${d.path}${adds}${d.projectedBytes !== null && d.projectedBytes > d.freeBytes ? " — more than is free: the sync stops before the disk fills" : ""}`]];
|
|
627
|
+
}
|
|
628
|
+
/** `Restarts` in mason status: how many times the run was started again, and the last one's reason (none: no row) —
|
|
629
|
+
* the run's own `engine.restarts` while it is up, the same fact read from the file while it is not (`restartsFact`) */
|
|
630
|
+
export function restartsRows(r) {
|
|
631
|
+
if (r === undefined)
|
|
632
|
+
return [];
|
|
633
|
+
if ("error" in r)
|
|
419
634
|
return [["Restarts", r.error]];
|
|
420
|
-
|
|
421
|
-
if (last === undefined)
|
|
635
|
+
if (r.last === null)
|
|
422
636
|
return [];
|
|
423
|
-
return [["Restarts", `${r.
|
|
637
|
+
return [["Restarts", `${r.count}, the last ${ago(r.last.at)}: ${r.last.why}, after ${r.last.upSeconds} s up (mason logs --start has its last lines)`]];
|
|
424
638
|
}
|
|
425
639
|
/**
|
|
426
640
|
* mason status with Mason stopped: the same rows and tables, "not running" where something is not. The
|
|
@@ -434,19 +648,23 @@ function stoppedStatus(project, json, hung = null) {
|
|
|
434
648
|
const src = projectSource(project);
|
|
435
649
|
const tables = "message" in src ? [] : src.tables.map((e) => e.table);
|
|
436
650
|
const database = "message" in src ? null : maskedUrl(src.resolved);
|
|
651
|
+
// the name a tool connects with once it starts: null when the file cannot say it (`source.error` says why)
|
|
652
|
+
const connect = projectDatabase(project);
|
|
653
|
+
const connectName = typeof connect === "string" ? connect : null;
|
|
437
654
|
const duckFile = path.join(project.dir, DUCK_FILE);
|
|
438
655
|
// the copy's file, when a run has made one: its size says what the next mason start resumes with
|
|
439
656
|
const bytes = fs.existsSync(duckFile) ? fs.statSync(duckFile).size : null;
|
|
440
|
-
const restarts =
|
|
657
|
+
const restarts = restartsFact(project.dir);
|
|
441
658
|
const lastStop = project.config.lastStop ?? null;
|
|
442
|
-
const next = hung === null ? ("message" in src && src.next?.length ? src.next : [["mason start", "start Mason for this project: what it
|
|
659
|
+
const next = hung === null ? ("message" in src && src.next?.length ? src.next : [["mason start", "start Mason for this project: what it synced is kept, and polled again"]])
|
|
443
660
|
: hung.problem.next ?? [["mason stop", "end it"], ["mason logs", "what it was doing"]];
|
|
444
661
|
const state = hung === null ? "not running" : "hung";
|
|
445
662
|
if (json) {
|
|
446
663
|
out(jsonText(STATUS, { running: false, ...(hung === null ? {} : { hung: { pid: hung.run.pid, problem: hung.problem.problem, why: hung.problem.why } }),
|
|
664
|
+
postgres: { database: connectName },
|
|
447
665
|
project: { name: project.config.name, folder: project.folder },
|
|
448
666
|
source: { name: "message" in src ? null : src.name, file: "message" in src ? null : src.file, url: database, error: "message" in src ? errorJson(src) : null },
|
|
449
|
-
engine: {
|
|
667
|
+
engine: { file: duckFile, bytes, restarts }, tables: tables.map((table) => ({ table })),
|
|
450
668
|
lastRun: project.config.lastRun ?? null, lastStop }, next));
|
|
451
669
|
return hung === null ? 0 : 1;
|
|
452
670
|
}
|
|
@@ -457,9 +675,9 @@ function stoppedStatus(project, json, hung = null) {
|
|
|
457
675
|
...(database === null ? [] : [["Database", `${database} (not checked: Mason ${hung === null ? "is not running" : "does not answer"})`]]),
|
|
458
676
|
["Mason", hung === null ? `${version()} · not running${project.config.lastRun ? ` (last started ${ago(project.config.lastRun)}${ended})` : ""}`
|
|
459
677
|
: `${version()}, pid ${hung.run.pid} · running but does not answer its commands: ${hung.problem.why}`],
|
|
460
|
-
["
|
|
678
|
+
["Synced data", bytes === null ? "none yet (mason start makes it)" : `${duckFile} · ${fileSize(bytes)}`],
|
|
461
679
|
...restartsRows(restarts),
|
|
462
|
-
["Postgres API", hung === null ?
|
|
680
|
+
["Postgres API", hung === null ? `not running${connectName === null ? "" : ` (a tool connects with the database ${connectName})`}` : `${withPlaceholders(hung.run.postgresUrl)} (held by the hung run; it may not answer)`],
|
|
463
681
|
] }, st));
|
|
464
682
|
out("");
|
|
465
683
|
if (!tables.length)
|
|
@@ -473,8 +691,9 @@ function stoppedStatus(project, json, hung = null) {
|
|
|
473
691
|
out(endText({ tips: [], next }, st));
|
|
474
692
|
return hung === null ? 0 : 1;
|
|
475
693
|
}
|
|
476
|
-
|
|
477
|
-
|
|
694
|
+
/** How long ago a time was, as mason status says it (`12s ago`, `5 min ago`, `3 h ago`; the web page's `agoText`, held equal) */
|
|
695
|
+
export function ago(iso, now = Date.now()) {
|
|
696
|
+
const s = Math.max(0, Math.round((now - Date.parse(iso)) / 1000));
|
|
478
697
|
return s < 90 ? `${s}s ago` : s < 5400 ? `${Math.round(s / 60)} min ago` : `${Math.round(s / 3600)} h ago`;
|
|
479
698
|
}
|
|
480
699
|
// ------------------------------------------------------------------ mason tables
|
|
@@ -492,8 +711,8 @@ const TABLES_USAGE = `usage: mason tables [<table>…] [--all] [--json]
|
|
|
492
711
|
--all every table with its columns, not only the named ones
|
|
493
712
|
--json as data: { database, source: { name (the source's: its sources file's), file (the sources file) },
|
|
494
713
|
tables: [{ table, rows, bytes, primaryKey, comment, addable,
|
|
495
|
-
reason, added, addedColumns (the columns
|
|
496
|
-
file
|
|
714
|
+
reason, added, addedColumns (the columns it syncs: null when not added), copyKey (the key: the sources
|
|
715
|
+
file keys it by, null: its primary key), columns: [{ name, type, nullable, comment, references }] }], next }
|
|
497
716
|
`;
|
|
498
717
|
export const TABLES = {
|
|
499
718
|
// the project, not the run (the hands-on review of #451, item 15): what it lists is the database's catalog and the
|
|
@@ -530,7 +749,7 @@ export async function tables(argv) {
|
|
|
530
749
|
// what to run next: the first table not yet added, as a whole list or as the named tables
|
|
531
750
|
const one = shown.find((t) => !t.added && t.reason === null);
|
|
532
751
|
const next = f.positionals.length
|
|
533
|
-
? (one ? [[`mason add ${tableArg(one.table)}`, `
|
|
752
|
+
? (one ? [[`mason add ${tableArg(one.table)}`, `sync it into Mason (--columns ${one.columns.slice(0, 2).map((c) => c.name).join(",")}: those only)`]] : [])
|
|
534
753
|
: addSteps(shown, "tables");
|
|
535
754
|
if (json) {
|
|
536
755
|
out(jsonText(TABLES, { database: s.database, source: { name: s.name, file: s.file }, tables: shown.map((t) => ({
|
|
@@ -557,7 +776,7 @@ export async function tables(argv) {
|
|
|
557
776
|
}
|
|
558
777
|
for (const t of shown) {
|
|
559
778
|
const size = t.rows === null ? "rows unknown" : `~${n(t.rows)} rows`;
|
|
560
|
-
const added = t.added ? ` · added (${t.addedColumns.length === t.columns.length ? "every column" : t.addedColumns.join(", ")}${t.copyKey ? `;
|
|
779
|
+
const added = t.added ? ` · added (${t.addedColumns.length === t.columns.length ? "every column" : t.addedColumns.join(", ")}${t.copyKey ? `; keyed by ${t.copyKey.join(", ")}` : ""})` : "";
|
|
561
780
|
out(`${t.table} · ${size}${t.key.length ? ` · key ${t.key.join(", ")}` : ""}${added}`);
|
|
562
781
|
if (t.comment)
|
|
563
782
|
out(` ${t.comment}`);
|
|
@@ -648,7 +867,7 @@ async function withoutRun(cmd, p, json) {
|
|
|
648
867
|
return null;
|
|
649
868
|
}
|
|
650
869
|
if (st.kind === "up") {
|
|
651
|
-
failed(cmd, { message: `Mason started for ${p.config.name} while mason ${cmd} ran`, code: "refused", fix: `run mason ${cmd} again: it asks the run, which
|
|
870
|
+
failed(cmd, { message: `Mason started for ${p.config.name} while mason ${cmd} ran`, code: "refused", fix: `run mason ${cmd} again: it asks the run, which syncs at once` }, { json });
|
|
652
871
|
return null;
|
|
653
872
|
}
|
|
654
873
|
const c = await readCatalog(p);
|
|
@@ -656,7 +875,7 @@ async function withoutRun(cmd, p, json) {
|
|
|
656
875
|
failed(cmd, c, { json });
|
|
657
876
|
return null;
|
|
658
877
|
}
|
|
659
|
-
return { db: { tables: c.facts, database: c.database
|
|
878
|
+
return { db: { tables: c.facts, database: c.database },
|
|
660
879
|
tables: c.facts.map((t) => catalogTable(t, c.src.tables.find((e) => e.table === t.source))), file: c.src.file,
|
|
661
880
|
edit: () => sourceToEdit(p.folder, p.root, () => projectSource(p)) };
|
|
662
881
|
}
|
|
@@ -677,32 +896,48 @@ async function viaRun(run, route, body) {
|
|
|
677
896
|
return r.status === 200 ? { ok: r.body } : { status: r.status, error: String(r.body.error) };
|
|
678
897
|
}
|
|
679
898
|
// ------------------------------------------------------------------ mason add
|
|
680
|
-
const ADD_USAGE = `usage: mason add <table>… [--columns <column>,…] [--include <column>,…] [--key <column>,…] [--json]
|
|
899
|
+
const ADD_USAGE = `usage: mason add <table>… [--columns <column>,…] [--include <column>,…] [--key <column>,…] [--sync <sync>] [--json]
|
|
681
900
|
|
|
682
|
-
|
|
901
|
+
Syncs tables into Mason: each one is added to the project's sources file (mason/sources/<name>.yml), under tables:
|
|
683
902
|
mason add orders every column: orders: {}
|
|
684
903
|
mason add orders --columns id,status,total those columns: orders: { columns: [id, status, total] }
|
|
685
904
|
mason add orders customers several tables, every column of each
|
|
686
905
|
mason add users --include password_hash every column, and one that looks like a secret
|
|
687
|
-
mason add event_log --key tenant_id,id a table with no primary key,
|
|
688
|
-
|
|
906
|
+
mason add event_log --key tenant_id,id a table with no primary key, keyed by those columns
|
|
907
|
+
mason add orders --sync "poll 30s" polled every 30 seconds: orders: { sync: poll 30s }
|
|
908
|
+
A table is its name (orders) or schema.table (billing.invoices). Its primary key is always synced. The rest of the
|
|
689
909
|
file — your comments, other tables — stays as it was.
|
|
690
910
|
|
|
911
|
+
After its first sync, a table is kept current as the file's sync: says — at the top for every table (mason init
|
|
912
|
+
writes sync: off: read again only by mason sync <table>), or on its own entry. --sync writes the table's own:
|
|
913
|
+
off, poll <interval> (poll 5s, poll 5m, poll 1h; 1s to 24h), or replication (the database's change log, through
|
|
914
|
+
the project's slot). On a table already added, --sync alone changes its sync: line and nothing else; its synced data is
|
|
915
|
+
kept and caught up by one poll. Without --sync a new entry follows the file's — but a table of more than
|
|
916
|
+
${BIG_TABLE_ROWS.toLocaleString("en-US")} rows under a file that polls is written sync: off and said, since each poll scans a table whole on your
|
|
917
|
+
database, and a table replication cannot keep (no primary key: --key; REPLICA IDENTITY NOTHING) under a file whose
|
|
918
|
+
sync: is replication is written sync: poll 5m and said. A table of tens of millions of rows set to poll more often
|
|
919
|
+
than every minute is warned of, with an estimate of what each poll costs your database.
|
|
920
|
+
|
|
691
921
|
A column that looks like a secret (password, token, api_key, private_key, …) is left out unless named, and said:
|
|
692
|
-
it is written as exclude: [password_hash], since anyone with a user's password can read every
|
|
922
|
+
it is written as exclude: [password_hash], since anyone with a user's password can read every synced column. Name
|
|
693
923
|
one with --columns or --include to have it.
|
|
694
924
|
|
|
695
|
-
With Mason running the
|
|
925
|
+
With Mason running the first sync starts at once; mason wait returns when it is done and the table is served. Before
|
|
696
926
|
mason start (mason init && mason add orders && mason start) the entry is written, checked against the database as
|
|
697
|
-
the run checks it, and the
|
|
698
|
-
and the table is
|
|
927
|
+
the run checks it, and the first sync begins at mason start. Adding a table again with other columns changes its entry,
|
|
928
|
+
and the table is synced again whole with them.
|
|
699
929
|
|
|
700
930
|
--columns <a,b,…> only these columns (one table at a time: the list is that table's)
|
|
701
931
|
--include <a,b,…> these columns as well as every other one — a column that looks like a secret, named on purpose
|
|
702
|
-
--key <a,b,…> for a table with no primary key: the columns that tell its rows apart, which it is
|
|
703
|
-
table at a time;
|
|
932
|
+
--key <a,b,…> for a table with no primary key: the columns that tell its rows apart, which it is keyed by (one
|
|
933
|
+
table at a time; polled or off: logical replication cannot apply a change by them)
|
|
934
|
+
--sync <sync> how it is kept current after its first sync, written on its entry: off, "poll 30s" or replication
|
|
704
935
|
--json as data: { added: [{ table, entry, columns (null: every one), exclude, key, addedKey (key columns
|
|
705
|
-
added to --columns), skippedSecrets, already (the file said so already)
|
|
936
|
+
added to --columns), skippedSecrets, already (the file said so already), sync: { value (off, poll
|
|
937
|
+
30s), from (entry: its own sync:, file: the file's, default: off, as the file writes none) },
|
|
938
|
+
bigOff (it is over ${BIG_TABLE_ROWS.toLocaleString("en-US")} rows and its entry was written sync: off), notReplicated (why
|
|
939
|
+
its entry was written sync: poll 5m under a file whose sync: is replication, or null), pollCost (an estimate of
|
|
940
|
+
what a frequent poll of a big table costs your database, or null) } or { table, error }],
|
|
706
941
|
file (the sources file), error (when a table was not added: { code, message, why }), next };
|
|
707
942
|
exit 1 when a table could not be added
|
|
708
943
|
`;
|
|
@@ -710,11 +945,18 @@ export const ADD = {
|
|
|
710
945
|
// the project, not the run: the sources file is the list, so a table is added before mason start as well as after
|
|
711
946
|
needs: "project",
|
|
712
947
|
name: "add",
|
|
713
|
-
summary: "
|
|
948
|
+
summary: "sync tables into Mason, adding them to the sources file: mason add orders (--columns id,status: those only)",
|
|
714
949
|
usage: ADD_USAGE,
|
|
715
|
-
options: { json: { type: "boolean" }, columns: { type: "string" }, include: { type: "string" }, key: { type: "string" } },
|
|
950
|
+
options: { json: { type: "boolean" }, columns: { type: "string" }, include: { type: "string" }, key: { type: "string" }, sync: { type: "string" } },
|
|
716
951
|
json: ["added", "file", "error", "next"],
|
|
717
952
|
};
|
|
953
|
+
/** `--sync`, read by the sources file's own grammar (src/lib/table-sync.ts parseSync): a sync, or the usage's problem */
|
|
954
|
+
export function addSync(v) {
|
|
955
|
+
if (v === undefined)
|
|
956
|
+
return undefined;
|
|
957
|
+
const s = parseSync(v);
|
|
958
|
+
return "problem" in s ? `--sync ${JSON.stringify(v)}: ${s.problem}` : s;
|
|
959
|
+
}
|
|
718
960
|
/**
|
|
719
961
|
* The tables and the columns each one's entry copies (add takes --columns, not a DSL):
|
|
720
962
|
* `["*"]` for every column, the --columns list for one table, and --include's columns on top of either; `key`, --key's
|
|
@@ -751,6 +993,12 @@ export async function add(argv) {
|
|
|
751
993
|
const specs = addSpecs(f.positionals, f.values.columns, f.values.include, f.values.key);
|
|
752
994
|
if (typeof specs === "string")
|
|
753
995
|
return failed("add", { message: specs, code: "usage", usage: ADD_USAGE }, { json });
|
|
996
|
+
const sync = addSync(f.values.sync);
|
|
997
|
+
if (typeof sync === "string")
|
|
998
|
+
return failed("add", { message: sync, code: "usage", usage: ADD_USAGE }, { json });
|
|
999
|
+
// --sync alone: a table already added keeps its columns and key, and only its sync: line is written
|
|
1000
|
+
const keep = sync !== undefined && f.values.columns === undefined && f.values.include === undefined && f.values.key === undefined;
|
|
1001
|
+
const how = { ...(sync !== undefined ? { sync } : {}), keep };
|
|
754
1002
|
const where = await reach(ADD, json);
|
|
755
1003
|
// (a state with no project never meets the need: reach said it)
|
|
756
1004
|
if (where === null || where.project === null)
|
|
@@ -761,8 +1009,11 @@ export async function add(argv) {
|
|
|
761
1009
|
return guarded("add", json, async () => {
|
|
762
1010
|
const s = await readState(run);
|
|
763
1011
|
return addTables({ tables: s.tables, file: s.source.file ?? null,
|
|
764
|
-
pick: (table, columns, key) => viaRun(run, "/api/pick", { table, picked: true, columns, ...(key ? { key } : {}) }),
|
|
765
|
-
|
|
1012
|
+
pick: (table, columns, key) => viaRun(run, "/api/pick", { table, picked: true, columns, ...(key ? { key } : {}), ...(sync ? { sync: syncText(sync) } : {}), keep }),
|
|
1013
|
+
// --sync alone on tables already added: no copy starts (each is caught up by one poll), so wait has nothing to wait for
|
|
1014
|
+
next: (n, wereAdded) => keep && wereAdded === n
|
|
1015
|
+
? [["mason status", `${n === 1 ? "its sync and when it was last synced: it is" : "their sync and when each was last synced: each is"} caught up by one poll, never synced again whole`], READY_STEPS[0]]
|
|
1016
|
+
: [["mason wait", `until ${n === 1 ? "its first sync finishes" : "their first syncs finish"} (the sync has started)`], READY_STEPS[0]] }, specs, json);
|
|
766
1017
|
});
|
|
767
1018
|
}
|
|
768
1019
|
// Mason down: the command writes the entry itself, checked as the run checks it; the copy begins at mason start
|
|
@@ -770,14 +1021,15 @@ export async function add(argv) {
|
|
|
770
1021
|
if (down === null)
|
|
771
1022
|
return 1;
|
|
772
1023
|
return addTables({ tables: down.tables, file: down.file,
|
|
773
|
-
pick: async (table, columns, key) => edited(() => addEntry(down.edit(), down.db, table, columns, key)),
|
|
774
|
-
next: () => [["mason start", "Mason in the background, for this project: the
|
|
775
|
-
["mason wait", "then, until every added table is
|
|
1024
|
+
pick: async (table, columns, key) => edited(() => addEntry(down.edit(), down.db, table, columns, key, how)),
|
|
1025
|
+
next: () => [["mason start", "Mason in the background, for this project: the first sync of every added table begins"],
|
|
1026
|
+
["mason wait", "then, until every added table is synced and served"]] }, specs, json);
|
|
776
1027
|
}
|
|
777
1028
|
/** `mason add`'s checklist and answer, the same whichever way the tables are added (`AddWay`). */
|
|
778
1029
|
async function addTables(way, specs, json) {
|
|
779
1030
|
const results = [];
|
|
780
1031
|
const added = [];
|
|
1032
|
+
let wereAdded = 0;
|
|
781
1033
|
let file = way.file;
|
|
782
1034
|
// the tables that could not be added, each with its reason and code: the command's failure, in the failure shape
|
|
783
1035
|
const refused = [];
|
|
@@ -815,8 +1067,11 @@ async function addTables(way, specs, json) {
|
|
|
815
1067
|
}
|
|
816
1068
|
const b = r.ok;
|
|
817
1069
|
file = b.file;
|
|
818
|
-
results.push({ table: b.table, entry: b.entry, columns: b.columns, exclude: b.exclude, key: b.key, addedKey: b.addedKey, skippedSecrets: b.skippedSecrets, already: b.already
|
|
1070
|
+
results.push({ table: b.table, entry: b.entry, columns: b.columns, exclude: b.exclude, key: b.key, addedKey: b.addedKey, skippedSecrets: b.skippedSecrets, already: b.already,
|
|
1071
|
+
sync: b.sync, bigOff: b.bigOff, notReplicated: b.notReplicated, pollCost: b.pollCost });
|
|
819
1072
|
added.push(b.table);
|
|
1073
|
+
if (t.added)
|
|
1074
|
+
wereAdded++;
|
|
820
1075
|
if (json)
|
|
821
1076
|
continue;
|
|
822
1077
|
const l = addedLines(b, shown(b.file));
|
|
@@ -824,7 +1079,7 @@ async function addTables(way, specs, json) {
|
|
|
824
1079
|
for (const x of l.notes)
|
|
825
1080
|
note(x);
|
|
826
1081
|
}
|
|
827
|
-
const next = added.length ? way.next(added.length) : [];
|
|
1082
|
+
const next = added.length ? way.next(added.length, wereAdded) : [];
|
|
828
1083
|
if (refused.length) {
|
|
829
1084
|
// A TABLE NOT ADDED IS THE COMMAND'S FAILURE, in the shape every failure has (found by the CLI session test of the private repo:
|
|
830
1085
|
// exit 1 with no error, and nothing on stderr): what is wrong, each table's reason, and next — the added ones' too
|
|
@@ -847,12 +1102,26 @@ async function addTables(way, specs, json) {
|
|
|
847
1102
|
return 0;
|
|
848
1103
|
}
|
|
849
1104
|
/** What `mason add` prints for a table added: its line (✔ …), then its notes under it (the web page says the same,
|
|
850
|
-
* src/web/
|
|
1105
|
+
* src/web/pages/tables.js; tests/web-page.test.ts holds the two equal). `file` is the sources file as the reader is shown it. */
|
|
851
1106
|
export function addedLines(b, file) {
|
|
852
1107
|
const copies = b.columns === null ? (b.exclude.length ? `every column but ${b.exclude.join(", ")}` : "every column") : b.columns.join(", ");
|
|
853
1108
|
const s = b.skippedSecrets;
|
|
854
|
-
|
|
855
|
-
|
|
1109
|
+
const name = tableArg(b.table);
|
|
1110
|
+
// HOW IT IS KEPT CURRENT, always said (issue 33: the default is never met by surprise): the sync: it follows, where it
|
|
1111
|
+
// is said, and the one line that changes it — a big table written off, with the alternatives
|
|
1112
|
+
const sync = b.bigOff ? `sync: off written on its entry: it has more than ${BIG_TABLE_ROWS.toLocaleString("en-US")} rows, and each poll scans it whole on your database — ` +
|
|
1113
|
+
`mason add ${name} --sync "poll 1h" polls it hourly; mason sync ${name} syncs it once, now; mason add ${name} --sync replication keeps it current from the change log`
|
|
1114
|
+
: b.notReplicated !== null ? `sync: poll 5m written on its entry, not the file's replication: ${b.notReplicated} — ` +
|
|
1115
|
+
`mason add ${name} --sync "poll 1h" polls it hourly; mason add ${name} --sync off reads it only by mason sync ${name}`
|
|
1116
|
+
: b.sync.value === "off" ? `sync: off (${b.sync.from === "entry" ? "its entry's" : b.sync.from === "file" ? "the file's" : "the default"}): after its first sync it is read again only by mason sync ${name} — ` +
|
|
1117
|
+
`mason add ${name} --sync "poll 5m" polls it every 5 minutes`
|
|
1118
|
+
: b.sync.value === "replication" ? `sync: replication (${b.sync.from === "entry" ? "its entry's" : "the file's"}): kept current from the database's change log after its first sync — ` +
|
|
1119
|
+
`mason add ${name} --sync "poll 5m" polls it instead`
|
|
1120
|
+
: `sync: ${b.sync.value} (${b.sync.from === "entry" ? "its entry's" : "the file's"}): polled every ${b.sync.value.slice("poll ".length)} after its first sync — mason add ${name} --sync off stops that`;
|
|
1121
|
+
return { line: `Added ${b.table} to ${file}: ${copies}${b.key ? `, keyed by ${b.key.join(", ")}` : ""}${b.already ? " (already so)" : ""}`, notes: [
|
|
1122
|
+
sync,
|
|
1123
|
+
...(b.pollCost !== null ? [b.pollCost] : []),
|
|
1124
|
+
...(b.addedKey.length ? [`key added: ${b.addedKey.join(", ")} (a synced table always has its key)`] : []),
|
|
856
1125
|
// a secret is readable by anyone with a user's password: left out, and said, with the way to include one
|
|
857
1126
|
...(s.length ? [`left out ${s.join(", ")}: ${s.length === 1 ? "it looks" : "they look"} like ${s.length === 1 ? "a secret" : "secrets"}, ` +
|
|
858
1127
|
`which anyone with a user's password could read (--include ${s[0]} adds it)`] : []),
|
|
@@ -862,8 +1131,8 @@ export function addedLines(b, file) {
|
|
|
862
1131
|
const REMOVE_USAGE = `usage: mason remove <table>… [--json]
|
|
863
1132
|
|
|
864
1133
|
Takes tables out of the project: each one's entry in the sources file (mason/sources/<name>.yml) is removed, the
|
|
865
|
-
rest of the file as it was. With Mason running it stops
|
|
866
|
-
stopped,
|
|
1134
|
+
rest of the file as it was. With Mason running it stops syncing the table and drops its synced data at once; with Mason
|
|
1135
|
+
stopped, what an earlier run synced is dropped at the next mason start.
|
|
867
1136
|
|
|
868
1137
|
mason remove customers
|
|
869
1138
|
mason remove customers billing.invoices several at once
|
|
@@ -874,7 +1143,7 @@ export const REMOVE = {
|
|
|
874
1143
|
// the project, not the run, as mason add
|
|
875
1144
|
needs: "project",
|
|
876
1145
|
name: "remove",
|
|
877
|
-
summary: "take tables out of the sources file: mason remove customers (its
|
|
1146
|
+
summary: "take tables out of the sources file: mason remove customers (its synced data is dropped)",
|
|
878
1147
|
usage: REMOVE_USAGE,
|
|
879
1148
|
options: { json: { type: "boolean" } },
|
|
880
1149
|
json: ["removed", "file", "next"],
|
|
@@ -910,7 +1179,7 @@ function removedAnswer(r, names, running, json) {
|
|
|
910
1179
|
}
|
|
911
1180
|
const b = r.ok;
|
|
912
1181
|
const next = running ? [["mason status", "the tables left"]]
|
|
913
|
-
: [["mason start", "Mason in the background, for this project: it
|
|
1182
|
+
: [["mason start", "Mason in the background, for this project: it syncs the tables left"], ["mason status", "the tables left"]];
|
|
914
1183
|
if (json) {
|
|
915
1184
|
out(jsonText(REMOVE, { removed: b.removed, file: b.file }, next));
|
|
916
1185
|
return 0;
|
|
@@ -925,38 +1194,39 @@ export function removedReport(removed, file, o = {}) {
|
|
|
925
1194
|
const one = removed.length === 1;
|
|
926
1195
|
return { headline: `Removed ${removed.join(", ")}`, details: [["from", file]],
|
|
927
1196
|
// Mason down (the web page always has a run): a copy an earlier run made is dropped by the next start's sync
|
|
928
|
-
tips: [o.running === false ? `
|
|
929
|
-
: `Mason stops
|
|
1197
|
+
tips: [o.running === false ? `what an earlier run synced of ${one ? "it" : "them"} is dropped at the next mason start`
|
|
1198
|
+
: `Mason stops syncing ${one ? "it" : "them"} and drops ${one ? "its synced data" : "their synced data"}`] };
|
|
930
1199
|
}
|
|
931
1200
|
// ------------------------------------------------------------------ mason wait
|
|
932
1201
|
/** An added table as mason wait --json says it: its copy, its error, and how it is kept current, as the run's state has
|
|
933
1202
|
* them (mason status --json's own fields, never derived again here) */
|
|
934
1203
|
export function waitTable(t) {
|
|
935
|
-
return { table: t.table, copy: t.copy, error: t.error, ...(t.
|
|
1204
|
+
return { table: t.table, copy: t.copy, error: t.error, ...(t.kept ? { kept: t.kept } : {}) };
|
|
936
1205
|
}
|
|
937
1206
|
const WAIT_USAGE = `usage: mason wait [--timeout <seconds>] [--json]
|
|
938
1207
|
|
|
939
|
-
Waits until every added table is
|
|
940
|
-
finished (in the order they finished), a bar for the ones still
|
|
1208
|
+
Waits until every added table is synced and served, showing every added table on a line of its own: 100% for the
|
|
1209
|
+
finished (in the order they finished), a bar for the ones still syncing, at the bottom. Run it after mason add.
|
|
941
1210
|
|
|
942
1211
|
mason add orders && mason wait
|
|
943
1212
|
mason wait --timeout 3600 a large table: wait up to an hour
|
|
944
1213
|
|
|
945
|
-
--timeout <seconds> how long to wait (default 600); the
|
|
1214
|
+
--timeout <seconds> how long to wait (default 600); the sync goes on after, and mason wait again picks it up
|
|
946
1215
|
--json as data, once it ends: { ready, failed, waiting, timedOut, tables: [{ table, copy, error,
|
|
947
|
-
|
|
948
|
-
|
|
949
|
-
|
|
950
|
-
|
|
1216
|
+
kept }] (copy: { phase, rowsCopied, progress: the chunks done, reading and for how long, the
|
|
1217
|
+
rate, the seconds left, while its chunks are read }; kept as mason status --json says it —
|
|
1218
|
+
{ by: poll, sync: { mode: off } or { mode: poll, every, seconds }, syncedAt (its last whole read
|
|
1219
|
+
or poll), nextAt (its next poll; null: off) }; kept's pending, when it applies, says why
|
|
1220
|
+
it is not kept current now, its phase included),
|
|
951
1221
|
stream (replication mode: where the stream is, as mason status --json says it), error (on a
|
|
952
1222
|
failure or a timeout: { code, message, next }), next }
|
|
953
1223
|
|
|
954
|
-
Exit status: 0 ready; 1 a
|
|
1224
|
+
Exit status: 0 ready; 1 a first sync failed (printed, with what to do; Mason tries it again by itself); 124 timed out.
|
|
955
1225
|
`;
|
|
956
1226
|
export const WAIT = {
|
|
957
1227
|
needs: "run",
|
|
958
1228
|
name: "wait",
|
|
959
|
-
summary: "wait until every added table is
|
|
1229
|
+
summary: "wait until every added table is synced and served (exit 0; 1 on a failure, with why)",
|
|
960
1230
|
usage: WAIT_USAGE,
|
|
961
1231
|
options: { json: { type: "boolean" }, timeout: { type: "string" } },
|
|
962
1232
|
json: ["ready", "failed", "waiting", "timedOut", "tables", "stream", "error", "next"],
|
|
@@ -1011,11 +1281,11 @@ export async function wait(argv) {
|
|
|
1011
1281
|
// the outcome and what to run next, the same for a person and under --json
|
|
1012
1282
|
const readyNext = picked.length ? [...READY_STEPS] : addSteps(s.tables);
|
|
1013
1283
|
const failure = r.failed.length
|
|
1014
|
-
? { message: r.failed.length === 1 ? r.failed[0] : `${r.failed.length}
|
|
1015
|
-
fix: "Mason tries a failed
|
|
1284
|
+
? { message: r.failed.length === 1 ? r.failed[0] : `${r.failed.length} first syncs failed`, code: "failed", why: r.failed.length === 1 ? undefined : r.failed.join("\n"),
|
|
1285
|
+
fix: "Mason tries a failed first sync again by itself; mason logs says why it failed and when it tries next",
|
|
1016
1286
|
next: [["mason logs", "the detail of each failure"], ["mason status", "the rest of the project"]] }
|
|
1017
1287
|
: !done && Date.now() >= until
|
|
1018
|
-
? { message: `not ready after ${timeout} s`, code: "timeout", why: r.waiting.join("\n"), fix: "nothing stopped: the
|
|
1288
|
+
? { message: `not ready after ${timeout} s`, code: "timeout", why: r.waiting.join("\n"), fix: "nothing stopped: the syncs go on",
|
|
1019
1289
|
next: [["mason wait", "keep waiting (--timeout <seconds> for longer)"], ["mason status", "what Mason is doing"]] }
|
|
1020
1290
|
: null;
|
|
1021
1291
|
if (json && (done || failure)) {
|
|
@@ -1025,10 +1295,10 @@ export async function wait(argv) {
|
|
|
1025
1295
|
// the block as it ends, too: every table at 100% beside Ready (when the last change was not printed yet)
|
|
1026
1296
|
// while at work: a table already copied can be queried now; the final Next is Ready's, printed once below
|
|
1027
1297
|
const copied = picked.some((t) => t.copy?.phase === "done");
|
|
1028
|
-
const meanwhile = done ? [] : [...(copied ? [["mason user add <name>", "a user for your BI tool or psql: a
|
|
1298
|
+
const meanwhile = done ? [] : [...(copied ? [["mason user add <name>", "a user for your BI tool or psql: a synced table can be queried now"]] : []), ["mason status", "what Mason is doing"]];
|
|
1029
1299
|
show([...tableLines(s), ...(s.stream ? ["Stream:", ...streamLines(s.stream).map((l) => ` ${l}`)] : [])], meanwhile); // done: the meanwhile Next is dropped
|
|
1030
1300
|
if (r.ready) {
|
|
1031
|
-
const head = !picked.length ? "Nothing to wait for: no table added yet." : "Ready: every added table is
|
|
1301
|
+
const head = !picked.length ? "Nothing to wait for: no table added yet." : "Ready: every added table is synced and served.";
|
|
1032
1302
|
if (!json)
|
|
1033
1303
|
out(withEnd(picked.length ? styleOut().green(head) : head, { next: readyNext }));
|
|
1034
1304
|
return 0;
|
|
@@ -1040,38 +1310,77 @@ export async function wait(argv) {
|
|
|
1040
1310
|
});
|
|
1041
1311
|
}
|
|
1042
1312
|
// ------------------------------------------------------------------ mason logs
|
|
1043
|
-
|
|
1313
|
+
/** What the sync's events leave out, said where they are listed (mason logs --help, the page's Sync events, held equal
|
|
1314
|
+
* by tests/web-page.test.ts): a scheduled poll that succeeds is no event, so a person who synced reads where it is. */
|
|
1315
|
+
export const EVENTS_LEAVE_OUT = "A scheduled poll that succeeds is not listed: the last one's result is each table's last sync (mason status, the Tables page's Last sync).";
|
|
1316
|
+
const LOGS_USAGE = `usage: mason logs [-n <lines>] [-f] [--start] [--requests [<filters>]] [--events [--failed]] [--running] [--json]
|
|
1044
1317
|
|
|
1045
|
-
The last lines of Mason's log: every
|
|
1318
|
+
The last lines of Mason's log: every sync, poll and statement it ran, warnings and errors with their reasons —
|
|
1046
1319
|
running or not. With --requests, the run's request log instead: every statement a tool sent the
|
|
1047
|
-
Postgres API that Mason answered, and every one it refused or
|
|
1048
|
-
and SQLSTATE, how long, the rows sent, the client's application_name (and the mason user's name), the
|
|
1049
|
-
the error's first line
|
|
1050
|
-
|
|
1320
|
+
Postgres API that Mason answered, and every one it refused, failed or cancelled, a line each, newest last — when, the
|
|
1321
|
+
outcome and SQLSTATE, how long, the rows sent, the client's application_name (and the mason user's name), the
|
|
1322
|
+
statement and the error's first line; the filters narrow it, and a page that has older rows ends with the line that
|
|
1323
|
+
reads them (--before). With --events, the sync's events: each first sync that failed or finished, each poll, catch-up or
|
|
1324
|
+
stream that failed, the database out of reach, a table current again, each sync asked by hand (mason sync, the
|
|
1325
|
+
page's Sync now) and what it did, and each restart of the run — a line each, newest last (a failure repeated word for
|
|
1326
|
+
word is said once an hour). A scheduled poll that succeeds is not listed: the last one's result is each table's last
|
|
1327
|
+
sync (mason status, the Tables page's Last sync). Both are kept MASON_HISTORY_DAYS days
|
|
1328
|
+
(default 7) in the run's synced data, and read through the running Mason. With --running, the statements running now
|
|
1329
|
+
instead, one per session, each with its number (mason cancel <session> --statement <n> stops one).
|
|
1051
1330
|
|
|
1052
1331
|
mason logs
|
|
1053
1332
|
mason logs -f keep printing new lines as Mason works, until Ctrl-C
|
|
1054
1333
|
mason logs -n 200 --start what mason start printed: its steps, and the warnings and errors since
|
|
1055
|
-
mason logs --requests --failed -n 20 the last 20 statements refused or
|
|
1334
|
+
mason logs --requests --failed -n 20 the last 20 statements refused, failed or cancelled
|
|
1335
|
+
mason logs --requests --since 2h --user metabase --outcome failed,cancelled
|
|
1336
|
+
mason logs --requests --slow --search orders the statements over orders that took a second or more
|
|
1337
|
+
mason logs --events --failed the sync's latest failures
|
|
1338
|
+
mason logs --running the statements running now, and their sessions
|
|
1056
1339
|
|
|
1057
|
-
-n, --lines <n> how many lines, or with --requests how many
|
|
1340
|
+
-n, --lines <n> how many lines, or with --requests or --events how many of them (default 40; 1 to 10000 for each)
|
|
1058
1341
|
-f, --follow keep printing new lines until Ctrl-C
|
|
1059
1342
|
--start mason start's own terminal instead (its steps, then only the warnings and errors)
|
|
1060
1343
|
--requests the Postgres API's requests, from the run's request log (Mason must be running)
|
|
1061
|
-
--
|
|
1062
|
-
--
|
|
1063
|
-
|
|
1064
|
-
|
|
1344
|
+
--events the sync's events and the run's restarts (Mason must be running)
|
|
1345
|
+
--failed with --requests: only the ones not answered (refused, failed or cancelled); with --events: only
|
|
1346
|
+
the failures
|
|
1347
|
+
--outcome <o,…> with --requests: only these outcomes — ok, refused (the statement's own fault), failed (Mason's or
|
|
1348
|
+
the machine's), cancelled (a cancel or a statement timeout, SQLSTATE 57014)
|
|
1349
|
+
--since <time> with --requests: started at or after it — an age (30m, 2h, 7d) or an ISO 8601 date or time with
|
|
1350
|
+
its zone (2026-10-28, 2026-10-28T09:00:00Z)
|
|
1351
|
+
--until <time> with --requests: started before it (as --since)
|
|
1352
|
+
--user <name> with --requests: the mason user's statements
|
|
1353
|
+
--client <name> with --requests: the statements of the client whose application_name it is
|
|
1354
|
+
--slow with --requests: only the ones that took ${(SLOW_MS / 1000).toLocaleString("en-US")} s or more (the slow ones mason status lists)
|
|
1355
|
+
--search <text> with --requests: only the statements whose text has it, in any case (at most ${MAX_SEARCH} characters)
|
|
1356
|
+
--before <pos> with --requests: the page before: the position a page answered with (its last Next line)
|
|
1357
|
+
--running the statements running now (Mason must be running)
|
|
1358
|
+
--json as data: { lines: [{ at, text }] } (the log's entries, oldest first; at is null for a line with no
|
|
1359
|
+
time); with --requests { requests: [{ requestId, startedAt, endedAt, session, applicationName,
|
|
1360
|
+
tokenName, user (the mason user), role, attributes (the user's values its role's row filters read, or
|
|
1361
|
+
null), method, outcome (ok, refused, failed, cancelled), status
|
|
1362
|
+
(completed, failed, cancelled: what a reader shows), sqlstate, message, detail, hint, rows,
|
|
1363
|
+
bytesSent, durationMs, query, queryTruncated }], keptDays, before (the position of the page before,
|
|
1364
|
+
or null: none older) }; with --events { events: [{ at, kind (copy_failed, poll_failed,
|
|
1365
|
+
catch_up_failed, stream_failed, source_unreachable, restart, copied, recovered, synced (a sync
|
|
1366
|
+
asked by hand), sync_changed (a table's sync: changed)), failed, table (null: the whole sync),
|
|
1367
|
+
message }], keptDays }; with --running { running: [{ session, user,
|
|
1368
|
+
applicationName, method, startedAt, runningMs, rows (sent so far), query, queryTruncated }] }; next
|
|
1065
1369
|
`;
|
|
1066
1370
|
export const LOGS = {
|
|
1067
1371
|
needs: "project",
|
|
1068
1372
|
name: "logs",
|
|
1069
|
-
summary: "Mason's log
|
|
1373
|
+
summary: "Mason's log (-f follows it); --requests: the Postgres API's statements; --events: the sync's; --running: running now",
|
|
1070
1374
|
usage: LOGS_USAGE,
|
|
1071
1375
|
options: { lines: { type: "string", short: "n" }, follow: { type: "boolean", short: "f" }, start: { type: "boolean" },
|
|
1072
|
-
requests: { type: "boolean" },
|
|
1073
|
-
|
|
1376
|
+
requests: { type: "boolean" }, events: { type: "boolean" }, failed: { type: "boolean" }, outcome: { type: "string" }, since: { type: "string" },
|
|
1377
|
+
until: { type: "string" }, user: { type: "string" }, client: { type: "string" }, slow: { type: "boolean" }, search: { type: "string" }, before: { type: "string" }, running: { type: "boolean" }, json: { type: "boolean" } },
|
|
1378
|
+
json: ["lines", "requests", "events", "keptDays", "before", "running", "next"],
|
|
1074
1379
|
};
|
|
1380
|
+
/** the request log's filters as mason logs spells them: -n, --since, … (src/cli/request-filter.ts names each so) */
|
|
1381
|
+
const flagOf = (k) => k === "limit" ? "-n" : `--${k}`;
|
|
1382
|
+
/** the filters of --requests (every one but -n) */
|
|
1383
|
+
const FILTER_FLAGS = ["failed", "outcome", "since", "until", "user", "client", "slow", "search", "before"];
|
|
1075
1384
|
/**
|
|
1076
1385
|
* One request of the run's ledger on one line (src/duck/requests.ts): when, the outcome and SQLSTATE, how long,
|
|
1077
1386
|
* the rows sent, who (application_name, a mason user's name), the statement on one line and the error's first line.
|
|
@@ -1081,55 +1390,181 @@ export function requestLine(r, st) {
|
|
|
1081
1390
|
const paint = r.outcome === "ok" ? st.green : r.outcome === "failed" ? st.red : st.yellow;
|
|
1082
1391
|
const rows = r.rows === null ? "" : `${n(r.rows)} row${r.rows === 1 ? "" : "s"}`;
|
|
1083
1392
|
const who = `${r.applicationName || "(no application_name)"}${r.tokenName ? ` (token ${r.tokenName})` : ""}${r.user ? ` (user ${r.user})` : ""}`;
|
|
1084
|
-
return `${r.startedAt.slice(0, 19).replace("T", " ")} ${paint(word.padEnd(
|
|
1393
|
+
return `${r.startedAt.slice(0, 19).replace("T", " ")} ${paint(word.padEnd(15))} ${msText(r.durationMs).padStart(9)} ${rows.padStart(11)} ${who} ` +
|
|
1085
1394
|
`${oneLine(r.query, 120) || "(no statement)"}${r.message ? ` — ${firstLine(r.message)}` : ""}`;
|
|
1086
1395
|
}
|
|
1396
|
+
/** One statement running now on one line (src/api/postgres/server.ts RunningStatement): its session, how long, who, what. */
|
|
1397
|
+
export function runningLine(r) {
|
|
1398
|
+
const who = `${r.applicationName || "(no application_name)"}${r.user ? ` (user ${r.user})` : ""}`;
|
|
1399
|
+
return `session ${String(r.session).padEnd(5)} statement ${String(r.statement).padEnd(6)} ${spanText(r.runningMs / 1000).padStart(8)} ${`${n(r.rows)} row${r.rows === 1 ? "" : "s"} sent`.padStart(14)} ${who} ` +
|
|
1400
|
+
`${oneLine(r.query, 120) || "(no statement)"}`;
|
|
1401
|
+
}
|
|
1402
|
+
/** The mason logs line that reads the same filters again, a page further back (`before`, the page's cursor). */
|
|
1403
|
+
function olderPage(q, before) {
|
|
1404
|
+
const quote = (v) => /^[\w.:,~+-]+$/.test(v) ? v : `'${v.replaceAll("'", "'\\''")}'`;
|
|
1405
|
+
const parts = ["mason logs --requests", ...(q.limit !== DEFAULT_REQUEST_LIMIT ? [`-n ${q.limit}`] : [])];
|
|
1406
|
+
if (q.failed === true)
|
|
1407
|
+
parts.push("--failed");
|
|
1408
|
+
if (q.outcomes !== undefined)
|
|
1409
|
+
parts.push(`--outcome ${q.outcomes.join(",")}`);
|
|
1410
|
+
for (const k of ["since", "until", "user", "client"]) {
|
|
1411
|
+
const v = q[k];
|
|
1412
|
+
if (v !== undefined)
|
|
1413
|
+
parts.push(`--${k} ${quote(v)}`);
|
|
1414
|
+
}
|
|
1415
|
+
if (q.minMs !== undefined)
|
|
1416
|
+
parts.push("--slow");
|
|
1417
|
+
if (q.search !== undefined)
|
|
1418
|
+
parts.push(`--search ${quote(q.search)}`);
|
|
1419
|
+
parts.push(`--before ${quote(before)}`);
|
|
1420
|
+
return parts.join(" ");
|
|
1421
|
+
}
|
|
1422
|
+
/**
|
|
1423
|
+
* WHAT THE REQUEST LOG DOES NOT LIST, said where a first statement is looked for (`mason logs --requests` with none, the
|
|
1424
|
+
* Queries page; src/web/shared.js, held equal by the tests): a catalog read, a SET or a SHOW is the protocol's chatter —
|
|
1425
|
+
* a BI tool sends hundreds as it connects — and is not recorded (src/api/postgres/server.ts `recordRequest`), so a
|
|
1426
|
+
* `select 1` typed to test the connection is answered and never appears.
|
|
1427
|
+
*/
|
|
1428
|
+
export const UNLISTED_STATEMENTS = "A statement that reads no synced table (select 1, SET, SHOW, a BI tool's catalog queries) is answered but not listed";
|
|
1087
1429
|
/** mason logs --requests: the run's ledger through its command server (one process holds the copy's file) */
|
|
1088
|
-
async function requestLogs(
|
|
1430
|
+
async function requestLogs(q, json) {
|
|
1089
1431
|
const r0 = await theRun(process.cwd());
|
|
1090
1432
|
if ("problem" in r0)
|
|
1091
1433
|
return failed("logs", r0, { json });
|
|
1092
1434
|
return guarded("logs", json, async () => {
|
|
1093
|
-
const reply = await callRun(r0.run, "GET", `/api/requests
|
|
1094
|
-
if (reply.status !== 200)
|
|
1095
|
-
return failed("logs",
|
|
1096
|
-
next: [["mason status", "the slow and refused queries of the last day"], ["mason logs", "Mason's log"]] }, { json });
|
|
1097
|
-
}
|
|
1435
|
+
const reply = await callRun(r0.run, "GET", `/api/requests?${requestFilterParams(q).toString()}`);
|
|
1436
|
+
if (reply.status !== 200)
|
|
1437
|
+
return failed("logs", runFailure(reply, [["mason status", "the slow and refused queries of the last day"], ["mason logs", "Mason's log"]]), { json });
|
|
1098
1438
|
const requests = reply.body.requests;
|
|
1099
1439
|
const keptDays = Number(reply.body.keptDays);
|
|
1100
|
-
const
|
|
1101
|
-
|
|
1440
|
+
const before = typeof reply.body.before === "string" ? reply.body.before : null;
|
|
1441
|
+
const filtered = q.failed === true || q.outcomes !== undefined || q.since !== undefined || q.until !== undefined || q.user !== undefined || q.client !== undefined ||
|
|
1442
|
+
q.minMs !== undefined || q.search !== undefined;
|
|
1443
|
+
const next = [
|
|
1444
|
+
...(before !== null ? [[olderPage(q, before), "the page before this one"]] : []),
|
|
1445
|
+
...(filtered ? [["mason logs --requests", "every request, answered or not"]] : [["mason logs --requests --failed", "only the ones not answered"]]),
|
|
1446
|
+
["mason status", "the last day's and week's statements, and the slow and refused ones grouped"],
|
|
1447
|
+
];
|
|
1102
1448
|
if (json) {
|
|
1103
|
-
out(jsonText(LOGS, { requests, keptDays }, next));
|
|
1449
|
+
out(jsonText(LOGS, { requests, keptDays, before }, next));
|
|
1104
1450
|
return 0;
|
|
1105
1451
|
}
|
|
1106
1452
|
const st = styleOut();
|
|
1107
1453
|
if (!requests.length)
|
|
1108
|
-
out(`No ${
|
|
1454
|
+
out(`No ${filtered ? "request that passes the filters" : "request"} in the last ${keptDays} day${keptDays === 1 ? "" : "s"} (MASON_HISTORY_DAYS).`);
|
|
1109
1455
|
for (const r of requests)
|
|
1110
1456
|
out(requestLine(r, st));
|
|
1457
|
+
out(endText({ next, ...(!requests.length && !filtered ? { tips: [`${UNLISTED_STATEMENTS}.`] } : {}) }, st));
|
|
1458
|
+
return 0;
|
|
1459
|
+
});
|
|
1460
|
+
}
|
|
1461
|
+
/** mason logs --events: the sync's events and the run's restarts through its command server (src/duck/events.ts) */
|
|
1462
|
+
async function eventLogs(limit, failedOnly, json) {
|
|
1463
|
+
const r0 = await theRun(process.cwd());
|
|
1464
|
+
if ("problem" in r0)
|
|
1465
|
+
return failed("logs", r0, { json });
|
|
1466
|
+
return guarded("logs", json, async () => {
|
|
1467
|
+
const reply = await callRun(r0.run, "GET", `/api/events?limit=${limit}${failedOnly ? "&failed=1" : ""}`);
|
|
1468
|
+
if (reply.status !== 200) {
|
|
1469
|
+
return failed("logs", runFailure(reply, [["mason status", "each synced table and its error now"], ["mason logs", "Mason's log, where each event is a line too"]]), { json });
|
|
1470
|
+
}
|
|
1471
|
+
const events = reply.body.events;
|
|
1472
|
+
const keptDays = Number(reply.body.keptDays);
|
|
1473
|
+
const next = failedOnly ? [["mason logs --events", "every event, the first syncs finished and the tables current again too"], ["mason status", "each synced table and its error now"]]
|
|
1474
|
+
: [["mason logs --events --failed", "only the failures"], ["mason status", "each synced table and its error now"]];
|
|
1475
|
+
if (json) {
|
|
1476
|
+
out(jsonText(LOGS, { events, keptDays }, next));
|
|
1477
|
+
return 0;
|
|
1478
|
+
}
|
|
1479
|
+
const st = styleOut();
|
|
1480
|
+
if (!events.length)
|
|
1481
|
+
out(`No ${failedOnly ? "sync failure" : "sync event"} in the last ${keptDays} day${keptDays === 1 ? "" : "s"} (MASON_HISTORY_DAYS).`);
|
|
1482
|
+
for (const e of events)
|
|
1483
|
+
out(eventLine(e, st));
|
|
1111
1484
|
out(endText({ next }, st));
|
|
1112
1485
|
return 0;
|
|
1113
1486
|
});
|
|
1114
1487
|
}
|
|
1488
|
+
/** mason logs --running: the statements the listener runs now, through the run's command server */
|
|
1489
|
+
async function runningLogs(json) {
|
|
1490
|
+
const r0 = await theRun(process.cwd());
|
|
1491
|
+
if ("problem" in r0)
|
|
1492
|
+
return failed("logs", r0, { json });
|
|
1493
|
+
return guarded("logs", json, async () => {
|
|
1494
|
+
const reply = await callRun(r0.run, "GET", "/api/running");
|
|
1495
|
+
if (reply.status !== 200)
|
|
1496
|
+
return failed("logs", runFailure(reply, [["mason logs", "Mason's log"]]), { json });
|
|
1497
|
+
const running = reply.body.running;
|
|
1498
|
+
const next = running.length
|
|
1499
|
+
? [[`mason cancel ${running[0].session} --statement ${running[0].statement}`, "stop a session's statement, as a client's cancel does"], ["mason logs --requests", "the statements answered, refused and cancelled"]]
|
|
1500
|
+
: [["mason logs --requests", "the statements answered, refused and cancelled"]];
|
|
1501
|
+
if (json) {
|
|
1502
|
+
out(jsonText(LOGS, { running }, next));
|
|
1503
|
+
return 0;
|
|
1504
|
+
}
|
|
1505
|
+
const st = styleOut();
|
|
1506
|
+
out(running.length ? st.bold(`Running now (${running.length}):`) : "No statement is running now.");
|
|
1507
|
+
for (const r of running)
|
|
1508
|
+
out(` ${runningLine(r)}`);
|
|
1509
|
+
out(endText({ next }, st));
|
|
1510
|
+
return 0;
|
|
1511
|
+
});
|
|
1512
|
+
}
|
|
1513
|
+
/** A refusal the run answered, in the Failure shape it carries (`failure`, src/cli/run-server.ts sendFailure). */
|
|
1514
|
+
export function runFailure(reply, next) {
|
|
1515
|
+
const f = reply.body.failure;
|
|
1516
|
+
const code = typeof f?.code === "string" ? f.code : reply.status === 400 ? "usage" : reply.status === 404 ? "not_found" : reply.status === 409 ? "refused" : "failed";
|
|
1517
|
+
return { message: typeof f?.message === "string" ? f.message : String(reply.body.error ?? `the run answered ${reply.status}`), code,
|
|
1518
|
+
...(typeof f?.why === "string" ? { why: f.why } : {}), ...(typeof f?.fix === "string" ? { fix: f.fix } : {}), next };
|
|
1519
|
+
}
|
|
1115
1520
|
export async function logs(argv) {
|
|
1116
1521
|
const f = parseCommand(LOGS, argv);
|
|
1117
1522
|
if (typeof f === "number")
|
|
1118
1523
|
return f;
|
|
1119
1524
|
const json = f.values.json === true;
|
|
1120
|
-
|
|
1121
|
-
|
|
1122
|
-
|
|
1123
|
-
|
|
1124
|
-
|
|
1525
|
+
if (f.values.requests && f.values.events)
|
|
1526
|
+
return failed("logs", { message: "--requests and --events are two lists: ask for one", code: "usage", fix: "mason logs --requests, or mason logs --events" }, { json });
|
|
1527
|
+
if (f.values.failed && !f.values.requests && !f.values.events)
|
|
1528
|
+
return failed("logs", { message: "--failed is a filter of --requests or --events", code: "usage", fix: "mason logs --requests --failed, or mason logs --events --failed" }, { json });
|
|
1529
|
+
// the request log's own filters (--failed is --events' too)
|
|
1530
|
+
const filter = FILTER_FLAGS.find((k) => k !== "failed" && f.values[k] !== undefined);
|
|
1531
|
+
if (filter !== undefined && !f.values.requests) {
|
|
1532
|
+
return failed("logs", { message: `--${filter} is a filter of --requests`, code: "usage", fix: `mason logs --requests --${filter} …` }, { json });
|
|
1533
|
+
}
|
|
1534
|
+
if (f.values.running && (f.values.requests || f.values.events || f.values.start || f.values.follow)) {
|
|
1535
|
+
const other = f.values.requests ? "--requests" : f.values.events ? "--events" : f.values.start ? "--start" : "-f";
|
|
1536
|
+
const whose = other === "--requests" ? "the request log's" : other === "--events" ? "the sync's events'" : "the log file's";
|
|
1537
|
+
return failed("logs", { message: `--running lists the statements running now; ${other} is ${whose}`, code: "usage", fix: `mason logs --running, or mason logs ${other}` }, { json });
|
|
1538
|
+
}
|
|
1539
|
+
if (f.values.running && f.values.lines !== undefined) {
|
|
1540
|
+
return failed("logs", { message: "--running lists every statement running now: -n (--lines) does not apply to it", code: "usage", fix: "mason logs --running" }, { json });
|
|
1541
|
+
}
|
|
1125
1542
|
if (f.values.requests && (f.values.start || f.values.follow))
|
|
1126
1543
|
return failed("logs", { message: `--requests lists the request log; ${f.values.start ? "--start" : "-f"} is the log file's`, code: "usage", fix: "mason logs --requests, or mason logs " + (f.values.start ? "--start" : "-f") }, { json });
|
|
1544
|
+
if (f.values.events && (f.values.start || f.values.follow))
|
|
1545
|
+
return failed("logs", { message: `--events lists the sync's events; ${f.values.start ? "--start" : "-f"} is the log file's`, code: "usage", fix: "mason logs --events, or mason logs " + (f.values.start ? "--start" : "-f") }, { json });
|
|
1127
1546
|
if (json && f.values.follow)
|
|
1128
1547
|
return failed("logs", { message: "-f prints lines as they come, and --json is one answer", code: "usage", fix: "mason logs --json, or mason logs -f" }, { json });
|
|
1548
|
+
if (f.values.running)
|
|
1549
|
+
return runningLogs(json);
|
|
1129
1550
|
if (f.values.requests) {
|
|
1130
|
-
|
|
1131
|
-
|
|
1132
|
-
|
|
1551
|
+
// the filters, read as the run reads its query string (src/cli/request-filter.ts): a mistake is said here, exit 2
|
|
1552
|
+
const text = (v) => typeof v === "string" ? v : undefined;
|
|
1553
|
+
const q = requestFilter({ limit: text(f.values.lines), failed: f.values.failed === true, outcome: text(f.values.outcome), since: text(f.values.since),
|
|
1554
|
+
until: text(f.values.until), user: text(f.values.user), client: text(f.values.client),
|
|
1555
|
+
slow: f.values.slow === true, search: text(f.values.search), before: text(f.values.before) }, Date.now(), flagOf);
|
|
1556
|
+
if ("message" in q)
|
|
1557
|
+
return failed("logs", { ...q, usage: LOGS_USAGE }, { json });
|
|
1558
|
+
return requestLogs(q, json);
|
|
1559
|
+
}
|
|
1560
|
+
const lines = f.values.lines === undefined ? 40 : Number(f.values.lines);
|
|
1561
|
+
if (!Number.isInteger(lines) || lines < 0)
|
|
1562
|
+
return failed("logs", { message: `-n (--lines) is a number of lines, not ${JSON.stringify(f.values.lines)}`, code: "usage" }, { json });
|
|
1563
|
+
if (f.values.events) {
|
|
1564
|
+
const limit = eventLimit(typeof f.values.lines === "string" ? f.values.lines : undefined, "-n");
|
|
1565
|
+
if (typeof limit !== "number")
|
|
1566
|
+
return failed("logs", { ...limit, usage: LOGS_USAGE }, { json });
|
|
1567
|
+
return eventLogs(limit, f.values.failed === true, json);
|
|
1133
1568
|
}
|
|
1134
1569
|
// a stopped run's log is still worth reading: the project's, running or not
|
|
1135
1570
|
const t = theProject(process.cwd());
|
|
@@ -1142,15 +1577,18 @@ export async function logs(argv) {
|
|
|
1142
1577
|
return failed("logs", { message: `${file} does not exist yet`, code: "nothing_yet", why: `Mason has not run for ${t.project.config.name}`, next: [["mason start", "start it, in the background"]] }, { json });
|
|
1143
1578
|
}
|
|
1144
1579
|
const text = fs.readFileSync(file, "utf8");
|
|
1145
|
-
|
|
1146
|
-
|
|
1147
|
-
|
|
1580
|
+
// WHY THE FILE, even with the run up: it is the whole record (the run's memory keeps only its last LOG_KEPT entries,
|
|
1581
|
+
// src/lib/log-buffer.ts) and the only one when the run is down. The entries are parsed by the buffer's own parser, so
|
|
1582
|
+
// `{ at, text }` is the shape of GET /api/logs too (the run's answer, which a `mason serve` run has no file for).
|
|
1583
|
+
// -n counts entries, a multi-line one being ONE; dev.log's lines (the start's steps, indented under each other) are a line each.
|
|
1584
|
+
const all = logEntries(text, { perLine: f.values.start === true });
|
|
1585
|
+
const last = all.slice(Math.max(0, all.length - lines));
|
|
1148
1586
|
if (json) {
|
|
1149
|
-
out(jsonText(LOGS, { lines:
|
|
1587
|
+
out(jsonText(LOGS, { lines: last }, [["mason logs --requests", "every statement the Postgres API met"]]));
|
|
1150
1588
|
return 0;
|
|
1151
1589
|
}
|
|
1152
|
-
for (const l of
|
|
1153
|
-
out(l);
|
|
1590
|
+
for (const l of last)
|
|
1591
|
+
out(logLineText(l));
|
|
1154
1592
|
if (!f.values.follow)
|
|
1155
1593
|
return 0;
|
|
1156
1594
|
let offset = Buffer.byteLength(text);
|
|
@@ -1176,19 +1614,19 @@ export async function logs(argv) {
|
|
|
1176
1614
|
// ------------------------------------------------------------------ mason stop
|
|
1177
1615
|
const STOP_USAGE = `usage: mason stop [--json]
|
|
1178
1616
|
|
|
1179
|
-
Stops Mason for this folder's project. The
|
|
1617
|
+
Stops Mason for this folder's project. The synced data and Mason's state stay: the next mason start resumes
|
|
1180
1618
|
in seconds, reading only what changed.
|
|
1181
1619
|
|
|
1182
1620
|
mason stop
|
|
1183
1621
|
|
|
1184
|
-
mason destroy removes the
|
|
1622
|
+
mason destroy removes the synced data and the state too (your files stay), to free the disk or begin again.
|
|
1185
1623
|
|
|
1186
1624
|
--json as data: { stopped: [name], killed (a run that did not answer and ignored SIGTERM), supervisor (its pid, when one was stopped), next }
|
|
1187
1625
|
`;
|
|
1188
1626
|
export const STOP = {
|
|
1189
1627
|
needs: "project",
|
|
1190
1628
|
name: "stop",
|
|
1191
|
-
summary: "stop Mason for this folder's project (the
|
|
1629
|
+
summary: "stop Mason for this folder's project (the synced data stays: the next mason start resumes)",
|
|
1192
1630
|
usage: STOP_USAGE,
|
|
1193
1631
|
options: { json: { type: "boolean" } },
|
|
1194
1632
|
json: ["stopped", "killed", "supervisor", "next"],
|
|
@@ -1198,7 +1636,7 @@ export async function stop(argv) {
|
|
|
1198
1636
|
if (typeof f === "number")
|
|
1199
1637
|
return f;
|
|
1200
1638
|
const json = f.values.json === true;
|
|
1201
|
-
const next = [["mason start", "resume, in seconds (in the project's folder)"], ["mason destroy", "remove its
|
|
1639
|
+
const next = [["mason start", "resume, in seconds (in the project's folder)"], ["mason destroy", "remove its synced data and its state; your files stay"]];
|
|
1202
1640
|
const r = await theRun(process.cwd());
|
|
1203
1641
|
if ("problem" in r && r.hung !== undefined) {
|
|
1204
1642
|
// A RUN THAT DOES NOT ANSWER (src/cli/runs.ts "alive and silent"): its supervisor first, so it is not started again
|
|
@@ -1225,7 +1663,7 @@ export async function stop(argv) {
|
|
|
1225
1663
|
return 0;
|
|
1226
1664
|
}
|
|
1227
1665
|
out(reportText({ headline: `Stopped ${hung.name}`, details: [["how", `${how}${sup === null ? "" : `; its supervisor (pid ${sup}) will not start it again`}`],
|
|
1228
|
-
["kept", "its
|
|
1666
|
+
["kept", "its synced data and its state (it recovers from its write-ahead log at the next start)"], ["log", "mason logs shows what it was doing"]] }));
|
|
1229
1667
|
out(endText({ next }));
|
|
1230
1668
|
return 0;
|
|
1231
1669
|
}
|
|
@@ -1261,7 +1699,8 @@ export async function stop(argv) {
|
|
|
1261
1699
|
return failed("stop", { message: `Mason for ${run.name} (pid ${run.pid}) is still running after 90 s`, code: "failed", next: [["mason logs", "what it is doing"]] }, { json });
|
|
1262
1700
|
// a replication slot outlives the run that reads it, and holds the database's WAL meanwhile: said where it is left
|
|
1263
1701
|
const recorded = readProject(run.root);
|
|
1264
|
-
|
|
1702
|
+
// what a start made on the database, as it recorded it (src/cli/dev.ts): a project none of whose tables is replicated has none
|
|
1703
|
+
const replicating = isProject(recorded) && (recorded.config.replication ?? null) !== null;
|
|
1265
1704
|
const named = isProject(recorded) ? recorded.config.publication ?? null : null;
|
|
1266
1705
|
// a demo project's database (src/cli/demo.ts) stays too, as the copy does: said where a person reads the stop
|
|
1267
1706
|
const demo = isProject(recorded) && recorded.config.demo === true;
|
|
@@ -1269,7 +1708,7 @@ export async function stop(argv) {
|
|
|
1269
1708
|
out(jsonText(STOP, { stopped: [run.name] }, next));
|
|
1270
1709
|
return 0;
|
|
1271
1710
|
}
|
|
1272
|
-
out(reportText({ headline: `Stopped ${run.name}`, details: [["kept", `its
|
|
1711
|
+
out(reportText({ headline: `Stopped ${run.name}`, details: [["kept", `its synced data and its state${demo ? ` and the demo database (${STATE_DIR}/${DEMO_DIR})` : ""}`],
|
|
1273
1712
|
...(replicating ? [["slot", `its replication slot stays on the database, holding WAL until the next mason start (mason destroy drops it)`],
|
|
1274
1713
|
["publication", named ? `${named}, its owner's: read as it is, never altered (mason destroy leaves it)` : "Mason's own (mason destroy drops it)"]] : [])] }));
|
|
1275
1714
|
out(endText({ next }));
|