datagrok-tools 6.7.7 → 6.7.9

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.
@@ -0,0 +1,526 @@
1
+ "use strict";
2
+
3
+ Object.defineProperty(exports, "__esModule", {
4
+ value: true
5
+ });
6
+ exports.OBSERVE_USAGE = void 0;
7
+ exports.fieldRows = fieldRows;
8
+ exports.handleObserve = handleObserve;
9
+ exports.parseDuration = parseDuration;
10
+ exports.parseTime = parseTime;
11
+ exports.setPath = setPath;
12
+ var fs = _interopRequireWildcard(require("fs"));
13
+ var _nodeDapi = require("../utils/node-dapi");
14
+ var _serverOutput = require("../utils/server-output");
15
+ var _walker = require("../utils/migrate/walker");
16
+ 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
+ /// `grok s observe` (alias `grok s o`): problems, problem rules, logger settings, errors, the timeline and cloud logs,
18
+ /// over `/problems`, `/admin/plugins/<key>/settings`, `/alerts/Test_rules`, `/log`, `/admin/metrics`, `/log/timeline`,
19
+ /// `/log/cloud/*`, `/log/archive/*` and `/docker/containers`.
20
+
21
+ const OBSERVE_USAGE = exports.OBSERVE_USAGE = `Usage: grok s observe <command> <verb> [args] (alias: grok s o)
22
+
23
+ problems list [--status <s>] [--state <s>] [--alert-status <s>] [--kind <k>] [--key <k>] [--since 7d] [--limit 100]
24
+ Comma lists or all; --alert-status open lists the open alerts
25
+ problems get <id|kind:key> One field per line, details by path
26
+ problems history <id|kind:key> [--limit 50]
27
+ problems status <id|kind:key> --data '{"status": "muted", "reason": "...", "until": "<ISO>"}' | --json body.json
28
+ problems ack <id|kind:key> --reason <text>
29
+ problems resolve <id|kind:key> --reason <text>
30
+ rules get The problem rules (Settings > Alerts), as JSON
31
+ rules put --json rules.json Replace them; the server checks them first
32
+ rules test Evaluate the saved rules once, now
33
+ logger get [<path>] The logger settings, or one value (exportSettings.0.types)
34
+ logger set --set <path>=<json> ... Change values; with --user <login> or --group <name|id>, that
35
+ [--for 30m | --until <ISO>] [--reason <text>] entry of userGroupSettings (a new one starts from
36
+ [--remove] All users), removed when it expires; --remove deletes it
37
+ logger history [--limit 20] The log-settings-changed audit records, with each change's diff
38
+ errors top [--since 24h | --date "this week" | --from <ISO> [--to <ISO>]] [--by message|signature]
39
+ [--package <name>] [--user <login>] [--source <s>] [--limit 10]
40
+ Ranked by distinct users, so errors nobody saw (scheduler, health
41
+ probes) come last: narrow those with --source server
42
+ errors get <signature|problem> [--since 7d] [--limit 50]
43
+ Each occurrence of an error (a signature from errors top, or the
44
+ id or kind:key of its problem): time, user, function, session
45
+ logs container [<name|id> [--since 1h | --from <ISO> [--to <ISO>]] [--grep <regex>] [--limit 20000]]
46
+ A docker container's log lines (its latest --limit, then the
47
+ window and --grep); no name, the containers and their status
48
+ timeline --session <id> [--from <ISO|-15m>] [--to <ISO>] [--limit 500]
49
+ A session's events and requests in a window: no --from, the last
50
+ 15 min; --to defaults to --from + 10 min, at most 2 h
51
+ logs cloud [--group <g>] [--since 1h | --from <ISO> [--to <ISO>]] [--filter <pattern>] [--limit 1000]
52
+ CloudWatch events; no --group, the instance's own group;
53
+ --filter is a CloudWatch filter pattern
54
+ logs archive list [--prefix <p>] [--since 7d] [--limit n]
55
+ Objects in the log archive (WORM bucket), by key; --since filters
56
+ only the page the server returned, so narrow it with --prefix
57
+ logs archive read <key> One archived object decoded to events
58
+ logs cloud and logs archive take [--connection <id>]; without it,
59
+ the instance's AWS role
60
+ logs capture [<login|group> [--for 30m] [--reason <text>]]
61
+ Save every API request of a user or a group for a while: logger set
62
+ of saveHttpRequests on their expiring entry (an entry that already
63
+ expires later keeps its time); no argument, the active captures;
64
+ read them with timeline --session <id>
65
+ logs capture stop <login|group> Remove that expiring entry; one that holds other settings is left
66
+ to logger set --remove
67
+
68
+ Durations are 30m (minutes), 24h, 7d, 2w; times are ISO 8601 (UTC without an offset) or
69
+ relative, written with = (--from=-2h).`;
70
+ const UNIT_MS = {
71
+ m: 60000,
72
+ h: 3600000,
73
+ d: 86400000,
74
+ w: 604800000
75
+ };
76
+
77
+ /** `30m`, `2h`, `7d`, `1w` in milliseconds; `m` is minutes, unlike `grok s pull --since`, where it is months. */
78
+ function parseDuration(value, flag) {
79
+ const m = /^(\d+)([mhdw])$/.exec(String(value ?? '').trim());
80
+ if (!m || Number(m[1]) <= 0) throw new Error(`${flag} expects a duration such as 30m, 2h, 7d or 1w, got '${value}'`);
81
+ return Number(m[1]) * UNIT_MS[m[2]];
82
+ }
83
+
84
+ /** ISO 8601 (UTC without an offset), or relative to [now] (`-2h`). */
85
+ function parseTime(value, flag, now = new Date()) {
86
+ let s = String(value ?? '').trim();
87
+ if (s.startsWith('-')) return new Date(now.getTime() - parseDuration(s.substring(1), flag));
88
+ if (/[T ].*[+-]\d{2}$/.test(s)) s = `${s}:00`;
89
+ const d = new Date(/^\d{4}-\d{2}-\d{2}$/.test(s) || /(Z|[+-]\d{2}:?\d{2})$/i.test(s) ? s : `${s}Z`);
90
+ if (!/^\d{4}-\d{2}-\d{2}/.test(s) || isNaN(d.getTime())) throw new Error(`${flag} expects an ISO time (2026-10-04T06:00) or a relative one (-2h), got '${value}'`);
91
+ return d;
92
+ }
93
+
94
+ /** `a.b.0.c=<json>` applied to [target]; a value that is not JSON is a string. */
95
+ function setPath(target, assignment) {
96
+ const at = assignment.indexOf('=');
97
+ if (at <= 0) throw new Error(`--set expects <path>=<json>, got '${assignment}'`);
98
+ const keys = assignment.slice(0, at).split('.');
99
+ const text = assignment.slice(at + 1);
100
+ let value;
101
+ try {
102
+ value = JSON.parse(text);
103
+ } catch {
104
+ value = text;
105
+ }
106
+ let node = target;
107
+ for (const key of keys.slice(0, -1)) node = node[key] ??= {};
108
+ node[keys[keys.length - 1]] = value;
109
+ return keys[0];
110
+ }
111
+ function readBody(argv) {
112
+ if (argv.json) return JSON.parse(fs.readFileSync(argv.json, 'utf8'));
113
+ if (argv.data !== undefined) return JSON.parse(String(argv.data));
114
+ throw new Error('Expected a body: --data \'<json>\' or --json <file>');
115
+ }
116
+ const iso = (value, flag) => value === undefined ? undefined : parseTime(value, flag).toISOString();
117
+
118
+ /** A record as `field, value` rows, nested objects by dotted path, so a table shows every value in full. */
119
+ function fieldRows(record, prefix = '') {
120
+ const rows = [];
121
+ for (const [key, value] of Object.entries(record ?? {})) if (value !== null && typeof value === 'object' && !Array.isArray(value)) rows.push(...fieldRows(value, `${prefix}${key}.`));else rows.push({
122
+ field: `${prefix}${key}`,
123
+ value
124
+ });
125
+ return rows;
126
+ }
127
+ async function handleObserve(dapi, command, verb, rest, argv, output) {
128
+ // A `:` cannot pass through a route parameter behind the proxy, so kind:key is looked up by the list's filters.
129
+ let id;
130
+ const needId = async () => {
131
+ const given = rest[0] === undefined ? '' : String(rest[0]);
132
+ const colon = given.indexOf(':');
133
+ if (!given || colon === 0 || colon === given.length - 1) throw new Error(`Usage: grok s observe ${command} ${verb} <id|kind:key>`);
134
+ if (colon < 0) {
135
+ id = given;
136
+ return;
137
+ }
138
+ const found = await dapi.problems.list({
139
+ kind: given.substring(0, colon),
140
+ key: given.substring(colon + 1),
141
+ limit: 1
142
+ });
143
+ if (!found?.length) throw new Error(`No problem '${given}'`);
144
+ id = found[0].id;
145
+ };
146
+ const problemPath = () => `/problems/${encodeURIComponent(id)}`;
147
+ // The route answers JSON as text/plain.
148
+ const settings = async key => {
149
+ const r = await dapi.raw('GET', `/admin/plugins/${key}/settings`);
150
+ return (typeof r === 'string' ? JSON.parse(r) : r)?.settings ?? {};
151
+ };
152
+ // A save within 5 s of the previous one is refused (429) until that one is applied.
153
+ const saveSettings = async (key, body) => {
154
+ for (let attempt = 0;; attempt++) {
155
+ try {
156
+ return await dapi.raw('POST', `/admin/plugins/${key}/settings`, body);
157
+ } catch (e) {
158
+ if (attempt >= 2 || e?.apiError?.errorCode !== 429) throw e;
159
+ await new Promise(resolve => setTimeout(resolve, 5000));
160
+ }
161
+ }
162
+ };
163
+ const saveLogger = body => saveSettings('logger', body);
164
+ switch (`${command} ${verb}`) {
165
+ case 'problems list':
166
+ (0, _serverOutput.printOutput)(await dapi.problems.list({
167
+ status: argv.status,
168
+ state: argv.state,
169
+ alertStatus: argv['alert-status'],
170
+ kind: argv.kind,
171
+ key: argv.key,
172
+ since: argv.since,
173
+ limit: argv.limit
174
+ }), output);
175
+ return true;
176
+ case 'problems get':
177
+ {
178
+ await needId();
179
+ const problem = await dapi.problems.find(id);
180
+ if (!problem) throw new Error(`No problem '${rest[0]}'`);
181
+ (0, _serverOutput.printOutput)(output === 'table' ? fieldRows(problem) : problem, output);
182
+ return true;
183
+ }
184
+ case 'problems history':
185
+ await needId();
186
+ (0, _serverOutput.printOutput)(await dapi.raw('GET', `${problemPath()}/history${(0, _nodeDapi.buildQuery)({
187
+ limit: argv.limit
188
+ })}`), output);
189
+ return true;
190
+ case 'problems status':
191
+ await needId();
192
+ (0, _serverOutput.printOutput)(await dapi.raw('POST', `${problemPath()}/status`, readBody(argv)), output);
193
+ return true;
194
+ case 'problems ack':
195
+ case 'problems resolve':
196
+ await needId();
197
+ (0, _serverOutput.printOutput)(await dapi.raw('POST', `${problemPath()}/${verb}`, {
198
+ reason: argv.reason
199
+ }), output);
200
+ return true;
201
+ case 'rules get':
202
+ {
203
+ const rules = (await settings('alerts'))?.problemRules;
204
+ (0, _serverOutput.printOutput)(rules ? JSON.parse(rules) : [], output === 'table' ? 'json' : output);
205
+ return true;
206
+ }
207
+ case 'rules put':
208
+ {
209
+ const rules = readBody(argv);
210
+ await saveSettings('alerts', {
211
+ problemRules: typeof rules === 'string' ? rules : JSON.stringify(rules)
212
+ });
213
+ if (output !== 'quiet') console.log('Saved the problem rules');
214
+ return true;
215
+ }
216
+ case 'rules test':
217
+ console.log(await dapi.raw('POST', '/alerts/Test_rules'));
218
+ return true;
219
+ case 'logger get':
220
+ {
221
+ let value = await settings('logger');
222
+ for (const key of rest[0] ? String(rest[0]).split('.') : []) value = value?.[key];
223
+ (0, _serverOutput.printOutput)(value, output === 'table' ? 'json' : output);
224
+ return true;
225
+ }
226
+ case 'logger set':
227
+ return loggerSet(dapi, argv, await settings('logger'), saveLogger, output);
228
+ case 'logger history':
229
+ {
230
+ const text = encodeURIComponent('eventType.name = "log-settings-changed"');
231
+ const events = (await dapi.raw('GET', `/log?text=${text}&limit=${argv.limit ?? 20}&page=1`)) ?? [];
232
+ (0, _serverOutput.printOutput)(events.map(e => {
233
+ const params = {};
234
+ for (const p of e.parameters ?? []) params[p.parameter?.name] = p.value;
235
+ const row = {
236
+ time: e.eventTime,
237
+ user: params.user,
238
+ changed: params.changed
239
+ };
240
+ return output === 'table' ? row : {
241
+ ...row,
242
+ diff: params.diff ? (0, _nodeDapi.tryParseJson)(params.diff) ?? params.diff : null
243
+ };
244
+ }), output);
245
+ return true;
246
+ }
247
+ case 'errors top':
248
+ {
249
+ if (argv.since !== undefined && (argv.from !== undefined || argv.date !== undefined)) throw new Error('--since is not used with --from or --date');
250
+ if (argv.date !== undefined && argv.from !== undefined) throw new Error('--date is not used with --from');
251
+ const metrics = await dapi.raw('GET', `/admin/metrics${(0, _nodeDapi.buildQuery)({
252
+ date_start: argv.since !== undefined ? new Date(Date.now() - parseDuration(argv.since, '--since')).toISOString() : iso(argv.from, '--from'),
253
+ date_end: iso(argv.to, '--to'),
254
+ date: argv.date,
255
+ limit: argv.limit,
256
+ errors_by: argv.by,
257
+ package: argv.package,
258
+ user: argv.user,
259
+ source: argv.source
260
+ })}`);
261
+ // The server answers the top `limit` of every source, in the global order.
262
+ const top = (metrics?.errors?.top ?? []).slice(0, Number(argv.limit ?? 10));
263
+ (0, _serverOutput.printOutput)(output === 'json' ? {
264
+ ...metrics?.errors,
265
+ top
266
+ } : top, output);
267
+ return true;
268
+ }
269
+ case 'errors get':
270
+ {
271
+ if (rest[0] === undefined) throw new Error('Usage: grok s observe errors get <signature|problem id|kind:key> [--since 7d] [--limit 50]');
272
+ const given = String(rest[0]);
273
+ if (given.includes(':')) await needId();
274
+ const problem = await dapi.problems.find(id ?? given);
275
+ const signature = problem ? problem.details?.signatureId : given;
276
+ if (!signature) throw new Error(`Problem '${given}' is not about an error: it has no signatureId`);
277
+ // An error with a stack is grouped by its stack hash, one without by its event type (ErrorIncidentProblem).
278
+ const text = `(eventType.id = "${signature}" or eventType.errorStackTraceHash = "${signature}") and ` + `eventTime > "${new Date(Date.now() - parseDuration(argv.since ?? '7d', '--since')).toISOString()}"`;
279
+ const events = (await dapi.raw('GET', `/log${(0, _nodeDapi.buildQuery)({
280
+ text,
281
+ limit: argv.limit ?? 50,
282
+ page: 1
283
+ })}`)) ?? [];
284
+ const logins = new Map();
285
+ for (const e of events) for (const p of e.parameters ?? []) if (p.parameter?.name === 'user' && p.value && !logins.has(p.value)) logins.set(p.value, (await dapi.users.find(p.value))?.login ?? p.value);
286
+ (0, _serverOutput.printOutput)(events.map(e => {
287
+ const params = {};
288
+ for (const p of e.parameters ?? []) params[p.parameter?.name] = p.value;
289
+ const row = {
290
+ time: e.eventTime,
291
+ source: params.source,
292
+ user: logins.get(params.user) ?? null,
293
+ function: params.function ?? null,
294
+ session: e.session?.id ?? null,
295
+ message: e.description
296
+ };
297
+ return output === 'table' ? row : {
298
+ ...row,
299
+ eventId: e.id,
300
+ params
301
+ };
302
+ }), output);
303
+ return true;
304
+ }
305
+ }
306
+ if (command === 'logs' && verb === 'capture') return capture(dapi, rest, argv, await settings('logger'), saveLogger, output);
307
+ if (command === 'logs') return logs(dapi, verb, rest, argv, output);
308
+ if (command === 'timeline') {
309
+ if (argv.session === undefined) throw new Error('Usage: grok s observe timeline --session <id> [--from <ISO|-15m>] [--to <ISO>] [--limit 500]');
310
+ needFrom(argv);
311
+ const window = argv.from === undefined ? {
312
+ from: iso('-15m', '--from'),
313
+ to: new Date().toISOString()
314
+ } : {
315
+ from: iso(argv.from, '--from'),
316
+ to: iso(argv.to, '--to')
317
+ };
318
+ (0, _serverOutput.printOutput)(await dapi.raw('GET', `/log/timeline${(0, _nodeDapi.buildQuery)({
319
+ session: argv.session,
320
+ ...window,
321
+ limit: argv.limit
322
+ })}`), output);
323
+ return true;
324
+ }
325
+ throw new Error(`Unknown command '${[command, verb].filter(Boolean).join(' ')}'\n${OBSERVE_USAGE}`);
326
+ }
327
+ const needFrom = argv => {
328
+ if (argv.to !== undefined && argv.from === undefined) throw new Error('--to needs --from');
329
+ };
330
+ async function logs(dapi, verb, rest, argv, output) {
331
+ const connection = argv.connection;
332
+ if (verb === 'cloud') {
333
+ if (argv.since !== undefined && argv.from !== undefined) throw new Error('--since is not used with --from');
334
+ needFrom(argv);
335
+ const group = argv.group ?? (await dapi.raw('GET', `/log/cloud/groups${(0, _nodeDapi.buildQuery)({
336
+ connection
337
+ })}`))?.[0];
338
+ if (!group) throw new Error('No log group to read: pass --group');
339
+ const window = argv.from !== undefined ? {
340
+ start: iso(argv.from, '--from'),
341
+ end: iso(argv.to, '--to')
342
+ } : {
343
+ start: new Date(Date.now() - parseDuration(argv.since ?? '1h', '--since')).toISOString()
344
+ };
345
+ (0, _serverOutput.printOutput)(await dapi.raw('GET', `/log/cloud/events${(0, _nodeDapi.buildQuery)({
346
+ connection,
347
+ group,
348
+ ...window,
349
+ filter: argv.filter,
350
+ limit: argv.limit,
351
+ format: 'json'
352
+ })}`), output);
353
+ return true;
354
+ }
355
+ if (verb === 'container') return containerLogs(dapi, rest[0], argv, output);
356
+ if (verb === 'archive' && rest[0] === 'list') {
357
+ let objects = (await dapi.raw('GET', `/log/archive/objects${(0, _nodeDapi.buildQuery)({
358
+ connection,
359
+ prefix: argv.prefix,
360
+ limit: argv.limit,
361
+ format: 'json'
362
+ })}`)) ?? [];
363
+ if (argv.since !== undefined) {
364
+ const from = Date.now() - parseDuration(argv.since, '--since');
365
+ objects = objects.filter(o => Date.parse(o.modified) >= from);
366
+ }
367
+ (0, _serverOutput.printOutput)(objects, output);
368
+ return true;
369
+ }
370
+ if (verb === 'archive' && rest[0] === 'read' && rest[1] !== undefined) {
371
+ (0, _serverOutput.printOutput)(await dapi.raw('GET', `/log/archive/events${(0, _nodeDapi.buildQuery)({
372
+ connection,
373
+ key: rest[1],
374
+ format: 'json'
375
+ })}`), output);
376
+ return true;
377
+ }
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>');
379
+ }
380
+
381
+ /// `logs container`: the server only cuts a container's log by line count, so the window and --grep apply here.
382
+ async function containerLogs(dapi, name, argv, output) {
383
+ if (name === undefined) {
384
+ const all = (await dapi.raw('GET', `/docker/containers${(0, _nodeDapi.buildQuery)({
385
+ limit: 1000
386
+ })}`)) ?? [];
387
+ (0, _serverOutput.printOutput)(all.map(c => ({
388
+ id: c.id,
389
+ name: c.name,
390
+ status: c.status
391
+ })), output);
392
+ return true;
393
+ }
394
+ if (argv.since !== undefined && argv.from !== undefined) throw new Error('--since is not used with --from');
395
+ needFrom(argv);
396
+ let id = String(name);
397
+ if (!_nodeDapi.UUID_RE.test(id)) {
398
+ const found = (await dapi.raw('GET', `/docker/containers${(0, _nodeDapi.buildQuery)({
399
+ text: id,
400
+ limit: 100
401
+ })}`)) ?? [];
402
+ const container = found.find(c => c.name === id);
403
+ if (!container) throw new Error(`No container '${id}'${found.length ? `; similar: ${found.map(c => c.name).join(', ')}` : ''}`);
404
+ id = container.id;
405
+ }
406
+ const from = argv.from !== undefined ? parseTime(argv.from, '--from').getTime() : argv.since !== undefined ? Date.now() - parseDuration(argv.since, '--since') : -Infinity;
407
+ const to = argv.to !== undefined ? parseTime(argv.to, '--to').getTime() : Infinity;
408
+ const grep = argv.grep === undefined ? null : new RegExp(String(argv.grep), 'i');
409
+ const text = await dapi.raw('GET', `/docker/containers/${id}/logs${(0, _nodeDapi.buildQuery)({
410
+ limit: argv.limit ?? 20000
411
+ })}`);
412
+ // A line without its own timestamp (a stack frame) belongs to the window of the line before it.
413
+ let inWindow = from === -Infinity && to === Infinity;
414
+ const lines = [];
415
+ for (const line of String(text ?? '').split('\n')) {
416
+ const stamp = /^(\d{4}-\d{2}-\d{2}T[\d:]{8})(?:\.\d+)?(Z|[+-]\d{2}:\d{2})/.exec(line);
417
+ if (stamp) {
418
+ const t = Date.parse(`${stamp[1]}${stamp[2]}`);
419
+ inWindow = t >= from && t <= to;
420
+ }
421
+ if (inWindow && line && (!grep || grep.test(line))) lines.push(line);
422
+ }
423
+ if (output === 'json') (0, _serverOutput.printOutput)(lines, output);else for (const line of lines) console.log(line);
424
+ return true;
425
+ }
426
+ async function loggerSet(dapi, argv, current, save, output) {
427
+ const sets = argv.set === undefined ? [] : (Array.isArray(argv.set) ? argv.set : [argv.set]).map(String);
428
+ const entry = argv.user !== undefined || argv.group !== undefined || argv.target !== undefined;
429
+ if (!sets.length && !entry) throw new Error('Usage: grok s observe logger set --set <path>=<json> ... [--user <login> | --group <name|id>]');
430
+ if (!entry && (argv.for !== undefined || argv.until !== undefined || argv.reason !== undefined || argv.remove)) throw new Error('--for, --until, --reason and --remove apply to a --user or --group entry');
431
+ const name = argv.user ?? argv.group ?? argv.name;
432
+ if (!entry) {
433
+ const body = {};
434
+ for (const s of sets) {
435
+ const key = setPath(current, s);
436
+ body[key] = current[key];
437
+ }
438
+ await save(body);
439
+ } else {
440
+ const group = argv.target ?? (argv.user !== undefined ? await dapi.groups.resolve(String(argv.user), {
441
+ personalOnly: true
442
+ }) : await dapi.groups.resolve(String(argv.group)));
443
+ const groups = {
444
+ ...current.userGroupSettings
445
+ };
446
+ const existing = groups[group.id];
447
+ if (argv.remove) {
448
+ if (!existing) throw new Error(`${name} has no logger entry`);
449
+ delete groups[group.id];
450
+ } else {
451
+ if (existing && !existing.expiresAt && (argv.for !== undefined || argv.until !== undefined)) throw new Error(`${name} has a logger entry that does not expire; change it without --for or --until`);
452
+ // A user's settings come from one entry, so a new one starts from All users instead of the defaults.
453
+ const settings = existing ? {
454
+ ...existing
455
+ } : {
456
+ ...groups[_walker.ALL_USERS]
457
+ };
458
+ if (!existing) delete settings.reason;
459
+ for (const s of sets) setPath(settings, s);
460
+ if (argv.for !== undefined) settings.expiresAt = new Date(Date.now() + parseDuration(argv.for, '--for')).toISOString();else if (argv.until !== undefined) {
461
+ settings.expiresAt = iso(argv.until, '--until');
462
+ if (Date.parse(settings.expiresAt) <= Date.now()) throw new Error(`--until ${argv.until} is in the past`);
463
+ }
464
+ if (argv.reason !== undefined) settings.reason = String(argv.reason);
465
+ groups[group.id] = settings;
466
+ }
467
+ await save({
468
+ userGroupSettings: groups
469
+ });
470
+ }
471
+ if (output !== 'quiet') console.log(!entry ? 'Saved the logger settings' : argv.remove ? `Removed the logger entry of ${name}` : `Saved the logger settings of ${name}`);
472
+ return true;
473
+ }
474
+
475
+ /// `logs capture`: logger set of saveHttpRequests, and the usage level it is saved at, on an expiring entry.
476
+ async function capture(dapi, rest, argv, current, save, output) {
477
+ const groups = current.userGroupSettings ?? {};
478
+ const stop = rest[0] === 'stop';
479
+ const name = stop ? rest[1] : rest[0];
480
+ if (name === undefined) {
481
+ if (stop) throw new Error('Usage: grok s observe logs capture stop <login|group>');
482
+ const rows = [];
483
+ for (const [id, s] of Object.entries(groups)) if (s?.saveHttpRequests === true && s.expiresAt) {
484
+ const group = await dapi.groups.find(id);
485
+ rows.push({
486
+ id,
487
+ name: group?.friendlyName ?? group?.name ?? id,
488
+ expiresAt: s.expiresAt,
489
+ reason: s.reason ?? ''
490
+ });
491
+ }
492
+ (0, _serverOutput.printOutput)(rows, output);
493
+ return true;
494
+ }
495
+ let group;
496
+ try {
497
+ group = await dapi.groups.resolve(String(name), {
498
+ personalOnly: true
499
+ });
500
+ } catch {
501
+ group = await dapi.groups.resolve(String(name));
502
+ }
503
+ const existing = groups[group.id];
504
+ if (stop) {
505
+ if (!existing?.saveHttpRequests) throw new Error(`No capture for ${name}`);
506
+ if (!existing.expiresAt) throw new Error(`The logger entry of ${name} does not expire; change it with logger set`);
507
+ const own = ['saveHttpRequests', 'postLevels', 'saveLevels', 'expiresAt', 'reason'];
508
+ const others = Object.keys(existing).filter(k => !own.includes(k) && JSON.stringify(existing[k]) !== JSON.stringify(groups[_walker.ALL_USERS]?.[k]));
509
+ if (others.length) throw new Error(`The logger entry of ${name} also holds ${others.join(', ')}; remove it with ` + `grok s o logger set --${group.personal ? 'user' : 'group'} ${name} --remove`);
510
+ return loggerSet(dapi, {
511
+ target: group,
512
+ name,
513
+ remove: true
514
+ }, current, save, output);
515
+ }
516
+ const base = existing ?? groups[_walker.ALL_USERS] ?? {};
517
+ const sets = ['saveHttpRequests=true', ...['postLevels', 'saveLevels'].filter(l => Array.isArray(base[l]) && !base[l].includes('usage')).map(l => `${l}=${JSON.stringify([...base[l], 'usage'])}`)];
518
+ const end = Math.max(Date.now() + parseDuration(argv.for ?? '30m', '--for'), Date.parse(existing?.expiresAt) || 0);
519
+ return loggerSet(dapi, {
520
+ target: group,
521
+ name,
522
+ set: sets,
523
+ reason: argv.reason,
524
+ until: new Date(end).toISOString()
525
+ }, current, save, output);
526
+ }
@@ -15,6 +15,7 @@ var _serverClient = require("../utils/server-client");
15
15
  var _serverOutput = require("../utils/server-output");
16
16
  var _serverMigrate = require("./server-migrate");
17
17
  var _serverDomains = require("./server-domains");
18
+ var _serverObserve = require("./server-observe");
18
19
  var _registry = require("../utils/migrate/registry");
19
20
  var _walker = require("../utils/migrate/walker");
20
21
  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); }
@@ -27,7 +28,7 @@ const ENTITY_TYPES = {
27
28
  reports: 'UserReport'
28
29
  };
29
30
  const ENTITIES = ['users', 'groups', 'functions', 'connections', 'queries', 'scripts', 'packages', 'reports', 'files', 'tables'];
30
- const COMMANDS = ['shares', 'domains', 'raw', 'batch', 'describe', 'healthcheck', 'sync', 'pull', 'push', 'migrate', 'diff', 'bundle', 'token'];
31
+ const COMMANDS = ['shares', 'domains', 'raw', 'batch', 'describe', 'healthcheck', 'sync', 'pull', 'push', 'migrate', 'diff', 'bundle', 'token', 'observe', 'o'];
31
32
  const VERBS = ['list', 'count', 'get', 'delete'];
32
33
  async function server(argv) {
33
34
  const args = argv['_'].slice(1);
@@ -45,6 +46,10 @@ async function server(argv) {
45
46
  console.log(HELP_SERVER);
46
47
  return true;
47
48
  }
49
+ if ((entity === 'observe' || entity === 'o') && !verb) {
50
+ console.log(_serverObserve.OBSERVE_USAGE);
51
+ return true;
52
+ }
48
53
  let client;
49
54
  try {
50
55
  client = await (0, _serverClient.createClient)(host, !!argv.admin);
@@ -60,6 +65,7 @@ async function server(argv) {
60
65
  // escaping to grok.js as an unhandled rejection (stack trace + exit 255)
61
66
  if (['pull', 'push', 'migrate', 'diff', 'bundle'].includes(entity)) return await (0, _serverMigrate.handleMigrate)(dapi, entity, [verb, ...rest].filter(Boolean), argv, output);
62
67
  if (entity === 'domains') return await (0, _serverDomains.handleDomains)(dapi, verb, rest, argv, output);
68
+ if (entity === 'observe' || entity === 'o') return await (0, _serverObserve.handleObserve)(dapi, verb, rest[0], rest.slice(1), argv, output);
63
69
  if (entity === 'batch') return await handleBatch(dapi, argv, verb, rest, output);
64
70
  // Shell scripts that used to curl /users/login/dev get a token the same way
65
71
  // every other command does, whatever credential the config holds.
@@ -927,6 +933,7 @@ Manage a Datagrok server from the command line.
927
933
  Entities:
928
934
  users, groups, functions, connections, queries, scripts, packages, reports, files, tables
929
935
  (plus domains, shares, batch, raw, describe, healthcheck, sync, pull/push/migrate/diff/bundle below)
936
+ observe (alias o): problems, problem rules, logger settings, errors, timeline, cloud logs; grok s observe prints its verbs
930
937
 
931
938
  Verbs:
932
939
  list List entities (--filter, --limit, --offset)
package/bin/grok.js CHANGED
@@ -1,7 +1,10 @@
1
1
  #!/usr/bin/env node
2
+ // minimist sets every boolean option to false when it is absent, and commands such as `grok api`
3
+ // reject any key they do not expect — so each boolean reaches only the commands that read it.
4
+ const commandBooleans = {dartium: ['test', 'stresstest'], 'send-email': ['server', 's']};
2
5
  const argv = require('minimist')(process.argv.slice(2), {
3
6
  alias: {k: 'key', h: 'help', r: 'recursive'},
4
- boolean: ['dartium', 'send-email'],
7
+ boolean: Object.keys(commandBooleans),
5
8
  // keep versions verbatim — minimist would coerce '1.10' to the number 1.1
6
9
  string: ['version', 'prefix'],
7
10
  });
@@ -59,8 +62,9 @@ function printUsage(command) {
59
62
  }
60
63
 
61
64
  const command = argv['_'][0];
62
- if (command !== 'test' && command !== 'stresstest')
63
- delete argv.dartium;
65
+ for (const [flag, owners] of Object.entries(commandBooleans))
66
+ if (!owners.includes(command))
67
+ delete argv[flag];
64
68
  if (command in commands) {
65
69
  try {
66
70
  if (argv['help']) {
@@ -12,7 +12,7 @@ const domainApp = (appNames, tables) => `
12
12
  The application${appNames.length > 1 ? 's' : ''} ${appNames.join(', ')} ` + `(over ${tables.join(', ')}) ${appNames.length > 1 ? 'have' : 'has'} been added successfully
13
13
 
14
14
  Next steps:
15
- pnpm install link @datagrok-libraries/u2 (a workspace dependency)
15
+ pnpm install link @datagrok-libraries/ems (a workspace dependency)
16
16
  grok build && grok publish publish the package and open the app from the browse tree
17
17
 
18
18
  The app is the u2 defaults alone — list, search, filters, entity page with children
@@ -3,6 +3,7 @@
3
3
  Object.defineProperty(exports, "__esModule", {
4
4
  value: true
5
5
  });
6
+ exports.ALL_USERS = void 0;
6
7
  exports.collectExternals = collectExternals;
7
8
  exports.compileFilter = compileFilter;
8
9
  exports.expand = expand;
@@ -28,7 +29,7 @@ const noProgress = () => {};
28
29
 
29
30
  /** What the bundle points at but does not carry: resolvable on the source, or dead there too. */
30
31
 
31
- const ALL_USERS = 'a4b45840-9a50-11e6-9cc9-8546b8bf62e6';
32
+ const ALL_USERS = exports.ALL_USERS = 'a4b45840-9a50-11e6-9cc9-8546b8bf62e6';
32
33
 
33
34
  /** One round trip per entity, tens of thousands per stand — modest, since the server is shared. */
34
35
  const WALK_CONCURRENCY = 12;
@@ -30,7 +30,7 @@ export interface External {id: string; type: string; nqName: string}
30
30
  /** What the bundle points at but does not carry: resolvable on the source, or dead there too. */
31
31
  export interface Outside {externals: External[]; dangling: string[]}
32
32
 
33
- const ALL_USERS = 'a4b45840-9a50-11e6-9cc9-8546b8bf62e6';
33
+ export const ALL_USERS = 'a4b45840-9a50-11e6-9cc9-8546b8bf62e6';
34
34
 
35
35
  /** One round trip per entity, tens of thousands per stand — modest, since the server is shared. */
36
36
  const WALK_CONCURRENCY = 12;