watch-tail 0.1.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/LICENSE +21 -0
- package/README.md +210 -0
- package/build/client/_app/immutable/assets/0.B1NsRRrX.css +2 -0
- package/build/client/_app/immutable/assets/0.B1NsRRrX.css.br +0 -0
- package/build/client/_app/immutable/assets/0.B1NsRRrX.css.gz +0 -0
- package/build/client/_app/immutable/chunks/Bjy-W4x2.js +81 -0
- package/build/client/_app/immutable/chunks/Bjy-W4x2.js.br +0 -0
- package/build/client/_app/immutable/chunks/Bjy-W4x2.js.gz +0 -0
- package/build/client/_app/immutable/chunks/DCKWcE8r.js +1 -0
- package/build/client/_app/immutable/chunks/DCKWcE8r.js.br +0 -0
- package/build/client/_app/immutable/chunks/DCKWcE8r.js.gz +0 -0
- package/build/client/_app/immutable/chunks/DDRhiz1-.js +1 -0
- package/build/client/_app/immutable/chunks/DDRhiz1-.js.br +0 -0
- package/build/client/_app/immutable/chunks/DDRhiz1-.js.gz +0 -0
- package/build/client/_app/immutable/chunks/ZAD2y5xJ.js +3 -0
- package/build/client/_app/immutable/chunks/ZAD2y5xJ.js.br +0 -0
- package/build/client/_app/immutable/chunks/ZAD2y5xJ.js.gz +0 -0
- package/build/client/_app/immutable/chunks/xihTtKlq.js +1 -0
- package/build/client/_app/immutable/chunks/xihTtKlq.js.br +1 -0
- package/build/client/_app/immutable/chunks/xihTtKlq.js.gz +0 -0
- package/build/client/_app/immutable/entry/app.DIoggGwf.js +2 -0
- package/build/client/_app/immutable/entry/app.DIoggGwf.js.br +0 -0
- package/build/client/_app/immutable/entry/app.DIoggGwf.js.gz +0 -0
- package/build/client/_app/immutable/entry/start._YXgjuSB.js +1 -0
- package/build/client/_app/immutable/entry/start._YXgjuSB.js.br +0 -0
- package/build/client/_app/immutable/entry/start._YXgjuSB.js.gz +0 -0
- package/build/client/_app/immutable/nodes/0.D5bYt7iX.js +1 -0
- package/build/client/_app/immutable/nodes/0.D5bYt7iX.js.br +0 -0
- package/build/client/_app/immutable/nodes/0.D5bYt7iX.js.gz +0 -0
- package/build/client/_app/immutable/nodes/1.DBX83mYX.js +1 -0
- package/build/client/_app/immutable/nodes/1.DBX83mYX.js.br +0 -0
- package/build/client/_app/immutable/nodes/1.DBX83mYX.js.gz +0 -0
- package/build/client/_app/immutable/nodes/2.DiH-2moC.js +5 -0
- package/build/client/_app/immutable/nodes/2.DiH-2moC.js.br +0 -0
- package/build/client/_app/immutable/nodes/2.DiH-2moC.js.gz +0 -0
- package/build/client/_app/version.json +1 -0
- package/build/client/_app/version.json.br +0 -0
- package/build/client/_app/version.json.gz +0 -0
- package/build/client/robots.txt +3 -0
- package/build/client/robots.txt.br +0 -0
- package/build/client/robots.txt.gz +0 -0
- package/build/env.js +70 -0
- package/build/env.js.map +1 -0
- package/build/handler.js +24 -0
- package/build/handler.js.map +1 -0
- package/build/index.js +147 -0
- package/build/index.js.map +1 -0
- package/build/server/chunks/chunks/env.js-l0sNRNKZ.js +2 -0
- package/build/server/chunks/chunks/env.js-l0sNRNKZ.js.map +1 -0
- package/build/server/chunks/chunks/env2.js-D7xrfc37.js +316 -0
- package/build/server/chunks/chunks/env2.js-D7xrfc37.js.map +1 -0
- package/build/server/chunks/chunks/exports.js-CvFPPbaH.js +263 -0
- package/build/server/chunks/chunks/exports.js-CvFPPbaH.js.map +1 -0
- package/build/server/chunks/chunks/internal.js-DdmUz1r3.js +1191 -0
- package/build/server/chunks/chunks/internal.js-DdmUz1r3.js.map +1 -0
- package/build/server/chunks/chunks/internal2.js-DZ8ZzzjN.js +22 -0
- package/build/server/chunks/chunks/internal2.js-DZ8ZzzjN.js.map +1 -0
- package/build/server/chunks/chunks/regions.js-IQPMyP5C.js +52 -0
- package/build/server/chunks/chunks/regions.js-IQPMyP5C.js.map +1 -0
- package/build/server/chunks/chunks/server.js-ByNB8uti.js +3659 -0
- package/build/server/chunks/chunks/server.js-ByNB8uti.js.map +1 -0
- package/build/server/chunks/chunks/shared-server.js-Dn5XheXH.js +22 -0
- package/build/server/chunks/chunks/shared-server.js-Dn5XheXH.js.map +1 -0
- package/build/server/chunks/chunks/shared.js-CgjUIvRK.js +776 -0
- package/build/server/chunks/chunks/shared.js-CgjUIvRK.js.map +1 -0
- package/build/server/chunks/chunks/state.js-ClLTNPGs.js +86 -0
- package/build/server/chunks/chunks/state.js-ClLTNPGs.js.map +1 -0
- package/build/server/chunks/chunks/time-range.js-BLMT8aU3.js +102 -0
- package/build/server/chunks/chunks/time-range.js-BLMT8aU3.js.map +1 -0
- package/build/server/chunks/chunks/uneval.js-DaakSYFQ.js +488 -0
- package/build/server/chunks/chunks/uneval.js-DaakSYFQ.js.map +1 -0
- package/build/server/chunks/chunks/utils.js-BIlHjgGW.js +933 -0
- package/build/server/chunks/chunks/utils.js-BIlHjgGW.js.map +1 -0
- package/build/server/chunks/entries/endpoints/api/health/_server.ts.js-aiNZfkco.js +41 -0
- package/build/server/chunks/entries/endpoints/api/health/_server.ts.js-aiNZfkco.js.map +1 -0
- package/build/server/chunks/entries/endpoints/api/log-groups/_server.ts.js-D143-vRl.js +120 -0
- package/build/server/chunks/entries/endpoints/api/log-groups/_server.ts.js-D143-vRl.js.map +1 -0
- package/build/server/chunks/entries/endpoints/api/regions/_server.ts.js-CDNL7msU.js +88 -0
- package/build/server/chunks/entries/endpoints/api/regions/_server.ts.js-CDNL7msU.js.map +1 -0
- package/build/server/chunks/entries/endpoints/api/stream/_server.ts.js-D_i-xW6_.js +610 -0
- package/build/server/chunks/entries/endpoints/api/stream/_server.ts.js-D_i-xW6_.js.map +1 -0
- package/build/server/chunks/entries/fallbacks/error.svelte.js-qFLa8EVy.js +17 -0
- package/build/server/chunks/entries/fallbacks/error.svelte.js-qFLa8EVy.js.map +1 -0
- package/build/server/chunks/entries/pages/_layout.svelte.js-Bx7lXkAi.js +22 -0
- package/build/server/chunks/entries/pages/_layout.svelte.js-Bx7lXkAi.js.map +1 -0
- package/build/server/chunks/entries/pages/_page.svelte.js-BsvaKqbk.js +1264 -0
- package/build/server/chunks/entries/pages/_page.svelte.js-BsvaKqbk.js.map +1 -0
- package/build/server/chunks/handler-nVqa-QPz.js +1735 -0
- package/build/server/chunks/handler-nVqa-QPz.js.map +1 -0
- package/build/server/chunks/index.js-CtbhHm7L.js +3803 -0
- package/build/server/chunks/index.js-CtbhHm7L.js.map +1 -0
- package/build/server/chunks/manifest.js-BwUys8gK.js +70 -0
- package/build/server/chunks/manifest.js-BwUys8gK.js.map +1 -0
- package/build/server/chunks/nodes/0.js-Dl0J9d3p.js +9 -0
- package/build/server/chunks/nodes/0.js-Dl0J9d3p.js.map +1 -0
- package/build/server/chunks/nodes/1.js-dXZVW92o.js +9 -0
- package/build/server/chunks/nodes/1.js-dXZVW92o.js.map +1 -0
- package/build/server/chunks/nodes/2.js-aMMUenQc.js +9 -0
- package/build/server/chunks/nodes/2.js-aMMUenQc.js.map +1 -0
- package/build/shims.js +33 -0
- package/build/shims.js.map +1 -0
- package/dist/cli/bin.js +9 -0
- package/dist/cli/completions.js +99 -0
- package/dist/cli/index.js +183 -0
- package/dist/cli/options.js +171 -0
- package/dist/cli/server.js +126 -0
- package/dist/cli/ui.js +75 -0
- package/dist/lib/cli/aws.js +264 -0
- package/dist/lib/time-range.js +106 -0
- package/package.json +106 -0
|
@@ -0,0 +1,610 @@
|
|
|
1
|
+
import { a as apiError, p as parseRegionParam, b as readEnv, r as resolveAwsConfig, c as createLogsClient, d as resolveEffectiveRegion, e as describeAwsError, R as REGION_PARAM_HINT } from '../../../../chunks/env2.js-D7xrfc37.js';
|
|
2
|
+
import { H as HISTORIC_PRESETS, p as presetDurationMs, i as isHistoricPreset } from '../../../../chunks/time-range.js-BLMT8aU3.js';
|
|
3
|
+
import '../../../../chunks/utils.js-BIlHjgGW.js';
|
|
4
|
+
import { FilterLogEventsCommand } from '@aws-sdk/client-cloudwatch-logs';
|
|
5
|
+
import '../../../../chunks/shared-server.js-Dn5XheXH.js';
|
|
6
|
+
import 'node:fs';
|
|
7
|
+
import 'node:path';
|
|
8
|
+
import '../../../../chunks/shared.js-CgjUIvRK.js';
|
|
9
|
+
import '../../../../chunks/uneval.js-DaakSYFQ.js';
|
|
10
|
+
|
|
11
|
+
//#region src/lib/server/filter.ts
|
|
12
|
+
/** Default lookback window when neither `startTime` nor `lookback` is usable. */
|
|
13
|
+
var DEFAULT_LOOKBACK_MS = 3e5;
|
|
14
|
+
/** CloudWatch Logs rejects `startTime` older than 14 days. */
|
|
15
|
+
var MAX_LOOKBACK_MS = 12096e5;
|
|
16
|
+
var DEFAULT_POLL_MS$1 = 1e3;
|
|
17
|
+
var MIN_POLL_MS = 250;
|
|
18
|
+
var MAX_POLL_MS = 15e3;
|
|
19
|
+
var DURATION_UNITS = {
|
|
20
|
+
ms: 1,
|
|
21
|
+
s: 1e3,
|
|
22
|
+
m: 6e4,
|
|
23
|
+
h: 36e5,
|
|
24
|
+
d: 864e5,
|
|
25
|
+
w: 6048e5
|
|
26
|
+
};
|
|
27
|
+
var DURATION_PATTERN = /^(\d+(?:\.\d+)?)\s*(ms|s|m|h|d|w)$/i;
|
|
28
|
+
var EPOCH_PATTERN = /^\d+$/;
|
|
29
|
+
/**
|
|
30
|
+
* Parses a single-unit duration such as `500ms`, `30s`, `15m`, `2h`, `3d` or
|
|
31
|
+
* `1w`. Returns `null` for anything invalid, negative or zero.
|
|
32
|
+
*/
|
|
33
|
+
function parseDurationMs(value) {
|
|
34
|
+
if (typeof value !== "string") return null;
|
|
35
|
+
const match = DURATION_PATTERN.exec(value.trim());
|
|
36
|
+
if (match === null) return null;
|
|
37
|
+
const amount = Number(match[1]);
|
|
38
|
+
const unit = DURATION_UNITS[match[2].toLowerCase()];
|
|
39
|
+
if (unit === void 0 || !Number.isFinite(amount)) return null;
|
|
40
|
+
const ms = amount * unit;
|
|
41
|
+
if (!Number.isFinite(ms) || ms <= 0) return null;
|
|
42
|
+
return Math.round(ms);
|
|
43
|
+
}
|
|
44
|
+
/**
|
|
45
|
+
* Parses a start point into epoch milliseconds.
|
|
46
|
+
*
|
|
47
|
+
* Accepts epoch milliseconds (`1700000000000`), a duration relative to `now`
|
|
48
|
+
* (`15m`) and ISO 8601 timestamps. Returns `null` when the value is missing or
|
|
49
|
+
* cannot be understood.
|
|
50
|
+
*/
|
|
51
|
+
function parseStartTime(value, now) {
|
|
52
|
+
if (typeof value !== "string") return null;
|
|
53
|
+
const trimmed = value.trim();
|
|
54
|
+
if (trimmed.length === 0) return null;
|
|
55
|
+
if (EPOCH_PATTERN.test(trimmed)) return Number(trimmed);
|
|
56
|
+
const duration = parseDurationMs(trimmed);
|
|
57
|
+
if (duration !== null) return now - duration;
|
|
58
|
+
const parsed = Date.parse(trimmed);
|
|
59
|
+
return Number.isNaN(parsed) ? null : parsed;
|
|
60
|
+
}
|
|
61
|
+
/** Resolves the effective start time, clamped to the last 14 days and to `now`. */
|
|
62
|
+
function resolveStartTime(input) {
|
|
63
|
+
const { now } = input;
|
|
64
|
+
let start = parseStartTime(input.startTime, now);
|
|
65
|
+
if (start === null) start = now - (parseDurationMs(input.lookback ?? "") ?? DEFAULT_LOOKBACK_MS);
|
|
66
|
+
const oldestAllowed = now - MAX_LOOKBACK_MS;
|
|
67
|
+
if (start > now) start = now;
|
|
68
|
+
if (start < oldestAllowed) start = oldestAllowed;
|
|
69
|
+
return Math.round(start);
|
|
70
|
+
}
|
|
71
|
+
/** Parses a poll interval in milliseconds, defaulting to 1000 and clamped to 250..15000. */
|
|
72
|
+
function clampPollMs(value) {
|
|
73
|
+
if (typeof value !== "string") return DEFAULT_POLL_MS$1;
|
|
74
|
+
const trimmed = value.trim();
|
|
75
|
+
if (trimmed.length === 0) return DEFAULT_POLL_MS$1;
|
|
76
|
+
const parsed = Number(trimmed);
|
|
77
|
+
if (!Number.isFinite(parsed)) return DEFAULT_POLL_MS$1;
|
|
78
|
+
return Math.min(Math.max(Math.round(parsed), MIN_POLL_MS), MAX_POLL_MS);
|
|
79
|
+
}
|
|
80
|
+
/** Default historic window when neither `range` nor `from`/`to` is given. */
|
|
81
|
+
var DEFAULT_HISTORIC_PRESET = "15m";
|
|
82
|
+
/** Parses the `mode` parameter; `null` for an unrecognised value. */
|
|
83
|
+
function parseMode(value) {
|
|
84
|
+
if (typeof value !== "string") return null;
|
|
85
|
+
const trimmed = value.trim().toLowerCase();
|
|
86
|
+
if (trimmed === "live" || trimmed === "historic") return trimmed;
|
|
87
|
+
return null;
|
|
88
|
+
}
|
|
89
|
+
/**
|
|
90
|
+
* Parses an absolute instant: epoch milliseconds or an ISO 8601 timestamp.
|
|
91
|
+
*
|
|
92
|
+
* Relative durations are deliberately rejected here so an end bound can never
|
|
93
|
+
* move with the clock.
|
|
94
|
+
*/
|
|
95
|
+
function parseInstant(value) {
|
|
96
|
+
if (typeof value !== "string") return null;
|
|
97
|
+
const trimmed = value.trim();
|
|
98
|
+
if (trimmed.length === 0) return null;
|
|
99
|
+
if (EPOCH_PATTERN.test(trimmed)) return Number(trimmed);
|
|
100
|
+
const parsed = Date.parse(trimmed);
|
|
101
|
+
return Number.isNaN(parsed) ? null : parsed;
|
|
102
|
+
}
|
|
103
|
+
/**
|
|
104
|
+
* Clamps a window to what CloudWatch Logs can return: 14 days back, ending now.
|
|
105
|
+
*
|
|
106
|
+
* Returns `null` when the requested window ends before the retention floor, so
|
|
107
|
+
* a request for month-old data fails loudly instead of returning a stub window.
|
|
108
|
+
*/
|
|
109
|
+
function clampWindow(start, end, now) {
|
|
110
|
+
const oldest = now - MAX_LOOKBACK_MS;
|
|
111
|
+
if (end <= oldest) return null;
|
|
112
|
+
const clampedStart = Math.min(Math.max(start, oldest), now);
|
|
113
|
+
const clampedEnd = Math.min(Math.max(end, clampedStart + 1), now);
|
|
114
|
+
return {
|
|
115
|
+
start: Math.round(clampedStart),
|
|
116
|
+
end: Math.round(clampedEnd),
|
|
117
|
+
clamped: clampedStart !== Math.round(start) || clampedEnd !== Math.round(end)
|
|
118
|
+
};
|
|
119
|
+
}
|
|
120
|
+
/**
|
|
121
|
+
* Resolves the request window into a start time plus, for historic mode, an end
|
|
122
|
+
* time.
|
|
123
|
+
*
|
|
124
|
+
* Historic windows come from a preset (`range=24h`), from explicit bounds
|
|
125
|
+
* (`from`/`to`, epoch ms or ISO 8601) or, when neither is given, from the
|
|
126
|
+
* smallest preset. Windows are clamped to the last 14 days and to `now` so the
|
|
127
|
+
* request can never ask CloudWatch for data it will not return.
|
|
128
|
+
*/
|
|
129
|
+
function resolveWindow(input) {
|
|
130
|
+
const { now } = input;
|
|
131
|
+
const requestedMode = input.mode === null || input.mode === void 0 ? "live" : input.mode.trim();
|
|
132
|
+
const mode = requestedMode === "" ? "live" : parseMode(requestedMode);
|
|
133
|
+
if (mode === null) return {
|
|
134
|
+
ok: false,
|
|
135
|
+
code: "invalid-mode",
|
|
136
|
+
message: "Invalid mode: expected \"live\" or \"historic\""
|
|
137
|
+
};
|
|
138
|
+
if (mode === "live") return {
|
|
139
|
+
ok: true,
|
|
140
|
+
mode,
|
|
141
|
+
startTime: resolveStartTime({
|
|
142
|
+
startTime: input.startTime,
|
|
143
|
+
lookback: input.lookback,
|
|
144
|
+
now
|
|
145
|
+
}),
|
|
146
|
+
endTime: null,
|
|
147
|
+
preset: null,
|
|
148
|
+
clamped: false
|
|
149
|
+
};
|
|
150
|
+
const from = input.from?.trim() ?? "";
|
|
151
|
+
const to = input.to?.trim() ?? "";
|
|
152
|
+
if (from.length > 0 || to.length > 0) {
|
|
153
|
+
if (from.length === 0 || to.length === 0) return {
|
|
154
|
+
ok: false,
|
|
155
|
+
code: "invalid-window",
|
|
156
|
+
message: "A custom window needs both \"from\" and \"to\""
|
|
157
|
+
};
|
|
158
|
+
const parsedFrom = parseStartTime(from, now);
|
|
159
|
+
const parsedTo = parseInstant(to);
|
|
160
|
+
if (parsedFrom === null) return {
|
|
161
|
+
ok: false,
|
|
162
|
+
code: "invalid-time",
|
|
163
|
+
message: `Invalid "from": expected epoch milliseconds, a duration such as 3h, or an ISO 8601 timestamp`
|
|
164
|
+
};
|
|
165
|
+
if (parsedTo === null) return {
|
|
166
|
+
ok: false,
|
|
167
|
+
code: "invalid-time",
|
|
168
|
+
message: "Invalid \"to\": expected epoch milliseconds or an ISO 8601 timestamp"
|
|
169
|
+
};
|
|
170
|
+
if (parsedTo <= parsedFrom) return {
|
|
171
|
+
ok: false,
|
|
172
|
+
code: "invalid-window",
|
|
173
|
+
message: "The end of the window must be after its start"
|
|
174
|
+
};
|
|
175
|
+
const clamped = clampWindow(parsedFrom, parsedTo, now);
|
|
176
|
+
if (clamped === null || clamped.end <= clamped.start) return {
|
|
177
|
+
ok: false,
|
|
178
|
+
code: "invalid-window",
|
|
179
|
+
message: "The requested window is outside the 14 days CloudWatch Logs keeps"
|
|
180
|
+
};
|
|
181
|
+
return {
|
|
182
|
+
ok: true,
|
|
183
|
+
mode,
|
|
184
|
+
startTime: clamped.start,
|
|
185
|
+
endTime: clamped.end,
|
|
186
|
+
preset: null,
|
|
187
|
+
clamped: clamped.clamped
|
|
188
|
+
};
|
|
189
|
+
}
|
|
190
|
+
const requestedRange = input.range?.trim() ?? "";
|
|
191
|
+
const preset = requestedRange === "" ? DEFAULT_HISTORIC_PRESET : isHistoricPreset(requestedRange) ? requestedRange : null;
|
|
192
|
+
if (preset === null) return {
|
|
193
|
+
ok: false,
|
|
194
|
+
code: "invalid-range",
|
|
195
|
+
message: `Invalid range: expected one of ${HISTORIC_PRESETS.join(", ")} or an explicit from/to window`
|
|
196
|
+
};
|
|
197
|
+
const end = Math.round(now);
|
|
198
|
+
return {
|
|
199
|
+
ok: true,
|
|
200
|
+
mode,
|
|
201
|
+
startTime: Math.round(now - presetDurationMs(preset)),
|
|
202
|
+
endTime: end,
|
|
203
|
+
preset,
|
|
204
|
+
clamped: false
|
|
205
|
+
};
|
|
206
|
+
}
|
|
207
|
+
//#endregion
|
|
208
|
+
//#region src/lib/server/sse.ts
|
|
209
|
+
var NEWLINE_PATTERN = /\r?\n/g;
|
|
210
|
+
/**
|
|
211
|
+
* Encodes a single server-sent event frame.
|
|
212
|
+
*
|
|
213
|
+
* The JSON payload is always kept on one line, so the frame is
|
|
214
|
+
* `event: <name>\ndata: <json>\n\n`.
|
|
215
|
+
*/
|
|
216
|
+
function sseFrame(event, data) {
|
|
217
|
+
return `event: ${event.replace(NEWLINE_PATTERN, " ").trim()}\ndata: ${(JSON.stringify(data) ?? "null").replace(NEWLINE_PATTERN, " ")}\n\n`;
|
|
218
|
+
}
|
|
219
|
+
//#endregion
|
|
220
|
+
//#region src/lib/server/tail.ts
|
|
221
|
+
var DEFAULT_SEEN_CAPACITY = 5e3;
|
|
222
|
+
var DEFAULT_POLL_MS = 1e3;
|
|
223
|
+
var DEFAULT_LIMIT = 1e3;
|
|
224
|
+
var DEFAULT_MAX_CONSECUTIVE_ERRORS = 8;
|
|
225
|
+
var MAX_BACKOFF_MS = 15e3;
|
|
226
|
+
/** Empty polls that end a historic window: CloudWatch can lag behind ingestion. */
|
|
227
|
+
var DEFAULT_IDLE_POLLS = 2;
|
|
228
|
+
/** Upper bound on events yielded for one historic window. */
|
|
229
|
+
var DEFAULT_MAX_EVENTS = 1e4;
|
|
230
|
+
/** Returns true when the value is an abort error raised by the fetch/SDK layer. */
|
|
231
|
+
function isAbortError(error) {
|
|
232
|
+
if (error instanceof Error) return error.name === "AbortError" || error.code === "ABORT_ERR";
|
|
233
|
+
return false;
|
|
234
|
+
}
|
|
235
|
+
/**
|
|
236
|
+
* Abort-aware sleep used by default.
|
|
237
|
+
*
|
|
238
|
+
* Resolves after `ms`, resolves immediately when the signal aborts and always
|
|
239
|
+
* clears its timer. An injected sleep may instead reject on abort, which
|
|
240
|
+
* {@link tailLogEvents} handles by re-checking the signal.
|
|
241
|
+
*/
|
|
242
|
+
var defaultSleep = (ms, signal) => new Promise((resolve) => {
|
|
243
|
+
if (signal?.aborted === true || !Number.isFinite(ms) || ms <= 0) {
|
|
244
|
+
resolve();
|
|
245
|
+
return;
|
|
246
|
+
}
|
|
247
|
+
const onAbort = () => {
|
|
248
|
+
clearTimeout(timer);
|
|
249
|
+
resolve();
|
|
250
|
+
};
|
|
251
|
+
const timer = setTimeout(() => {
|
|
252
|
+
signal?.removeEventListener("abort", onAbort);
|
|
253
|
+
resolve();
|
|
254
|
+
}, ms);
|
|
255
|
+
timer.unref?.();
|
|
256
|
+
signal?.addEventListener("abort", onAbort, { once: true });
|
|
257
|
+
});
|
|
258
|
+
/**
|
|
259
|
+
* Bounded first-in-first-out set of event ids.
|
|
260
|
+
*
|
|
261
|
+
* Keeps at most `capacity` ids (default 5000) so memory stays flat on long
|
|
262
|
+
* streams. The oldest id is evicted first.
|
|
263
|
+
*/
|
|
264
|
+
var SeenEventIds = class {
|
|
265
|
+
ids = /* @__PURE__ */ new Set();
|
|
266
|
+
capacity;
|
|
267
|
+
constructor(capacity = DEFAULT_SEEN_CAPACITY) {
|
|
268
|
+
const parsed = Number.isFinite(capacity) ? Math.floor(capacity) : DEFAULT_SEEN_CAPACITY;
|
|
269
|
+
this.capacity = parsed > 0 ? parsed : DEFAULT_SEEN_CAPACITY;
|
|
270
|
+
}
|
|
271
|
+
/** True when the id was recorded before. Null and empty ids are never recorded. */
|
|
272
|
+
has(id) {
|
|
273
|
+
return id !== null && id.length > 0 && this.ids.has(id);
|
|
274
|
+
}
|
|
275
|
+
/** Records an id, evicting the oldest entry when the capacity is reached. */
|
|
276
|
+
add(id) {
|
|
277
|
+
if (id === null || id.length === 0 || this.ids.has(id)) return;
|
|
278
|
+
if (this.ids.size >= this.capacity) {
|
|
279
|
+
const oldest = this.ids.values().next();
|
|
280
|
+
if (!oldest.done) this.ids.delete(oldest.value);
|
|
281
|
+
}
|
|
282
|
+
this.ids.add(id);
|
|
283
|
+
}
|
|
284
|
+
/** Number of ids currently remembered. */
|
|
285
|
+
get size() {
|
|
286
|
+
return this.ids.size;
|
|
287
|
+
}
|
|
288
|
+
};
|
|
289
|
+
/**
|
|
290
|
+
* Converts a poll result into the events worth sending downstream.
|
|
291
|
+
*
|
|
292
|
+
* Drops ids seen in earlier polls and duplicates inside the same batch, then
|
|
293
|
+
* records the ids of the accepted events. Events without an id are always kept
|
|
294
|
+
* and never recorded.
|
|
295
|
+
*/
|
|
296
|
+
function selectNewEvents(events, seen) {
|
|
297
|
+
const selected = [];
|
|
298
|
+
if (events === void 0 || events.length === 0) return selected;
|
|
299
|
+
const freshIds = /* @__PURE__ */ new Set();
|
|
300
|
+
for (const event of events) {
|
|
301
|
+
const id = event.eventId !== void 0 && event.eventId.length > 0 ? event.eventId : null;
|
|
302
|
+
if (id !== null) {
|
|
303
|
+
if (seen.has(id) || freshIds.has(id)) continue;
|
|
304
|
+
freshIds.add(id);
|
|
305
|
+
}
|
|
306
|
+
const dto = {
|
|
307
|
+
id,
|
|
308
|
+
timestamp: typeof event.timestamp === "number" ? event.timestamp : 0,
|
|
309
|
+
message: event.message ?? ""
|
|
310
|
+
};
|
|
311
|
+
if (event.logStreamName !== void 0) dto.streamName = event.logStreamName;
|
|
312
|
+
if (typeof event.ingestionTime === "number") dto.ingestionTime = event.ingestionTime;
|
|
313
|
+
selected.push(dto);
|
|
314
|
+
}
|
|
315
|
+
for (const id of freshIds) seen.add(id);
|
|
316
|
+
return selected;
|
|
317
|
+
}
|
|
318
|
+
/** Highest timestamp in `events`, never lower than `previous`. */
|
|
319
|
+
function nextCursor(previous, events) {
|
|
320
|
+
let cursor = Number.isFinite(previous) ? previous : 0;
|
|
321
|
+
for (const event of events) if (Number.isFinite(event.timestamp) && event.timestamp > cursor) cursor = event.timestamp;
|
|
322
|
+
return cursor;
|
|
323
|
+
}
|
|
324
|
+
/**
|
|
325
|
+
* Polls `FilterLogEvents` and yields batches as they arrive.
|
|
326
|
+
*
|
|
327
|
+
* `startTime` is inclusive upstream, so the cursor advances to the newest
|
|
328
|
+
* timestamp seen and repeated events are removed by {@link SeenEventIds}.
|
|
329
|
+
* Nothing is new in a poll => sleep `pollIntervalMs`. Errors are yielded as
|
|
330
|
+
* `error` batches and retried with exponential backoff
|
|
331
|
+
* (`pollIntervalMs * 2^consecutiveErrors`, capped at 15000 ms) until
|
|
332
|
+
* `maxConsecutiveErrors` (default 8) failures in a row, then the generator
|
|
333
|
+
* returns. An aborted signal ends the generator without throwing.
|
|
334
|
+
*/
|
|
335
|
+
async function* tailLogEvents(options) {
|
|
336
|
+
const { client, logGroupName, startTime, pollIntervalMs = DEFAULT_POLL_MS, limit = DEFAULT_LIMIT, filterPattern, signal, sleep = defaultSleep, maxConsecutiveErrors = DEFAULT_MAX_CONSECUTIVE_ERRORS, logStreamNamePrefix, endTime = null, idlePolls = DEFAULT_IDLE_POLLS, maxEvents = DEFAULT_MAX_EVENTS } = options;
|
|
337
|
+
const historic = typeof endTime === "number" && Number.isFinite(endTime);
|
|
338
|
+
const windowEnd = historic ? endTime : null;
|
|
339
|
+
const idlePollsAllowed = Number.isFinite(idlePolls) && idlePolls > 0 ? Math.floor(idlePolls) : DEFAULT_IDLE_POLLS;
|
|
340
|
+
const eventCap = Number.isFinite(maxEvents) && maxEvents > 0 ? Math.floor(maxEvents) : DEFAULT_MAX_EVENTS;
|
|
341
|
+
const pollMs = Number.isFinite(pollIntervalMs) && pollIntervalMs > 0 ? Math.min(Math.round(pollIntervalMs), MAX_BACKOFF_MS) : DEFAULT_POLL_MS;
|
|
342
|
+
const pageLimit = Number.isFinite(limit) && limit > 0 ? Math.floor(limit) : DEFAULT_LIMIT;
|
|
343
|
+
const maxErrors = Number.isFinite(maxConsecutiveErrors) && maxConsecutiveErrors > 0 ? Math.floor(maxConsecutiveErrors) : DEFAULT_MAX_CONSECUTIVE_ERRORS;
|
|
344
|
+
const baseInput = {
|
|
345
|
+
logGroupName,
|
|
346
|
+
startTime,
|
|
347
|
+
limit: pageLimit
|
|
348
|
+
};
|
|
349
|
+
if (windowEnd !== null) baseInput.endTime = windowEnd;
|
|
350
|
+
if (filterPattern !== void 0 && filterPattern.length > 0) baseInput.filterPattern = filterPattern;
|
|
351
|
+
if (logStreamNamePrefix !== void 0 && logStreamNamePrefix.length > 0) baseInput.logStreamNamePrefix = logStreamNamePrefix;
|
|
352
|
+
const isAborted = () => signal?.aborted === true;
|
|
353
|
+
const wait = async (ms) => {
|
|
354
|
+
if (isAborted()) return true;
|
|
355
|
+
try {
|
|
356
|
+
await sleep(ms, signal);
|
|
357
|
+
} catch {}
|
|
358
|
+
return isAborted();
|
|
359
|
+
};
|
|
360
|
+
const seen = new SeenEventIds();
|
|
361
|
+
let cursor = Number.isFinite(startTime) ? startTime : 0;
|
|
362
|
+
let consecutiveErrors = 0;
|
|
363
|
+
let emitted = 0;
|
|
364
|
+
let emptyPolls = 0;
|
|
365
|
+
for (;;) {
|
|
366
|
+
if (isAborted()) return;
|
|
367
|
+
try {
|
|
368
|
+
const input = {
|
|
369
|
+
...baseInput,
|
|
370
|
+
startTime: cursor
|
|
371
|
+
};
|
|
372
|
+
const response = await client.send(new FilterLogEventsCommand(input), { abortSignal: signal });
|
|
373
|
+
consecutiveErrors = 0;
|
|
374
|
+
const events = selectNewEvents(response.events, seen);
|
|
375
|
+
if (events.length > 0) {
|
|
376
|
+
emptyPolls = 0;
|
|
377
|
+
cursor = nextCursor(cursor, events);
|
|
378
|
+
emitted += events.length;
|
|
379
|
+
yield {
|
|
380
|
+
type: "events",
|
|
381
|
+
events
|
|
382
|
+
};
|
|
383
|
+
if (historic && emitted >= eventCap) {
|
|
384
|
+
yield {
|
|
385
|
+
type: "end",
|
|
386
|
+
reason: "event-limit"
|
|
387
|
+
};
|
|
388
|
+
return;
|
|
389
|
+
}
|
|
390
|
+
if (windowEnd !== null && cursor >= windowEnd) {
|
|
391
|
+
yield {
|
|
392
|
+
type: "end",
|
|
393
|
+
reason: "window-complete"
|
|
394
|
+
};
|
|
395
|
+
return;
|
|
396
|
+
}
|
|
397
|
+
continue;
|
|
398
|
+
}
|
|
399
|
+
if (historic) {
|
|
400
|
+
emptyPolls += 1;
|
|
401
|
+
if (emptyPolls >= idlePollsAllowed) {
|
|
402
|
+
yield {
|
|
403
|
+
type: "end",
|
|
404
|
+
reason: "window-complete"
|
|
405
|
+
};
|
|
406
|
+
return;
|
|
407
|
+
}
|
|
408
|
+
}
|
|
409
|
+
} catch (error) {
|
|
410
|
+
if (isAborted() || isAbortError(error)) return;
|
|
411
|
+
consecutiveErrors += 1;
|
|
412
|
+
const described = describeAwsError(error);
|
|
413
|
+
yield {
|
|
414
|
+
type: "error",
|
|
415
|
+
message: described.message,
|
|
416
|
+
code: described.code
|
|
417
|
+
};
|
|
418
|
+
if (consecutiveErrors >= maxErrors) {
|
|
419
|
+
if (historic) yield {
|
|
420
|
+
type: "end",
|
|
421
|
+
reason: "repeated-errors"
|
|
422
|
+
};
|
|
423
|
+
return;
|
|
424
|
+
}
|
|
425
|
+
if (await wait(Math.min(pollMs * 2 ** consecutiveErrors, MAX_BACKOFF_MS))) return;
|
|
426
|
+
continue;
|
|
427
|
+
}
|
|
428
|
+
if (await wait(pollMs)) return;
|
|
429
|
+
}
|
|
430
|
+
}
|
|
431
|
+
//#endregion
|
|
432
|
+
//#region src/routes/api/stream/+server.ts
|
|
433
|
+
/** Send a `ping` after this much silence. */
|
|
434
|
+
var PING_INTERVAL_MS = 15e3;
|
|
435
|
+
/** Stop the stream after this many consecutive failed polls. */
|
|
436
|
+
var MAX_CONSECUTIVE_ERRORS = 8;
|
|
437
|
+
var SSE_HEADERS = {
|
|
438
|
+
"content-type": "text/event-stream; charset=utf-8",
|
|
439
|
+
"cache-control": "no-cache, no-transform",
|
|
440
|
+
connection: "keep-alive",
|
|
441
|
+
"x-accel-buffering": "no"
|
|
442
|
+
};
|
|
443
|
+
/**
|
|
444
|
+
* `GET /api/stream` - server-sent events for one log group.
|
|
445
|
+
*
|
|
446
|
+
* Emits `ready` first, then `log` per batch, `ping` after 15 s of silence and
|
|
447
|
+
* a final `end`. The region is optional: without it the tailer uses a client
|
|
448
|
+
* that resolves the region from the ambient AWS configuration. The tailer is
|
|
449
|
+
* cancelled through `request.signal`, and every timer and listener is removed
|
|
450
|
+
* so the dev server can exit.
|
|
451
|
+
*/
|
|
452
|
+
var GET = async ({ url, request }) => {
|
|
453
|
+
const logGroupName = url.searchParams.get("group")?.trim() ?? "";
|
|
454
|
+
if (logGroupName.length === 0) return apiError(400, "Query parameter \"group\" is required", "missing-group");
|
|
455
|
+
const parsedRegion = parseRegionParam(url.searchParams.get("region"));
|
|
456
|
+
if (!parsedRegion.ok) return apiError(400, REGION_PARAM_HINT, "invalid-region");
|
|
457
|
+
const env = readEnv();
|
|
458
|
+
const config = resolveAwsConfig(env, parsedRegion.region);
|
|
459
|
+
const filterPattern = url.searchParams.get("filterPattern")?.trim();
|
|
460
|
+
const streamWindow = resolveWindow({
|
|
461
|
+
mode: url.searchParams.get("mode"),
|
|
462
|
+
range: url.searchParams.get("range"),
|
|
463
|
+
from: url.searchParams.get("from"),
|
|
464
|
+
to: url.searchParams.get("to"),
|
|
465
|
+
startTime: url.searchParams.get("startTime"),
|
|
466
|
+
lookback: url.searchParams.get("lookback"),
|
|
467
|
+
now: Date.now()
|
|
468
|
+
});
|
|
469
|
+
if (!streamWindow.ok) return apiError(400, streamWindow.message, streamWindow.code);
|
|
470
|
+
const pollIntervalMs = clampPollMs(url.searchParams.get("poll"));
|
|
471
|
+
let client;
|
|
472
|
+
let region;
|
|
473
|
+
try {
|
|
474
|
+
client = createLogsClient(config);
|
|
475
|
+
region = await resolveEffectiveRegion(client, config);
|
|
476
|
+
} catch (error) {
|
|
477
|
+
const described = describeAwsError(error);
|
|
478
|
+
return apiError(502, described.message, described.code);
|
|
479
|
+
}
|
|
480
|
+
const encoder = new TextEncoder();
|
|
481
|
+
const bodyAbort = new AbortController();
|
|
482
|
+
let streamController;
|
|
483
|
+
let closed = false;
|
|
484
|
+
let pingTimer;
|
|
485
|
+
let cleanedUp = false;
|
|
486
|
+
const clearPing = () => {
|
|
487
|
+
if (pingTimer !== void 0) {
|
|
488
|
+
clearTimeout(pingTimer);
|
|
489
|
+
pingTimer = void 0;
|
|
490
|
+
}
|
|
491
|
+
};
|
|
492
|
+
const armPing = () => {
|
|
493
|
+
if (closed) return;
|
|
494
|
+
clearPing();
|
|
495
|
+
pingTimer = setTimeout(() => {
|
|
496
|
+
pingTimer = void 0;
|
|
497
|
+
if (closed) return;
|
|
498
|
+
enqueue(sseFrame("ping", { at: Date.now() }), false);
|
|
499
|
+
armPing();
|
|
500
|
+
}, PING_INTERVAL_MS);
|
|
501
|
+
pingTimer.unref?.();
|
|
502
|
+
};
|
|
503
|
+
const enqueue = (chunk, rearm = true) => {
|
|
504
|
+
if (closed || streamController === void 0) return;
|
|
505
|
+
try {
|
|
506
|
+
streamController.enqueue(encoder.encode(chunk));
|
|
507
|
+
} catch {
|
|
508
|
+
closed = true;
|
|
509
|
+
return;
|
|
510
|
+
}
|
|
511
|
+
if (rearm) armPing();
|
|
512
|
+
};
|
|
513
|
+
const closeStream = () => {
|
|
514
|
+
closed = true;
|
|
515
|
+
try {
|
|
516
|
+
streamController?.close();
|
|
517
|
+
} catch {}
|
|
518
|
+
};
|
|
519
|
+
const writeEnd = (reason) => {
|
|
520
|
+
clearPing();
|
|
521
|
+
const payload = { reason };
|
|
522
|
+
try {
|
|
523
|
+
streamController?.enqueue(encoder.encode(sseFrame("end", payload)));
|
|
524
|
+
} catch {}
|
|
525
|
+
closeStream();
|
|
526
|
+
};
|
|
527
|
+
const onAbort = () => {
|
|
528
|
+
bodyAbort.abort();
|
|
529
|
+
};
|
|
530
|
+
const cleanup = () => {
|
|
531
|
+
if (cleanedUp) return;
|
|
532
|
+
cleanedUp = true;
|
|
533
|
+
clearPing();
|
|
534
|
+
request.signal.removeEventListener("abort", onAbort);
|
|
535
|
+
client.destroy();
|
|
536
|
+
};
|
|
537
|
+
const pump = async () => {
|
|
538
|
+
let consecutiveErrors = 0;
|
|
539
|
+
let reason = "completed";
|
|
540
|
+
try {
|
|
541
|
+
for await (const batch of tailLogEvents({
|
|
542
|
+
client,
|
|
543
|
+
logGroupName,
|
|
544
|
+
startTime: streamWindow.startTime,
|
|
545
|
+
endTime: streamWindow.endTime,
|
|
546
|
+
pollIntervalMs,
|
|
547
|
+
filterPattern,
|
|
548
|
+
signal: bodyAbort.signal,
|
|
549
|
+
maxConsecutiveErrors: MAX_CONSECUTIVE_ERRORS
|
|
550
|
+
})) {
|
|
551
|
+
if (closed || bodyAbort.signal.aborted) break;
|
|
552
|
+
if (batch.type === "events") {
|
|
553
|
+
const payload = { events: batch.events };
|
|
554
|
+
consecutiveErrors = 0;
|
|
555
|
+
enqueue(sseFrame("log", payload));
|
|
556
|
+
} else if (batch.type === "end") {
|
|
557
|
+
reason = batch.reason;
|
|
558
|
+
break;
|
|
559
|
+
} else {
|
|
560
|
+
const payload = { message: batch.message };
|
|
561
|
+
if (batch.code !== void 0) payload.code = batch.code;
|
|
562
|
+
consecutiveErrors += 1;
|
|
563
|
+
enqueue(sseFrame("error", payload));
|
|
564
|
+
}
|
|
565
|
+
}
|
|
566
|
+
if (reason === "completed") {
|
|
567
|
+
if (bodyAbort.signal.aborted) reason = "client-disconnected";
|
|
568
|
+
else if (consecutiveErrors >= MAX_CONSECUTIVE_ERRORS) reason = "repeated-errors";
|
|
569
|
+
}
|
|
570
|
+
} catch (error) {
|
|
571
|
+
const described = describeAwsError(error);
|
|
572
|
+
const payload = { message: described.message };
|
|
573
|
+
if (described.code !== void 0) payload.code = described.code;
|
|
574
|
+
enqueue(sseFrame("error", payload));
|
|
575
|
+
reason = "failed";
|
|
576
|
+
} finally {
|
|
577
|
+
cleanup();
|
|
578
|
+
}
|
|
579
|
+
writeEnd(reason);
|
|
580
|
+
};
|
|
581
|
+
request.signal.addEventListener("abort", onAbort, { once: true });
|
|
582
|
+
const stream = new ReadableStream({
|
|
583
|
+
start(controller) {
|
|
584
|
+
streamController = controller;
|
|
585
|
+
const payload = {
|
|
586
|
+
region,
|
|
587
|
+
logGroupName,
|
|
588
|
+
endpoint: config.endpoint,
|
|
589
|
+
startTime: streamWindow.startTime,
|
|
590
|
+
endTime: streamWindow.endTime,
|
|
591
|
+
mode: streamWindow.mode,
|
|
592
|
+
preset: streamWindow.preset,
|
|
593
|
+
clamped: streamWindow.clamped
|
|
594
|
+
};
|
|
595
|
+
enqueue(sseFrame("ready", payload));
|
|
596
|
+
armPing();
|
|
597
|
+
pump();
|
|
598
|
+
},
|
|
599
|
+
cancel() {
|
|
600
|
+
bodyAbort.abort();
|
|
601
|
+
clearPing();
|
|
602
|
+
closeStream();
|
|
603
|
+
cleanup();
|
|
604
|
+
}
|
|
605
|
+
});
|
|
606
|
+
return new Response(stream, { headers: SSE_HEADERS });
|
|
607
|
+
};
|
|
608
|
+
|
|
609
|
+
export { GET };
|
|
610
|
+
//# sourceMappingURL=_server.ts.js-D_i-xW6_.js.map
|