querysub 0.459.0 → 0.461.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/.claude/settings.local.json +2 -1
- package/package.json +2 -2
- package/src/-b-authorities/dnsAuthority.ts +23 -15
- package/src/-g-core-values/NodeCapabilities.ts +3 -0
- package/src/-h-path-value-serialize/PathValueSerializer.ts +11 -3
- package/src/0-path-value-core/PathRouter.ts +6 -0
- package/src/0-path-value-core/PathWatcher.ts +1 -1
- package/src/0-path-value-core/pathValueCore.ts +4 -7
- package/src/1-path-client/RemoteWatcher.ts +8 -3
- package/src/1-path-client/pathValueClientWatcher.ts +3 -0
- package/src/2-proxy/PathValueProxyWatcher.ts +1 -1
- package/src/2-proxy/TransactionDelayer.ts +1 -1
- package/src/3-path-functions/PathFunctionHelpers.ts +13 -8
- package/src/3-path-functions/PathFunctionRunner.ts +2 -0
- package/src/4-querysub/Querysub.ts +0 -1
- package/src/4-querysub/QuerysubController.ts +1 -7
- package/src/config.ts +9 -0
- package/src/config2.ts +7 -1
- package/src/deployManager/components/MachinePicker.tsx +40 -0
- package/src/deployManager/components/ServiceDetailPage.tsx +2 -5
- package/src/deployManager/components/ServicesListPage.tsx +2 -0
- package/src/deployManager/components/Tools.tsx +165 -0
- package/src/deployManager/setupMachineMain.ts +74 -23
- package/src/diagnostics/charts/Chart.tsx +240 -0
- package/src/diagnostics/grossStats/GrossStatsPage.tsx +48 -83
- package/src/diagnostics/logs/IndexedLogs/BufferIndex.ts +22 -35
- package/src/diagnostics/logs/IndexedLogs/BufferUnitIndex.ts +39 -47
- package/src/diagnostics/logs/IndexedLogs/IndexedLogs.ts +3 -3
- package/src/diagnostics/logs/IndexedLogs/MCPIndexedLogs.ts +18 -3
- package/src/diagnostics/logs/IndexedLogs/MCPIndexedLogsEntry.ts +1 -0
- package/src/diagnostics/managementPages.tsx +58 -58
- package/src/diagnostics/misc-pages/DNSPage.tsx +344 -0
- package/test.ts +46 -70
- package/src/diagnostics/AuditLogPage.tsx +0 -147
- package/src/diagnostics/NodeConnectionsPage.tsx +0 -167
|
@@ -3,9 +3,8 @@ import { qreact } from "../../4-dom/qreact";
|
|
|
3
3
|
import { css } from "../../4-dom/css";
|
|
4
4
|
import { Querysub } from "../../4-querysub/QuerysubController";
|
|
5
5
|
import { SocketFunction } from "socket-function/SocketFunction";
|
|
6
|
-
import { formatNumber } from "socket-function/src/formatting/format";
|
|
6
|
+
import { formatDateTime, formatNumber } from "socket-function/src/formatting/format";
|
|
7
7
|
import { timeInHour, timeInMinute } from "socket-function/src/misc";
|
|
8
|
-
import { cacheArgsEqual } from "socket-function/src/caching";
|
|
9
8
|
import { t } from "../../2-proxy/schema2";
|
|
10
9
|
import { getSyncedController } from "../../library-components/SyncedController";
|
|
11
10
|
import { GrossStatsController } from "./GrossStatsController";
|
|
@@ -14,12 +13,12 @@ import {
|
|
|
14
13
|
GrossStatsField,
|
|
15
14
|
GrossStatsBucket,
|
|
16
15
|
} from "../../5-diagnostics/gross-stats/grossStats";
|
|
16
|
+
import { ChartBar, ChartData } from "../charts/Chart";
|
|
17
17
|
export { GrossStatsController } from "./GrossStatsController";
|
|
18
18
|
|
|
19
19
|
module.hotreload = true;
|
|
20
20
|
|
|
21
21
|
const REFRESH_INTERVAL_MS = 5 * timeInMinute;
|
|
22
|
-
const CHART_WIDTH = 1200;
|
|
23
22
|
const CHART_HEIGHT = 300;
|
|
24
23
|
|
|
25
24
|
const TIME_RANGES: { label: string; ms: number }[] = [
|
|
@@ -58,65 +57,33 @@ function shortNodeId(nodeId: string): string {
|
|
|
58
57
|
|
|
59
58
|
let grossStatsController = getSyncedController(GrossStatsController);
|
|
60
59
|
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
// only change when refreshed).
|
|
65
|
-
const renderChartPNG = cacheArgsEqual((
|
|
60
|
+
type ChartRow = { time: number; value: number };
|
|
61
|
+
|
|
62
|
+
function buildChartRows(
|
|
66
63
|
selectedField: GrossStatsField,
|
|
67
64
|
rangeMs: number,
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
nodeIds: readonly string[],
|
|
71
|
-
bucketsArrays: readonly GrossStatsBucket[][],
|
|
72
|
-
): { pngUrl: string; maxTotal: number } => {
|
|
73
|
-
let canvas = document.createElement("canvas");
|
|
74
|
-
canvas.width = width;
|
|
75
|
-
canvas.height = height;
|
|
76
|
-
let ctx = canvas.getContext("2d")!;
|
|
77
|
-
ctx.fillStyle = "hsl(0, 0%, 97%)";
|
|
78
|
-
ctx.fillRect(0, 0, width, height);
|
|
79
|
-
|
|
65
|
+
bucketsArrays: GrossStatsBucket[][],
|
|
66
|
+
): { rows: ChartRow[]; peak: number } {
|
|
80
67
|
let now = Date.now();
|
|
81
68
|
let windowStart = now - rangeMs;
|
|
82
|
-
|
|
83
|
-
let
|
|
84
|
-
for (let
|
|
85
|
-
perPixel.push({ perNode: new Array(nodeIds.length).fill(0), total: 0 });
|
|
86
|
-
}
|
|
87
|
-
let maxTotal = 0;
|
|
88
|
-
for (let i = 0; i < nodeIds.length; i++) {
|
|
89
|
-
let buckets = bucketsArrays[i];
|
|
69
|
+
// Sum across selected nodes, keyed by bucket time (each bucket is one minute).
|
|
70
|
+
let sumByTime = new Map<number, number>();
|
|
71
|
+
for (let buckets of bucketsArrays) {
|
|
90
72
|
if (!buckets) continue;
|
|
91
73
|
for (let bucket of buckets) {
|
|
92
74
|
if (bucket.time < windowStart) continue;
|
|
93
|
-
let
|
|
94
|
-
|
|
95
|
-
let cell = perPixel[x];
|
|
96
|
-
let val = bucket.deltas[selectedField] || 0;
|
|
97
|
-
cell.perNode[i] += val;
|
|
98
|
-
cell.total += val;
|
|
99
|
-
if (cell.total > maxTotal) maxTotal = cell.total;
|
|
75
|
+
let v = bucket.deltas[selectedField] || 0;
|
|
76
|
+
sumByTime.set(bucket.time, (sumByTime.get(bucket.time) || 0) + v);
|
|
100
77
|
}
|
|
101
78
|
}
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
if (
|
|
107
|
-
let y = height;
|
|
108
|
-
for (let i = 0; i < nodeIds.length; i++) {
|
|
109
|
-
let val = cell.perNode[i];
|
|
110
|
-
if (!val) continue;
|
|
111
|
-
let segHeight = (val / maxTotal) * height;
|
|
112
|
-
ctx.fillStyle = colorForNode(i);
|
|
113
|
-
ctx.fillRect(x, y - segHeight, 1, segHeight);
|
|
114
|
-
y -= segHeight;
|
|
115
|
-
}
|
|
79
|
+
let times = Array.from(sumByTime.keys()).sort((a, b) => a - b);
|
|
80
|
+
let rows: ChartRow[] = times.map(t => ({ time: t, value: sumByTime.get(t)! }));
|
|
81
|
+
let peak = 0;
|
|
82
|
+
for (let r of rows) {
|
|
83
|
+
if (r.value > peak) peak = r.value;
|
|
116
84
|
}
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
}, 5);
|
|
85
|
+
return { rows, peak };
|
|
86
|
+
}
|
|
120
87
|
|
|
121
88
|
export class GrossStatsPage extends qreact.Component {
|
|
122
89
|
refreshTimer: ReturnType<typeof setInterval> | undefined;
|
|
@@ -137,7 +104,7 @@ export class GrossStatsPage extends qreact.Component {
|
|
|
137
104
|
<span>Range:</span>
|
|
138
105
|
{TIME_RANGES.map(r =>
|
|
139
106
|
<button
|
|
140
|
-
className={css.pad2(8, 4) + (state.rangeMs === r.ms ? " " + css.hsl(210, 70, 60).hslcolor(0, 0, 100).
|
|
107
|
+
className={css.pad2(8, 4) + (state.rangeMs === r.ms ? " " + css.hsl(210, 70, 60).hslcolor(0, 0, 100).boldStyle : "")}
|
|
141
108
|
onClick={() => Querysub.commit(() => { state.rangeMs = r.ms; })}
|
|
142
109
|
>{r.label}</button>
|
|
143
110
|
)}
|
|
@@ -146,7 +113,7 @@ export class GrossStatsPage extends qreact.Component {
|
|
|
146
113
|
<span>Field:</span>
|
|
147
114
|
{GROSS_STATS_FIELDS.map(f =>
|
|
148
115
|
<button
|
|
149
|
-
className={css.pad2(8, 4) + (state.selectedField === f ? " " + css.hsl(140, 60, 50).hslcolor(0, 0, 100).
|
|
116
|
+
className={css.pad2(8, 4) + (state.selectedField === f ? " " + css.hsl(140, 60, 50).hslcolor(0, 0, 100).boldStyle : "")}
|
|
150
117
|
onClick={() => Querysub.commit(() => { state.selectedField = f; })}
|
|
151
118
|
>{f}</button>
|
|
152
119
|
)}
|
|
@@ -254,44 +221,42 @@ export class GrossStatsPage extends qreact.Component {
|
|
|
254
221
|
let bucketsArrays: GrossStatsBucket[][] = selectedNodeIds.map(n => bucketsByNode.get(n) ?? []);
|
|
255
222
|
let anyLoading = !result;
|
|
256
223
|
|
|
257
|
-
let
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
224
|
+
let { rows, peak } = buildChartRows(state.selectedField, state.rangeMs, bucketsArrays);
|
|
225
|
+
|
|
226
|
+
let chartData: ChartData<ChartRow> = {
|
|
227
|
+
rows,
|
|
228
|
+
columns: {
|
|
229
|
+
time: {
|
|
230
|
+
title: "Time",
|
|
231
|
+
isDateType: true,
|
|
232
|
+
formatter: (v) => formatDateTime(Number(v)),
|
|
233
|
+
},
|
|
234
|
+
value: {
|
|
235
|
+
title: state.selectedField,
|
|
236
|
+
formatter: (v) => formatNumber(Number(v)),
|
|
237
|
+
},
|
|
238
|
+
},
|
|
239
|
+
};
|
|
267
240
|
|
|
268
241
|
return <div className={css.vbox(8).pad2(8).fillWidth}>
|
|
269
242
|
<h2>Cluster Stats</h2>
|
|
270
243
|
{this.renderControls(allNodeIds)}
|
|
271
|
-
<div className={css.vbox(4)}>
|
|
272
|
-
<div className={css.hbox(8).
|
|
273
|
-
<span>peak: {formatNumber(
|
|
244
|
+
<div className={css.vbox(4).fillWidth}>
|
|
245
|
+
<div className={css.hbox(8).boldStyle}>
|
|
246
|
+
<span>peak: {formatNumber(peak)} {state.selectedField} / minute</span>
|
|
274
247
|
</div>
|
|
275
|
-
<
|
|
276
|
-
|
|
277
|
-
width={CHART_WIDTH}
|
|
278
|
-
height={CHART_HEIGHT}
|
|
279
|
-
className={css.hsl(0, 0, 100)}
|
|
248
|
+
<div
|
|
249
|
+
className={css.fillWidth.height(CHART_HEIGHT).hsl(0, 0, 100)}
|
|
280
250
|
style={anyLoading ? { opacity: 0.5 } : undefined}
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
251
|
+
>
|
|
252
|
+
<ChartBar<ChartRow>
|
|
253
|
+
data={chartData}
|
|
254
|
+
xAxis="time"
|
|
255
|
+
yAxis="value"
|
|
256
|
+
/>
|
|
286
257
|
</div>
|
|
287
258
|
</div>
|
|
288
259
|
{this.renderTable(selectedNodeIds, bucketsArrays)}
|
|
289
260
|
</div>;
|
|
290
261
|
}
|
|
291
262
|
}
|
|
292
|
-
|
|
293
|
-
function formatLocalTime(ms: number): string {
|
|
294
|
-
let d = new Date(ms);
|
|
295
|
-
let pad = (n: number) => String(n).padStart(2, "0");
|
|
296
|
-
return `${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())} ${pad(d.getHours())}:${pad(d.getMinutes())}`;
|
|
297
|
-
}
|
|
@@ -300,9 +300,6 @@ export class BufferIndex {
|
|
|
300
300
|
dataReader: Reader;
|
|
301
301
|
params: SearchParams;
|
|
302
302
|
keepIterating: () => boolean;
|
|
303
|
-
// Returns true iff the caller actually retained the value. We use that
|
|
304
|
-
// to drive the per-file matchCount cap below: see the note at the
|
|
305
|
-
// `matchesPattern(buffer)` call for why we can't blindly count emits.
|
|
306
303
|
onResult: (match: Buffer) => boolean;
|
|
307
304
|
results: IndexedLogResults;
|
|
308
305
|
allSearchUnits: Unit[][];
|
|
@@ -331,7 +328,20 @@ export class BufferIndex {
|
|
|
331
328
|
}, `BufferIndex|readLocalBlocks`);
|
|
332
329
|
});
|
|
333
330
|
|
|
334
|
-
|
|
331
|
+
// NOTE: The per-file matchCount cap (commented out below in both the
|
|
332
|
+
// block loop and the inner buffer loop) is intentionally disabled.
|
|
333
|
+
// We tested (see test.ts) and confirmed that blocks within a file
|
|
334
|
+
// aren't time-ordered — the move-to-public pipeline can leave a
|
|
335
|
+
// late-index block holding earlier-time entries than earlier-index
|
|
336
|
+
// blocks, and buffers within a single block aren't time-ordered
|
|
337
|
+
// either. The old `matchCount >= params.limit` short-circuit assumed
|
|
338
|
+
// ordering and was silently dropping blocks whose entries would have
|
|
339
|
+
// survived the top-K trim (broad queries lost their earliest
|
|
340
|
+
// matches). Without the cap we scan every candidate block per file,
|
|
341
|
+
// but the index pre-filter bounds the work and it isn't measurably
|
|
342
|
+
// slower in practice. If blocks/buffers ever get written in
|
|
343
|
+
// guaranteed time order this code can be re-enabled.
|
|
344
|
+
// let matchCount = 0;
|
|
335
345
|
let blockSearchTimeStart = Date.now();
|
|
336
346
|
|
|
337
347
|
results.totalBlockCount += indexEntries.length;
|
|
@@ -344,7 +354,8 @@ export class BufferIndex {
|
|
|
344
354
|
const step = iterateForward ? 1 : -1;
|
|
345
355
|
|
|
346
356
|
for (let i = startIdx; iterateForward ? i < endIdx : i > endIdx; i += step) {
|
|
347
|
-
if (matchCount >= params.limit || !config.keepIterating()) break;
|
|
357
|
+
// if (matchCount >= params.limit || !config.keepIterating()) break;
|
|
358
|
+
if (!config.keepIterating()) break;
|
|
348
359
|
await config.results.limitGroup?.wait();
|
|
349
360
|
const blockIndex = i;
|
|
350
361
|
|
|
@@ -393,38 +404,16 @@ export class BufferIndex {
|
|
|
393
404
|
const bufferStep = iterateForward ? 1 : -1;
|
|
394
405
|
|
|
395
406
|
for (let bufferIndex = bufferStartIdx; iterateForward ? bufferIndex < bufferEndIdx : bufferIndex > bufferEndIdx; bufferIndex += bufferStep) {
|
|
396
|
-
//
|
|
397
|
-
//
|
|
398
|
-
//
|
|
399
|
-
// inside them are not), so stopping mid-block on a match
|
|
400
|
-
// count would drop earlier-time buffers we haven't reached
|
|
401
|
-
// yet. The block-level cap above is the only safe stop;
|
|
402
|
-
// here we only honor cross-file `keepIterating` (which
|
|
403
|
-
// applies to the whole file at once, so it's safe at any
|
|
404
|
-
// granularity).
|
|
407
|
+
// See the note above the outer loop for why the
|
|
408
|
+
// matchCount-based stop is gone.
|
|
409
|
+
// if (matchCount >= params.limit || !config.keepIterating()) break;
|
|
405
410
|
if (!config.keepIterating()) break;
|
|
406
411
|
await config.results.limitGroup?.wait();
|
|
407
412
|
|
|
408
413
|
let buffer = buffers[bufferIndex];
|
|
409
414
|
if (matchesPattern(buffer)) {
|
|
410
|
-
|
|
411
|
-
//
|
|
412
|
-
// can reject for reasons we don't see from here — most
|
|
413
|
-
// notably time-range filtering (an entry whose time is
|
|
414
|
-
// outside the search window matched the text pattern
|
|
415
|
-
// but isn't a real hit). Counting rejected emits would
|
|
416
|
-
// let a stretch of out-of-window matches at the start
|
|
417
|
-
// of a file blow the per-file cap and short-circuit the
|
|
418
|
-
// scan before we reach the in-window region.
|
|
419
|
-
//
|
|
420
|
-
// The cost is that we keep matching and calling onResult
|
|
421
|
-
// through those out-of-window blocks (mild inefficiency).
|
|
422
|
-
// We can't skip ahead — buffers are scanned linearly and
|
|
423
|
-
// we don't know up front which entries the caller will
|
|
424
|
-
// reject — so this is the best we can do here.
|
|
425
|
-
if (config.onResult(buffer)) {
|
|
426
|
-
matchCount++;
|
|
427
|
-
}
|
|
415
|
+
config.onResult(buffer);
|
|
416
|
+
// matchCount++;
|
|
428
417
|
}
|
|
429
418
|
}
|
|
430
419
|
} catch (e: any) {
|
|
@@ -559,9 +548,7 @@ export class BufferIndex {
|
|
|
559
548
|
params: SearchParams;
|
|
560
549
|
|
|
561
550
|
keepIterating: () => boolean;
|
|
562
|
-
//
|
|
563
|
-
// per-file matchCount cap so out-of-window emits don't short-circuit
|
|
564
|
-
// the scan.
|
|
551
|
+
// Return value is unused — see `findLocal.onResult`.
|
|
565
552
|
onResult: (match: Buffer) => boolean;
|
|
566
553
|
results: IndexedLogResults;
|
|
567
554
|
}): Promise<{
|
|
@@ -505,9 +505,6 @@ export class BufferUnitIndex {
|
|
|
505
505
|
params: SearchParams;
|
|
506
506
|
allSearchUnits: Unit[][];
|
|
507
507
|
keepIterating: () => boolean;
|
|
508
|
-
// Returns true iff the caller actually retained the value. Drives the
|
|
509
|
-
// `matchCounts` cap below — see the comment at the `matchesPattern`
|
|
510
|
-
// call for why we can't blindly count emits.
|
|
511
508
|
onResult: (match: Buffer) => boolean;
|
|
512
509
|
index: Buffer;
|
|
513
510
|
reader: Reader;
|
|
@@ -534,26 +531,42 @@ export class BufferUnitIndex {
|
|
|
534
531
|
// Read blocks and search for matches
|
|
535
532
|
let blockSearchTimeStart = Date.now();
|
|
536
533
|
await measureBlock(async () => {
|
|
537
|
-
|
|
538
|
-
|
|
534
|
+
// NOTE: The matchCount / matchCounts tracking and the
|
|
535
|
+
// `stopIterating` cap below are commented out, not deleted.
|
|
536
|
+
// We tested (see test.ts) and confirmed that blocks within a
|
|
537
|
+
// file aren't actually time-ordered — the move-to-public
|
|
538
|
+
// pipeline can leave a late-index block holding earlier-time
|
|
539
|
+
// entries than earlier-index blocks. The old `relevantCount
|
|
540
|
+
// >= params.limit` short-circuit assumed time-ordered blocks
|
|
541
|
+
// and was silently skipping blocks whose entries would have
|
|
542
|
+
// survived the top-K trim (broad queries lost their earliest
|
|
543
|
+
// matches). The same applied to the inner-buffer cap (buffers
|
|
544
|
+
// within a block aren't time-ordered either). Removing both
|
|
545
|
+
// caps means we scan every candidate block per file, but the
|
|
546
|
+
// index pre-filter bounds the work and it isn't measurably
|
|
547
|
+
// slower in practice. If blocks ever get written in
|
|
548
|
+
// guaranteed time order, this code can be re-enabled.
|
|
549
|
+
// let matchCount = 0;
|
|
550
|
+
// let matchCounts = list(blockCount).fill(0);
|
|
539
551
|
|
|
540
552
|
const searchBlock = async (blockIndex: number) => {
|
|
541
553
|
if (!candidateBlocksSet.has(blockIndex)) return;
|
|
542
|
-
// Check if we should stop iterating based on match counts and direction
|
|
543
|
-
let stopIterating = () => {
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
};
|
|
556
|
-
if (stopIterating()) return;
|
|
554
|
+
// // Check if we should stop iterating based on match counts and direction
|
|
555
|
+
// let stopIterating = () => {
|
|
556
|
+
// let relevantCount = 0;
|
|
557
|
+
// if (params.searchFromStart) {
|
|
558
|
+
// for (let i = 0; i <= blockIndex; i++) {
|
|
559
|
+
// relevantCount += matchCounts[i];
|
|
560
|
+
// }
|
|
561
|
+
// } else {
|
|
562
|
+
// for (let i = blockIndex; i < blockCount; i++) {
|
|
563
|
+
// relevantCount += matchCounts[i];
|
|
564
|
+
// }
|
|
565
|
+
// }
|
|
566
|
+
// return relevantCount >= params.limit || !keepIterating();
|
|
567
|
+
// };
|
|
568
|
+
// if (stopIterating()) return;
|
|
569
|
+
if (!keepIterating()) return;
|
|
557
570
|
|
|
558
571
|
let debugOffsets = {
|
|
559
572
|
startOffset: 0,
|
|
@@ -580,38 +593,17 @@ export class BufferUnitIndex {
|
|
|
580
593
|
const step = iterateForward ? 1 : -1;
|
|
581
594
|
|
|
582
595
|
for (let i = startIdx; iterateForward ? i < endIdx : i > endIdx; i += step) {
|
|
583
|
-
//
|
|
584
|
-
//
|
|
585
|
-
//
|
|
586
|
-
// inside them are not), so a mid-block stop on
|
|
587
|
-
// `relevantCount >= limit` would drop earlier-time
|
|
588
|
-
// buffers we haven't reached yet. Block-level
|
|
589
|
-
// `stopIterating` is the safe granularity; here we
|
|
590
|
-
// only honor cross-file `keepIterating`, which applies
|
|
591
|
-
// to the whole file at once.
|
|
596
|
+
// See the note at the top of this function for why
|
|
597
|
+
// the matchCount-based stop is gone (commented out).
|
|
598
|
+
// if (stopIterating()) break;
|
|
592
599
|
if (!keepIterating()) break;
|
|
593
600
|
await results.limitGroup?.wait();
|
|
594
601
|
|
|
595
602
|
const buffer = await this.getBufferFromBlock(blockReader, i);
|
|
596
603
|
if (matchesPattern(buffer)) {
|
|
597
|
-
|
|
598
|
-
//
|
|
599
|
-
//
|
|
600
|
-
// for reasons opaque to us — most notably
|
|
601
|
-
// time-range filtering. Counting rejected emits
|
|
602
|
-
// would let a stretch of out-of-window matches at
|
|
603
|
-
// the start of a file blow the per-file cap and
|
|
604
|
-
// short-circuit the scan before we reach the
|
|
605
|
-
// in-window region. The cost is that we keep
|
|
606
|
-
// matching and calling onResult through those
|
|
607
|
-
// out-of-window blocks (mild inefficiency); we
|
|
608
|
-
// can't skip ahead because buffers are scanned
|
|
609
|
-
// linearly and we don't know up front which
|
|
610
|
-
// entries the caller will reject.
|
|
611
|
-
if (config.onResult(buffer)) {
|
|
612
|
-
matchCount++;
|
|
613
|
-
matchCounts[blockIndex]++;
|
|
614
|
-
}
|
|
604
|
+
config.onResult(buffer);
|
|
605
|
+
// matchCount++;
|
|
606
|
+
// matchCounts[blockIndex]++;
|
|
615
607
|
}
|
|
616
608
|
}
|
|
617
609
|
} catch (e: any) {
|
|
@@ -286,11 +286,11 @@ export class IndexedLogs<T> {
|
|
|
286
286
|
let added = false;
|
|
287
287
|
const tryAdd = async (nodeId: string, preferredOnly = false) => {
|
|
288
288
|
if (added) return;
|
|
289
|
-
let hasLogger = await timeoutToUndefinedSilent(
|
|
289
|
+
let hasLogger = await timeoutToUndefinedSilent(10_000, IndexedLogShimController.nodes[nodeId].hasLogger(this.config.name));
|
|
290
290
|
if (!hasLogger) return false;
|
|
291
291
|
// NOTE: Prefer to do the searching on the move logs service. However, if it's not available, any service can do searching. It just might lag that server...
|
|
292
292
|
if (preferredOnly) {
|
|
293
|
-
let metadata = await timeoutToUndefinedSilent(
|
|
293
|
+
let metadata = await timeoutToUndefinedSilent(10_000, NodeCapabilitiesController.nodes[nodeId].getMetadata());
|
|
294
294
|
if (!metadata?.entryPoint.includes("movelogs")) return false;
|
|
295
295
|
}
|
|
296
296
|
added = true;
|
|
@@ -400,13 +400,13 @@ export class IndexedLogs<T> {
|
|
|
400
400
|
onResult: (match: T) => void;
|
|
401
401
|
onResults?: (results: IndexedLogResults) => Promise<boolean>;
|
|
402
402
|
}): Promise<IndexedLogResults> {
|
|
403
|
-
|
|
404
403
|
let { params } = config;
|
|
405
404
|
if (params.pathOverrides && config.params.only === "public") {
|
|
406
405
|
// Fine, if they provided path overrides, and they are all public, just read them, as they'll resolve the same no matter where we read from
|
|
407
406
|
} else if (config.params.forceReadProduction && !isPublic()) {
|
|
408
407
|
let machineNodes = await this.getMachineNodes();
|
|
409
408
|
if (machineNodes.length === 0) throw new Error(`Cannot find any public nodes to read from`);
|
|
409
|
+
console.log(`Picking machine node ${machineNodes[0]} to read from`);
|
|
410
410
|
return await this.clientFind({
|
|
411
411
|
...config,
|
|
412
412
|
nodeId: machineNodes[0],
|
|
@@ -157,6 +157,7 @@ export class MCPIndexedLogs {
|
|
|
157
157
|
direction: Direction;
|
|
158
158
|
columns: string[];
|
|
159
159
|
limit?: number;
|
|
160
|
+
forceRefresh?: boolean;
|
|
160
161
|
}): Promise<SearchResult> {
|
|
161
162
|
let limit = config.limit ?? 100;
|
|
162
163
|
let startTime = normalizeTime(config.startTime, "startTime");
|
|
@@ -176,7 +177,7 @@ export class MCPIndexedLogs {
|
|
|
176
177
|
let machineId = config.machine === "local" ? getOwnMachineId() : config.machine;
|
|
177
178
|
|
|
178
179
|
let moveStart = Date.now();
|
|
179
|
-
let moveOutcome = await this.ensureMovedThrough(machineId, endTime);
|
|
180
|
+
let moveOutcome = await this.ensureMovedThrough(machineId, endTime, config.forceRefresh);
|
|
180
181
|
console.log(`[search] ensureMovedThrough ${moveOutcome} in ${formatTime(Date.now() - moveStart)}`);
|
|
181
182
|
|
|
182
183
|
let loggers = await getLoggers2Async();
|
|
@@ -203,6 +204,7 @@ export class MCPIndexedLogs {
|
|
|
203
204
|
loggerName,
|
|
204
205
|
startTime,
|
|
205
206
|
endTime,
|
|
207
|
+
forceRefresh: config.forceRefresh,
|
|
206
208
|
});
|
|
207
209
|
totalPathsSeen += paths.length;
|
|
208
210
|
|
|
@@ -479,9 +481,9 @@ export class MCPIndexedLogs {
|
|
|
479
481
|
// (e.g. older versions still running). Records moved-through up to
|
|
480
482
|
// now - MOVE_GRACE so we skip this on subsequent calls covering the same
|
|
481
483
|
// window.
|
|
482
|
-
private async ensureMovedThrough(machineId: string, endTime: number): Promise<"cached" | "no-node" | "moved"> {
|
|
484
|
+
private async ensureMovedThrough(machineId: string, endTime: number, forceRefresh?: boolean): Promise<"cached" | "no-node" | "moved"> {
|
|
483
485
|
let lastMoved = this.movedThroughByMachine.get(machineId) ?? 0;
|
|
484
|
-
if (lastMoved >= endTime) return "cached";
|
|
486
|
+
if (!forceRefresh && lastMoved >= endTime) return "cached";
|
|
485
487
|
|
|
486
488
|
let nodeIds = await this.findRemoteNodesOnMachine(machineId);
|
|
487
489
|
if (nodeIds.length === 0) {
|
|
@@ -493,6 +495,14 @@ export class MCPIndexedLogs {
|
|
|
493
495
|
let answered = false;
|
|
494
496
|
for (let nodeId of nodeIds) {
|
|
495
497
|
try {
|
|
498
|
+
if (forceRefresh) {
|
|
499
|
+
console.log(`MCPIndexedLogs: forceRefresh — flushing ${loggerName} on ${nodeId} unconditionally`);
|
|
500
|
+
await IndexedLogShimController.nodes[nodeId].forceMoveLogsToPublic({
|
|
501
|
+
indexedLogsName: loggerName,
|
|
502
|
+
});
|
|
503
|
+
answered = true;
|
|
504
|
+
break;
|
|
505
|
+
}
|
|
496
506
|
let hasPending = await timeoutToUndefinedSilent(
|
|
497
507
|
5000,
|
|
498
508
|
IndexedLogShimController.nodes[nodeId].hasPendingInRange({
|
|
@@ -564,6 +574,7 @@ export class MCPIndexedLogs {
|
|
|
564
574
|
loggerName: LoggerName;
|
|
565
575
|
startTime: number;
|
|
566
576
|
endTime: number;
|
|
577
|
+
forceRefresh?: boolean;
|
|
567
578
|
}): Promise<TimeFilePath[]> {
|
|
568
579
|
let bucketStart = Math.floor(config.startTime / timeInHour) * timeInHour;
|
|
569
580
|
let bucketEnd = Math.ceil(config.endTime / timeInHour) * timeInHour;
|
|
@@ -575,6 +586,10 @@ export class MCPIndexedLogs {
|
|
|
575
586
|
if (now - v.time > PATHS_CACHE_TTL) this.pathsCache.delete(k);
|
|
576
587
|
}
|
|
577
588
|
|
|
589
|
+
if (config.forceRefresh) {
|
|
590
|
+
this.pathsCache.delete(key);
|
|
591
|
+
}
|
|
592
|
+
|
|
578
593
|
let cached = this.pathsCache.get(key);
|
|
579
594
|
if (cached && now - cached.time <= PATHS_CACHE_TTL) {
|
|
580
595
|
return cached.paths;
|
|
@@ -56,6 +56,7 @@ Note: each segment between operators ideally has at least 4 contiguous character
|
|
|
56
56
|
columns: { type: "array", items: { type: "string" }, description: "Which fields to project onto each row. Use [] to get just metadata; use allColumns from a prior result to pick more." },
|
|
57
57
|
limit: { type: "number", default: 100 },
|
|
58
58
|
logTypes: { type: "string", description: "Optional pipe-separated list restricting which log streams to scan. Allowed values: log, info, warn, error. Examples: \"warn|error\", \"log\". Omit (default) to search all four." },
|
|
59
|
+
forceRefresh: { type: "boolean", description: "If true, bypass the path-cache (TimeFileTree.findAllPaths) and re-walk the archive folders. Use this when recent log files appear missing because the cache is stale." },
|
|
59
60
|
},
|
|
60
61
|
required: ["query", "machine", "startTime", "endTime", "direction", "columns"],
|
|
61
62
|
},
|