querysub 0.635.0 → 0.637.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/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "querysub",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.637.0",
|
|
4
4
|
"main": "index.js",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"note1": "note on node-forge fork, see https://github.com/digitalbazaar/forge/issues/744 for details",
|
|
@@ -79,7 +79,7 @@
|
|
|
79
79
|
"node-forge": "https://github.com/sliftist/forge#e618181b469b07bdc70b968b0391beb8ef5fecd6",
|
|
80
80
|
"pako": "^2.1.0",
|
|
81
81
|
"peggy": "^5.0.6",
|
|
82
|
-
"sliftutils": "^1.7.
|
|
82
|
+
"sliftutils": "^1.7.107",
|
|
83
83
|
"socket-function": "^1.2.31",
|
|
84
84
|
"terser": "^5.31.0",
|
|
85
85
|
"typenode": "^6.6.1",
|
|
@@ -6,24 +6,40 @@ import { Querysub } from "../../../4-querysub/Querysub";
|
|
|
6
6
|
import { t } from "../../../2-proxy/schema2";
|
|
7
7
|
import { isDefined } from "../../../misc";
|
|
8
8
|
import { formatNumber, formatTime, formatDateTime } from "socket-function/src/formatting/format";
|
|
9
|
-
import { timeInSecond } from "socket-function/src/misc";
|
|
9
|
+
import { timeInMinute, timeInSecond } from "socket-function/src/misc";
|
|
10
10
|
import type { LogFileInfo } from "sliftutils/storage/StreamingLogs";
|
|
11
11
|
import { MachineServiceController } from "../../machineSchema";
|
|
12
12
|
import { getStorageServers } from "./storageServers";
|
|
13
|
-
import { Table } from "../../../5-diagnostics/Table";
|
|
13
|
+
import { Table, TableType } from "../../../5-diagnostics/Table";
|
|
14
|
+
import { showModal } from "../../../5-diagnostics/Modal";
|
|
15
|
+
import { FullscreenModal } from "../../../5-diagnostics/FullscreenModal";
|
|
14
16
|
import { URLParam } from "../../../library-components/URLParam";
|
|
15
17
|
import { InputLabelURL } from "../../../library-components/InputLabel";
|
|
16
18
|
import { Button } from "../../../library-components/Button";
|
|
19
|
+
import { atomic } from "../../../2-proxy/PathValueProxyWatcher";
|
|
17
20
|
import { TimeRangeSelector, getTimeRange } from "../../../diagnostics/logs/TimeRangeSelector";
|
|
18
21
|
import { StorageLogsSynced } from "./storageLogsController";
|
|
19
|
-
import { searchStorageLogs, invalidateStorageLogSearchCache } from "./storageLogSearch";
|
|
22
|
+
import { searchStorageLogs, invalidateStorageLogSearchCache, FailedLogFile } from "./storageLogSearch";
|
|
20
23
|
|
|
21
24
|
module.hotreload = true;
|
|
22
25
|
|
|
23
26
|
export const storageLogSearchParam = new URLParam("storageLogSearch", "");
|
|
27
|
+
// true adds a field as a column, false removes a default - same shape as the log viewer's selectedFields
|
|
28
|
+
const storageLogFieldsParam = new URLParam("storageLogFields", {} as Record<string, boolean>);
|
|
29
|
+
|
|
30
|
+
const defaultSelectedFields = {
|
|
31
|
+
time: true,
|
|
32
|
+
path: true,
|
|
33
|
+
kind: true,
|
|
34
|
+
bucketName: true,
|
|
35
|
+
};
|
|
24
36
|
|
|
25
37
|
// Fixed columns first so every search reads the same way; the entries' own fields follow in the order they appear.
|
|
26
38
|
const LEADING_COLUMNS = ["server", "file", "time", "kind"];
|
|
39
|
+
const FIELD_CHIP_COLOR = { h: 0, s: 0, l: 92 };
|
|
40
|
+
const FIELD_CHIP_ACTIVE_COLOR = { h: 210, s: 60, l: 78 };
|
|
41
|
+
const FIELD_COUNT_COLOR = { h: 0, s: 0, l: 40 };
|
|
42
|
+
const FIELD_COUNT_FONT_SIZE = 12;
|
|
27
43
|
const ERROR_COLOR = { h: 0, s: 60, l: 40 };
|
|
28
44
|
const AGE_COLOR = { h: 0, s: 0, l: 40 };
|
|
29
45
|
const AGE_FONT_SIZE = 13;
|
|
@@ -36,6 +52,67 @@ type ServerLogListing = {
|
|
|
36
52
|
fetchTime?: number;
|
|
37
53
|
};
|
|
38
54
|
|
|
55
|
+
const MODAL_FIELD_NAME_MIN_WIDTH = 140;
|
|
56
|
+
const VIEW_BUTTON_ACTIVE_HUE = 210;
|
|
57
|
+
|
|
58
|
+
// Shared between the modal and the table's View buttons, so the button of the row on display highlights and arrow keys move the selection
|
|
59
|
+
const viewedRowState = Querysub.createLocalSchema("storageLogsViewedRow", {
|
|
60
|
+
open: t.boolean,
|
|
61
|
+
index: t.number,
|
|
62
|
+
});
|
|
63
|
+
// The rows the open modal navigates. Plain memory: the modal re-renders on index changes, this is just the data those changes index into.
|
|
64
|
+
let viewedRows: Record<string, unknown>[] = [];
|
|
65
|
+
|
|
66
|
+
class RowViewModal extends qreact.Component<{ onClose: () => void }> {
|
|
67
|
+
componentDidMount() {
|
|
68
|
+
document.addEventListener("keydown", this.onKeyDown);
|
|
69
|
+
}
|
|
70
|
+
componentWillUnmount() {
|
|
71
|
+
document.removeEventListener("keydown", this.onKeyDown);
|
|
72
|
+
}
|
|
73
|
+
private onKeyDown = (e: KeyboardEvent) => {
|
|
74
|
+
if (e.key !== "ArrowUp" && e.key !== "ArrowDown") return;
|
|
75
|
+
e.preventDefault();
|
|
76
|
+
let delta = e.key === "ArrowDown" ? 1 : -1;
|
|
77
|
+
// A document listener is not a Querysub-dispatched event, so the synced write needs an explicit commit
|
|
78
|
+
Querysub.commit(() => {
|
|
79
|
+
let index = viewedRowState().index + delta;
|
|
80
|
+
if (index < 0 || index >= viewedRows.length) return;
|
|
81
|
+
viewedRowState().index = index;
|
|
82
|
+
});
|
|
83
|
+
};
|
|
84
|
+
render() {
|
|
85
|
+
let index = viewedRowState().index;
|
|
86
|
+
let row = viewedRows[index];
|
|
87
|
+
if (!row) return undefined;
|
|
88
|
+
return <FullscreenModal onCancel={this.props.onClose}>
|
|
89
|
+
<div className={css.vbox(6).pad2(16)}>
|
|
90
|
+
<div className={css.colorhsl(0, 0, 45)}>Row {index + 1} of {formatNumber(viewedRows.length)} (↑/↓ to move)</div>
|
|
91
|
+
{Object.entries(row).map(([field, value]) => <div key={field} className={css.hbox(10)}>
|
|
92
|
+
<div className={css.boldStyle.minWidth(MODAL_FIELD_NAME_MIN_WIDTH)}>{field}</div>
|
|
93
|
+
<div className={css.fontFamily("monospace").whiteSpace("pre-wrap")}>
|
|
94
|
+
{field === "time" && typeof value === "number" && `${formatDateTime(value)} (${value})`
|
|
95
|
+
|| typeof value === "string" && value
|
|
96
|
+
|| JSON.stringify(value)}
|
|
97
|
+
</div>
|
|
98
|
+
</div>)}
|
|
99
|
+
</div>
|
|
100
|
+
</FullscreenModal>;
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
function showRowModal(rows: Record<string, unknown>[], index: number): void {
|
|
105
|
+
viewedRows = rows;
|
|
106
|
+
viewedRowState().open = true;
|
|
107
|
+
viewedRowState().index = index;
|
|
108
|
+
let close = showModal({
|
|
109
|
+
content: <RowViewModal onClose={() => {
|
|
110
|
+
viewedRowState().open = false;
|
|
111
|
+
close.close();
|
|
112
|
+
}} />
|
|
113
|
+
});
|
|
114
|
+
}
|
|
115
|
+
|
|
39
116
|
/** Whether a log file could hold entries in the range - a live file (no endTime) runs to now. */
|
|
40
117
|
function fileOverlapsRange(file: LogFileInfo, startTime: number, endTime: number): boolean {
|
|
41
118
|
let fileEnd = file.endTime ?? Date.now();
|
|
@@ -53,7 +130,8 @@ export class StorageLogsPage extends qreact.Component {
|
|
|
53
130
|
/** The oldest fetch time among the results being displayed */
|
|
54
131
|
resultsFetchTime: t.number,
|
|
55
132
|
rows: t.atomic<Record<string, unknown>[]>([]),
|
|
56
|
-
|
|
133
|
+
fields: t.atomic<{ field: string; count: number }[]>([]),
|
|
134
|
+
failedFiles: t.atomic<FailedLogFile[]>([]),
|
|
57
135
|
error: t.string,
|
|
58
136
|
});
|
|
59
137
|
|
|
@@ -85,19 +163,51 @@ export class StorageLogsPage extends qreact.Component {
|
|
|
85
163
|
return selected;
|
|
86
164
|
}
|
|
87
165
|
|
|
88
|
-
|
|
166
|
+
/** fresh drops the log file listings and the per-file search cache first, so everything searched is refetched. */
|
|
167
|
+
private search(fresh?: boolean) {
|
|
89
168
|
if (this.state.searching) return;
|
|
90
|
-
// State and synced reads are captured HERE, in the tracked part, before the async work
|
|
169
|
+
// State and synced reads are captured HERE, in the tracked part, before the async work (and before the invalidation below, which would turn the listings back into loading)
|
|
91
170
|
let listings = this.getListings();
|
|
92
|
-
let
|
|
171
|
+
let cachedFiles = this.getSelectedFiles(listings).map(file => ({ url: file.url, name: file.name }));
|
|
172
|
+
let urls = listings.map(listing => listing.url);
|
|
93
173
|
let query = storageLogSearchParam.value;
|
|
94
174
|
let { startTime, endTime, searchFromStart } = getTimeRange();
|
|
175
|
+
let synced = StorageLogsSynced(SocketFunction.browserNodeId());
|
|
176
|
+
if (fresh) {
|
|
177
|
+
invalidateStorageLogSearchCache();
|
|
178
|
+
synced.getServerLogFiles.resetAll();
|
|
179
|
+
}
|
|
95
180
|
this.state.searching = true;
|
|
96
181
|
this.state.error = "";
|
|
97
182
|
this.state.doneFiles = 0;
|
|
98
|
-
this.state.totalFiles =
|
|
183
|
+
this.state.totalFiles = fresh ? 0 : cachedFiles.length;
|
|
99
184
|
this.state.currentFile = "";
|
|
185
|
+
// Cleared up front, so anything on screen during the search is never stale results from the previous one
|
|
186
|
+
this.state.rows = [];
|
|
187
|
+
this.state.fields = [];
|
|
188
|
+
this.state.failedFiles = [];
|
|
189
|
+
this.state.durationTime = 0;
|
|
190
|
+
this.state.resultsFetchTime = 0;
|
|
100
191
|
Querysub.onCommitFinished(async () => {
|
|
192
|
+
let files = cachedFiles;
|
|
193
|
+
if (fresh) {
|
|
194
|
+
// The selection has to wait for the fresh listings; a server that fails costs only its own files (its error also shows on its listing row, which refetches the same way)
|
|
195
|
+
files = [];
|
|
196
|
+
await Promise.all(urls.map(async url => {
|
|
197
|
+
try {
|
|
198
|
+
let listing = await synced.getServerLogFiles.promise(url);
|
|
199
|
+
for (let file of listing.files) {
|
|
200
|
+
if (!fileOverlapsRange(file, startTime, endTime)) continue;
|
|
201
|
+
files.push({ url, name: file.name });
|
|
202
|
+
}
|
|
203
|
+
} catch (e) {
|
|
204
|
+
console.error(`Listing log files on ${url} failed, continuing with the other servers:`, (e as Error).stack ?? e);
|
|
205
|
+
}
|
|
206
|
+
}));
|
|
207
|
+
Querysub.commit(() => {
|
|
208
|
+
this.state.totalFiles = files.length;
|
|
209
|
+
});
|
|
210
|
+
}
|
|
101
211
|
try {
|
|
102
212
|
let result = await searchStorageLogs({
|
|
103
213
|
files,
|
|
@@ -111,27 +221,30 @@ export class StorageLogsPage extends qreact.Component {
|
|
|
111
221
|
},
|
|
112
222
|
});
|
|
113
223
|
let rows: Record<string, unknown>[] = [];
|
|
114
|
-
let columns: string[] = [...LEADING_COLUMNS];
|
|
115
224
|
for (let file of result.files) {
|
|
116
225
|
for (let entry of file.entries) {
|
|
117
226
|
let fields: Record<string, unknown> = entry && typeof entry === "object" ? entry as Record<string, unknown> : { value: String(entry) };
|
|
118
227
|
// The file selection is only as granular as whole files, so the entries themselves still need the range applied
|
|
119
228
|
let time = fields.time;
|
|
120
229
|
if (typeof time === "number" && (time < startTime || time > endTime)) continue;
|
|
121
|
-
for (let key of Object.keys(fields)) {
|
|
122
|
-
if (!columns.includes(key)) {
|
|
123
|
-
columns.push(key);
|
|
124
|
-
}
|
|
125
|
-
}
|
|
126
230
|
rows.push({ server: file.url, file: file.name, ...fields });
|
|
127
231
|
}
|
|
128
232
|
}
|
|
129
233
|
// Ordered the way the time range UI says: from the end (newest first) by default, oldest first when searching from the start
|
|
130
234
|
let direction = searchFromStart ? 1 : -1;
|
|
131
235
|
rows.sort((a, b) => direction * ((typeof a.time === "number" && a.time || 0) - (typeof b.time === "number" && b.time || 0)));
|
|
236
|
+
// Seeded so the fixed fields lead the chip row; ones no entry has are dropped below
|
|
237
|
+
let fieldCounts = new Map<string, number>(LEADING_COLUMNS.map(field => [field, 0]));
|
|
238
|
+
for (let row of rows) {
|
|
239
|
+
for (let key of Object.keys(row)) {
|
|
240
|
+
fieldCounts.set(key, (fieldCounts.get(key) || 0) + 1);
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
let fields = [...fieldCounts.entries()].filter(([, count]) => count > 0).map(([field, count]) => ({ field, count }));
|
|
132
244
|
Querysub.commit(() => {
|
|
133
245
|
this.state.rows = rows;
|
|
134
|
-
this.state.
|
|
246
|
+
this.state.fields = fields;
|
|
247
|
+
this.state.failedFiles = result.failures;
|
|
135
248
|
this.state.durationTime = result.durationTime;
|
|
136
249
|
this.state.resultsFetchTime = result.oldestFetchTime;
|
|
137
250
|
});
|
|
@@ -153,10 +266,49 @@ export class StorageLogsPage extends qreact.Component {
|
|
|
153
266
|
let selectedBytes = selectedFiles.reduce((sum, file) => sum + file.size, 0);
|
|
154
267
|
let loadedListings = listings.filter(listing => listing.fetchTime);
|
|
155
268
|
let oldestListingTime = loadedListings.length && Math.min(...loadedListings.map(listing => listing.fetchTime || 0)) || 0;
|
|
156
|
-
let now = Querysub.nowDelayed(
|
|
269
|
+
let now = Querysub.nowDelayed(timeInMinute);
|
|
270
|
+
|
|
271
|
+
// Defaults show unless explicitly toggled off, plus whatever was explicitly toggled on
|
|
272
|
+
let selectedSet = new Set<string>();
|
|
273
|
+
for (let field of Object.keys(defaultSelectedFields)) {
|
|
274
|
+
if (atomic(storageLogFieldsParam.value[field]) === undefined) {
|
|
275
|
+
selectedSet.add(field);
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
for (let [key, value] of Object.entries(storageLogFieldsParam.value)) {
|
|
279
|
+
if (value) {
|
|
280
|
+
selectedSet.add(key);
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
// Columns follow the chip row's order; selections with no matching field this search (ex, persisted from an earlier one) go last
|
|
284
|
+
let selectedFields = this.state.fields.map(x => x.field).filter(field => selectedSet.has(field));
|
|
285
|
+
for (let field of selectedSet) {
|
|
286
|
+
if (!selectedFields.includes(field)) {
|
|
287
|
+
selectedFields.push(field);
|
|
288
|
+
}
|
|
289
|
+
}
|
|
157
290
|
|
|
158
|
-
|
|
159
|
-
|
|
291
|
+
// Object identity map, as the formatter only receives the row itself and highlighting needs its index
|
|
292
|
+
let rows = this.state.rows;
|
|
293
|
+
let rowIndexes = new Map(rows.map((row, index) => [row, index]));
|
|
294
|
+
let columnsConfig: TableType<Record<string, unknown>>["columns"] = {};
|
|
295
|
+
columnsConfig["view"] = {
|
|
296
|
+
title: "View",
|
|
297
|
+
formatter: (x, context) => {
|
|
298
|
+
let row = context?.row;
|
|
299
|
+
if (!row) return undefined;
|
|
300
|
+
let index = rowIndexes.get(row) ?? -1;
|
|
301
|
+
let viewed = viewedRowState().open && viewedRowState().index === index;
|
|
302
|
+
return <Button
|
|
303
|
+
flavor="tiny"
|
|
304
|
+
hue={viewed ? VIEW_BUTTON_ACTIVE_HUE : undefined}
|
|
305
|
+
onClick={() => showRowModal(rows, index)}
|
|
306
|
+
>
|
|
307
|
+
View
|
|
308
|
+
</Button>;
|
|
309
|
+
},
|
|
310
|
+
};
|
|
311
|
+
for (let column of selectedFields) {
|
|
160
312
|
if (column === "time") {
|
|
161
313
|
columnsConfig[column] = {
|
|
162
314
|
title: "Time",
|
|
@@ -177,7 +329,8 @@ export class StorageLogsPage extends qreact.Component {
|
|
|
177
329
|
StorageLogsSynced(SocketFunction.browserNodeId()).getServerLogFiles.resetAll();
|
|
178
330
|
invalidateStorageLogSearchCache();
|
|
179
331
|
this.state.rows = [];
|
|
180
|
-
this.state.
|
|
332
|
+
this.state.fields = [];
|
|
333
|
+
this.state.failedFiles = [];
|
|
181
334
|
this.state.resultsFetchTime = 0;
|
|
182
335
|
this.state.durationTime = 0;
|
|
183
336
|
}}
|
|
@@ -207,6 +360,15 @@ export class StorageLogsPage extends qreact.Component {
|
|
|
207
360
|
>
|
|
208
361
|
{this.state.searching && `Searching ${this.state.doneFiles}/${this.state.totalFiles}...` || "Search"}
|
|
209
362
|
</Button>
|
|
363
|
+
<Button
|
|
364
|
+
flavor="large"
|
|
365
|
+
hue={120}
|
|
366
|
+
title="Invalidates the log file listings and the cached search results, then searches"
|
|
367
|
+
onClick={() => this.search(true)}
|
|
368
|
+
className={this.state.searching && css.opacity(0.5) || undefined}
|
|
369
|
+
>
|
|
370
|
+
Fresh Search
|
|
371
|
+
</Button>
|
|
210
372
|
</div>
|
|
211
373
|
<div className={css.vbox(4)}>
|
|
212
374
|
{listings.map(listing => {
|
|
@@ -230,7 +392,34 @@ export class StorageLogsPage extends qreact.Component {
|
|
|
230
392
|
</div>
|
|
231
393
|
</div>
|
|
232
394
|
{this.state.error && <div className={css.colorhsl(ERROR_COLOR.h, ERROR_COLOR.s, ERROR_COLOR.l).whiteSpace("pre-wrap")}>{this.state.error}</div>}
|
|
233
|
-
{!!this.state.
|
|
395
|
+
{!!this.state.failedFiles.length && <div className={css.vbox(2).colorhsl(ERROR_COLOR.h, ERROR_COLOR.s, ERROR_COLOR.l)}>
|
|
396
|
+
<div className={css.boldStyle}>{formatNumber(this.state.failedFiles.length)} files failed to search (their entries are missing from the results):</div>
|
|
397
|
+
{this.state.failedFiles.map(failure => <div key={`${failure.url}|${failure.name}`} className={css.ellipsis} title={failure.error}>
|
|
398
|
+
{failure.url} {failure.name}
|
|
399
|
+
</div>)}
|
|
400
|
+
</div>}
|
|
401
|
+
{!!this.state.rows.length && <div className={css.vbox(8)}>
|
|
402
|
+
<div className={css.hbox(6, 4).wrap.alignItems("center")}>
|
|
403
|
+
{this.state.fields.map(({ field, count }) => {
|
|
404
|
+
let selected = selectedSet.has(field);
|
|
405
|
+
let color = selected ? FIELD_CHIP_ACTIVE_COLOR : FIELD_CHIP_COLOR;
|
|
406
|
+
return <div
|
|
407
|
+
key={field}
|
|
408
|
+
className={
|
|
409
|
+
css.hbox(5).alignItems("center").pad2(8, 4).button.whiteSpace("nowrap")
|
|
410
|
+
.hsl(color.h, color.s, color.l).bord2(color.h, color.s, color.l - 20)
|
|
411
|
+
}
|
|
412
|
+
onClick={() => {
|
|
413
|
+
let newValues = { ...storageLogFieldsParam.value };
|
|
414
|
+
newValues[field] = !selected;
|
|
415
|
+
storageLogFieldsParam.value = newValues;
|
|
416
|
+
}}
|
|
417
|
+
>
|
|
418
|
+
<span className={css.boldStyle}>{field}</span>
|
|
419
|
+
<span className={css.fontSize(FIELD_COUNT_FONT_SIZE).colorhsl(FIELD_COUNT_COLOR.h, FIELD_COUNT_COLOR.s, FIELD_COUNT_COLOR.l)}>{formatNumber(count)}</span>
|
|
420
|
+
</div>;
|
|
421
|
+
})}
|
|
422
|
+
</div>
|
|
234
423
|
<Table
|
|
235
424
|
rows={this.state.rows}
|
|
236
425
|
columns={columnsConfig}
|
|
@@ -60,13 +60,20 @@ export type StorageLogSearchProgress = {
|
|
|
60
60
|
currentFile: string;
|
|
61
61
|
};
|
|
62
62
|
|
|
63
|
-
|
|
63
|
+
export type FailedLogFile = {
|
|
64
|
+
url: string;
|
|
65
|
+
name: string;
|
|
66
|
+
error: string;
|
|
67
|
+
};
|
|
68
|
+
|
|
69
|
+
/** Searches the query across the given files (per-file, so progress is per-file and each file's result caches on its own). A file that fails costs only its own results - it lands in failures and the rest of the search continues. */
|
|
64
70
|
export async function searchStorageLogs(config: {
|
|
65
71
|
files: { url: string; name: string }[];
|
|
66
72
|
query: string;
|
|
67
73
|
onProgress: (progress: StorageLogSearchProgress) => void;
|
|
68
74
|
}): Promise<{
|
|
69
75
|
files: SearchedLogFile[];
|
|
76
|
+
failures: FailedLogFile[];
|
|
70
77
|
durationTime: number;
|
|
71
78
|
/** The oldest fetch time among the results used, so the UI can say how stale the display is. */
|
|
72
79
|
oldestFetchTime: number;
|
|
@@ -74,16 +81,24 @@ export async function searchStorageLogs(config: {
|
|
|
74
81
|
let startTime = Date.now();
|
|
75
82
|
let doneFiles = 0;
|
|
76
83
|
let results: SearchedLogFile[] = [];
|
|
84
|
+
let failures: FailedLogFile[] = [];
|
|
77
85
|
let runFile = runInParallel({ parallelCount: PARALLEL_FILE_SEARCHES }, async (file: { url: string; name: string }) => {
|
|
78
86
|
config.onProgress({ doneFiles, totalFiles: config.files.length, currentFile: file.name });
|
|
79
|
-
|
|
80
|
-
|
|
87
|
+
try {
|
|
88
|
+
let result = await searchFile(file.url, file.name, config.query);
|
|
89
|
+
results.push({ url: file.url, name: file.name, entries: result.entries, fetchTime: result.fetchTime });
|
|
90
|
+
} catch (e) {
|
|
91
|
+
let error = (e as Error).stack ?? String(e);
|
|
92
|
+
console.error(`Searching log file ${file.name} on ${file.url} failed, continuing with the other files:`, error);
|
|
93
|
+
failures.push({ url: file.url, name: file.name, error });
|
|
94
|
+
}
|
|
81
95
|
doneFiles++;
|
|
82
96
|
config.onProgress({ doneFiles, totalFiles: config.files.length, currentFile: file.name });
|
|
83
97
|
});
|
|
84
98
|
await Promise.all(config.files.map(runFile));
|
|
85
99
|
return {
|
|
86
100
|
files: results,
|
|
101
|
+
failures,
|
|
87
102
|
durationTime: Date.now() - startTime,
|
|
88
103
|
oldestFetchTime: results.length && Math.min(...results.map(x => x.fetchTime)) || Date.now(),
|
|
89
104
|
};
|