datagrok-tools 6.7.8 → 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.
@@ -0,0 +1,854 @@
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.matchFilter = matchFilter;
10
+ exports.parseDuration = parseDuration;
11
+ exports.parseTime = parseTime;
12
+ exports.printRequestLog = printRequestLog;
13
+ exports.setPath = setPath;
14
+ var fs = _interopRequireWildcard(require("fs"));
15
+ var _crypto = require("crypto");
16
+ var _nodeDapi = require("../utils/node-dapi");
17
+ var _serverOutput = require("../utils/server-output");
18
+ var _walker = require("../utils/migrate/walker");
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); }
20
+ /// `grok s observe` (alias `grok s o`): problems, problem rules, logger settings, errors, the timeline and cloud logs,
21
+ /// over `/problems`, `/admin/plugins/<key>/settings`, `/alerts/Test_rules`, `/log`, `/admin/metrics`, `/log/timeline`,
22
+ /// `/log/cloud/*`, `/log/archive/*`, `/log/match`, `/log/activations` and `/docker/containers`.
23
+
24
+ const OBSERVE_USAGE = exports.OBSERVE_USAGE = `Usage: grok s observe <command> <verb> [args] (alias: grok s o)
25
+
26
+ problems list [--status <s>] [--state <s>] [--alert-status <s>] [--kind <k>] [--key <k>] [--since 7d] [--limit 100]
27
+ Comma lists or all; --alert-status open lists the open alerts
28
+ problems get <id|kind:key> One field per line, details by path
29
+ problems history <id|kind:key> [--limit 50]
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
33
+ problems ack <id|kind:key> --reason <text>
34
+ problems resolve <id|kind:key> --reason <text>
35
+ rules get The problem rules (Settings > Alerts), as JSON
36
+ rules put --json rules.json Replace them; the server checks them first
37
+ rules test Evaluate the saved rules once, now
38
+ logger get [<path>] The logger settings, or one value (exportSettings.0.types)
39
+ logger set --set <path>=<json> ... Change values; with --user <login> or --group <name|id>, that
40
+ [--for 30m | --until <ISO>] [--reason <text>] entry of userGroupSettings (a new one starts from
41
+ [--remove] All users), removed when it expires; --remove deletes it
42
+ logger history [--limit 20] The log-settings-changed audit records, with each change's diff
43
+ errors top [--since 24h | --date "this week" | --from <ISO> [--to <ISO>]] [--by message|signature]
44
+ [--package <name>] [--user <login>] [--source <s>] [--limit 10]
45
+ Ranked by distinct users, so errors nobody saw (scheduler, health
46
+ probes) come last: narrow those with --source server
47
+ errors get <signature|problem> [--since 7d] [--limit 50]
48
+ Each occurrence of an error (a signature from errors top, or the
49
+ id or kind:key of its problem): time, user, function, session
50
+ logs container [<name|id> [--since 1h | --from <ISO> [--to <ISO>]] [--grep <regex>] [--limit 20000]]
51
+ A docker container's log lines (its latest --limit, then the
52
+ window and --grep); no name, the containers and their status
53
+ timeline --session <id> [--from <ISO|-15m>] [--to <ISO>] [--limit 500]
54
+ A session's events and requests in a window: no --from, the last
55
+ 15 min; --to defaults to --from + 10 min, at most 2 h
56
+ logs cloud [--group <g>] [--since 1h | --from <ISO> [--to <ISO>]] [--filter <pattern>] [--limit 1000]
57
+ CloudWatch events; no --group, the instance's own group;
58
+ --filter is a CloudWatch filter pattern
59
+ logs archive list [--prefix <p>] [--since 7d] [--limit n]
60
+ Objects in the log archive (WORM bucket), by key; --since filters
61
+ only the page the server returned, so narrow it with --prefix
62
+ logs archive read <key> One archived object decoded to events
63
+ logs cloud and logs archive take [--connection <id>]; without it,
64
+ the instance's AWS role
65
+ logs capture [<login|group> [--for 30m] [--reason <text>]]
66
+ Save every API request of a user or a group for a while: logger set
67
+ of saveHttpRequests on their expiring entry (an entry that already
68
+ expires later keeps its time); no argument, the active captures;
69
+ read them with timeline --session <id>
70
+ logs capture stop <login|group> Remove that expiring entry; one that holds other settings is left
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)
96
+
97
+ Durations are 30m (minutes), 24h, 7d, 2w; times are ISO 8601 (UTC without an offset) or
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.`;
104
+ const UNIT_MS = {
105
+ s: 1000,
106
+ m: 60000,
107
+ h: 3600000,
108
+ d: 86400000,
109
+ w: 604800000
110
+ };
111
+
112
+ /** `30m`, `2h`, `7d`, `1w` in milliseconds; `m` is minutes, unlike `grok s pull --since`, where it is months. */
113
+ function parseDuration(value, flag) {
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}'`);
116
+ return Number(m[1]) * UNIT_MS[m[2]];
117
+ }
118
+
119
+ /** ISO 8601 (UTC without an offset), or relative to [now] (`-2h`). */
120
+ function parseTime(value, flag, now = new Date()) {
121
+ let s = String(value ?? '').trim();
122
+ if (s.startsWith('-')) return new Date(now.getTime() - parseDuration(s.substring(1), flag));
123
+ if (/[T ].*[+-]\d{2}$/.test(s)) s = `${s}:00`;
124
+ const d = new Date(/^\d{4}-\d{2}-\d{2}$/.test(s) || /(Z|[+-]\d{2}:?\d{2})$/i.test(s) ? s : `${s}Z`);
125
+ 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}'`);
126
+ return d;
127
+ }
128
+
129
+ /** `a.b.0.c=<json>` applied to [target]; a value that is not JSON is a string. */
130
+ function setPath(target, assignment) {
131
+ const at = assignment.indexOf('=');
132
+ if (at <= 0) throw new Error(`--set expects <path>=<json>, got '${assignment}'`);
133
+ const keys = assignment.slice(0, at).split('.');
134
+ const text = assignment.slice(at + 1);
135
+ let value;
136
+ try {
137
+ value = JSON.parse(text);
138
+ } catch {
139
+ value = text;
140
+ }
141
+ let node = target;
142
+ for (const key of keys.slice(0, -1)) node = node[key] ??= {};
143
+ node[keys[keys.length - 1]] = value;
144
+ return keys[0];
145
+ }
146
+ function readBody(argv) {
147
+ if (argv.json) return JSON.parse(fs.readFileSync(argv.json, 'utf8'));
148
+ if (argv.data !== undefined) return JSON.parse(String(argv.data));
149
+ throw new Error('Expected a body: --data \'<json>\' or --json <file>');
150
+ }
151
+ const iso = (value, flag) => value === undefined ? undefined : parseTime(value, flag).toISOString();
152
+
153
+ /** A record as `field, value` rows, nested objects by dotted path, so a table shows every value in full. */
154
+ function fieldRows(record, prefix = '') {
155
+ const rows = [];
156
+ 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({
157
+ field: `${prefix}${key}`,
158
+ value
159
+ });
160
+ return rows;
161
+ }
162
+ async function handleObserve(dapi, command, verb, rest, argv, output) {
163
+ // A `:` cannot pass through a route parameter behind the proxy, so kind:key is looked up by the list's filters.
164
+ let id;
165
+ const needId = async () => {
166
+ const given = rest[0] === undefined ? '' : String(rest[0]);
167
+ const colon = given.indexOf(':');
168
+ if (!given || colon === 0 || colon === given.length - 1) throw new Error(`Usage: grok s observe ${command} ${verb} <id|kind:key>`);
169
+ if (colon < 0) {
170
+ id = given;
171
+ return;
172
+ }
173
+ const found = await dapi.problems.list({
174
+ kind: given.substring(0, colon),
175
+ key: given.substring(colon + 1),
176
+ limit: 1
177
+ });
178
+ if (!found?.length) throw new Error(`No problem '${given}'`);
179
+ id = found[0].id;
180
+ };
181
+ const problemPath = () => `/problems/${encodeURIComponent(id)}`;
182
+ // The route answers JSON as text/plain.
183
+ const settings = async key => {
184
+ const r = await dapi.raw('GET', `/admin/plugins/${key}/settings`);
185
+ return (typeof r === 'string' ? JSON.parse(r) : r)?.settings ?? {};
186
+ };
187
+ // A save within 5 s of the previous one is refused (429) until that one is applied.
188
+ const saveSettings = async (key, body) => {
189
+ for (let attempt = 0;; attempt++) {
190
+ try {
191
+ return await dapi.raw('POST', `/admin/plugins/${key}/settings`, body);
192
+ } catch (e) {
193
+ if (attempt >= 2 || e?.apiError?.errorCode !== 429) throw e;
194
+ await new Promise(resolve => setTimeout(resolve, 5000));
195
+ }
196
+ }
197
+ };
198
+ const saveLogger = body => saveSettings('logger', body);
199
+ switch (`${command} ${verb}`) {
200
+ case 'problems list':
201
+ (0, _serverOutput.printOutput)(await dapi.problems.list({
202
+ status: argv.status,
203
+ state: argv.state,
204
+ alertStatus: argv['alert-status'],
205
+ kind: argv.kind,
206
+ key: argv.key,
207
+ since: argv.since,
208
+ limit: argv.limit
209
+ }), output);
210
+ return true;
211
+ case 'problems get':
212
+ {
213
+ await needId();
214
+ const problem = await dapi.problems.find(id);
215
+ if (!problem) throw new Error(`No problem '${rest[0]}'`);
216
+ (0, _serverOutput.printOutput)(output === 'table' ? fieldRows(problem) : problem, output);
217
+ return true;
218
+ }
219
+ case 'problems history':
220
+ await needId();
221
+ (0, _serverOutput.printOutput)(await dapi.raw('GET', `${problemPath()}/history${(0, _nodeDapi.buildQuery)({
222
+ limit: argv.limit
223
+ })}`), output);
224
+ return true;
225
+ case 'problems status':
226
+ await needId();
227
+ (0, _serverOutput.printOutput)(await dapi.raw('POST', `${problemPath()}/status`, readBody(argv)), output);
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
+ }
240
+ case 'problems ack':
241
+ case 'problems resolve':
242
+ await needId();
243
+ (0, _serverOutput.printOutput)(await dapi.raw('POST', `${problemPath()}/${verb}`, {
244
+ reason: argv.reason
245
+ }), output);
246
+ return true;
247
+ case 'rules get':
248
+ {
249
+ const rules = (await settings('alerts'))?.problemRules;
250
+ (0, _serverOutput.printOutput)(rules ? JSON.parse(rules) : [], output === 'table' ? 'json' : output);
251
+ return true;
252
+ }
253
+ case 'rules put':
254
+ {
255
+ const rules = readBody(argv);
256
+ await saveSettings('alerts', {
257
+ problemRules: typeof rules === 'string' ? rules : JSON.stringify(rules)
258
+ });
259
+ if (output !== 'quiet') console.log('Saved the problem rules');
260
+ return true;
261
+ }
262
+ case 'rules test':
263
+ console.log(await dapi.raw('POST', '/alerts/Test_rules'));
264
+ return true;
265
+ case 'logger get':
266
+ {
267
+ let value = await settings('logger');
268
+ for (const key of rest[0] ? String(rest[0]).split('.') : []) value = value?.[key];
269
+ (0, _serverOutput.printOutput)(value, output === 'table' ? 'json' : output);
270
+ return true;
271
+ }
272
+ case 'logger set':
273
+ return loggerSet(dapi, argv, await settings('logger'), saveLogger, output);
274
+ case 'logger history':
275
+ {
276
+ const text = encodeURIComponent('eventType.name = "log-settings-changed"');
277
+ const events = (await dapi.raw('GET', `/log?text=${text}&limit=${argv.limit ?? 20}&page=1`)) ?? [];
278
+ (0, _serverOutput.printOutput)(events.map(e => {
279
+ const params = {};
280
+ for (const p of e.parameters ?? []) params[p.parameter?.name] = p.value;
281
+ const row = {
282
+ time: e.eventTime,
283
+ user: params.user,
284
+ changed: params.changed
285
+ };
286
+ return output === 'table' ? row : {
287
+ ...row,
288
+ diff: params.diff ? (0, _nodeDapi.tryParseJson)(params.diff) ?? params.diff : null
289
+ };
290
+ }), output);
291
+ return true;
292
+ }
293
+ case 'errors top':
294
+ {
295
+ if (argv.since !== undefined && (argv.from !== undefined || argv.date !== undefined)) throw new Error('--since is not used with --from or --date');
296
+ if (argv.date !== undefined && argv.from !== undefined) throw new Error('--date is not used with --from');
297
+ const metrics = await dapi.raw('GET', `/admin/metrics${(0, _nodeDapi.buildQuery)({
298
+ date_start: argv.since !== undefined ? new Date(Date.now() - parseDuration(argv.since, '--since')).toISOString() : iso(argv.from, '--from'),
299
+ date_end: iso(argv.to, '--to'),
300
+ date: argv.date,
301
+ limit: argv.limit,
302
+ errors_by: argv.by,
303
+ package: argv.package,
304
+ user: argv.user,
305
+ source: argv.source
306
+ })}`);
307
+ // The server answers the top `limit` of every source, in the global order.
308
+ const top = (metrics?.errors?.top ?? []).slice(0, Number(argv.limit ?? 10));
309
+ (0, _serverOutput.printOutput)(output === 'json' ? {
310
+ ...metrics?.errors,
311
+ top
312
+ } : top, output);
313
+ return true;
314
+ }
315
+ case 'errors get':
316
+ {
317
+ if (rest[0] === undefined) throw new Error('Usage: grok s observe errors get <signature|problem id|kind:key> [--since 7d] [--limit 50]');
318
+ const given = String(rest[0]);
319
+ if (given.includes(':')) await needId();
320
+ const problem = await dapi.problems.find(id ?? given);
321
+ const signature = problem ? problem.details?.signatureId : given;
322
+ if (!signature) throw new Error(`Problem '${given}' is not about an error: it has no signatureId`);
323
+ // An error with a stack is grouped by its stack hash, one without by its event type (ErrorIncidentProblem).
324
+ const text = `(eventType.id = "${signature}" or eventType.errorStackTraceHash = "${signature}") and ` + `eventTime > "${new Date(Date.now() - parseDuration(argv.since ?? '7d', '--since')).toISOString()}"`;
325
+ const events = (await dapi.raw('GET', `/log${(0, _nodeDapi.buildQuery)({
326
+ text,
327
+ limit: argv.limit ?? 50,
328
+ page: 1
329
+ })}`)) ?? [];
330
+ const logins = new Map();
331
+ 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);
332
+ (0, _serverOutput.printOutput)(events.map(e => {
333
+ const params = {};
334
+ for (const p of e.parameters ?? []) params[p.parameter?.name] = p.value;
335
+ const row = {
336
+ time: e.eventTime,
337
+ source: params.source,
338
+ user: logins.get(params.user) ?? null,
339
+ function: params.function ?? null,
340
+ session: e.session?.id ?? null,
341
+ message: e.description
342
+ };
343
+ return output === 'table' ? row : {
344
+ ...row,
345
+ eventId: e.id,
346
+ params
347
+ };
348
+ }), output);
349
+ return true;
350
+ }
351
+ }
352
+ if (command === 'groups' || command === 'endpoints' || command === 'logs' && verb === 'activations') return logGroups(dapi, command, verb, rest, argv, await settings('logger'), saveLogger, output);
353
+ if (command === 'logs' && verb === 'capture') return capture(dapi, rest, argv, await settings('logger'), saveLogger, output);
354
+ if (command === 'logs') return logs(dapi, verb, rest, argv, output);
355
+ if (command === 'timeline') {
356
+ if (argv.session === undefined) throw new Error('Usage: grok s observe timeline --session <id> [--from <ISO|-15m>] [--to <ISO>] [--limit 500]');
357
+ needFrom(argv);
358
+ const now = Date.now();
359
+ const window = argv.from === undefined ? {
360
+ from: new Date(now - 15 * 60000).toISOString(),
361
+ to: new Date(now).toISOString()
362
+ } : {
363
+ from: iso(argv.from, '--from'),
364
+ to: iso(argv.to, '--to')
365
+ };
366
+ (0, _serverOutput.printOutput)(await dapi.raw('GET', `/log/timeline${(0, _nodeDapi.buildQuery)({
367
+ session: argv.session,
368
+ ...window,
369
+ limit: argv.limit
370
+ })}`), output);
371
+ return true;
372
+ }
373
+ throw new Error(`Unknown command '${[command, verb].filter(Boolean).join(' ')}'\n${OBSERVE_USAGE}`);
374
+ }
375
+ const needFrom = argv => {
376
+ if (argv.to !== undefined && argv.from === undefined) throw new Error('--to needs --from');
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
+ }
606
+ async function logs(dapi, verb, rest, argv, output) {
607
+ const connection = argv.connection;
608
+ if (verb === 'cloud') {
609
+ if (argv.since !== undefined && argv.from !== undefined) throw new Error('--since is not used with --from');
610
+ needFrom(argv);
611
+ const group = argv.group ?? (await dapi.raw('GET', `/log/cloud/groups${(0, _nodeDapi.buildQuery)({
612
+ connection
613
+ })}`))?.[0];
614
+ if (!group) throw new Error('No log group to read: pass --group');
615
+ const window = argv.from !== undefined ? {
616
+ start: iso(argv.from, '--from'),
617
+ end: iso(argv.to, '--to')
618
+ } : {
619
+ start: new Date(Date.now() - parseDuration(argv.since ?? '1h', '--since')).toISOString()
620
+ };
621
+ (0, _serverOutput.printOutput)(await dapi.raw('GET', `/log/cloud/events${(0, _nodeDapi.buildQuery)({
622
+ connection,
623
+ group,
624
+ ...window,
625
+ filter: argv.filter,
626
+ limit: argv.limit,
627
+ format: 'json'
628
+ })}`), output);
629
+ return true;
630
+ }
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
+ }
654
+ if (verb === 'archive' && rest[0] === 'list') {
655
+ let objects = (await dapi.raw('GET', `/log/archive/objects${(0, _nodeDapi.buildQuery)({
656
+ connection,
657
+ prefix: argv.prefix,
658
+ limit: argv.limit,
659
+ format: 'json'
660
+ })}`)) ?? [];
661
+ if (argv.since !== undefined) {
662
+ const from = Date.now() - parseDuration(argv.since, '--since');
663
+ objects = objects.filter(o => Date.parse(o.modified) >= from);
664
+ }
665
+ (0, _serverOutput.printOutput)(objects, output);
666
+ return true;
667
+ }
668
+ if (verb === 'archive' && rest[0] === 'read' && rest[1] !== undefined) {
669
+ (0, _serverOutput.printOutput)(await dapi.raw('GET', `/log/archive/events${(0, _nodeDapi.buildQuery)({
670
+ connection,
671
+ key: rest[1],
672
+ format: 'json'
673
+ })}`), output);
674
+ return true;
675
+ }
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
+ }
707
+ }
708
+
709
+ /// `logs container`: the server only cuts a container's log by line count, so the window and --grep apply here.
710
+ async function containerLogs(dapi, name, argv, output) {
711
+ if (name === undefined) {
712
+ const all = (await dapi.raw('GET', `/docker/containers${(0, _nodeDapi.buildQuery)({
713
+ limit: 1000
714
+ })}`)) ?? [];
715
+ (0, _serverOutput.printOutput)(all.map(c => ({
716
+ id: c.id,
717
+ name: c.name,
718
+ status: c.status
719
+ })), output);
720
+ return true;
721
+ }
722
+ if (argv.since !== undefined && argv.from !== undefined) throw new Error('--since is not used with --from');
723
+ needFrom(argv);
724
+ let id = String(name);
725
+ if (!_nodeDapi.UUID_RE.test(id)) {
726
+ const found = (await dapi.raw('GET', `/docker/containers${(0, _nodeDapi.buildQuery)({
727
+ text: id,
728
+ limit: 100
729
+ })}`)) ?? [];
730
+ const container = found.find(c => c.name === id);
731
+ if (!container) throw new Error(`No container '${id}'${found.length ? `; similar: ${found.map(c => c.name).join(', ')}` : ''}`);
732
+ id = container.id;
733
+ }
734
+ const from = argv.from !== undefined ? parseTime(argv.from, '--from').getTime() : argv.since !== undefined ? Date.now() - parseDuration(argv.since, '--since') : -Infinity;
735
+ const to = argv.to !== undefined ? parseTime(argv.to, '--to').getTime() : Infinity;
736
+ const grep = argv.grep === undefined ? null : new RegExp(String(argv.grep), 'i');
737
+ const text = await dapi.raw('GET', `/docker/containers/${id}/logs${(0, _nodeDapi.buildQuery)({
738
+ limit: argv.limit ?? 20000
739
+ })}`);
740
+ // A line without its own timestamp (a stack frame) belongs to the window of the line before it.
741
+ let inWindow = from === -Infinity && to === Infinity;
742
+ const lines = [];
743
+ for (const line of String(text ?? '').split('\n')) {
744
+ const stamp = /^(\d{4}-\d{2}-\d{2}T[\d:]{8})(?:\.\d+)?(Z|[+-]\d{2}:\d{2})/.exec(line);
745
+ if (stamp) {
746
+ const t = Date.parse(`${stamp[1]}${stamp[2]}`);
747
+ inWindow = t >= from && t <= to;
748
+ }
749
+ if (inWindow && line && (!grep || grep.test(line))) lines.push(line);
750
+ }
751
+ if (output === 'json') (0, _serverOutput.printOutput)(lines, output);else for (const line of lines) console.log(line);
752
+ return true;
753
+ }
754
+ async function loggerSet(dapi, argv, current, save, output) {
755
+ const sets = argv.set === undefined ? [] : (Array.isArray(argv.set) ? argv.set : [argv.set]).map(String);
756
+ const entry = argv.user !== undefined || argv.group !== undefined || argv.target !== undefined;
757
+ if (!sets.length && !entry) throw new Error('Usage: grok s observe logger set --set <path>=<json> ... [--user <login> | --group <name|id>]');
758
+ 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');
759
+ const name = argv.user ?? argv.group ?? argv.name;
760
+ if (!entry) {
761
+ const body = {};
762
+ for (const s of sets) {
763
+ const key = setPath(current, s);
764
+ body[key] = current[key];
765
+ }
766
+ await save(body);
767
+ } else {
768
+ const group = argv.target ?? (argv.user !== undefined ? await dapi.groups.resolve(String(argv.user), {
769
+ personalOnly: true
770
+ }) : await dapi.groups.resolve(String(argv.group)));
771
+ const groups = {
772
+ ...current.userGroupSettings
773
+ };
774
+ const existing = groups[group.id];
775
+ if (argv.remove) {
776
+ if (!existing) throw new Error(`${name} has no logger entry`);
777
+ delete groups[group.id];
778
+ } else {
779
+ 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`);
780
+ // A user's settings come from one entry, so a new one starts from All users instead of the defaults.
781
+ const settings = existing ? {
782
+ ...existing
783
+ } : {
784
+ ...groups[_walker.ALL_USERS]
785
+ };
786
+ if (!existing) delete settings.reason;
787
+ for (const s of sets) setPath(settings, s);
788
+ if (argv.for !== undefined) settings.expiresAt = new Date(Date.now() + parseDuration(argv.for, '--for')).toISOString();else if (argv.until !== undefined) {
789
+ settings.expiresAt = iso(argv.until, '--until');
790
+ if (Date.parse(settings.expiresAt) <= Date.now()) throw new Error(`--until ${argv.until} is in the past`);
791
+ }
792
+ if (argv.reason !== undefined) settings.reason = String(argv.reason);
793
+ groups[group.id] = settings;
794
+ }
795
+ await save({
796
+ userGroupSettings: groups
797
+ });
798
+ }
799
+ if (output !== 'quiet') console.log(!entry ? 'Saved the logger settings' : argv.remove ? `Removed the logger entry of ${name}` : `Saved the logger settings of ${name}`);
800
+ return true;
801
+ }
802
+
803
+ /// `logs capture`: logger set of saveHttpRequests, and the usage level it is saved at, on an expiring entry.
804
+ async function capture(dapi, rest, argv, current, save, output) {
805
+ const groups = current.userGroupSettings ?? {};
806
+ const stop = rest[0] === 'stop';
807
+ const name = stop ? rest[1] : rest[0];
808
+ if (name === undefined) {
809
+ if (stop) throw new Error('Usage: grok s observe logs capture stop <login|group>');
810
+ const rows = [];
811
+ for (const [id, s] of Object.entries(groups)) if (s?.saveHttpRequests === true && s.expiresAt) {
812
+ const group = await dapi.groups.find(id);
813
+ rows.push({
814
+ id,
815
+ name: group?.friendlyName ?? group?.name ?? id,
816
+ expiresAt: s.expiresAt,
817
+ reason: s.reason ?? ''
818
+ });
819
+ }
820
+ (0, _serverOutput.printOutput)(rows, output);
821
+ return true;
822
+ }
823
+ let group;
824
+ try {
825
+ group = await dapi.groups.resolve(String(name), {
826
+ personalOnly: true
827
+ });
828
+ } catch {
829
+ group = await dapi.groups.resolve(String(name));
830
+ }
831
+ const existing = groups[group.id];
832
+ if (stop) {
833
+ if (!existing?.saveHttpRequests) throw new Error(`No capture for ${name}`);
834
+ if (!existing.expiresAt) throw new Error(`The logger entry of ${name} does not expire; change it with logger set`);
835
+ const own = ['saveHttpRequests', 'postLevels', 'saveLevels', 'expiresAt', 'reason'];
836
+ const others = Object.keys(existing).filter(k => !own.includes(k) && JSON.stringify(existing[k]) !== JSON.stringify(groups[_walker.ALL_USERS]?.[k]));
837
+ 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`);
838
+ return loggerSet(dapi, {
839
+ target: group,
840
+ name,
841
+ remove: true
842
+ }, current, save, output);
843
+ }
844
+ const base = existing ?? groups[_walker.ALL_USERS] ?? {};
845
+ const sets = ['saveHttpRequests=true', ...['postLevels', 'saveLevels'].filter(l => Array.isArray(base[l]) && !base[l].includes('usage')).map(l => `${l}=${JSON.stringify([...base[l], 'usage'])}`)];
846
+ const end = Math.max(Date.now() + parseDuration(argv.for ?? '30m', '--for'), Date.parse(existing?.expiresAt) || 0);
847
+ return loggerSet(dapi, {
848
+ target: group,
849
+ name,
850
+ set: sets,
851
+ reason: argv.reason,
852
+ until: new Date(end).toISOString()
853
+ }, current, save, output);
854
+ }