datagrok-tools 6.7.9 → 6.7.10
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/.devcontainer/Dockerfile.pkg_dev +1 -1
- package/.turbo/turbo-build.log +2 -2
- package/CHANGELOG.md +22 -1
- package/CLAUDE.md +1 -0
- package/GROK_S.md +69 -1
- package/README.md +14 -0
- package/bin/commands/api.js +29 -8
- package/bin/commands/check.js +43 -1
- package/bin/commands/connect-folder.js +411 -0
- package/bin/commands/help.js +66 -0
- package/bin/commands/publish.js +6 -0
- package/bin/commands/server-observe.js +335 -7
- package/bin/commands/server.js +65 -5
- package/bin/commands/src.js +458 -0
- package/bin/grok.js +9 -5
- package/bin/utils/connect-folder/docker.js +244 -0
- package/bin/utils/connect-folder/docker.ts +259 -0
- package/bin/utils/ent-helpers.js +1 -1
- package/bin/utils/node-dapi.js +29 -7
- package/bin/utils/server-client.js +8 -1
- package/bin/utils/test-utils.js +54 -2
- package/package.json +1 -1
- package/plugins/func-gen-plugin.js +1 -1
- package/vitest.config.mts +4 -0
|
@@ -6,17 +6,20 @@ Object.defineProperty(exports, "__esModule", {
|
|
|
6
6
|
exports.OBSERVE_USAGE = void 0;
|
|
7
7
|
exports.fieldRows = fieldRows;
|
|
8
8
|
exports.handleObserve = handleObserve;
|
|
9
|
+
exports.matchFilter = matchFilter;
|
|
9
10
|
exports.parseDuration = parseDuration;
|
|
10
11
|
exports.parseTime = parseTime;
|
|
12
|
+
exports.printRequestLog = printRequestLog;
|
|
11
13
|
exports.setPath = setPath;
|
|
12
14
|
var fs = _interopRequireWildcard(require("fs"));
|
|
15
|
+
var _crypto = require("crypto");
|
|
13
16
|
var _nodeDapi = require("../utils/node-dapi");
|
|
14
17
|
var _serverOutput = require("../utils/server-output");
|
|
15
18
|
var _walker = require("../utils/migrate/walker");
|
|
16
19
|
function _interopRequireWildcard(e, t) { if ("function" == typeof WeakMap) var r = new WeakMap(), n = new WeakMap(); return (_interopRequireWildcard = function (e, t) { if (!t && e && e.__esModule) return e; var o, i, f = { __proto__: null, default: e }; if (null === e || "object" != typeof e && "function" != typeof e) return f; if (o = t ? n : r) { if (o.has(e)) return o.get(e); o.set(e, f); } for (const t in e) "default" !== t && {}.hasOwnProperty.call(e, t) && ((i = (o = Object.defineProperty) && Object.getOwnPropertyDescriptor(e, t)) && (i.get || i.set) ? o(f, t, i) : f[t] = e[t]); return f; })(e, t); }
|
|
17
20
|
/// `grok s observe` (alias `grok s o`): problems, problem rules, logger settings, errors, the timeline and cloud logs,
|
|
18
21
|
/// over `/problems`, `/admin/plugins/<key>/settings`, `/alerts/Test_rules`, `/log`, `/admin/metrics`, `/log/timeline`,
|
|
19
|
-
/// `/log/cloud/*`, `/log/archive
|
|
22
|
+
/// `/log/cloud/*`, `/log/archive/*`, `/log/match`, `/log/activations` and `/docker/containers`.
|
|
20
23
|
|
|
21
24
|
const OBSERVE_USAGE = exports.OBSERVE_USAGE = `Usage: grok s observe <command> <verb> [args] (alias: grok s o)
|
|
22
25
|
|
|
@@ -25,6 +28,8 @@ const OBSERVE_USAGE = exports.OBSERVE_USAGE = `Usage: grok s observe <command> <
|
|
|
25
28
|
problems get <id|kind:key> One field per line, details by path
|
|
26
29
|
problems history <id|kind:key> [--limit 50]
|
|
27
30
|
problems status <id|kind:key> --data '{"status": "muted", "reason": "...", "until": "<ISO>"}' | --json body.json
|
|
31
|
+
problems severity <id|kind:key> <info|warning|critical|none> [--reason <text>]
|
|
32
|
+
Pin the severity its raises use; none unpins it
|
|
28
33
|
problems ack <id|kind:key> --reason <text>
|
|
29
34
|
problems resolve <id|kind:key> --reason <text>
|
|
30
35
|
rules get The problem rules (Settings > Alerts), as JSON
|
|
@@ -64,10 +69,40 @@ const OBSERVE_USAGE = exports.OBSERVE_USAGE = `Usage: grok s observe <command> <
|
|
|
64
69
|
read them with timeline --session <id>
|
|
65
70
|
logs capture stop <login|group> Remove that expiring entry; one that holds other settings is left
|
|
66
71
|
to logger set --remove
|
|
72
|
+
logs match ['<filter>'] [--level l1,l2] [--user <login>] [--package <p>] [--source <s>] [--type <t>]
|
|
73
|
+
[--function <f>] [--session <id>] [--grep <regex>] [--space <path>] [--since 1h] [--limit 1000]
|
|
74
|
+
Saved and in-memory records a log filter matches, newest first: the
|
|
75
|
+
filter of a log group (Settings > Logger), quoted as one argument, e.g.
|
|
76
|
+
'level in (error, warning) and not text ~ ResizeObserver'; the flags add
|
|
77
|
+
conditions joined by and; --grep and --space match case-sensitively
|
|
78
|
+
(--space: the space and its subspaces)
|
|
79
|
+
logs activations [stop <group id> <key>]
|
|
80
|
+
Triggered log groups that are on now; stop ends one
|
|
81
|
+
logs request <logId> What the requests of one command logged (grok s ... --log prints
|
|
82
|
+
its logId), gathered from every instance's memory; once only
|
|
83
|
+
groups [list] Log groups (Settings > Logger): what is recorded and where it goes
|
|
84
|
+
groups get <name|id>
|
|
85
|
+
groups add --name <n> [--kind records|alerts|drop] [--levels error,warning] [--flags db,auth]
|
|
86
|
+
[--filter '<filter>'] [--to db,console,<endpoint name>[:format]] [--who <group ids>]
|
|
87
|
+
[--until 2h|<ISO> [--reason <text>]] [--when '<filter>' --scope session|user|global --for 15m --before 60s]
|
|
88
|
+
[--alert-kinds health,storage] [--min-severity warning] [--audit-types t1,t2] [--usage-types t1,t2]
|
|
89
|
+
groups set <name|id> [same flags] [--enabled false] [--always]
|
|
90
|
+
Only the flags given change
|
|
91
|
+
groups rm <name|id>
|
|
92
|
+
groups put --json groups.json Replace every log group with a JSON array
|
|
93
|
+
groups reset Back to the default log groups
|
|
94
|
+
endpoints [list] | endpoints rm <name|id>
|
|
95
|
+
Where log groups can send records (OTLP, CloudWatch, Google, ClickHouse)
|
|
67
96
|
|
|
68
97
|
Durations are 30m (minutes), 24h, 7d, 2w; times are ISO 8601 (UTC without an offset) or
|
|
69
|
-
relative, written with = (--from=-2h)
|
|
98
|
+
relative, written with = (--from=-2h).
|
|
99
|
+
|
|
100
|
+
Any grok s command takes --log [<spec>]: its requests share one logId, every instance keeps what they
|
|
101
|
+
log at <spec> (verbose, standard, levels, debug flags or all, comma-separated) in memory, and the
|
|
102
|
+
command gathers and prints it to stderr at the end, as logs request does; functions run --debug adds
|
|
103
|
+
the debug call's own log.`;
|
|
70
104
|
const UNIT_MS = {
|
|
105
|
+
s: 1000,
|
|
71
106
|
m: 60000,
|
|
72
107
|
h: 3600000,
|
|
73
108
|
d: 86400000,
|
|
@@ -76,8 +111,8 @@ const UNIT_MS = {
|
|
|
76
111
|
|
|
77
112
|
/** `30m`, `2h`, `7d`, `1w` in milliseconds; `m` is minutes, unlike `grok s pull --since`, where it is months. */
|
|
78
113
|
function parseDuration(value, flag) {
|
|
79
|
-
const m = /^(\d+)([
|
|
80
|
-
if (!m || Number(m[1]) <= 0) throw new Error(`${flag} expects a duration such as 30m, 2h, 7d or 1w, got '${value}'`);
|
|
114
|
+
const m = /^(\d+)([smhdw])$/.exec(String(value ?? '').trim());
|
|
115
|
+
if (!m || Number(m[1]) <= 0) throw new Error(`${flag} expects a duration such as 60s, 30m, 2h, 7d or 1w, got '${value}'`);
|
|
81
116
|
return Number(m[1]) * UNIT_MS[m[2]];
|
|
82
117
|
}
|
|
83
118
|
|
|
@@ -191,6 +226,17 @@ async function handleObserve(dapi, command, verb, rest, argv, output) {
|
|
|
191
226
|
await needId();
|
|
192
227
|
(0, _serverOutput.printOutput)(await dapi.raw('POST', `${problemPath()}/status`, readBody(argv)), output);
|
|
193
228
|
return true;
|
|
229
|
+
case 'problems severity':
|
|
230
|
+
{
|
|
231
|
+
await needId();
|
|
232
|
+
const severity = rest[1];
|
|
233
|
+
if (!['info', 'warning', 'critical', 'none'].includes(severity)) throw new Error('Usage: grok s observe problems severity <id|kind:key> <info|warning|critical|none> [--reason <text>]');
|
|
234
|
+
(0, _serverOutput.printOutput)(await dapi.raw('POST', `${problemPath()}/severity`, {
|
|
235
|
+
severity: severity === 'none' ? null : severity,
|
|
236
|
+
reason: argv.reason
|
|
237
|
+
}), output);
|
|
238
|
+
return true;
|
|
239
|
+
}
|
|
194
240
|
case 'problems ack':
|
|
195
241
|
case 'problems resolve':
|
|
196
242
|
await needId();
|
|
@@ -303,14 +349,16 @@ async function handleObserve(dapi, command, verb, rest, argv, output) {
|
|
|
303
349
|
return true;
|
|
304
350
|
}
|
|
305
351
|
}
|
|
352
|
+
if (command === 'groups' || command === 'endpoints' || command === 'logs' && verb === 'activations') return logGroups(dapi, command, verb, rest, argv, await settings('logger'), saveLogger, output);
|
|
306
353
|
if (command === 'logs' && verb === 'capture') return capture(dapi, rest, argv, await settings('logger'), saveLogger, output);
|
|
307
354
|
if (command === 'logs') return logs(dapi, verb, rest, argv, output);
|
|
308
355
|
if (command === 'timeline') {
|
|
309
356
|
if (argv.session === undefined) throw new Error('Usage: grok s observe timeline --session <id> [--from <ISO|-15m>] [--to <ISO>] [--limit 500]');
|
|
310
357
|
needFrom(argv);
|
|
358
|
+
const now = Date.now();
|
|
311
359
|
const window = argv.from === undefined ? {
|
|
312
|
-
from:
|
|
313
|
-
to: new Date().toISOString()
|
|
360
|
+
from: new Date(now - 15 * 60000).toISOString(),
|
|
361
|
+
to: new Date(now).toISOString()
|
|
314
362
|
} : {
|
|
315
363
|
from: iso(argv.from, '--from'),
|
|
316
364
|
to: iso(argv.to, '--to')
|
|
@@ -327,6 +375,234 @@ async function handleObserve(dapi, command, verb, rest, argv, output) {
|
|
|
327
375
|
const needFrom = argv => {
|
|
328
376
|
if (argv.to !== undefined && argv.from === undefined) throw new Error('--to needs --from');
|
|
329
377
|
};
|
|
378
|
+
const oneOf = (value, flag, choices) => {
|
|
379
|
+
if (choices && !choices.includes(value)) throw new Error(`${flag} is one of ${choices.join(', ')}, not '${value}'`);
|
|
380
|
+
return value;
|
|
381
|
+
};
|
|
382
|
+
|
|
383
|
+
/// The comma list of `--<flag>`, each value one of [choices] when given.
|
|
384
|
+
const list = (argv, flag, choices) => {
|
|
385
|
+
if (argv[flag] === true) throw new Error(`--${flag} needs a value`);
|
|
386
|
+
return String(argv[flag]).split(',').map(x => x.trim()).filter(Boolean).map(v => oneOf(v, `--${flag}`, choices));
|
|
387
|
+
};
|
|
388
|
+
const LEVELS = ['error', 'warning', 'info', 'debug', 'audit', 'usage'];
|
|
389
|
+
const DEBUG_FLAGS = ['db', 'socket', 'query', 'hash', 'storage', 'credentials', 'balloon', 'startup', 'srcmaps', 'docker', 'auth', 'ai-indexing', 'email', 'all'];
|
|
390
|
+
|
|
391
|
+
/// A duration in whole units of [unit] ms; 0 is allowed.
|
|
392
|
+
const durationIn = (value, flag, unit, unitName) => {
|
|
393
|
+
if (String(value).trim() === '0') return 0;
|
|
394
|
+
const ms = parseDuration(value, flag);
|
|
395
|
+
if (ms % unit !== 0) throw new Error(`${flag} expects whole ${unitName}, got '${value}'`);
|
|
396
|
+
return ms / unit;
|
|
397
|
+
};
|
|
398
|
+
|
|
399
|
+
/// A log group of the logger settings by id or name.
|
|
400
|
+
function findGroup(groups, ref) {
|
|
401
|
+
const byId = groups.find(g => g.id === ref);
|
|
402
|
+
if (byId) return byId;
|
|
403
|
+
const named = groups.filter(g => g.name === ref);
|
|
404
|
+
if (named.length !== 1) throw new Error(named.length ? `More than one log group is named '${ref}': use its id` : `No log group '${ref}'`);
|
|
405
|
+
return named[0];
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
/// `groups add|set` flags applied to [g]; only the flags given change it.
|
|
409
|
+
function applyGroupFlags(g, argv) {
|
|
410
|
+
if (argv.name !== undefined) g.name = String(argv.name);
|
|
411
|
+
if (argv.kind !== undefined) g.kind = oneOf(String(argv.kind), '--kind', ['records', 'alerts', 'drop']);
|
|
412
|
+
if (argv.enabled !== undefined) g.enabled = argv.enabled === true || argv.enabled === 'true';
|
|
413
|
+
if (argv.who !== undefined) g.who = list(argv, 'who');
|
|
414
|
+
if (argv.levels !== undefined) g.levels = list(argv, 'levels', LEVELS);
|
|
415
|
+
if (argv.flags !== undefined) g.debugFlags = list(argv, 'flags', DEBUG_FLAGS);
|
|
416
|
+
if (argv['audit-types'] !== undefined) g.auditTypes = list(argv, 'audit-types');
|
|
417
|
+
if (argv['usage-types'] !== undefined) g.usageTypes = list(argv, 'usage-types');
|
|
418
|
+
if (argv.filter !== undefined) g.filter = String(argv.filter) || null;
|
|
419
|
+
if (argv['alert-kinds'] !== undefined) g.alertKinds = list(argv, 'alert-kinds');
|
|
420
|
+
if (argv['min-severity'] !== undefined) g.alertMinSeverity = oneOf(String(argv['min-severity']), '--min-severity', ['info', 'warning', 'critical']);
|
|
421
|
+
if (argv.to !== undefined) {
|
|
422
|
+
const old = g.saveTo ?? [];
|
|
423
|
+
const to = list(argv, 'to');
|
|
424
|
+
if (!to.length) throw new Error('--to needs a value');
|
|
425
|
+
g.saveTo = to.map(t => {
|
|
426
|
+
const [destination, format] = t.split(':');
|
|
427
|
+
return {
|
|
428
|
+
...(old.find(x => x.destination === destination) ?? {}),
|
|
429
|
+
destination,
|
|
430
|
+
...(format ? {
|
|
431
|
+
format
|
|
432
|
+
} : {})
|
|
433
|
+
};
|
|
434
|
+
});
|
|
435
|
+
}
|
|
436
|
+
if (argv.until !== undefined) {
|
|
437
|
+
g.activation = 'until';
|
|
438
|
+
g.until = /^\d+[smhdw]$/.test(String(argv.until)) ? new Date(Date.now() + parseDuration(String(argv.until), '--until')).toISOString() : iso(argv.until, '--until');
|
|
439
|
+
}
|
|
440
|
+
if (argv.reason !== undefined) g.reason = String(argv.reason);
|
|
441
|
+
if (argv.when !== undefined) {
|
|
442
|
+
g.activation = 'triggered';
|
|
443
|
+
g.triggerWhen = String(argv.when);
|
|
444
|
+
}
|
|
445
|
+
if (argv.scope !== undefined) g.triggerScope = oneOf(String(argv.scope), '--scope', ['session', 'user', 'global']);
|
|
446
|
+
if (argv.for !== undefined) g.activeMinutes = durationIn(argv.for, '--for', 60000, 'minutes');
|
|
447
|
+
if (argv.before !== undefined) g.flushBeforeSeconds = durationIn(argv.before, '--before', 1000, 'seconds');
|
|
448
|
+
if (argv.always) g.activation = 'always';
|
|
449
|
+
return g;
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
/// `groups ...`, `endpoints ...` and `logs activations`: the log groups and endpoints of Settings > Logger.
|
|
453
|
+
async function logGroups(dapi, command, verb, rest, argv, current, save, output) {
|
|
454
|
+
if (command === 'endpoints') {
|
|
455
|
+
const endpoints = current.exportSettings ?? [];
|
|
456
|
+
if (verb === 'list' || verb === undefined) {
|
|
457
|
+
(0, _serverOutput.printOutput)(output === 'table' ? endpoints.map(e => ({
|
|
458
|
+
name: e.name ?? '',
|
|
459
|
+
type: e['#type'],
|
|
460
|
+
enabled: e.isEnabled !== false,
|
|
461
|
+
target: e.endpoint ?? e.url ?? e.cloudWatchLogGroup ?? e.logName ?? '',
|
|
462
|
+
id: e.id
|
|
463
|
+
})) : endpoints, output);
|
|
464
|
+
return true;
|
|
465
|
+
}
|
|
466
|
+
if (verb === 'rm' && rest[0] !== undefined) {
|
|
467
|
+
const ref = String(rest[0]);
|
|
468
|
+
const e = endpoints.find(x => x.id === ref || x.name === ref);
|
|
469
|
+
if (!e) throw new Error(`No endpoint '${ref}'`);
|
|
470
|
+
current.exportSettings = endpoints.filter(x => x !== e);
|
|
471
|
+
for (const g of current.logGroups ?? []) g.saveTo = (g.saveTo ?? []).filter(t => t.destination !== e.id && t.destination !== e.name);
|
|
472
|
+
await save(current);
|
|
473
|
+
(0, _serverOutput.printOutput)({
|
|
474
|
+
removed: e.name ?? e.id
|
|
475
|
+
}, output);
|
|
476
|
+
return true;
|
|
477
|
+
}
|
|
478
|
+
throw new Error('Usage: grok s observe endpoints list | endpoints rm <name|id> (add one with logger set --set ' + '\'exportSettings=[...]\' or GROK_PARAMETERS settings.logger.exportSettings)');
|
|
479
|
+
}
|
|
480
|
+
if (command === 'logs' && verb === 'activations') {
|
|
481
|
+
if (rest[0] === 'stop') {
|
|
482
|
+
if (rest[1] === undefined || rest[2] === undefined) throw new Error('Usage: grok s observe logs activations stop <group id> <key>');
|
|
483
|
+
await dapi.raw('DELETE', `/log/activations${(0, _nodeDapi.buildQuery)({
|
|
484
|
+
group: rest[1],
|
|
485
|
+
key: rest[2]
|
|
486
|
+
})}`);
|
|
487
|
+
(0, _serverOutput.printOutput)({
|
|
488
|
+
stopped: `${rest[1]} ${rest[2]}`
|
|
489
|
+
}, output);
|
|
490
|
+
return true;
|
|
491
|
+
}
|
|
492
|
+
(0, _serverOutput.printOutput)(await dapi.raw('GET', '/log/activations'), output);
|
|
493
|
+
return true;
|
|
494
|
+
}
|
|
495
|
+
const groups = current.logGroups ?? [];
|
|
496
|
+
// A server older than the effective list answers null until groups are saved; starting from none would drop the defaults.
|
|
497
|
+
if (['add', 'set', 'rm'].includes(verb) && !Array.isArray(current.logGroups)) throw new Error('The server does not return its default log groups, so they cannot be edited one by one: ' + 'replace them all with groups put --json <file>');
|
|
498
|
+
const ref = () => {
|
|
499
|
+
if (rest[0] === undefined) throw new Error(`Usage: grok s observe groups ${verb} <name|id>`);
|
|
500
|
+
return findGroup(groups, String(rest[0]));
|
|
501
|
+
};
|
|
502
|
+
switch (verb) {
|
|
503
|
+
case undefined:
|
|
504
|
+
case 'list':
|
|
505
|
+
(0, _serverOutput.printOutput)(output === 'table' ? groups.map(g => ({
|
|
506
|
+
name: g.name ?? '',
|
|
507
|
+
kind: g.kind ?? 'records',
|
|
508
|
+
enabled: g.enabled !== false,
|
|
509
|
+
active: g.activation ?? 'always',
|
|
510
|
+
levels: (g.levels ?? []).join(',') || 'all',
|
|
511
|
+
filter: g.filter ?? '',
|
|
512
|
+
to: (g.saveTo ?? []).map(t => t.format ? `${t.destination}:${t.format}` : t.destination).join(','),
|
|
513
|
+
id: g.id
|
|
514
|
+
})) : groups, output);
|
|
515
|
+
return true;
|
|
516
|
+
case 'get':
|
|
517
|
+
(0, _serverOutput.printOutput)(ref(), output === 'table' ? 'json' : output);
|
|
518
|
+
return true;
|
|
519
|
+
case 'add':
|
|
520
|
+
{
|
|
521
|
+
if (argv.name === undefined) throw new Error('Usage: grok s observe groups add --name <name> [--kind records|alerts|drop] [--levels error,warning] ' + '[--flags db] [--filter \'<filter>\'] [--to db,console,<endpoint>[:format]] [--who <group ids>] [--until 2h|<ISO>] ' + '[--when \'<filter>\' --scope session --for 15m --before 60s]');
|
|
522
|
+
const g = applyGroupFlags({
|
|
523
|
+
id: (0, _crypto.randomUUID)(),
|
|
524
|
+
kind: 'records',
|
|
525
|
+
enabled: true,
|
|
526
|
+
who: [],
|
|
527
|
+
levels: [],
|
|
528
|
+
debugFlags: [],
|
|
529
|
+
activation: 'always',
|
|
530
|
+
saveTo: [{
|
|
531
|
+
destination: 'db'
|
|
532
|
+
}]
|
|
533
|
+
}, argv);
|
|
534
|
+
current.logGroups = [...groups, g];
|
|
535
|
+
await save(current);
|
|
536
|
+
(0, _serverOutput.printOutput)(g, output === 'table' ? 'json' : output);
|
|
537
|
+
return true;
|
|
538
|
+
}
|
|
539
|
+
case 'set':
|
|
540
|
+
{
|
|
541
|
+
const g = ref();
|
|
542
|
+
applyGroupFlags(g, argv);
|
|
543
|
+
current.logGroups = groups;
|
|
544
|
+
await save(current);
|
|
545
|
+
(0, _serverOutput.printOutput)(g, output === 'table' ? 'json' : output);
|
|
546
|
+
return true;
|
|
547
|
+
}
|
|
548
|
+
case 'rm':
|
|
549
|
+
{
|
|
550
|
+
const g = ref();
|
|
551
|
+
if (g.protected) throw new Error(`'${g.name ?? g.id}' is the audit group: it can only be switched off (--enabled false), which needs EditAuditLogging`);
|
|
552
|
+
current.logGroups = groups.filter(x => x !== g);
|
|
553
|
+
await save(current);
|
|
554
|
+
(0, _serverOutput.printOutput)({
|
|
555
|
+
removed: g.name ?? g.id
|
|
556
|
+
}, output);
|
|
557
|
+
return true;
|
|
558
|
+
}
|
|
559
|
+
case 'reset':
|
|
560
|
+
current.logGroups = null;
|
|
561
|
+
await save(current);
|
|
562
|
+
(0, _serverOutput.printOutput)({
|
|
563
|
+
reset: 'the default log groups apply'
|
|
564
|
+
}, output);
|
|
565
|
+
return true;
|
|
566
|
+
case 'put':
|
|
567
|
+
{
|
|
568
|
+
const replaced = readBody(argv);
|
|
569
|
+
if (!Array.isArray(replaced)) throw new Error('groups put takes a JSON array of log groups');
|
|
570
|
+
current.logGroups = replaced.map(g => ({
|
|
571
|
+
id: (0, _crypto.randomUUID)(),
|
|
572
|
+
...g
|
|
573
|
+
}));
|
|
574
|
+
await save(current);
|
|
575
|
+
(0, _serverOutput.printOutput)({
|
|
576
|
+
groups: current.logGroups.length
|
|
577
|
+
}, output);
|
|
578
|
+
return true;
|
|
579
|
+
}
|
|
580
|
+
}
|
|
581
|
+
throw new Error('Usage: grok s observe groups list | get <name|id> | add --name <n> ... | set <name|id> ... | rm <name|id> | put --json <file> | reset');
|
|
582
|
+
}
|
|
583
|
+
|
|
584
|
+
/// The log filter text of `logs match`: the expression given, and a condition per flag, joined by `and`.
|
|
585
|
+
function matchFilter(rest, argv) {
|
|
586
|
+
const parts = rest.length ? [rest.join(' ')] : [];
|
|
587
|
+
const word = v => /^[^\s=<>~!(),"']+$/.test(v) && !/^(and|or|not)$/i.test(v) ? v : `"${v.replace(/\\/g, '\\\\').replace(/"/g, '\\"')}"`;
|
|
588
|
+
const values = (flag, split) => {
|
|
589
|
+
const given = argv[flag] === true ? [] : split ? String(argv[flag]).split(',').map(x => x.trim()).filter(Boolean) : [String(argv[flag])].filter(Boolean);
|
|
590
|
+
if (!given.length) throw new Error(`--${flag} needs a value`);
|
|
591
|
+
return given;
|
|
592
|
+
};
|
|
593
|
+
for (const field of ['level', 'type', 'user', 'package', 'source', 'function', 'session']) {
|
|
594
|
+
if (argv[field] === undefined) continue;
|
|
595
|
+
const given = values(field, true);
|
|
596
|
+
parts.push(given.length > 1 ? `${field} in (${given.map(word).join(', ')})` : `${field} = ${word(given[0])}`);
|
|
597
|
+
}
|
|
598
|
+
if (argv.grep !== undefined) parts.push(`text =~ ${word(values('grep', false)[0])}`);
|
|
599
|
+
// = is case-sensitive and startsWith is not, so one anchored regex keeps the space and its subspaces alike.
|
|
600
|
+
if (argv.space !== undefined) {
|
|
601
|
+
const space = values('space', false)[0].replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
602
|
+
parts.push(`space =~ ${word(`^${space}(:|$)`)}`);
|
|
603
|
+
}
|
|
604
|
+
return parts.length > 1 ? parts.map(p => `(${p})`).join(' and ') : parts[0] ?? '';
|
|
605
|
+
}
|
|
330
606
|
async function logs(dapi, verb, rest, argv, output) {
|
|
331
607
|
const connection = argv.connection;
|
|
332
608
|
if (verb === 'cloud') {
|
|
@@ -353,6 +629,28 @@ async function logs(dapi, verb, rest, argv, output) {
|
|
|
353
629
|
return true;
|
|
354
630
|
}
|
|
355
631
|
if (verb === 'container') return containerLogs(dapi, rest[0], argv, output);
|
|
632
|
+
if (verb === 'match') {
|
|
633
|
+
const filter = matchFilter(rest, argv);
|
|
634
|
+
const since = `${Math.round(parseDuration(argv.since ?? '1h', '--since') / 1000)}s`;
|
|
635
|
+
(0, _serverOutput.printOutput)(await dapi.raw('GET', `/log/match${(0, _nodeDapi.buildQuery)({
|
|
636
|
+
filter,
|
|
637
|
+
since,
|
|
638
|
+
limit: argv.limit,
|
|
639
|
+
format: 'json'
|
|
640
|
+
})}`), output);
|
|
641
|
+
return true;
|
|
642
|
+
}
|
|
643
|
+
if (verb === 'request' && rest[0] !== undefined) {
|
|
644
|
+
const rows = await readRequestLog(dapi, String(rest[0]));
|
|
645
|
+
(0, _serverOutput.printOutput)(output === 'table' ? rows.map(r => ({
|
|
646
|
+
time: r.time,
|
|
647
|
+
level: r.level,
|
|
648
|
+
flag: r.flag ?? '',
|
|
649
|
+
source: r.source ?? '',
|
|
650
|
+
message: r.message
|
|
651
|
+
})) : rows, output);
|
|
652
|
+
return true;
|
|
653
|
+
}
|
|
356
654
|
if (verb === 'archive' && rest[0] === 'list') {
|
|
357
655
|
let objects = (await dapi.raw('GET', `/log/archive/objects${(0, _nodeDapi.buildQuery)({
|
|
358
656
|
connection,
|
|
@@ -375,7 +673,37 @@ async function logs(dapi, verb, rest, argv, output) {
|
|
|
375
673
|
})}`), output);
|
|
376
674
|
return true;
|
|
377
675
|
}
|
|
378
|
-
throw new Error('Usage: grok s observe logs cloud [--group <g>] ... | logs container [<name|id>] | ' + 'logs archive list [--prefix <p>] | logs archive read <key> | logs capture [<login|group>] | ' + 'logs capture stop <login|group>');
|
|
676
|
+
throw new Error('Usage: grok s observe logs cloud [--group <g>] ... | logs container [<name|id>] | ' + 'logs match [<filter>] | logs archive list [--prefix <p>] | logs archive read <key> | logs request <logId> | logs capture [<login|group>] | ' + 'logs capture stop <login|group>');
|
|
677
|
+
}
|
|
678
|
+
|
|
679
|
+
/// `GET /log/requests/<logId>`: what every isolate of every instance kept in memory for the id, as JSON lines,
|
|
680
|
+
/// oldest first. Gathering hands the records over, so a second read returns none.
|
|
681
|
+
async function readRequestLog(dapi, logId) {
|
|
682
|
+
return (await dapi.raw('GET', `/log/requests/${encodeURIComponent(logId)}`)) ?? [];
|
|
683
|
+
}
|
|
684
|
+
|
|
685
|
+
/** After a command run with `--log`: its log on stderr, so stdout keeps the result; JSON lines under `--output json`. */
|
|
686
|
+
async function printRequestLog(dapi, logId, output) {
|
|
687
|
+
let rows;
|
|
688
|
+
try {
|
|
689
|
+
rows = await readRequestLog(dapi, logId);
|
|
690
|
+
} catch (e) {
|
|
691
|
+
process.stderr.write(`--log: could not read the log of ${logId}: ${e?.message ?? e}\n`);
|
|
692
|
+
return;
|
|
693
|
+
}
|
|
694
|
+
if (output === 'json') {
|
|
695
|
+
for (const r of rows) process.stderr.write(JSON.stringify({
|
|
696
|
+
logId,
|
|
697
|
+
...r
|
|
698
|
+
}) + '\n');
|
|
699
|
+
return;
|
|
700
|
+
}
|
|
701
|
+
process.stderr.write(`── log ${logId}: ${rows.length} record${rows.length === 1 ? '' : 's'} (grok s o logs request ${logId})\n`);
|
|
702
|
+
for (const r of rows) {
|
|
703
|
+
const tags = [r.flag, r.source].filter(Boolean).join(' ');
|
|
704
|
+
process.stderr.write(`${String(r.time ?? '').substring(11, 23)} ${String(r.level ?? '').toUpperCase().padEnd(7)} ` + `${tags ? `[${tags}] ` : ''}${r.message ?? ''}\n`);
|
|
705
|
+
if (r.stackTrace) process.stderr.write(String(r.stackTrace).replace(/^/gm, ' ') + '\n');
|
|
706
|
+
}
|
|
379
707
|
}
|
|
380
708
|
|
|
381
709
|
/// `logs container`: the server only cuts a container's log by line count, so the window and --grep apply here.
|
package/bin/commands/server.js
CHANGED
|
@@ -10,6 +10,7 @@ exports.resolveManifestSources = resolveManifestSources;
|
|
|
10
10
|
exports.server = server;
|
|
11
11
|
var fs = _interopRequireWildcard(require("fs"));
|
|
12
12
|
var path = _interopRequireWildcard(require("path"));
|
|
13
|
+
var _crypto = require("crypto");
|
|
13
14
|
var _nodeDapi = require("../utils/node-dapi");
|
|
14
15
|
var _serverClient = require("../utils/server-client");
|
|
15
16
|
var _serverOutput = require("../utils/server-output");
|
|
@@ -28,7 +29,7 @@ const ENTITY_TYPES = {
|
|
|
28
29
|
reports: 'UserReport'
|
|
29
30
|
};
|
|
30
31
|
const ENTITIES = ['users', 'groups', 'functions', 'connections', 'queries', 'scripts', 'packages', 'reports', 'files', 'tables'];
|
|
31
|
-
const COMMANDS = ['shares', 'domains', 'raw', 'batch', 'describe', 'healthcheck', 'sync', 'pull', 'push', 'migrate', 'diff', 'bundle', 'token', 'observe', 'o'];
|
|
32
|
+
const COMMANDS = ['shares', 'domains', 'raw', 'batch', 'describe', 'healthcheck', 'status', 'sync', 'pull', 'push', 'migrate', 'diff', 'bundle', 'token', 'observe', 'o'];
|
|
32
33
|
const VERBS = ['list', 'count', 'get', 'delete'];
|
|
33
34
|
async function server(argv) {
|
|
34
35
|
const args = argv['_'].slice(1);
|
|
@@ -51,8 +52,13 @@ async function server(argv) {
|
|
|
51
52
|
return true;
|
|
52
53
|
}
|
|
53
54
|
let client;
|
|
55
|
+
const logSpec = (0, _serverClient.logSpecOf)(argv);
|
|
56
|
+
const logId = logSpec || entity === 'functions' && verb === 'run' && isDebug(argv) ? (0, _crypto.randomUUID)() : undefined;
|
|
54
57
|
try {
|
|
55
|
-
client = await (0, _serverClient.createClient)(host, !!argv.admin
|
|
58
|
+
client = await (0, _serverClient.createClient)(host, !!argv.admin, logId ? {
|
|
59
|
+
logId,
|
|
60
|
+
spec: logSpec
|
|
61
|
+
} : undefined);
|
|
56
62
|
} catch (err) {
|
|
57
63
|
// a bad alias, URL or key is not a usage error: no help dump, just the reason
|
|
58
64
|
(0, _serverOutput.printError)(err);
|
|
@@ -76,6 +82,7 @@ async function server(argv) {
|
|
|
76
82
|
if (entity === 'raw') return await handleRaw(dapi, verb, rest, argv, output);
|
|
77
83
|
if (entity === 'describe') return await handleDescribe(dapi, verb ?? rest[0], output);
|
|
78
84
|
if (entity === 'healthcheck') return await handleHealthcheck(dapi, argv, output);
|
|
85
|
+
if (entity === 'status') return await handleStatus(dapi, output);
|
|
79
86
|
if (entity === 'sync') return await handleSync(dapi, verb, rest, argv, output);
|
|
80
87
|
if (entity === 'functions' && verb === 'run') return await handleFuncRun(dapi, rest, argv, output);
|
|
81
88
|
if (entity === 'functions' && verb === 'list') return await handleFunctionsList(dapi, argv, limit, offset, filter, output);
|
|
@@ -184,8 +191,15 @@ async function server(argv) {
|
|
|
184
191
|
});
|
|
185
192
|
process.exitCode = 1;
|
|
186
193
|
return true;
|
|
194
|
+
} finally {
|
|
195
|
+
if (logId) await (0, _serverObserve.printRequestLog)(dapi, logId, output);
|
|
187
196
|
}
|
|
188
197
|
}
|
|
198
|
+
|
|
199
|
+
/** `--debug` is not a declared boolean (grok publish reads it too), so it can arrive as a string. */
|
|
200
|
+
function isDebug(argv) {
|
|
201
|
+
return argv.debug !== undefined && argv.debug !== false && argv.debug !== 'false';
|
|
202
|
+
}
|
|
189
203
|
async function handleFilesPut(dapi, rest, output) {
|
|
190
204
|
const [localPath, remotePath] = rest;
|
|
191
205
|
if (!localPath || !remotePath) {
|
|
@@ -598,6 +612,39 @@ async function handleHealthcheck(dapi, argv, output) {
|
|
|
598
612
|
if (services.length) (0, _serverOutput.printOutput)(services, output);else if (module) throw new Error(`Module '${module}' is not reported by the server`);else if (output !== 'quiet') console.log('(no services reported)');
|
|
599
613
|
return true;
|
|
600
614
|
}
|
|
615
|
+
async function handleStatus(dapi, output) {
|
|
616
|
+
const [info, extended] = await Promise.all([dapi.serverStatus(), dapi.serverStatus(true)]);
|
|
617
|
+
if (output === 'json') {
|
|
618
|
+
(0, _serverOutput.printOutput)({
|
|
619
|
+
server: info,
|
|
620
|
+
extended
|
|
621
|
+
}, output);
|
|
622
|
+
return true;
|
|
623
|
+
}
|
|
624
|
+
if (output === 'quiet') {
|
|
625
|
+
console.log(info?.Version ?? '');
|
|
626
|
+
return true;
|
|
627
|
+
}
|
|
628
|
+
const http = info?.['Http Server'];
|
|
629
|
+
const fields = {
|
|
630
|
+
...info,
|
|
631
|
+
'Http Server': undefined
|
|
632
|
+
};
|
|
633
|
+
const width = Math.max(...Object.keys(fields).map(k => k.length)) + 2;
|
|
634
|
+
for (const [k, v] of Object.entries(fields)) if (v !== undefined) console.log(`${(k + ':').padEnd(width)}${v ?? ''}`);
|
|
635
|
+
if (http) console.log(`${'Http Server:'.padEnd(width)}total ${http.total}, active ${http.active}, idle ${http.idle}, closing ${http.closing}`);
|
|
636
|
+
// Non-admins get the plain count back; admins get {"METHOD url": inFlight}.
|
|
637
|
+
const active = extended?.['Http Server']?.active;
|
|
638
|
+
if (active && typeof active === 'object') {
|
|
639
|
+
console.log('');
|
|
640
|
+
const rows = Object.entries(active).map(([request, count]) => ({
|
|
641
|
+
request,
|
|
642
|
+
count
|
|
643
|
+
}));
|
|
644
|
+
if (rows.length) (0, _serverOutput.printOutput)(rows, output);else console.log('(no requests in flight)');
|
|
645
|
+
}
|
|
646
|
+
return true;
|
|
647
|
+
}
|
|
601
648
|
async function handleDescribe(dapi, entityType, output) {
|
|
602
649
|
if (!entityType) {
|
|
603
650
|
(0, _serverOutput.printError)(new Error('Usage: grok s describe <entity|type> e.g. grok s describe connections, grok s describe Project'));
|
|
@@ -617,9 +664,11 @@ async function handleDescribe(dapi, entityType, output) {
|
|
|
617
664
|
return true;
|
|
618
665
|
}
|
|
619
666
|
async function handleFuncRun(dapi, rest, argv, output) {
|
|
620
|
-
|
|
667
|
+
// `--debug 'Pkg:f()'` hands the call to --debug.
|
|
668
|
+
let funcName = rest[0] ?? (typeof argv.debug === 'string' ? argv.debug : undefined);
|
|
669
|
+
const debug = isDebug(argv);
|
|
621
670
|
if (!funcName) {
|
|
622
|
-
(0, _serverOutput.printError)(new Error('Usage: grok s functions run <Name:function(args...)> [--json params.json]'));
|
|
671
|
+
(0, _serverOutput.printError)(new Error('Usage: grok s functions run <Name:function(args...)> [--json params.json] [--debug]'));
|
|
623
672
|
return false;
|
|
624
673
|
}
|
|
625
674
|
let params = {};
|
|
@@ -637,7 +686,7 @@ async function handleFuncRun(dapi, rest, argv, output) {
|
|
|
637
686
|
params = parsed.params;
|
|
638
687
|
if (Object.keys(params).some(k => /^\d+$/.test(k))) params = (0, _nodeDapi.mapPositionalParams)(params, (await dapi.functions.find(funcName))?.parameterInfos, funcName);
|
|
639
688
|
}
|
|
640
|
-
const result = await dapi.functions.run(funcName, params);
|
|
689
|
+
const result = await dapi.functions.run(funcName, params, debug);
|
|
641
690
|
(0, _serverOutput.printOutput)(result, output);
|
|
642
691
|
return true;
|
|
643
692
|
}
|
|
@@ -953,6 +1002,8 @@ Special commands:
|
|
|
953
1002
|
grok s raw <METHOD> <path> [--json f | --data j] Any API endpoint; path is API-relative (/users/current), /api prefix optional
|
|
954
1003
|
grok s describe <entity|type> Fields of an entity type (registry record + a live sample)
|
|
955
1004
|
grok s healthcheck [--module <name>] Check server + per-module health
|
|
1005
|
+
grok s status Server build, uptime, DB pool and HTTP connections
|
|
1006
|
+
(/info/server); admins also see requests in flight
|
|
956
1007
|
grok s shares add <entity> <group>[,<group>...] [--access View|Edit]
|
|
957
1008
|
Share an entity with one or more groups
|
|
958
1009
|
grok s shares list <entity-id-or-name> List who an entity is shared with
|
|
@@ -1057,6 +1108,13 @@ Options:
|
|
|
1057
1108
|
--offset <n> Start offset (default: 0)
|
|
1058
1109
|
-r, --recursive Recursive (for files list)
|
|
1059
1110
|
--verbose Print the stack of a runtime failure, not just its message
|
|
1111
|
+
--log [<spec>] Keep what this command's requests log at <spec>, then collect it from
|
|
1112
|
+
every server instance and print it to stderr: verbose (default),
|
|
1113
|
+
standard, levels (error, warning, info, debug, audit, usage) and debug
|
|
1114
|
+
flags (db, storage, credentials, ..., or all), comma-separated. Only
|
|
1115
|
+
these requests are affected; no permission is needed
|
|
1116
|
+
--debug functions run: a debug call, as the query debugger runs it; its log (the
|
|
1117
|
+
query's stages) prints the same way
|
|
1060
1118
|
--json <file> Read a JSON body from a file (save, functions run, batch, raw)
|
|
1061
1119
|
--data '<json>' Inline JSON body for raw
|
|
1062
1120
|
-O, --output-file Write table download to a file instead of stdout
|
|
@@ -1087,6 +1145,8 @@ Examples:
|
|
|
1087
1145
|
grok s connections test --json conn.json
|
|
1088
1146
|
grok s functions run 'Chem:smilesToMw("ccc")'
|
|
1089
1147
|
grok s functions run Chem:test --json params.json
|
|
1148
|
+
grok s functions run 'MyPkg:MyQuery()' --debug # the query's own log
|
|
1149
|
+
grok s functions run UsageAnalysis:Groups --log verbose,db # and every SQL statement it ran
|
|
1090
1150
|
grok s functions list --type script --language python --limit 20
|
|
1091
1151
|
grok s functions list --package Chem --type query
|
|
1092
1152
|
grok s functions list --package PowerPack --type package --filter 'name contains "grid"'
|