@basis-theory-labs/cli 4.2.0 → 4.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +219 -8
- package/dist/commands/proxies/logs/read.d.ts +16 -0
- package/dist/commands/proxies/logs/read.js +72 -0
- package/dist/commands/proxies/logs/tail.d.ts +15 -0
- package/dist/commands/proxies/logs/tail.js +66 -0
- package/dist/commands/proxies/logs.js +1 -1
- package/dist/commands/reactors/logs/read.d.ts +15 -0
- package/dist/commands/reactors/logs/read.js +65 -0
- package/dist/commands/reactors/logs/tail.d.ts +14 -0
- package/dist/commands/reactors/logs/tail.js +60 -0
- package/dist/commands/reactors/logs.js +1 -1
- package/dist/logs/events.d.ts +60 -0
- package/dist/logs/events.js +589 -0
- package/dist/logs/format.d.ts +10 -0
- package/dist/logs/format.js +162 -0
- package/dist/logs/index.js +17 -0
- package/dist/logs/runtime.d.ts +18 -0
- package/dist/logs/runtime.js +364 -0
- package/dist/logs/time.d.ts +16 -0
- package/dist/logs/time.js +74 -0
- package/oclif.manifest.json +288 -3
- package/package.json +2 -2
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/* Public log fields intentionally mirror the Events JSON contract. */
|
|
3
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
4
|
+
exports.formatRuntimeLogRecord = void 0;
|
|
5
|
+
const unsafeTerminalCharacters = /[\u007F-\u009F\u061C\u200E\u200F\u2028-\u202E\u2066-\u2069]/gu;
|
|
6
|
+
const serialize = (value, spacing) => JSON.stringify(value, undefined, spacing).replace(unsafeTerminalCharacters, (character) => `\\u${character.charCodeAt(0).toString(16).padStart(4, '0')}`);
|
|
7
|
+
const inlineText = (value) => serialize(value).slice(1, -1);
|
|
8
|
+
const colorize = (text, color) => color ? `\u001B[${color}m${text}\u001B[0m` : text;
|
|
9
|
+
const recordColor = (record) => {
|
|
10
|
+
var _a;
|
|
11
|
+
if (record.record_type === 'lifecycle') {
|
|
12
|
+
if (record.outcome === 'failure') {
|
|
13
|
+
return '31';
|
|
14
|
+
}
|
|
15
|
+
if (record.outcome === 'success') {
|
|
16
|
+
return '32';
|
|
17
|
+
}
|
|
18
|
+
return '36';
|
|
19
|
+
}
|
|
20
|
+
switch ((_a = record.level) === null || _a === void 0 ? void 0 : _a.toLowerCase()) {
|
|
21
|
+
case 'debug':
|
|
22
|
+
return '2';
|
|
23
|
+
case 'info':
|
|
24
|
+
return undefined;
|
|
25
|
+
case 'warn':
|
|
26
|
+
case 'warning':
|
|
27
|
+
return '33';
|
|
28
|
+
case 'error':
|
|
29
|
+
return '31';
|
|
30
|
+
default:
|
|
31
|
+
return '36';
|
|
32
|
+
}
|
|
33
|
+
};
|
|
34
|
+
const publicRecord = (record) => ({
|
|
35
|
+
occurred_at: record.occurred_at,
|
|
36
|
+
...(record.level !== undefined ? { level: record.level } : {}),
|
|
37
|
+
...(record.event !== undefined ? { event: record.event } : {}),
|
|
38
|
+
...(record.trace_id !== undefined ? { trace_id: record.trace_id } : {}),
|
|
39
|
+
...(record.transform_type !== undefined
|
|
40
|
+
? { transform_type: record.transform_type }
|
|
41
|
+
: {}),
|
|
42
|
+
...(Object.prototype.hasOwnProperty.call(record, 'message')
|
|
43
|
+
? { message: record.message }
|
|
44
|
+
: {}),
|
|
45
|
+
...(record.outcome !== undefined ? { outcome: record.outcome } : {}),
|
|
46
|
+
...(record.duration_ms !== undefined
|
|
47
|
+
? { duration_ms: record.duration_ms }
|
|
48
|
+
: {}),
|
|
49
|
+
...(record.error !== undefined ? { error: record.error } : {}),
|
|
50
|
+
...(record.attributes !== undefined ? { attributes: record.attributes } : {}),
|
|
51
|
+
});
|
|
52
|
+
const highlightJson = (json, record) => {
|
|
53
|
+
let depth = 0;
|
|
54
|
+
let rootField;
|
|
55
|
+
// Consume whole strings so escaped quotes cannot become structural tokens.
|
|
56
|
+
return json.replace(/"(?:\\.|[^"\\])*"(:)?|[{}[\],]/gu, (fragment, keySuffix) => {
|
|
57
|
+
var _a;
|
|
58
|
+
if (fragment === '{' || fragment === '[') {
|
|
59
|
+
depth++;
|
|
60
|
+
return fragment;
|
|
61
|
+
}
|
|
62
|
+
if (fragment === '}' || fragment === ']') {
|
|
63
|
+
depth--;
|
|
64
|
+
return fragment;
|
|
65
|
+
}
|
|
66
|
+
if (fragment === ',') {
|
|
67
|
+
if (depth === 1) {
|
|
68
|
+
rootField = undefined;
|
|
69
|
+
}
|
|
70
|
+
return fragment;
|
|
71
|
+
}
|
|
72
|
+
if (keySuffix !== undefined) {
|
|
73
|
+
const key = fragment.slice(0, -keySuffix.length);
|
|
74
|
+
if (depth === 1) {
|
|
75
|
+
rootField = JSON.parse(key);
|
|
76
|
+
}
|
|
77
|
+
return `${colorize(key, '2')}${keySuffix}`;
|
|
78
|
+
}
|
|
79
|
+
let color;
|
|
80
|
+
if (rootField === 'error') {
|
|
81
|
+
color = '31';
|
|
82
|
+
}
|
|
83
|
+
else if (depth === 1 && rootField === 'level') {
|
|
84
|
+
color = recordColor(record);
|
|
85
|
+
}
|
|
86
|
+
else if (depth === 1 && rootField === 'message') {
|
|
87
|
+
const level = (_a = record.level) === null || _a === void 0 ? void 0 : _a.toLowerCase();
|
|
88
|
+
if (level === 'error') {
|
|
89
|
+
color = '31';
|
|
90
|
+
}
|
|
91
|
+
else if (level === 'warn' || level === 'warning') {
|
|
92
|
+
color = '33';
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
else if (depth === 1 &&
|
|
96
|
+
rootField === 'outcome' &&
|
|
97
|
+
record.record_type === 'lifecycle' &&
|
|
98
|
+
(record.outcome === 'success' || record.outcome === 'failure')) {
|
|
99
|
+
color = recordColor(record);
|
|
100
|
+
}
|
|
101
|
+
return colorize(fragment, color);
|
|
102
|
+
});
|
|
103
|
+
};
|
|
104
|
+
const prettyRecord = (record, options) => {
|
|
105
|
+
var _a;
|
|
106
|
+
const color = options.color ? recordColor(record) : undefined;
|
|
107
|
+
const prefix = options.includeResourceHeader
|
|
108
|
+
? `${colorize(`${record.resource_type} ${inlineText(record.resource_id)}`, options.color ? '36' : undefined)}\n`
|
|
109
|
+
: '';
|
|
110
|
+
const trace = record.trace_id !== undefined
|
|
111
|
+
? ` trace=${inlineText(record.trace_id)}`
|
|
112
|
+
: '';
|
|
113
|
+
const transform = record.transform_type !== undefined ? ` ${record.transform_type}` : '';
|
|
114
|
+
let summary;
|
|
115
|
+
if (record.record_type === 'lifecycle') {
|
|
116
|
+
const event = record.event !== undefined ? inlineText(record.event) : '';
|
|
117
|
+
const lifecycle = [
|
|
118
|
+
record.outcome !== undefined
|
|
119
|
+
? `outcome=${inlineText(record.outcome)}`
|
|
120
|
+
: undefined,
|
|
121
|
+
record.duration_ms !== undefined
|
|
122
|
+
? `duration=${record.duration_ms}ms`
|
|
123
|
+
: undefined,
|
|
124
|
+
]
|
|
125
|
+
.filter((value) => value !== undefined)
|
|
126
|
+
.join(' ');
|
|
127
|
+
const message = Object.prototype.hasOwnProperty.call(record, 'message')
|
|
128
|
+
? ` message=${record.message === null
|
|
129
|
+
? 'null'
|
|
130
|
+
: inlineText(record.message)}`
|
|
131
|
+
: '';
|
|
132
|
+
summary = `${colorize(event, color)}${trace}${transform}${lifecycle ? ` ${lifecycle}` : ''}${message}`;
|
|
133
|
+
}
|
|
134
|
+
else {
|
|
135
|
+
const severity = inlineText(((_a = record.level) !== null && _a !== void 0 ? _a : 'log').toUpperCase());
|
|
136
|
+
const message = Object.prototype.hasOwnProperty.call(record, 'message')
|
|
137
|
+
? record.message === null
|
|
138
|
+
? 'null'
|
|
139
|
+
: inlineText(record.message)
|
|
140
|
+
: '';
|
|
141
|
+
summary = `${colorize(severity, color)}${trace}${transform} ${message}`;
|
|
142
|
+
}
|
|
143
|
+
const attributes = record.attributes !== undefined ? ` | ${serialize(record.attributes)}` : '';
|
|
144
|
+
const lines = [`${record.occurred_at} ${summary}${attributes}`];
|
|
145
|
+
if (record.error !== undefined) {
|
|
146
|
+
lines.push(` error:\n${serialize(record.error, 2)
|
|
147
|
+
.split('\n')
|
|
148
|
+
.map((line) => ` ${line}`)
|
|
149
|
+
.join('\n')}`);
|
|
150
|
+
}
|
|
151
|
+
// The shared writer adds the second newline separating pretty entries.
|
|
152
|
+
return `${prefix}${lines.join('\n')}\n`;
|
|
153
|
+
};
|
|
154
|
+
const formatRuntimeLogRecord = (record, options) => {
|
|
155
|
+
if (options.format === 'pretty') {
|
|
156
|
+
return prettyRecord(record, options);
|
|
157
|
+
}
|
|
158
|
+
const json = serialize(publicRecord(record), options.format === 'json-pretty' ? 2 : undefined);
|
|
159
|
+
return `${options.color ? highlightJson(json, record) : json}\n`;
|
|
160
|
+
};
|
|
161
|
+
exports.formatRuntimeLogRecord = formatRuntimeLogRecord;
|
|
162
|
+
/* eslint-enable camelcase, unicorn/no-null */
|
package/dist/logs/index.js
CHANGED
|
@@ -6,13 +6,30 @@ const connect_1 = require("./connect");
|
|
|
6
6
|
const server_1 = require("./server");
|
|
7
7
|
const DEFAULT_LOGS_SERVER_PORT = 8220;
|
|
8
8
|
exports.DEFAULT_LOGS_SERVER_PORT = DEFAULT_LOGS_SERVER_PORT;
|
|
9
|
+
const legacyWarningHeading = () => {
|
|
10
|
+
const heading = 'Warning: You are using deprecated tunnel-based logging.';
|
|
11
|
+
const color = process.stderr.isTTY === true &&
|
|
12
|
+
process.env.NO_COLOR === undefined &&
|
|
13
|
+
process.env.TERM !== 'dumb';
|
|
14
|
+
return color ? `\u001B[33m${heading}\u001B[0m\n` : `${heading}\n`;
|
|
15
|
+
};
|
|
9
16
|
const showProxyLogs = async (bt, id, port = DEFAULT_LOGS_SERVER_PORT) => {
|
|
17
|
+
process.stderr.write(`${legacyWarningHeading()}This command opens a local tunnel and updates logging configuration.\n\n` +
|
|
18
|
+
`For runtime logs, use: bt proxies logs tail <id>\n` +
|
|
19
|
+
`Enable runtime logging and use a key with event:read permission. Logs may take time to appear.\n` +
|
|
20
|
+
`Learn more and set up runtime logs:\n` +
|
|
21
|
+
`https://developers.basistheory.com/docs/concepts/runtimes/runtime-logs\n\n`);
|
|
10
22
|
const url = await (0, server_1.createLogServer)(port);
|
|
11
23
|
await (0, connect_1.connectToProxy)(bt, id, url);
|
|
12
24
|
(0, utils_1.cleanUpOnExit)(() => (0, connect_1.disconnectFromProxy)(bt, id));
|
|
13
25
|
};
|
|
14
26
|
exports.showProxyLogs = showProxyLogs;
|
|
15
27
|
const showReactorLogs = async (bt, id, port = DEFAULT_LOGS_SERVER_PORT) => {
|
|
28
|
+
process.stderr.write(`${legacyWarningHeading()}This command opens a local tunnel and updates logging configuration.\n\n` +
|
|
29
|
+
`For runtime logs, use: bt reactors logs tail <id>\n` +
|
|
30
|
+
`Enable runtime logging and use a key with event:read permission. Logs may take time to appear.\n` +
|
|
31
|
+
`Learn more and set up runtime logs:\n` +
|
|
32
|
+
`https://developers.basistheory.com/docs/concepts/runtimes/runtime-logs\n\n`);
|
|
16
33
|
const url = await (0, server_1.createLogServer)(port);
|
|
17
34
|
await (0, connect_1.connectToReactor)(bt, id, url);
|
|
18
35
|
(0, utils_1.cleanUpOnExit)(() => (0, connect_1.disconnectFromReactor)(bt, id));
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import type { EventLogFilters, ResourceType } from './events';
|
|
2
|
+
import type { RuntimeLogFormat } from './format';
|
|
3
|
+
import type { LogWindow } from './time';
|
|
4
|
+
interface RuntimeLogOptions extends EventLogFilters {
|
|
5
|
+
apiKey: string;
|
|
6
|
+
apiBaseUrl?: string;
|
|
7
|
+
resourceType: ResourceType;
|
|
8
|
+
resourceId: string;
|
|
9
|
+
format: RuntimeLogFormat;
|
|
10
|
+
window?: LogWindow;
|
|
11
|
+
}
|
|
12
|
+
/** Retrieve one finite query window, including every continuation, then finish. */
|
|
13
|
+
declare const readEventLogs: (options: RuntimeLogOptions & {
|
|
14
|
+
window: LogWindow;
|
|
15
|
+
}) => Promise<void>;
|
|
16
|
+
/** Retrieve initial context, then keep reading overlapping live windows. */
|
|
17
|
+
declare const followEventLogs: (options: RuntimeLogOptions) => Promise<void>;
|
|
18
|
+
export { followEventLogs, readEventLogs };
|
|
@@ -0,0 +1,364 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/* Sequential loops preserve cursor exhaustion and ordered output behavior. */
|
|
3
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
4
|
+
exports.readEventLogs = exports.followEventLogs = void 0;
|
|
5
|
+
const events_1 = require("./events");
|
|
6
|
+
const format_1 = require("./format");
|
|
7
|
+
const time_1 = require("./time");
|
|
8
|
+
const LOOKBACK_MS = time_1.DEFAULT_LOOKBACK_MS;
|
|
9
|
+
const POLL_INTERVAL_MS = 5000;
|
|
10
|
+
const REQUEST_DEADLINE_MS = 20000;
|
|
11
|
+
const CYCLE_DEADLINE_MS = 60000;
|
|
12
|
+
const MAX_PAGES = 125;
|
|
13
|
+
const MAX_BATCHES = 2500;
|
|
14
|
+
const MAX_CYCLE_BYTES = 64 * 1024 * 1024;
|
|
15
|
+
const MAX_IDENTITIES = 100000;
|
|
16
|
+
const MAX_IDENTITY_BYTES = 16 * 1024 * 1024;
|
|
17
|
+
const reasonOf = (signal) => signal.reason instanceof Error ? signal.reason : new Error('Logs cancelled');
|
|
18
|
+
const checkDeadline = (signal, deadline) => {
|
|
19
|
+
if (signal.aborted) {
|
|
20
|
+
throw reasonOf(signal);
|
|
21
|
+
}
|
|
22
|
+
if (performance.now() >= deadline) {
|
|
23
|
+
throw new Error('Events log window exceeded its 60-second deadline');
|
|
24
|
+
}
|
|
25
|
+
};
|
|
26
|
+
const waitFor = (milliseconds, signal) => new Promise((resolve, reject) => {
|
|
27
|
+
if (signal.aborted) {
|
|
28
|
+
reject(reasonOf(signal));
|
|
29
|
+
return;
|
|
30
|
+
}
|
|
31
|
+
const onAbort = () => {
|
|
32
|
+
// The handler is registered only after the timer has been initialized.
|
|
33
|
+
// eslint-disable-next-line @typescript-eslint/no-use-before-define
|
|
34
|
+
clearTimeout(timer);
|
|
35
|
+
signal.removeEventListener('abort', onAbort);
|
|
36
|
+
reject(reasonOf(signal));
|
|
37
|
+
};
|
|
38
|
+
const timer = setTimeout(() => {
|
|
39
|
+
signal.removeEventListener('abort', onAbort);
|
|
40
|
+
resolve();
|
|
41
|
+
}, milliseconds);
|
|
42
|
+
signal.addEventListener('abort', onAbort, { once: true });
|
|
43
|
+
});
|
|
44
|
+
// Each request needs its own cancellation closure over that request's controller.
|
|
45
|
+
const abortOnSignal = (controller, signal) => () => controller.abort(reasonOf(signal));
|
|
46
|
+
// A successful callback alone is insufficient when write() reports backpressure.
|
|
47
|
+
// Listen before write() so synchronous test streams cannot lose a drain event.
|
|
48
|
+
const writeLine = (stream, line, signal) => new Promise((resolve, reject) => {
|
|
49
|
+
if (signal.aborted) {
|
|
50
|
+
reject(reasonOf(signal));
|
|
51
|
+
return;
|
|
52
|
+
}
|
|
53
|
+
const streamError = stream.errored;
|
|
54
|
+
// Already-failed streams need not emit another error after a write callback.
|
|
55
|
+
if (stream.destroyed || streamError) {
|
|
56
|
+
reject(streamError !== null && streamError !== void 0 ? streamError : new Error('Output stream is closed'));
|
|
57
|
+
return;
|
|
58
|
+
}
|
|
59
|
+
let callbackDone = false;
|
|
60
|
+
let drained = false;
|
|
61
|
+
let returned = false;
|
|
62
|
+
let settled = false;
|
|
63
|
+
const finish = (error) => {
|
|
64
|
+
if (settled || (!error && !(returned && callbackDone && drained))) {
|
|
65
|
+
return;
|
|
66
|
+
}
|
|
67
|
+
settled = true;
|
|
68
|
+
// Handlers cannot run until cleanup and every listener are initialized.
|
|
69
|
+
// eslint-disable-next-line @typescript-eslint/no-use-before-define
|
|
70
|
+
cleanup();
|
|
71
|
+
if (error) {
|
|
72
|
+
reject(error);
|
|
73
|
+
}
|
|
74
|
+
else {
|
|
75
|
+
resolve();
|
|
76
|
+
}
|
|
77
|
+
};
|
|
78
|
+
const onDrain = () => {
|
|
79
|
+
drained = true;
|
|
80
|
+
finish();
|
|
81
|
+
};
|
|
82
|
+
const onError = (error) => finish(error);
|
|
83
|
+
const onAbort = () => finish(reasonOf(signal));
|
|
84
|
+
const cleanup = () => {
|
|
85
|
+
stream.removeListener('drain', onDrain);
|
|
86
|
+
stream.removeListener('error', onError);
|
|
87
|
+
signal.removeEventListener('abort', onAbort);
|
|
88
|
+
};
|
|
89
|
+
stream.on('drain', onDrain);
|
|
90
|
+
stream.on('error', onError);
|
|
91
|
+
signal.addEventListener('abort', onAbort, { once: true });
|
|
92
|
+
try {
|
|
93
|
+
const accepted = stream.write(`${line}\n`, 'utf8', (error) => {
|
|
94
|
+
// Writable emits an error after its failed callback. Keep the listeners
|
|
95
|
+
// alive until that event, rather than leaving it unhandled after cleanup.
|
|
96
|
+
if (!error) {
|
|
97
|
+
callbackDone = true;
|
|
98
|
+
finish();
|
|
99
|
+
}
|
|
100
|
+
});
|
|
101
|
+
returned = true;
|
|
102
|
+
drained || (drained = accepted);
|
|
103
|
+
finish();
|
|
104
|
+
}
|
|
105
|
+
catch (error) {
|
|
106
|
+
finish(error instanceof Error ? error : new Error('Output write failed'));
|
|
107
|
+
}
|
|
108
|
+
});
|
|
109
|
+
const recordIdentity = (record) => {
|
|
110
|
+
var _a;
|
|
111
|
+
return JSON.stringify([
|
|
112
|
+
record.tenant_id,
|
|
113
|
+
record.resource_type,
|
|
114
|
+
record.resource_id,
|
|
115
|
+
(_a = record.transform_type) !== null && _a !== void 0 ? _a : null,
|
|
116
|
+
record.invocation_id,
|
|
117
|
+
record.sequence,
|
|
118
|
+
]);
|
|
119
|
+
};
|
|
120
|
+
const isBrokenPipe = (error) => error.code === 'EPIPE';
|
|
121
|
+
/** Both actions share bounded window reads and the complete output/cancellation lifecycle. */
|
|
122
|
+
const streamEventLogs = async (options, mode) => {
|
|
123
|
+
var _a, _b, _c;
|
|
124
|
+
const initialWindow = (_a = options.window) !== null && _a !== void 0 ? _a : (mode === 'tail' ? (0, time_1.resolveTailWindow)({}) : (0, time_1.resolveLogWindow)({}));
|
|
125
|
+
const isLiveTailStart = mode === 'tail' && initialWindow.start === initialWindow.end;
|
|
126
|
+
const origin = (0, events_1.resolveEventsOrigin)(options.apiBaseUrl);
|
|
127
|
+
const session = new AbortController();
|
|
128
|
+
let stopStatus;
|
|
129
|
+
const stop = (status, error) => {
|
|
130
|
+
if (!session.signal.aborted) {
|
|
131
|
+
stopStatus = status;
|
|
132
|
+
session.abort(error);
|
|
133
|
+
}
|
|
134
|
+
};
|
|
135
|
+
const onInterrupt = () => stop(130, new Error('SIGINT'));
|
|
136
|
+
const onTerminate = () => stop(143, new Error('SIGTERM'));
|
|
137
|
+
const onStdoutError = (error) => {
|
|
138
|
+
if (isBrokenPipe(error)) {
|
|
139
|
+
stop(0, error);
|
|
140
|
+
}
|
|
141
|
+
else {
|
|
142
|
+
session.abort(error);
|
|
143
|
+
}
|
|
144
|
+
};
|
|
145
|
+
const onStderrError = (error) => session.abort(error);
|
|
146
|
+
const color = process.stdout.isTTY === true &&
|
|
147
|
+
process.env.NO_COLOR === undefined &&
|
|
148
|
+
process.env.TERM !== 'dumb';
|
|
149
|
+
const identities = new Map();
|
|
150
|
+
let identityBytes = 0;
|
|
151
|
+
let resourceHeaderWritten = false;
|
|
152
|
+
let lastCompletedEnd = isLiveTailStart
|
|
153
|
+
? initialWindow.end
|
|
154
|
+
: undefined;
|
|
155
|
+
// Oclif's eager stdout error handler throws before command-level cleanup.
|
|
156
|
+
// Own stdout failures during log reads, preserving the prior handlers on exit.
|
|
157
|
+
const stdoutErrorListeners = process.stdout.rawListeners('error');
|
|
158
|
+
for (const listener of stdoutErrorListeners) {
|
|
159
|
+
process.stdout.removeListener('error', listener);
|
|
160
|
+
}
|
|
161
|
+
process.on('SIGINT', onInterrupt);
|
|
162
|
+
process.on('SIGTERM', onTerminate);
|
|
163
|
+
process.stdout.on('error', onStdoutError);
|
|
164
|
+
process.stderr.on('error', onStderrError);
|
|
165
|
+
try {
|
|
166
|
+
if (isLiveTailStart) {
|
|
167
|
+
await writeLine(process.stderr, 'Waiting for logs...', session.signal);
|
|
168
|
+
}
|
|
169
|
+
while (!session.signal.aborted) {
|
|
170
|
+
const end = lastCompletedEnd === undefined ? initialWindow.end : Date.now();
|
|
171
|
+
if (lastCompletedEnd !== undefined && end <= lastCompletedEnd) {
|
|
172
|
+
await waitFor(POLL_INTERVAL_MS, session.signal);
|
|
173
|
+
continue;
|
|
174
|
+
}
|
|
175
|
+
const cycleStarted = performance.now();
|
|
176
|
+
const deadline = cycleStarted + CYCLE_DEADLINE_MS;
|
|
177
|
+
const start = lastCompletedEnd === undefined
|
|
178
|
+
? initialWindow.start
|
|
179
|
+
: Math.max(initialWindow.start, end - LOOKBACK_MS);
|
|
180
|
+
const cycle = new AbortController();
|
|
181
|
+
const cancelCycle = () => cycle.abort(reasonOf(session.signal));
|
|
182
|
+
const cycleTimer = setTimeout(() => cycle.abort(new Error('Events log window exceeded its 60-second deadline')), CYCLE_DEADLINE_MS);
|
|
183
|
+
session.signal.addEventListener('abort', cancelCycle, { once: true });
|
|
184
|
+
let receivedBytes = 0;
|
|
185
|
+
let returnedBatches = 0;
|
|
186
|
+
let pages = 0;
|
|
187
|
+
let cursor;
|
|
188
|
+
const cursors = new Set();
|
|
189
|
+
const countCycleBytes = (bytes) => {
|
|
190
|
+
receivedBytes += bytes;
|
|
191
|
+
checkDeadline(cycle.signal, deadline);
|
|
192
|
+
if (receivedBytes > MAX_CYCLE_BYTES) {
|
|
193
|
+
throw new Error('Events log window exceeded its 64 MiB received-byte limit');
|
|
194
|
+
}
|
|
195
|
+
};
|
|
196
|
+
try {
|
|
197
|
+
do {
|
|
198
|
+
checkDeadline(cycle.signal, deadline);
|
|
199
|
+
if (pages >= MAX_PAGES) {
|
|
200
|
+
throw new Error('Events log window exceeded its 125-page limit');
|
|
201
|
+
}
|
|
202
|
+
let page;
|
|
203
|
+
let retrying = false;
|
|
204
|
+
for (let attempt = 1; attempt <= 3; attempt++) {
|
|
205
|
+
checkDeadline(cycle.signal, deadline);
|
|
206
|
+
const request = new AbortController();
|
|
207
|
+
const cancelRequest = abortOnSignal(request, cycle.signal);
|
|
208
|
+
const requestDeadline = Math.min(performance.now() + REQUEST_DEADLINE_MS, deadline);
|
|
209
|
+
const requestTimer = setTimeout(() => request.abort(new Error('Events request deadline exceeded')), Math.max(0, requestDeadline - performance.now()));
|
|
210
|
+
cycle.signal.addEventListener('abort', cancelRequest, {
|
|
211
|
+
once: true,
|
|
212
|
+
});
|
|
213
|
+
try {
|
|
214
|
+
page = await (0, events_1.readEventPage)({
|
|
215
|
+
origin,
|
|
216
|
+
apiKey: options.apiKey,
|
|
217
|
+
resourceType: options.resourceType,
|
|
218
|
+
resourceId: options.resourceId,
|
|
219
|
+
traceId: options.traceId,
|
|
220
|
+
transformType: options.transformType,
|
|
221
|
+
start,
|
|
222
|
+
end,
|
|
223
|
+
cursor,
|
|
224
|
+
signal: request.signal,
|
|
225
|
+
onBytes: countCycleBytes,
|
|
226
|
+
});
|
|
227
|
+
checkDeadline(cycle.signal, deadline);
|
|
228
|
+
if (performance.now() >= requestDeadline) {
|
|
229
|
+
throw new events_1.EventsReadError('Events request deadline exceeded', {
|
|
230
|
+
retryable: true,
|
|
231
|
+
});
|
|
232
|
+
}
|
|
233
|
+
break;
|
|
234
|
+
}
|
|
235
|
+
catch (error) {
|
|
236
|
+
clearTimeout(requestTimer);
|
|
237
|
+
cycle.signal.removeEventListener('abort', cancelRequest);
|
|
238
|
+
checkDeadline(cycle.signal, deadline);
|
|
239
|
+
if (!(error instanceof events_1.EventsReadError) ||
|
|
240
|
+
!error.retryable ||
|
|
241
|
+
attempt === 3) {
|
|
242
|
+
throw error;
|
|
243
|
+
}
|
|
244
|
+
if (!retrying) {
|
|
245
|
+
await writeLine(process.stderr, 'Events temporarily unavailable; retrying the current page.', cycle.signal);
|
|
246
|
+
retrying = true;
|
|
247
|
+
}
|
|
248
|
+
// Never advance the cursor or requested window after a failed page.
|
|
249
|
+
await waitFor(Math.max(POLL_INTERVAL_MS, (_b = error.retryAfterMs) !== null && _b !== void 0 ? _b : 0), cycle.signal);
|
|
250
|
+
}
|
|
251
|
+
finally {
|
|
252
|
+
clearTimeout(requestTimer);
|
|
253
|
+
cycle.signal.removeEventListener('abort', cancelRequest);
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
if (!page) {
|
|
257
|
+
throw new Error('Events page could not be read');
|
|
258
|
+
}
|
|
259
|
+
if (retrying) {
|
|
260
|
+
await writeLine(process.stderr, 'Events reads recovered.', cycle.signal);
|
|
261
|
+
}
|
|
262
|
+
pages++;
|
|
263
|
+
returnedBatches += page.batches.length;
|
|
264
|
+
if (returnedBatches > MAX_BATCHES) {
|
|
265
|
+
throw new Error('Events log window exceeded its batch limit');
|
|
266
|
+
}
|
|
267
|
+
if (page.next !== null && cursors.has(page.next)) {
|
|
268
|
+
throw new Error('Events returned a repeated pagination cursor');
|
|
269
|
+
}
|
|
270
|
+
for (const batch of page.batches) {
|
|
271
|
+
for (const record of batch) {
|
|
272
|
+
checkDeadline(cycle.signal, deadline);
|
|
273
|
+
const identity = recordIdentity(record);
|
|
274
|
+
const timestamp = Date.parse(record.event_timestamp);
|
|
275
|
+
const retained = identities.get(identity);
|
|
276
|
+
if (retained) {
|
|
277
|
+
retained.timestamp = Math.max(retained.timestamp, timestamp);
|
|
278
|
+
continue;
|
|
279
|
+
}
|
|
280
|
+
const bytes = Buffer.byteLength(identity, 'utf8');
|
|
281
|
+
if (identities.size >= MAX_IDENTITIES ||
|
|
282
|
+
identityBytes + bytes > MAX_IDENTITY_BYTES) {
|
|
283
|
+
throw new Error(`Events ${mode} exceeded its retained-identity limit`);
|
|
284
|
+
}
|
|
285
|
+
await writeLine(process.stdout, (0, format_1.formatRuntimeLogRecord)(record, {
|
|
286
|
+
format: options.format,
|
|
287
|
+
color,
|
|
288
|
+
includeResourceHeader: options.format === 'pretty' && !resourceHeaderWritten,
|
|
289
|
+
}), cycle.signal);
|
|
290
|
+
resourceHeaderWritten || (resourceHeaderWritten = options.format === 'pretty');
|
|
291
|
+
checkDeadline(cycle.signal, deadline);
|
|
292
|
+
identities.set(identity, {
|
|
293
|
+
timestamp,
|
|
294
|
+
bytes,
|
|
295
|
+
});
|
|
296
|
+
identityBytes += bytes;
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
cursor = (_c = page.next) !== null && _c !== void 0 ? _c : undefined;
|
|
300
|
+
if (cursor !== undefined) {
|
|
301
|
+
cursors.add(cursor);
|
|
302
|
+
}
|
|
303
|
+
} while (cursor !== undefined);
|
|
304
|
+
if (lastCompletedEnd === undefined && returnedBatches === 0) {
|
|
305
|
+
await writeLine(process.stderr, mode === 'read'
|
|
306
|
+
? 'No matching runtime logs found in the requested time window.'
|
|
307
|
+
: 'No matching runtime logs found in the initial window. Waiting for new logs...', cycle.signal);
|
|
308
|
+
}
|
|
309
|
+
checkDeadline(cycle.signal, deadline);
|
|
310
|
+
lastCompletedEnd = end;
|
|
311
|
+
for (const [identity, retained] of identities) {
|
|
312
|
+
if (retained.timestamp < lastCompletedEnd - LOOKBACK_MS) {
|
|
313
|
+
identities.delete(identity);
|
|
314
|
+
identityBytes -= retained.bytes;
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
}
|
|
318
|
+
finally {
|
|
319
|
+
clearTimeout(cycleTimer);
|
|
320
|
+
session.signal.removeEventListener('abort', cancelCycle);
|
|
321
|
+
}
|
|
322
|
+
if (mode === 'read') {
|
|
323
|
+
break;
|
|
324
|
+
}
|
|
325
|
+
const delay = POLL_INTERVAL_MS - (performance.now() - cycleStarted);
|
|
326
|
+
if (delay > 0) {
|
|
327
|
+
await waitFor(delay, session.signal);
|
|
328
|
+
}
|
|
329
|
+
}
|
|
330
|
+
}
|
|
331
|
+
catch (error) {
|
|
332
|
+
if (stopStatus === undefined) {
|
|
333
|
+
// Callback-based streams can surface EPIPE before their error event.
|
|
334
|
+
if (error instanceof Error && isBrokenPipe(error)) {
|
|
335
|
+
stopStatus = 0;
|
|
336
|
+
}
|
|
337
|
+
else {
|
|
338
|
+
const message = error instanceof Error ? error.message : 'Unknown Events log failure';
|
|
339
|
+
throw new Error(`Partial ${mode}: ${message}. Already displayed records are retained, but completeness is not guaranteed; retry with a smaller window if a work limit was reached.`);
|
|
340
|
+
}
|
|
341
|
+
}
|
|
342
|
+
}
|
|
343
|
+
finally {
|
|
344
|
+
session.abort(new Error('Logs closed'));
|
|
345
|
+
process.removeListener('SIGINT', onInterrupt);
|
|
346
|
+
process.removeListener('SIGTERM', onTerminate);
|
|
347
|
+
process.stdout.removeListener('error', onStdoutError);
|
|
348
|
+
for (let index = stdoutErrorListeners.length - 1; index >= 0; index--) {
|
|
349
|
+
process.stdout.prependListener('error', stdoutErrorListeners[index]);
|
|
350
|
+
}
|
|
351
|
+
process.stderr.removeListener('error', onStderrError);
|
|
352
|
+
if (stopStatus !== undefined) {
|
|
353
|
+
process.exitCode = stopStatus;
|
|
354
|
+
}
|
|
355
|
+
}
|
|
356
|
+
};
|
|
357
|
+
/** Retrieve one finite query window, including every continuation, then finish. */
|
|
358
|
+
const readEventLogs = (options) => streamEventLogs(options, 'read');
|
|
359
|
+
exports.readEventLogs = readEventLogs;
|
|
360
|
+
/** Retrieve initial context, then keep reading overlapping live windows. */
|
|
361
|
+
const followEventLogs = (options) => streamEventLogs(options, 'tail');
|
|
362
|
+
exports.followEventLogs = followEventLogs;
|
|
363
|
+
/* eslint-enable unicorn/no-null */
|
|
364
|
+
/* eslint-enable no-await-in-loop, no-continue */
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
declare const DEFAULT_LOOKBACK_MS = 300000;
|
|
2
|
+
interface LogWindow {
|
|
3
|
+
start: number;
|
|
4
|
+
end: number;
|
|
5
|
+
}
|
|
6
|
+
/** Resolve explicit relative values against one clock, and defaults against the end. */
|
|
7
|
+
declare const resolveLogWindow: (flags: {
|
|
8
|
+
since?: string;
|
|
9
|
+
until?: string;
|
|
10
|
+
}, now?: number) => LogWindow;
|
|
11
|
+
/** Resolve tail history only when an explicit --since value is supplied. */
|
|
12
|
+
declare const resolveTailWindow: (flags: {
|
|
13
|
+
since?: string;
|
|
14
|
+
}, now?: number) => LogWindow;
|
|
15
|
+
export type { LogWindow };
|
|
16
|
+
export { DEFAULT_LOOKBACK_MS, resolveLogWindow, resolveTailWindow };
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.resolveTailWindow = exports.resolveLogWindow = exports.DEFAULT_LOOKBACK_MS = void 0;
|
|
4
|
+
const DEFAULT_LOOKBACK_MS = 300000;
|
|
5
|
+
exports.DEFAULT_LOOKBACK_MS = DEFAULT_LOOKBACK_MS;
|
|
6
|
+
const invalidTime = (flag) => {
|
|
7
|
+
throw new Error(`--${flag} must be a positive whole-number duration (30s, 5m, 2h, 1d, 1w) or an ISO 8601 timestamp with a timezone and at most millisecond precision.`);
|
|
8
|
+
};
|
|
9
|
+
const parseTime = (value, now, flag) => {
|
|
10
|
+
const duration = /^(\d+)([smhdw])$/u.exec(value);
|
|
11
|
+
let instant;
|
|
12
|
+
if (duration) {
|
|
13
|
+
const units = {
|
|
14
|
+
s: 1000,
|
|
15
|
+
m: 60000,
|
|
16
|
+
h: 3600000,
|
|
17
|
+
d: 86400000,
|
|
18
|
+
w: 604800000,
|
|
19
|
+
};
|
|
20
|
+
const milliseconds = Number(duration[1]) * units[duration[2]];
|
|
21
|
+
if (!Number.isSafeInteger(milliseconds) || milliseconds <= 0) {
|
|
22
|
+
return invalidTime(flag);
|
|
23
|
+
}
|
|
24
|
+
instant = now - milliseconds;
|
|
25
|
+
}
|
|
26
|
+
else {
|
|
27
|
+
const timestamp =
|
|
28
|
+
// eslint-disable-next-line security/detect-unsafe-regex -- Fixed-width ISO components cannot cause catastrophic backtracking.
|
|
29
|
+
/^(\d{4})-(\d{2})-(\d{2})T([01]\d|2[0-3]):([0-5]\d):([0-5]\d)(?:\.\d{1,3})?(Z|[+-](?:[01]\d|2[0-3]):[0-5]\d)$/u.exec(value);
|
|
30
|
+
if (!timestamp) {
|
|
31
|
+
return invalidTime(flag);
|
|
32
|
+
}
|
|
33
|
+
const year = Number(timestamp[1]);
|
|
34
|
+
const month = Number(timestamp[2]);
|
|
35
|
+
const day = Number(timestamp[3]);
|
|
36
|
+
if (year < 1970 ||
|
|
37
|
+
month < 1 ||
|
|
38
|
+
month > 12 ||
|
|
39
|
+
day < 1 ||
|
|
40
|
+
day > new Date(Date.UTC(year, month, 0)).getUTCDate()) {
|
|
41
|
+
return invalidTime(flag);
|
|
42
|
+
}
|
|
43
|
+
instant = Date.parse(value);
|
|
44
|
+
}
|
|
45
|
+
if (!Number.isSafeInteger(instant) ||
|
|
46
|
+
instant < 0 ||
|
|
47
|
+
!Number.isFinite(new Date(instant).getTime())) {
|
|
48
|
+
return invalidTime(flag);
|
|
49
|
+
}
|
|
50
|
+
return instant;
|
|
51
|
+
};
|
|
52
|
+
/** Resolve explicit relative values against one clock, and defaults against the end. */
|
|
53
|
+
const resolveLogWindow = (flags, now = Date.now()) => {
|
|
54
|
+
const end = flags.until === undefined ? now : parseTime(flags.until, now, 'until');
|
|
55
|
+
const start = flags.since === undefined
|
|
56
|
+
? end - DEFAULT_LOOKBACK_MS
|
|
57
|
+
: parseTime(flags.since, now, 'since');
|
|
58
|
+
if (start < 0 || start >= end) {
|
|
59
|
+
throw new Error('The log window must start before it ends, on or after 1970-01-01.');
|
|
60
|
+
}
|
|
61
|
+
return {
|
|
62
|
+
start,
|
|
63
|
+
end,
|
|
64
|
+
};
|
|
65
|
+
};
|
|
66
|
+
exports.resolveLogWindow = resolveLogWindow;
|
|
67
|
+
/** Resolve tail history only when an explicit --since value is supplied. */
|
|
68
|
+
const resolveTailWindow = (flags, now = Date.now()) => flags.since === undefined
|
|
69
|
+
? {
|
|
70
|
+
start: now,
|
|
71
|
+
end: now,
|
|
72
|
+
}
|
|
73
|
+
: resolveLogWindow(flags, now);
|
|
74
|
+
exports.resolveTailWindow = resolveTailWindow;
|