@deepsql/mcp 0.16.0 → 0.17.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/CLAUDE.md +3 -1
- package/deepsql-phase1-lib.js +251 -0
- package/package.json +1 -1
- package/skills/SKILL_BODY.md +7 -0
- package/src/auth/store.js +3 -3
- package/src/cli.js +30 -0
- package/src/commands/growth.js +458 -0
- package/src/commands/growth.test.js +439 -0
- package/src/commands/mcp.js +9 -9
- package/src/commands/slow-queries.js +116 -1
- package/src/user-home.js +29 -0
|
@@ -0,0 +1,458 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* `deepsql growth` — table growth analytics.
|
|
5
|
+
*
|
|
6
|
+
* Backend has a full `/growth-monitoring/*` controller with size/row time
|
|
7
|
+
* series, anomaly detection (sudden growth spikes flagged by severity),
|
|
8
|
+
* predictions, and per-table alert configs. Until this command shipped,
|
|
9
|
+
* none of that was reachable from the CLI or the MCP — the agent and
|
|
10
|
+
* terminal users could only get to it by hand-rolling SQL against
|
|
11
|
+
* `table_stats_history`. Now they can ask DeepSQL directly.
|
|
12
|
+
*
|
|
13
|
+
* trends [--connection <c>] [--table <t>] [--days N=30] GET /growth-monitoring/trends/{cid}
|
|
14
|
+
* history [--connection <c>] [--table <t>] [--days N=7] GET /growth-monitoring/history/{cid}
|
|
15
|
+
* anomalies [--connection <c>] [--table <t>] [--unack] [--days N=30] GET /growth-monitoring/anomalies/{cid}
|
|
16
|
+
* ack <anomalyId> POST /growth-monitoring/anomalies/{id}/acknowledge
|
|
17
|
+
* capture [--connection <c>] POST /growth-monitoring/capture/{cid} (admin)
|
|
18
|
+
* config show [--connection <c>] [--table <t>] GET /growth-monitoring/config/{cid}
|
|
19
|
+
* config set --file <path> POST /growth-monitoring/config (admin)
|
|
20
|
+
*
|
|
21
|
+
* `capture` and `config set` are the only mutations; both are admin-level
|
|
22
|
+
* but neither writes user data, so we don't gate them behind --confirm.
|
|
23
|
+
* The other subcommands are pure reads.
|
|
24
|
+
*/
|
|
25
|
+
|
|
26
|
+
const fs = require("node:fs");
|
|
27
|
+
const { ApiError, request } = require("../api/client");
|
|
28
|
+
const { resolveSession } = require("./_session");
|
|
29
|
+
const { resolveConnectionId } = require("./_connections");
|
|
30
|
+
|
|
31
|
+
const SUBCOMMANDS = {
|
|
32
|
+
trends: cmdTrends,
|
|
33
|
+
history: cmdHistory,
|
|
34
|
+
anomalies: cmdAnomalies,
|
|
35
|
+
ack: cmdAck,
|
|
36
|
+
capture: cmdCapture,
|
|
37
|
+
config: cmdConfig,
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
async function run(opts, io = {}) {
|
|
41
|
+
const sub = opts.positional[0] || "trends";
|
|
42
|
+
const handler = SUBCOMMANDS[sub];
|
|
43
|
+
if (!handler) {
|
|
44
|
+
throw new Error(
|
|
45
|
+
`Unknown growth subcommand: ${sub}. ` +
|
|
46
|
+
`Try one of: trends, history, anomalies, ack <id>, capture, config show|set.`,
|
|
47
|
+
);
|
|
48
|
+
}
|
|
49
|
+
return wrap(handler)({ ...opts, positional: opts.positional.slice(1) }, io);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function wrap(handler) {
|
|
53
|
+
return async (opts, io) => {
|
|
54
|
+
try {
|
|
55
|
+
return await handler(opts, io);
|
|
56
|
+
} catch (err) {
|
|
57
|
+
if (err instanceof ApiError && err.status === 403) {
|
|
58
|
+
throw new Error(
|
|
59
|
+
"Access denied — growth-monitoring requires permissions on this connection.",
|
|
60
|
+
);
|
|
61
|
+
}
|
|
62
|
+
if (err instanceof ApiError && err.status === 404) {
|
|
63
|
+
throw new Error(err.message || "Resource not found.");
|
|
64
|
+
}
|
|
65
|
+
throw err;
|
|
66
|
+
}
|
|
67
|
+
};
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
// ─── trends ────────────────────────────────────────────────────────────────
|
|
71
|
+
//
|
|
72
|
+
// The most agent-friendly subcommand: rolls up the time series into per-
|
|
73
|
+
// table headlines ("orders: +18% / 30d, 240MB → 285MB"). Default subcommand
|
|
74
|
+
// because "what's growing?" is the most common question.
|
|
75
|
+
|
|
76
|
+
async function cmdTrends(opts, { stdout = process.stdout } = {}) {
|
|
77
|
+
const session = resolveSession(opts);
|
|
78
|
+
const connectionId = await resolveConnectionId(session, opts.connection);
|
|
79
|
+
const days = clampDays(opts.days, 30);
|
|
80
|
+
|
|
81
|
+
const response = await request(
|
|
82
|
+
session.baseUrl,
|
|
83
|
+
`/growth-monitoring/trends/${encodeURIComponent(connectionId)}`,
|
|
84
|
+
{
|
|
85
|
+
token: session.token,
|
|
86
|
+
query: { tableName: opts.table || null, days },
|
|
87
|
+
},
|
|
88
|
+
);
|
|
89
|
+
|
|
90
|
+
if (opts.json) {
|
|
91
|
+
stdout.write(`${JSON.stringify(response, null, 2)}\n`);
|
|
92
|
+
return;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
const trends = response?.trends || {};
|
|
96
|
+
const sizeOverTime = Array.isArray(trends.sizeOverTime) ? trends.sizeOverTime : [];
|
|
97
|
+
if (sizeOverTime.length === 0) {
|
|
98
|
+
stdout.write(
|
|
99
|
+
`No growth data for this connection in the last ${days} day(s). ` +
|
|
100
|
+
`Run \`deepsql growth capture --connection <c>\` to take a fresh ` +
|
|
101
|
+
`snapshot if monitoring is enabled, or wait for the scheduled cycle.\n`,
|
|
102
|
+
);
|
|
103
|
+
return;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
// Group by table and compute first / last snapshot per table.
|
|
107
|
+
const byTable = new Map();
|
|
108
|
+
for (const point of sizeOverTime) {
|
|
109
|
+
const t = point.table || "(unknown)";
|
|
110
|
+
if (!byTable.has(t)) byTable.set(t, []);
|
|
111
|
+
byTable.get(t).push(point);
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
// Sort tables by absolute growth (largest first) so the most-changed
|
|
115
|
+
// tables sit at the top of the output — what an agent or operator
|
|
116
|
+
// actually needs to see first.
|
|
117
|
+
const rows = [];
|
|
118
|
+
for (const [table, points] of byTable.entries()) {
|
|
119
|
+
points.sort((a, b) => String(a.timestamp).localeCompare(String(b.timestamp)));
|
|
120
|
+
const first = points[0];
|
|
121
|
+
const last = points[points.length - 1];
|
|
122
|
+
const firstBytes = first?.sizeBytes ?? 0;
|
|
123
|
+
const lastBytes = last?.sizeBytes ?? 0;
|
|
124
|
+
const deltaBytes = lastBytes - firstBytes;
|
|
125
|
+
const deltaPct = firstBytes > 0 ? (deltaBytes / firstBytes) * 100 : null;
|
|
126
|
+
rows.push({
|
|
127
|
+
table,
|
|
128
|
+
firstBytes,
|
|
129
|
+
lastBytes,
|
|
130
|
+
deltaBytes,
|
|
131
|
+
deltaPct,
|
|
132
|
+
snapshots: points.length,
|
|
133
|
+
});
|
|
134
|
+
}
|
|
135
|
+
rows.sort((a, b) => Math.abs(b.deltaBytes) - Math.abs(a.deltaBytes));
|
|
136
|
+
|
|
137
|
+
stdout.write(
|
|
138
|
+
`${rows.length} table${rows.length === 1 ? "" : "s"} with growth data ` +
|
|
139
|
+
`over the last ${days} day(s):\n\n`,
|
|
140
|
+
);
|
|
141
|
+
for (const r of rows) {
|
|
142
|
+
const arrow = r.deltaBytes >= 0 ? "↑" : "↓";
|
|
143
|
+
const pct = r.deltaPct != null ? signedPct(r.deltaPct) : "n/a";
|
|
144
|
+
stdout.write(
|
|
145
|
+
` ${arrow} ${r.table.padEnd(40)} ` +
|
|
146
|
+
`${formatBytes(r.firstBytes)} → ${formatBytes(r.lastBytes)} ` +
|
|
147
|
+
`(${pct}, ${formatBytes(Math.abs(r.deltaBytes))} ${arrow === "↑" ? "added" : "freed"}, ` +
|
|
148
|
+
`${r.snapshots} snapshots)\n`,
|
|
149
|
+
);
|
|
150
|
+
}
|
|
151
|
+
stdout.write(
|
|
152
|
+
`\nUse \`deepsql growth history --table <name>\` for per-snapshot detail, ` +
|
|
153
|
+
`or \`deepsql growth anomalies\` for sudden spikes.\n`,
|
|
154
|
+
);
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
// ─── history ───────────────────────────────────────────────────────────────
|
|
158
|
+
|
|
159
|
+
async function cmdHistory(opts, { stdout = process.stdout } = {}) {
|
|
160
|
+
const session = resolveSession(opts);
|
|
161
|
+
const connectionId = await resolveConnectionId(session, opts.connection);
|
|
162
|
+
const days = clampDays(opts.days, 7);
|
|
163
|
+
|
|
164
|
+
const response = await request(
|
|
165
|
+
session.baseUrl,
|
|
166
|
+
`/growth-monitoring/history/${encodeURIComponent(connectionId)}`,
|
|
167
|
+
{
|
|
168
|
+
token: session.token,
|
|
169
|
+
query: { tableName: opts.table || null, days },
|
|
170
|
+
},
|
|
171
|
+
);
|
|
172
|
+
|
|
173
|
+
if (opts.json) {
|
|
174
|
+
stdout.write(`${JSON.stringify(response, null, 2)}\n`);
|
|
175
|
+
return;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
const history = Array.isArray(response?.history) ? response.history : [];
|
|
179
|
+
if (history.length === 0) {
|
|
180
|
+
stdout.write(`No history rows in the last ${days} day(s).\n`);
|
|
181
|
+
return;
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
stdout.write(`${history.length} snapshot${history.length === 1 ? "" : "s"} ` +
|
|
185
|
+
`over ${days} day(s):\n\n`);
|
|
186
|
+
for (const h of history) {
|
|
187
|
+
const ts = (h.snapshotTimestamp || "").substring(0, 19).replace("T", " ");
|
|
188
|
+
const sz = formatBytes(h.sizeBytes);
|
|
189
|
+
const rows = h.rowCount != null ? `${formatNumber(h.rowCount)} rows` : "rows: n/a";
|
|
190
|
+
const growth = h.sizeGrowthPercent != null
|
|
191
|
+
? ` Δ ${signedPct(h.sizeGrowthPercent)}`
|
|
192
|
+
: "";
|
|
193
|
+
const bloat = h.bloatPercent != null && h.bloatPercent > 0
|
|
194
|
+
? `, bloat ${h.bloatPercent.toFixed(1)}%`
|
|
195
|
+
: "";
|
|
196
|
+
stdout.write(` ${ts} ${h.tableName.padEnd(36)} ${sz.padStart(10)} ${rows.padStart(16)}${growth}${bloat}\n`);
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
// ─── anomalies ─────────────────────────────────────────────────────────────
|
|
201
|
+
|
|
202
|
+
async function cmdAnomalies(opts, { stdout = process.stdout } = {}) {
|
|
203
|
+
const session = resolveSession(opts);
|
|
204
|
+
const connectionId = await resolveConnectionId(session, opts.connection);
|
|
205
|
+
const days = clampDays(opts.days, 30);
|
|
206
|
+
|
|
207
|
+
const response = await request(
|
|
208
|
+
session.baseUrl,
|
|
209
|
+
`/growth-monitoring/anomalies/${encodeURIComponent(connectionId)}`,
|
|
210
|
+
{
|
|
211
|
+
token: session.token,
|
|
212
|
+
query: {
|
|
213
|
+
tableName: opts.table || null,
|
|
214
|
+
unacknowledgedOnly: opts.unack ? "true" : null,
|
|
215
|
+
days,
|
|
216
|
+
},
|
|
217
|
+
},
|
|
218
|
+
);
|
|
219
|
+
|
|
220
|
+
if (opts.json) {
|
|
221
|
+
stdout.write(`${JSON.stringify(response, null, 2)}\n`);
|
|
222
|
+
return;
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
const anomalies = Array.isArray(response?.anomalies) ? response.anomalies : [];
|
|
226
|
+
const stats = response?.statistics || {};
|
|
227
|
+
|
|
228
|
+
if (anomalies.length === 0) {
|
|
229
|
+
const scope = opts.unack ? "unacknowledged " : "";
|
|
230
|
+
stdout.write(`No ${scope}anomalies in the last ${days} day(s).\n`);
|
|
231
|
+
return;
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
const total = stats.total ?? anomalies.length;
|
|
235
|
+
const crit = stats.critical ?? 0;
|
|
236
|
+
const warn = stats.warning ?? 0;
|
|
237
|
+
const unack = stats.unacknowledged ?? 0;
|
|
238
|
+
stdout.write(`${total} anomal${total === 1 ? "y" : "ies"} ` +
|
|
239
|
+
`(${crit} critical, ${warn} warning, ${unack} unacknowledged) ` +
|
|
240
|
+
`over ${days} day(s):\n\n`);
|
|
241
|
+
|
|
242
|
+
for (const a of anomalies) {
|
|
243
|
+
const sev = (a.severity || "INFO").padEnd(8);
|
|
244
|
+
const marker = sev.startsWith("CRITICAL") ? "✗" : sev.startsWith("WARNING") ? "⚠" : "ℹ";
|
|
245
|
+
const ts = (a.detectionTimestamp || "").substring(0, 19).replace("T", " ");
|
|
246
|
+
const type = a.anomalyType || "?";
|
|
247
|
+
const sizeStr = a.previousSizeBytes != null && a.currentSizeBytes != null
|
|
248
|
+
? `${formatBytes(a.previousSizeBytes)} → ${formatBytes(a.currentSizeBytes)}`
|
|
249
|
+
: "";
|
|
250
|
+
const pct = a.sizeGrowthPercent != null ? ` (${signedPct(a.sizeGrowthPercent)})` : "";
|
|
251
|
+
const ackMark = a.acknowledged ? " [acked]" : "";
|
|
252
|
+
stdout.write(` ${marker} [${sev.trim()}] ${ts} ${a.tableName} ${type}${ackMark}\n`);
|
|
253
|
+
if (sizeStr) stdout.write(` size: ${sizeStr}${pct}\n`);
|
|
254
|
+
if (a.description) stdout.write(` ${a.description}\n`);
|
|
255
|
+
stdout.write(` id: ${a.id}\n\n`);
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
if (unack > 0) {
|
|
259
|
+
stdout.write(`Use \`deepsql growth ack <id>\` to acknowledge an anomaly.\n`);
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
// ─── ack ───────────────────────────────────────────────────────────────────
|
|
264
|
+
|
|
265
|
+
async function cmdAck(opts, { stdout = process.stdout } = {}) {
|
|
266
|
+
const anomalyId = opts.positional[0];
|
|
267
|
+
if (!anomalyId) {
|
|
268
|
+
throw new Error("Usage: deepsql growth ack <anomalyId>");
|
|
269
|
+
}
|
|
270
|
+
const session = resolveSession(opts);
|
|
271
|
+
|
|
272
|
+
const response = await request(
|
|
273
|
+
session.baseUrl,
|
|
274
|
+
`/growth-monitoring/anomalies/${encodeURIComponent(anomalyId)}/acknowledge`,
|
|
275
|
+
{ method: "POST", token: session.token },
|
|
276
|
+
);
|
|
277
|
+
|
|
278
|
+
if (opts.json) {
|
|
279
|
+
stdout.write(`${JSON.stringify(response, null, 2)}\n`);
|
|
280
|
+
return;
|
|
281
|
+
}
|
|
282
|
+
if (response?.success) {
|
|
283
|
+
stdout.write(`Acknowledged anomaly ${anomalyId}.\n`);
|
|
284
|
+
} else {
|
|
285
|
+
stdout.write(`${response?.message || "Acknowledge attempt did not return success."}\n`);
|
|
286
|
+
process.exitCode = 1;
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
// ─── capture ───────────────────────────────────────────────────────────────
|
|
291
|
+
|
|
292
|
+
async function cmdCapture(opts, { stdout = process.stdout } = {}) {
|
|
293
|
+
const session = resolveSession(opts);
|
|
294
|
+
const connectionId = await resolveConnectionId(session, opts.connection);
|
|
295
|
+
|
|
296
|
+
const response = await request(
|
|
297
|
+
session.baseUrl,
|
|
298
|
+
`/growth-monitoring/capture/${encodeURIComponent(connectionId)}`,
|
|
299
|
+
{ method: "POST", token: session.token },
|
|
300
|
+
);
|
|
301
|
+
|
|
302
|
+
if (opts.json) {
|
|
303
|
+
stdout.write(`${JSON.stringify(response, null, 2)}\n`);
|
|
304
|
+
return;
|
|
305
|
+
}
|
|
306
|
+
stdout.write(
|
|
307
|
+
`${response?.message || "Snapshot capture requested."}\n` +
|
|
308
|
+
`The job runs asynchronously on the backend; re-run ` +
|
|
309
|
+
`\`deepsql growth trends\` in a minute or two to see the new data.\n`,
|
|
310
|
+
);
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
// ─── config ────────────────────────────────────────────────────────────────
|
|
314
|
+
//
|
|
315
|
+
// Two flavors: `config show` (read) and `config set --file <p>` (admin
|
|
316
|
+
// write). The set form takes the full GrowthAlertConfiguration JSON
|
|
317
|
+
// body so we don't have to enumerate every threshold flag — the schema
|
|
318
|
+
// is wider than most CLI inputs warrant and JSON keeps us honest.
|
|
319
|
+
|
|
320
|
+
async function cmdConfig(opts, io = {}) {
|
|
321
|
+
const action = opts.positional[0] || "show";
|
|
322
|
+
if (action === "show") return cmdConfigShow(opts, io);
|
|
323
|
+
if (action === "set") return cmdConfigSet(opts, io);
|
|
324
|
+
throw new Error(`Unknown config action: ${action}. Try \`show\` or \`set --file <path>\`.`);
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
async function cmdConfigShow(opts, { stdout = process.stdout } = {}) {
|
|
328
|
+
const session = resolveSession(opts);
|
|
329
|
+
const connectionId = await resolveConnectionId(session, opts.connection);
|
|
330
|
+
const response = await request(
|
|
331
|
+
session.baseUrl,
|
|
332
|
+
`/growth-monitoring/config/${encodeURIComponent(connectionId)}`,
|
|
333
|
+
{ token: session.token, query: { tableName: opts.table || null } },
|
|
334
|
+
);
|
|
335
|
+
|
|
336
|
+
if (opts.json) {
|
|
337
|
+
stdout.write(`${JSON.stringify(response, null, 2)}\n`);
|
|
338
|
+
return;
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
const single = response?.configuration;
|
|
342
|
+
const list = Array.isArray(response?.configurations) ? response.configurations : null;
|
|
343
|
+
|
|
344
|
+
if (single) {
|
|
345
|
+
renderConfig(stdout, single);
|
|
346
|
+
return;
|
|
347
|
+
}
|
|
348
|
+
if (!list || list.length === 0) {
|
|
349
|
+
stdout.write(
|
|
350
|
+
"No growth-monitoring configurations for this connection. " +
|
|
351
|
+
"Defaults are applied. To customize thresholds, " +
|
|
352
|
+
"POST a GrowthAlertConfiguration JSON via " +
|
|
353
|
+
"`deepsql growth config set --file <path>`.\n",
|
|
354
|
+
);
|
|
355
|
+
return;
|
|
356
|
+
}
|
|
357
|
+
stdout.write(`${list.length} alert configuration${list.length === 1 ? "" : "s"}:\n`);
|
|
358
|
+
for (const c of list) {
|
|
359
|
+
stdout.write("\n");
|
|
360
|
+
renderConfig(stdout, c);
|
|
361
|
+
}
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
async function cmdConfigSet(opts, { stdout = process.stdout, stderr = process.stderr } = {}) {
|
|
365
|
+
if (!opts.file) {
|
|
366
|
+
throw new Error(
|
|
367
|
+
"Usage: deepsql growth config set --file <path-to-config.json>\n" +
|
|
368
|
+
"The JSON body must match GrowthAlertConfiguration (connectionId required).",
|
|
369
|
+
);
|
|
370
|
+
}
|
|
371
|
+
let body;
|
|
372
|
+
try {
|
|
373
|
+
body = JSON.parse(fs.readFileSync(opts.file, "utf8"));
|
|
374
|
+
} catch (err) {
|
|
375
|
+
throw new Error(`Could not read/parse ${opts.file}: ${err.message}`);
|
|
376
|
+
}
|
|
377
|
+
if (!body.connectionId) {
|
|
378
|
+
throw new Error("Config JSON must include a `connectionId` field.");
|
|
379
|
+
}
|
|
380
|
+
const session = resolveSession(opts);
|
|
381
|
+
const response = await request(
|
|
382
|
+
session.baseUrl,
|
|
383
|
+
"/growth-monitoring/config",
|
|
384
|
+
{ method: "POST", token: session.token, json: body },
|
|
385
|
+
);
|
|
386
|
+
|
|
387
|
+
if (opts.json) {
|
|
388
|
+
stdout.write(`${JSON.stringify(response, null, 2)}\n`);
|
|
389
|
+
return;
|
|
390
|
+
}
|
|
391
|
+
if (response?.success) {
|
|
392
|
+
stdout.write(`Saved growth-monitoring configuration.\n`);
|
|
393
|
+
if (response.configuration) renderConfig(stdout, response.configuration);
|
|
394
|
+
} else {
|
|
395
|
+
stderr.write(`${response?.message || "Save did not return success."}\n`);
|
|
396
|
+
process.exitCode = 1;
|
|
397
|
+
}
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
// ─── helpers ───────────────────────────────────────────────────────────────
|
|
401
|
+
|
|
402
|
+
function clampDays(value, fallback) {
|
|
403
|
+
if (value == null) return fallback;
|
|
404
|
+
const n = Number.parseInt(value, 10);
|
|
405
|
+
if (!Number.isFinite(n) || n < 1) return fallback;
|
|
406
|
+
return Math.min(365, n);
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
function formatBytes(bytes) {
|
|
410
|
+
if (bytes == null) return "?";
|
|
411
|
+
const abs = Math.abs(bytes);
|
|
412
|
+
if (abs < 1024) return `${bytes} B`;
|
|
413
|
+
if (abs < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB`;
|
|
414
|
+
if (abs < 1024 * 1024 * 1024) return `${(bytes / (1024 * 1024)).toFixed(1)} MB`;
|
|
415
|
+
if (abs < 1024 * 1024 * 1024 * 1024) return `${(bytes / (1024 * 1024 * 1024)).toFixed(2)} GB`;
|
|
416
|
+
return `${(bytes / (1024 * 1024 * 1024 * 1024)).toFixed(2)} TB`;
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
function formatNumber(n) {
|
|
420
|
+
if (n == null) return "?";
|
|
421
|
+
return Number(n).toLocaleString("en-US");
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
function signedPct(n) {
|
|
425
|
+
if (n == null) return "n/a";
|
|
426
|
+
const sign = n > 0 ? "+" : "";
|
|
427
|
+
return `${sign}${Number(n).toFixed(1)}%`;
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
function renderConfig(stdout, c) {
|
|
431
|
+
const scope = c.tableName ? `table=${c.tableName}` : "connection-wide";
|
|
432
|
+
stdout.write(`Configuration (${scope}):\n`);
|
|
433
|
+
if (c.percentageGrowthWarning != null) {
|
|
434
|
+
stdout.write(` growth % warning=${c.percentageGrowthWarning}% critical=${c.percentageGrowthCritical}%\n`);
|
|
435
|
+
}
|
|
436
|
+
if (c.absoluteGrowthWarningBytes != null) {
|
|
437
|
+
stdout.write(` growth bytes warning=${formatBytes(c.absoluteGrowthWarningBytes)} critical=${formatBytes(c.absoluteGrowthCriticalBytes)}\n`);
|
|
438
|
+
}
|
|
439
|
+
if (c.rowSpikeWarning != null) {
|
|
440
|
+
stdout.write(` row spike warning=${formatNumber(c.rowSpikeWarning)} critical=${formatNumber(c.rowSpikeCritical)}` +
|
|
441
|
+
(c.rowSpikePercentage != null ? ` pct=${c.rowSpikePercentage}%` : "") + "\n");
|
|
442
|
+
}
|
|
443
|
+
if (c.zScoreThreshold != null) {
|
|
444
|
+
stdout.write(` z-score threshold: ${c.zScoreThreshold}\n`);
|
|
445
|
+
}
|
|
446
|
+
if (c.historicalWindowHours != null) {
|
|
447
|
+
stdout.write(` historical window: ${c.historicalWindowHours} hours\n`);
|
|
448
|
+
}
|
|
449
|
+
if (c.minHoursBetweenAlerts != null) {
|
|
450
|
+
stdout.write(` min hours between alerts: ${c.minHoursBetweenAlerts}\n`);
|
|
451
|
+
}
|
|
452
|
+
if (c.notificationChannels) stdout.write(` channels: ${c.notificationChannels}\n`);
|
|
453
|
+
if (c.emailRecipients) stdout.write(` email: ${c.emailRecipients}\n`);
|
|
454
|
+
if (c.webhookUrl) stdout.write(` webhook: ${c.webhookUrl}\n`);
|
|
455
|
+
stdout.write(` enabled: ${c.isEnabled !== false}\n`);
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
module.exports = { run };
|