querysub 0.455.0 → 0.457.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 +1 -1
- package/src/-a-archives/archivesBackBlaze.ts +17 -14
- package/src/-f-node-discovery/NodeDiscovery.ts +40 -7
- package/src/-h-path-value-serialize/PathValueSerializer.ts +17 -15
- package/src/0-path-value-core/pathValueCore.ts +1 -1
- package/src/5-diagnostics/Table.tsx +97 -67
- package/src/diagnostics/logs/IndexedLogs/IndexedLogs.ts +11 -1
- package/src/diagnostics/logs/IndexedLogs/LogViewer3.tsx +20 -20
- package/src/diagnostics/logs/IndexedLogs/MCPIndexedLogs.ts +49 -69
package/package.json
CHANGED
|
@@ -516,8 +516,9 @@ export class ArchivesBackblaze {
|
|
|
516
516
|
fnc: (api: B2Api) => Promise<T>,
|
|
517
517
|
retries = 3
|
|
518
518
|
): Promise<T> {
|
|
519
|
-
let api
|
|
519
|
+
let api: B2Api | undefined;
|
|
520
520
|
try {
|
|
521
|
+
api = await this.getBucketAPI();
|
|
521
522
|
return await fnc(api);
|
|
522
523
|
} catch (err: any) {
|
|
523
524
|
if (retries <= 0) throw err;
|
|
@@ -570,23 +571,25 @@ export class ArchivesBackblaze {
|
|
|
570
571
|
}
|
|
571
572
|
|
|
572
573
|
if (err.stack.includes(`getaddrinfo ENOTFOUND`)) {
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
574
|
+
if (api) {
|
|
575
|
+
let urlObj = new URL(api.apiUrl);
|
|
576
|
+
let hostname = urlObj.hostname;
|
|
577
|
+
let lookupAddresses = await new Promise(resolve => {
|
|
578
|
+
dns.lookup(hostname, (err, addresses) => {
|
|
579
|
+
resolve(addresses);
|
|
580
|
+
});
|
|
578
581
|
});
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
582
|
+
let resolveAddresses = await new Promise(resolve => {
|
|
583
|
+
dns.resolve4(hostname, (err, addresses) => {
|
|
584
|
+
resolve(addresses);
|
|
585
|
+
});
|
|
583
586
|
});
|
|
584
|
-
|
|
585
|
-
|
|
587
|
+
console.error(`[${context}] getaddrinfo ENOTFOUND ${hostname}`, { lookupAddresses, resolveAddresses, apiUrl: api.apiUrl, fullError: err.stack });
|
|
588
|
+
}
|
|
586
589
|
}
|
|
587
590
|
|
|
588
|
-
//
|
|
589
|
-
|
|
591
|
+
// NOTE: The AI thought case that happens when we run out of retries, that's stupid. This obviously isn't the case. This is the case when it's a normal error, as in the file doesn't exist, we need to throw. We absolutely should not warn here. Warning here wouldn't be anything. It would just be saying, oh, we checked if a file and it didn't, which is normal, which is why we check if a file exists.
|
|
592
|
+
|
|
590
593
|
throw err;
|
|
591
594
|
}
|
|
592
595
|
}
|
|
@@ -418,16 +418,27 @@ async function fastMemorySync() {
|
|
|
418
418
|
}
|
|
419
419
|
}
|
|
420
420
|
let checkNodes = shuffle(Array.from(aliveNodes), Date.now()).slice(0, API_AUDIT_COUNT);
|
|
421
|
-
let
|
|
422
|
-
checkNodes.map(async
|
|
423
|
-
let nodes = await timeoutToUndefinedSilent(200, NodeDiscoveryController.nodes[
|
|
421
|
+
let perPeerNodes = await Promise.all(
|
|
422
|
+
checkNodes.map(async peerNodeId => {
|
|
423
|
+
let nodes = await timeoutToUndefinedSilent(200, NodeDiscoveryController.nodes[peerNodeId].getAllNodeIds());
|
|
424
424
|
if (!nodes) {
|
|
425
|
-
deadNodes.set(
|
|
425
|
+
deadNodes.set(peerNodeId, Date.now());
|
|
426
426
|
}
|
|
427
|
-
return nodes || [];
|
|
427
|
+
return { peerNodeId, nodes: nodes || [] };
|
|
428
428
|
})
|
|
429
429
|
);
|
|
430
|
-
let
|
|
430
|
+
let reportedBy = new Map<string, string[]>();
|
|
431
|
+
for (let { peerNodeId, nodes } of perPeerNodes) {
|
|
432
|
+
for (let nodeId of nodes) {
|
|
433
|
+
let reporters = reportedBy.get(nodeId);
|
|
434
|
+
if (!reporters) {
|
|
435
|
+
reporters = [];
|
|
436
|
+
reportedBy.set(nodeId, reporters);
|
|
437
|
+
}
|
|
438
|
+
reporters.push(peerNodeId);
|
|
439
|
+
}
|
|
440
|
+
}
|
|
441
|
+
let otherNodes = Array.from(reportedBy.keys());
|
|
431
442
|
// This would log WAY too much, because we poll a lot, because we want to minimize downtime
|
|
432
443
|
//console.log(magenta(`Fast memory sync at ${formatVeryNiceDateTime(Date.now())}, nodes found ${otherNodes.length}`), getDebuggerUrl());
|
|
433
444
|
|
|
@@ -436,8 +447,30 @@ async function fastMemorySync() {
|
|
|
436
447
|
// sync now.
|
|
437
448
|
let missingNodes = otherNodes.filter(nodeId => !allNodeIds2.has(nodeId));
|
|
438
449
|
if (missingNodes.length > 0) {
|
|
439
|
-
|
|
450
|
+
let missingNodeReporters = Object.fromEntries(
|
|
451
|
+
missingNodes.map(nodeId => [nodeId, reportedBy.get(nodeId) ?? []])
|
|
452
|
+
);
|
|
453
|
+
console.log(yellow(`Node list is missing nodes, resyncing node`), { missingNodes, missingNodeReporters, otherNodes });
|
|
440
454
|
await syncArchives();
|
|
455
|
+
|
|
456
|
+
// If after syncing our own archive view those nodes are STILL missing, then they are
|
|
457
|
+
// ghosts in the peer's in-memory allNodeIds2 — push a targeted resync to each peer that
|
|
458
|
+
// told us about a ghost, so they can drop it from their own allNodeIds2.
|
|
459
|
+
let stillMissing = missingNodes.filter(nodeId => !allNodeIds2.has(nodeId));
|
|
460
|
+
if (stillMissing.length > 0) {
|
|
461
|
+
let peersToResync = new Set<string>();
|
|
462
|
+
for (let nodeId of stillMissing) {
|
|
463
|
+
for (let peerNodeId of reportedBy.get(nodeId) ?? []) {
|
|
464
|
+
if (isOwnNodeId(peerNodeId)) continue;
|
|
465
|
+
peersToResync.add(peerNodeId);
|
|
466
|
+
}
|
|
467
|
+
}
|
|
468
|
+
let reason = `ghost-resync: peer reported nodes not in our archive after sync: ${stillMissing.join("|")}`;
|
|
469
|
+
console.log(yellow(`Pushing targeted resync to ${peersToResync.size} peers`), { stillMissing, peers: Array.from(peersToResync) });
|
|
470
|
+
for (let peerNodeId of peersToResync) {
|
|
471
|
+
ignoreErrors(NodeDiscoveryController.nodes[peerNodeId].resyncNodes(reason));
|
|
472
|
+
}
|
|
473
|
+
}
|
|
441
474
|
}
|
|
442
475
|
}
|
|
443
476
|
|
|
@@ -997,23 +997,23 @@ class PathValueSerializer {
|
|
|
997
997
|
public getPathValue(pathValue: PathValue | undefined, noMutate?: "noMutate"): unknown {
|
|
998
998
|
if (!pathValue) return undefined;
|
|
999
999
|
if (pathValue.isValueLazy) {
|
|
1000
|
-
// NOTE: If this throws, it likely means you used atomicObjectWrite.
|
|
1001
|
-
// Use atomicObjectWriteNoFreeze or doAtomicWrites instead.
|
|
1002
|
-
if (!noMutate) {
|
|
1003
|
-
try {
|
|
1004
|
-
pathValue.isValueLazy = false;
|
|
1005
|
-
} catch { }
|
|
1006
|
-
}
|
|
1007
1000
|
let buffer = this.lazyValues.get(pathValue.value as {});
|
|
1008
1001
|
if (!buffer) {
|
|
1009
|
-
|
|
1010
|
-
return pathValue.value;
|
|
1002
|
+
throw new Error(`Expected lazy value to have a buffer, but it didn't. Lazy ref has a typeof ${typeof pathValue.value} (${String(pathValue.value)})`);
|
|
1011
1003
|
}
|
|
1012
1004
|
let newValue = recursiveFreeze(cbor.decode(buffer));
|
|
1013
|
-
if (!noMutate) {
|
|
1005
|
+
if (!noMutate && !pathValue.isValueLazy) {
|
|
1006
|
+
// NOTE: If this throws, it likely means you used atomicObjectWrite.
|
|
1007
|
+
// Use atomicObjectWriteNoFreeze or doAtomicWrites instead.
|
|
1014
1008
|
try {
|
|
1009
|
+
pathValue.isValueLazy = false;
|
|
1015
1010
|
pathValue.value = newValue;
|
|
1016
|
-
} catch {
|
|
1011
|
+
} catch {
|
|
1012
|
+
// In theory, it could be that you can set the is value lazy property but not the value property. In which case we want to reset the isValazy property back. Otherwise, we will no longer try to deserialize it, and we will emit the lazy placeholder instead of the actual value next time.
|
|
1013
|
+
try {
|
|
1014
|
+
pathValue.isValueLazy = true;
|
|
1015
|
+
} catch { }
|
|
1016
|
+
}
|
|
1017
1017
|
}
|
|
1018
1018
|
return newValue;
|
|
1019
1019
|
}
|
|
@@ -1027,8 +1027,7 @@ class PathValueSerializer {
|
|
|
1027
1027
|
// NOTE: Did you pass a raw PathValue and then try to use PathValueSerializer with it?
|
|
1028
1028
|
// - Instead you should pass a buffer serialized with pathValueSerializer.serialize and
|
|
1029
1029
|
// deserialized with pathValueSerializer.deserialize.
|
|
1030
|
-
|
|
1031
|
-
return pathValue.value;
|
|
1030
|
+
throw new Error(`Expected lazy value to have a buffer, but it didn't. Lazy ref has a typeof ${typeof pathValue.value} (${String(pathValue.value)})`);
|
|
1032
1031
|
}
|
|
1033
1032
|
return buffer;
|
|
1034
1033
|
}
|
|
@@ -1036,8 +1035,11 @@ class PathValueSerializer {
|
|
|
1036
1035
|
}
|
|
1037
1036
|
|
|
1038
1037
|
private getBuffer(pathValue: PathValue): Buffer {
|
|
1039
|
-
|
|
1040
|
-
|
|
1038
|
+
if (pathValue.isValueLazy) {
|
|
1039
|
+
let buffer = this.lazyValues.get(pathValue.value as {});
|
|
1040
|
+
if (!buffer) throw new Error(`Expected lazy value to have a buffer, but it didn't. Lazy ref has a typeof ${typeof pathValue.value} (${String(pathValue.value)})`);
|
|
1041
|
+
return buffer;
|
|
1042
|
+
}
|
|
1041
1043
|
return cborEncoder().encode(pathValue.value);
|
|
1042
1044
|
}
|
|
1043
1045
|
public compareValuePaths(a: PathValue | undefined, b: PathValue | undefined) {
|
|
@@ -746,7 +746,7 @@ class AuthorityPathValueStorage {
|
|
|
746
746
|
|
|
747
747
|
if (isDebugLogEnabled() && !config?.doNotArchive) {
|
|
748
748
|
for (let value of newValues) {
|
|
749
|
-
auditLog("INGEST VALUE", { path: value.path, timeId: value.time.time });
|
|
749
|
+
auditLog("INGEST VALUE", { path: value.path, timeId: value.time.time, batchTime: now });
|
|
750
750
|
}
|
|
751
751
|
}
|
|
752
752
|
|
|
@@ -5,6 +5,7 @@ import preact from "preact";
|
|
|
5
5
|
import { showModal } from "./Modal";
|
|
6
6
|
import { FullscreenModal } from "./FullscreenModal";
|
|
7
7
|
import { canHaveChildren } from "socket-function/src/types";
|
|
8
|
+
import { measureBlock } from "socket-function/src/profiling/measure";
|
|
8
9
|
|
|
9
10
|
// Undefined means we infer the column
|
|
10
11
|
// Null means the column is removed
|
|
@@ -24,6 +25,13 @@ export type TableType<RowT extends RowType = RowType> = {
|
|
|
24
25
|
|
|
25
26
|
module.hotreload = true;
|
|
26
27
|
|
|
28
|
+
const SHALLOW_CACHE_MAX_SIZE = 100_000;
|
|
29
|
+
type ShallowCacheEntry = {
|
|
30
|
+
columns: ColumnsType;
|
|
31
|
+
cells: preact.ComponentChild[];
|
|
32
|
+
};
|
|
33
|
+
const shallowCacheMap = new Map<RowType, ShallowCacheEntry>();
|
|
34
|
+
|
|
27
35
|
export class Table<RowT extends RowType> extends qreact.Component<TableType<RowT> & {
|
|
28
36
|
class?: string;
|
|
29
37
|
cellClass?: string;
|
|
@@ -34,6 +42,10 @@ export class Table<RowT extends RowType> extends qreact.Component<TableType<RowT
|
|
|
34
42
|
characterLimit?: number;
|
|
35
43
|
dark?: boolean;
|
|
36
44
|
|
|
45
|
+
// If true, cache the rendered cells per row (keyed by row reference + columns reference).
|
|
46
|
+
// Resets the cache wholesale once it exceeds SHALLOW_CACHE_MAX_SIZE.
|
|
47
|
+
shallowCache?: boolean;
|
|
48
|
+
|
|
37
49
|
getRowAttributes?: (row: RowT, index: number) => preact.JSX.HTMLAttributes<HTMLTableRowElement>;
|
|
38
50
|
}> {
|
|
39
51
|
state = {
|
|
@@ -60,75 +72,93 @@ export class Table<RowT extends RowType> extends qreact.Component<TableType<RowT
|
|
|
60
72
|
<th class={css.pad2(8, 4) + cellClass}>{column?.title || toSpaceCase(columnName)}</th>
|
|
61
73
|
)}
|
|
62
74
|
</tr>
|
|
63
|
-
{rows.map((row, index) =>
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
75
|
+
{rows.map((row, index) => {
|
|
76
|
+
const renderCells = () => Object.entries(columns).filter(x => x[1] !== null).map(([columnName, column]) => {
|
|
77
|
+
let value = row[columnName];
|
|
78
|
+
let formatter = column?.formatter || "guess";
|
|
79
|
+
let result = measureBlock(() => formatValue(value, formatter, { row, columnName }), `formatValue|${columnName}`);
|
|
80
|
+
let renderedObj = renderTrimmed({
|
|
81
|
+
content: result,
|
|
82
|
+
lineLimit,
|
|
83
|
+
characterLimit,
|
|
84
|
+
});
|
|
85
|
+
let attributes = { ...renderedObj.outerAttributes };
|
|
86
|
+
attributes.class = attributes.class || "";
|
|
87
|
+
attributes.class += " " + css.whiteSpace("pre-wrap").pad2(8, 4);
|
|
88
|
+
attributes.class += cellClass;
|
|
89
|
+
// If the inner content looks like a VNode, take it's attributes and unwrap it,
|
|
90
|
+
// so it can fill the entire cell.
|
|
91
|
+
let innerContent = renderedObj.innerContent;
|
|
92
|
+
if (
|
|
93
|
+
canHaveChildren(innerContent) && "props" in innerContent
|
|
94
|
+
&& canHaveChildren(innerContent.props)
|
|
95
|
+
// Table can't show gap, so don;t collapse if it has gap.
|
|
96
|
+
&& !((innerContent.props?.class as string + " " + innerContent.props?.className as string).includes("gap"))
|
|
97
|
+
// NOTE: I'm not sure why we were only elevating the children if we only had 1 child?
|
|
98
|
+
// Changing this will break things, but it should be better overall.
|
|
99
|
+
// && "children" in innerContent.props
|
|
100
|
+
// && (
|
|
101
|
+
// Array.isArray(innerContent.props.children) && innerContent.props.children.length === 1
|
|
102
|
+
// || !Array.isArray(innerContent.props.children)
|
|
103
|
+
// )
|
|
104
|
+
// AND, it is a div (a tags shouldn't be unwrapped)
|
|
105
|
+
&& (innerContent.type === "div")
|
|
106
|
+
) {
|
|
107
|
+
attributes.class += " " + (innerContent.props.class || "");
|
|
108
|
+
attributes.class += " " + (innerContent.props.className || "");
|
|
109
|
+
let baseOnClick = attributes.onClick;
|
|
110
|
+
let props = innerContent.props;
|
|
111
|
+
attributes.onClick = (e) => {
|
|
112
|
+
if (baseOnClick) baseOnClick(e);
|
|
113
|
+
(props as any).onClick?.(e);
|
|
114
|
+
};
|
|
115
|
+
for (let key in props) {
|
|
116
|
+
if (key === "class") continue;
|
|
117
|
+
if (key === "onClick") continue;
|
|
118
|
+
(attributes as any)[key] = props[key];
|
|
119
|
+
}
|
|
120
|
+
innerContent = props.children as any;
|
|
71
121
|
}
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
//
|
|
101
|
-
//
|
|
102
|
-
// )
|
|
103
|
-
|
|
104
|
-
&& (innerContent.type === "div")
|
|
105
|
-
) {
|
|
106
|
-
attributes.class += " " + (innerContent.props.class || "");
|
|
107
|
-
attributes.class += " " + (innerContent.props.className || "");
|
|
108
|
-
let baseOnClick = attributes.onClick;
|
|
109
|
-
let props = innerContent.props;
|
|
110
|
-
attributes.onClick = (e) => {
|
|
111
|
-
if (baseOnClick) baseOnClick(e);
|
|
112
|
-
(props as any).onClick?.(e);
|
|
113
|
-
};
|
|
114
|
-
for (let key in props) {
|
|
115
|
-
if (key === "class") continue;
|
|
116
|
-
if (key === "onClick") continue;
|
|
117
|
-
(attributes as any)[key] = props[key];
|
|
118
|
-
}
|
|
119
|
-
innerContent = props.children as any;
|
|
122
|
+
return <td
|
|
123
|
+
{...attributes}
|
|
124
|
+
data-column={columnName}
|
|
125
|
+
// If it ever becomes non-table cell, it breaks the table
|
|
126
|
+
style={{ display: "table-cell" }}
|
|
127
|
+
>
|
|
128
|
+
{innerContent}
|
|
129
|
+
</td>;
|
|
130
|
+
});
|
|
131
|
+
let cells: preact.ComponentChild[];
|
|
132
|
+
if (this.props.shallowCache) {
|
|
133
|
+
const cached = shallowCacheMap.get(row);
|
|
134
|
+
if (cached && cached.columns === columns) {
|
|
135
|
+
cells = cached.cells;
|
|
136
|
+
} else {
|
|
137
|
+
if (shallowCacheMap.size >= SHALLOW_CACHE_MAX_SIZE) {
|
|
138
|
+
shallowCacheMap.clear();
|
|
139
|
+
}
|
|
140
|
+
cells = renderCells();
|
|
141
|
+
shallowCacheMap.set(row, { columns: columns as ColumnsType, cells });
|
|
142
|
+
}
|
|
143
|
+
} else {
|
|
144
|
+
cells = renderCells();
|
|
145
|
+
}
|
|
146
|
+
return (
|
|
147
|
+
<tr
|
|
148
|
+
class={
|
|
149
|
+
(index % 2 === 1 && css.hsla(0, 0, 100, 0.25) || "")
|
|
150
|
+
// NOTE: We don't set z-index, so children z-index values can be laid out above our siblings
|
|
151
|
+
// (if we set z-index it will create a new z-index order under us, and our children will never
|
|
152
|
+
// be able to above our siblings, which breaks palettes).
|
|
153
|
+
+ css.relative
|
|
120
154
|
}
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
</td>;
|
|
129
|
-
})}
|
|
130
|
-
</tr>
|
|
131
|
-
))}
|
|
155
|
+
{...this.props.getRowAttributes?.(row, index)}
|
|
156
|
+
>
|
|
157
|
+
<td class={css.center}>{index + 1}</td>
|
|
158
|
+
{cells}
|
|
159
|
+
</tr>
|
|
160
|
+
);
|
|
161
|
+
})}
|
|
132
162
|
{allRows.length > rows.length && <tr>
|
|
133
163
|
<td
|
|
134
164
|
colSpan={1 + Object.keys(columns).length}
|
|
@@ -620,7 +620,17 @@ export class IndexedLogs<T> {
|
|
|
620
620
|
dataReader,
|
|
621
621
|
params: {
|
|
622
622
|
...config.params,
|
|
623
|
-
limit
|
|
623
|
+
// Per-file cap = global `limit`, NOT `limit - matchCount`.
|
|
624
|
+
// Subtracting the running count would starve parallel
|
|
625
|
+
// files: once one file fills matchCount, every other
|
|
626
|
+
// in-flight file gets limit=0 and stops scanning, even
|
|
627
|
+
// though `isSourceRelevant` may still want their earlier
|
|
628
|
+
// matches to displace the kept top-K. Within a single
|
|
629
|
+
// file blocks are time-ordered in the scan direction,
|
|
630
|
+
// so capping at `limit` per file is safe — anything
|
|
631
|
+
// past the first `limit` matches in this file couldn't
|
|
632
|
+
// survive the top-K sort.
|
|
633
|
+
limit: config.params.limit,
|
|
624
634
|
},
|
|
625
635
|
keepIterating: () => !results.cancel && progressTracker.isSourceRelevant(path),
|
|
626
636
|
onResult: (match: Buffer) => {
|
|
@@ -38,7 +38,7 @@ import { startTimeParam, endTimeParam } from "../TimeRangeSelector";
|
|
|
38
38
|
import { formatSearchString } from "./LogViewerParams";
|
|
39
39
|
|
|
40
40
|
let excludePendingResults = new URLParam("excludePendingResults", false);
|
|
41
|
-
let limitURL = new URLParam("limit",
|
|
41
|
+
let limitURL = new URLParam("limit", 200);
|
|
42
42
|
let enableLogsURL = new URLParam("enableLogs", true);
|
|
43
43
|
let enableInfosURL = new URLParam("enableInfos", true);
|
|
44
44
|
let enableWarningsURL = new URLParam("enableWarnings", true);
|
|
@@ -46,8 +46,9 @@ let enableErrorsURL = new URLParam("enableErrors", true);
|
|
|
46
46
|
|
|
47
47
|
let savedPathsURL = new URLParam("savedPaths", "");
|
|
48
48
|
let selectedFieldsURL = new URLParam("selectedFields", {} as Record<string, boolean>);
|
|
49
|
-
|
|
50
|
-
|
|
49
|
+
|
|
50
|
+
// NOTE: Because this doesn't cache properly, it lags a lot, so we shouldn't try to use it.
|
|
51
|
+
let showLifecycleColumnURL = new URLParam("showlifecycle", false);
|
|
51
52
|
|
|
52
53
|
const defaultSelectedFields = {
|
|
53
54
|
param0: true,
|
|
@@ -74,7 +75,7 @@ export class LogViewer3 extends qreact.Component {
|
|
|
74
75
|
hasSearched: t.boolean(false),
|
|
75
76
|
forceMoveStartTime: t.atomic<number | undefined>(undefined),
|
|
76
77
|
forceMoveEndTime: t.atomic<number | undefined>(undefined),
|
|
77
|
-
showLifecycleColumn: t.boolean(
|
|
78
|
+
showLifecycleColumn: t.boolean(false),
|
|
78
79
|
});
|
|
79
80
|
|
|
80
81
|
lifecycleController = LifeCyclesController(SocketFunction.browserNodeId());
|
|
@@ -267,7 +268,8 @@ export class LogViewer3 extends qreact.Component {
|
|
|
267
268
|
let updateResults = throttleFunction(100, () => {
|
|
268
269
|
if (this.searchSequenceNumber !== currentSequenceNumber) return;
|
|
269
270
|
Querysub.commitLocal(() => {
|
|
270
|
-
|
|
271
|
+
// LIMIT, as the other values will be fragmented and so are confusing.
|
|
272
|
+
this.state.results = results.slice(0, limitURL.value);
|
|
271
273
|
});
|
|
272
274
|
});
|
|
273
275
|
|
|
@@ -493,17 +495,19 @@ export class LogViewer3 extends qreact.Component {
|
|
|
493
495
|
|
|
494
496
|
for (let field of selectedFields) {
|
|
495
497
|
let column: ColumnType<unknown, LogDatum> = {};
|
|
496
|
-
if (field === "time") {
|
|
497
|
-
column.formatter = (x: unknown) =>
|
|
498
|
-
} else if (field === "__machineId") {
|
|
499
|
-
column.formatter = (x: unknown, context) => {
|
|
500
|
-
if (!context?.row || !context.row.__machineId) return <ObjectDisplay value={x} />;
|
|
501
|
-
return <MachineThreadInfo machineId={context.row.__machineId} threadId={context.row.__threadId || undefined} />;
|
|
502
|
-
};
|
|
503
|
-
}
|
|
504
|
-
if (!column.formatter) {
|
|
505
|
-
column.formatter = (x: unknown) => <ObjectDisplay value={x} />;
|
|
498
|
+
if (field === "time" || field === "timeId") {
|
|
499
|
+
column.formatter = (x: unknown) => <span title={formatDateTimeDetailed(Number(x))}>{formatDateTimeDetailed(Number(x))}</span>;
|
|
506
500
|
}
|
|
501
|
+
// ObjectDisplay is too slow.
|
|
502
|
+
// else if (field === "__machineId") {
|
|
503
|
+
// column.formatter = (x: unknown, context) => {
|
|
504
|
+
// if (!context?.row || !context.row.__machineId) return <ObjectDisplay value={x} />;
|
|
505
|
+
// return <MachineThreadInfo machineId={context.row.__machineId} threadId={context.row.__threadId || undefined} />;
|
|
506
|
+
// };
|
|
507
|
+
// }
|
|
508
|
+
// if (!column.formatter) {
|
|
509
|
+
// column.formatter = (x: unknown) => <ObjectDisplay value={x} />;
|
|
510
|
+
// }
|
|
507
511
|
columns[field] = column;
|
|
508
512
|
}
|
|
509
513
|
|
|
@@ -620,11 +624,6 @@ export class LogViewer3 extends qreact.Component {
|
|
|
620
624
|
selectedFieldsURL.value = newValues;
|
|
621
625
|
}}
|
|
622
626
|
/>
|
|
623
|
-
<InputLabelURL
|
|
624
|
-
label="Use Relative Time"
|
|
625
|
-
checkbox
|
|
626
|
-
url={useRelativeTimeURL}
|
|
627
|
-
/>
|
|
628
627
|
<InputLabelURL
|
|
629
628
|
label="Show Lifecycles"
|
|
630
629
|
checkbox
|
|
@@ -640,6 +639,7 @@ export class LogViewer3 extends qreact.Component {
|
|
|
640
639
|
</div>}
|
|
641
640
|
|
|
642
641
|
<Table
|
|
642
|
+
shallowCache
|
|
643
643
|
rows={this.state.results}
|
|
644
644
|
columns={columns}
|
|
645
645
|
lineLimit={4}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { timeInHour, timeInMinute, timeInSecond, timeoutToError, timeoutToUndefined, timeoutToUndefinedSilent } from "socket-function/src/misc";
|
|
1
|
+
import { sort, timeInHour, timeInMinute, timeInSecond, timeoutToError, timeoutToUndefined, timeoutToUndefinedSilent } from "socket-function/src/misc";
|
|
2
2
|
import { lazy } from "socket-function/src/caching";
|
|
3
3
|
import { getMachineId } from "../../../-a-auth/certs";
|
|
4
4
|
import { getAllNodeIds, getOwnMachineId, isOwnNodeId } from "../../../-f-node-discovery/NodeDiscovery";
|
|
@@ -27,32 +27,6 @@ const PROGRESS_LOG_INTERVAL = timeInSecond * 5;
|
|
|
27
27
|
const LOGGER_NAMES = ["logs/log", "logs/info", "logs/warn", "logs/error"] as const;
|
|
28
28
|
type LoggerName = typeof LOGGER_NAMES[number];
|
|
29
29
|
|
|
30
|
-
// Public-facing short names callers pass in via the `logTypes` parameter, mapped
|
|
31
|
-
// to the internal logger names above.
|
|
32
|
-
const EXTERNAL_TO_INTERNAL_LOGGER: Record<string, LoggerName> = {
|
|
33
|
-
"log": "logs/log",
|
|
34
|
-
"info": "logs/info",
|
|
35
|
-
"warn": "logs/warn",
|
|
36
|
-
"error": "logs/error",
|
|
37
|
-
};
|
|
38
|
-
|
|
39
|
-
// Parses the caller's `logTypes` string (e.g. "warn|error") into the matching
|
|
40
|
-
// internal logger names. Empty / undefined means "all four" (no restriction).
|
|
41
|
-
function parseLogTypes(value: string | undefined): readonly LoggerName[] {
|
|
42
|
-
if (!value) return LOGGER_NAMES;
|
|
43
|
-
let parts = value.split("|").map(s => s.trim().toLowerCase()).filter(s => s);
|
|
44
|
-
if (parts.length === 0) return LOGGER_NAMES;
|
|
45
|
-
let result: LoggerName[] = [];
|
|
46
|
-
for (let p of parts) {
|
|
47
|
-
let internal = EXTERNAL_TO_INTERNAL_LOGGER[p];
|
|
48
|
-
if (!internal) {
|
|
49
|
-
throw new Error(`logTypes: unknown log type ${JSON.stringify(p)}; expected one of log, info, warn, error (separated by "|")`);
|
|
50
|
-
}
|
|
51
|
-
if (!result.includes(internal)) result.push(internal);
|
|
52
|
-
}
|
|
53
|
-
return result;
|
|
54
|
-
}
|
|
55
|
-
|
|
56
30
|
// Per-logger accounting for one search. Byte counts are raw buffer sizes.
|
|
57
31
|
export type LoggerStats = {
|
|
58
32
|
// Files in range matching the requested machine.
|
|
@@ -168,11 +142,8 @@ function createProgressLogger(): (message: string) => void {
|
|
|
168
142
|
}
|
|
169
143
|
|
|
170
144
|
export class MCPIndexedLogs {
|
|
171
|
-
//
|
|
172
|
-
|
|
173
|
-
// a search scoped to only some loggers doesn't poison the cache for the
|
|
174
|
-
// others.
|
|
175
|
-
private movedThroughByMachineLogger = new Map<string, number>();
|
|
145
|
+
// machineId -> latest timestamp guaranteed to already be moved-to-public.
|
|
146
|
+
private movedThroughByMachine = new Map<string, number>();
|
|
176
147
|
|
|
177
148
|
// Cache: `${type}|${loggerName}|${startBucket}|${endBucket}` -> { time, paths }.
|
|
178
149
|
// Buckets are hour-aligned start/end so adjacent searches reuse work.
|
|
@@ -186,15 +157,14 @@ export class MCPIndexedLogs {
|
|
|
186
157
|
direction: Direction;
|
|
187
158
|
columns: string[];
|
|
188
159
|
limit?: number;
|
|
189
|
-
// Optional pipe-separated list restricting which log streams to scan
|
|
190
|
-
// (e.g. "warn|error", "log"). Omit / empty = all four streams.
|
|
191
|
-
logTypes?: string;
|
|
192
160
|
}): Promise<SearchResult> {
|
|
193
161
|
let limit = config.limit ?? 100;
|
|
194
162
|
let startTime = normalizeTime(config.startTime, "startTime");
|
|
195
163
|
let endTime = normalizeTime(config.endTime, "endTime");
|
|
196
|
-
|
|
197
|
-
|
|
164
|
+
// `time` is always projected — the final sort needs it, and callers can't
|
|
165
|
+
// meaningfully read a log row without knowing when it happened.
|
|
166
|
+
let columns = config.columns.includes("time") ? config.columns : ["time", ...config.columns];
|
|
167
|
+
console.log(`[search] query=${JSON.stringify(config.query)} | machine=${config.machine} | startTime=${formatDateTime(startTime)} | endTime=${formatDateTime(endTime)} | direction=${config.direction} | columns=[${config.columns.join(",")}] | limit=${config.limit ?? "(default)"}`);
|
|
198
168
|
let now = Date.now();
|
|
199
169
|
if (endTime > now - END_TIME_MIN_AGE) {
|
|
200
170
|
throw new Error(`endTime must be at least ${formatTime(END_TIME_MIN_AGE)} in the past (got ${formatTime(now - endTime)} ago)`);
|
|
@@ -206,7 +176,7 @@ export class MCPIndexedLogs {
|
|
|
206
176
|
let machineId = config.machine === "local" ? getOwnMachineId() : config.machine;
|
|
207
177
|
|
|
208
178
|
let moveStart = Date.now();
|
|
209
|
-
let moveOutcome = await this.ensureMovedThrough(machineId, endTime
|
|
179
|
+
let moveOutcome = await this.ensureMovedThrough(machineId, endTime);
|
|
210
180
|
console.log(`[search] ensureMovedThrough ${moveOutcome} in ${formatTime(Date.now() - moveStart)}`);
|
|
211
181
|
|
|
212
182
|
let loggers = await getLoggers2Async();
|
|
@@ -223,7 +193,7 @@ export class MCPIndexedLogs {
|
|
|
223
193
|
|
|
224
194
|
let pathsStart = Date.now();
|
|
225
195
|
let totalPathsSeen = 0;
|
|
226
|
-
await Promise.all(
|
|
196
|
+
await Promise.all(LOGGER_NAMES.map(async (loggerName) => {
|
|
227
197
|
let logger = this.getLoggerByName(loggers, loggerName);
|
|
228
198
|
let archives = logger.debugGetCachedLogs({ type: useType });
|
|
229
199
|
|
|
@@ -270,6 +240,9 @@ export class MCPIndexedLogs {
|
|
|
270
240
|
}));
|
|
271
241
|
console.log(`[search] read ${allFiles.length} files in ${formatTime(Date.now() - searchStart)}`);
|
|
272
242
|
|
|
243
|
+
let dir = config.direction === "fromStart" ? 1 : -1;
|
|
244
|
+
sort(readFiles, x => x.entry.path.startTime * dir);
|
|
245
|
+
|
|
273
246
|
// Phase 2: scan the already-read files in time order, applying a moving
|
|
274
247
|
// cutoff once we have `limit` rows: any unprocessed file whose entire range
|
|
275
248
|
// is past the cutoff cannot contribute results we'd keep.
|
|
@@ -282,7 +255,6 @@ export class MCPIndexedLogs {
|
|
|
282
255
|
scanCount++;
|
|
283
256
|
logScanProgress(`[search] scanning files ${scanCount}/${readFiles.length}`);
|
|
284
257
|
|
|
285
|
-
if (resultRows.length >= limit) break;
|
|
286
258
|
if (buffers === undefined) continue;
|
|
287
259
|
|
|
288
260
|
if (cutoff !== undefined) {
|
|
@@ -303,7 +275,7 @@ export class MCPIndexedLogs {
|
|
|
303
275
|
limit,
|
|
304
276
|
queryBuffer,
|
|
305
277
|
matchesPattern,
|
|
306
|
-
columns
|
|
278
|
+
columns,
|
|
307
279
|
startTime,
|
|
308
280
|
endTime,
|
|
309
281
|
stats,
|
|
@@ -326,18 +298,26 @@ export class MCPIndexedLogs {
|
|
|
326
298
|
let totals = createEmptyLoggerStats();
|
|
327
299
|
for (let name of LOGGER_NAMES) addLoggerStats(totals, fileCounts[name]);
|
|
328
300
|
|
|
329
|
-
|
|
330
|
-
|
|
301
|
+
// Files from different loggers can overlap in time, so rows come out of
|
|
302
|
+
// phase 2 only roughly time-ordered. Sort by row time in the scan
|
|
303
|
+
// direction and slice to `limit`. The slice is the *only* place the
|
|
304
|
+
// global limit is enforced on the returned set — per-file/per-block caps
|
|
305
|
+
// upstream are bounded by `limit` to keep memory sane but don't define
|
|
306
|
+
// truncation by themselves.
|
|
307
|
+
sort(resultRows, r => Number(r.time) * dir);
|
|
308
|
+
let totalMatched = resultRows.length;
|
|
309
|
+
if (totalMatched > limit) resultRows = resultRows.slice(0, limit);
|
|
310
|
+
|
|
311
|
+
let limitHit = totalMatched > limit;
|
|
312
|
+
console.log(`[search] done in ${formatTime(Date.now() - searchStart)} (filesScanned=${totals.scanned}/${allFiles.length} scannedBytes=${formatNumber(totals.scannedBytes)}B blocksMatched=${totals.blocksMatched} blocksRead=${totals.blocksRead} blockBytesRead=${formatNumber(totals.blockBytesRead)}B matched=${totalMatched} returned=${resultRows.length} limit=${limit}${limitHit ? " HIT" : ""})`);
|
|
331
313
|
console.log(`[search] buffer types: stream=${stats.typeCounts.stream} bulk=${stats.typeCounts.bulk}`);
|
|
332
314
|
console.log(`[search] timing: readFiles=${formatTime(totals.readFilesMs)} findMatchingBlocks=${formatTime(totals.findMatchingBlocksMs)} getBlockBuffers=${formatTime(totals.getBlockBuffersMs)}`);
|
|
333
315
|
|
|
334
316
|
// Trim the internal LoggerStats down to just total + scanned. The rest
|
|
335
317
|
// (bytes/blocks/timing) stays in the console.log above and is NOT
|
|
336
|
-
// returned — see the warning on SearchResult.
|
|
337
|
-
// the loggers we actually searched, so a caller who scoped to
|
|
338
|
-
// `warn|error` doesn't see misleading 0s for the loggers they skipped.
|
|
318
|
+
// returned — see the warning on SearchResult.
|
|
339
319
|
let files: Record<string, { total: number; scanned: number }> = {};
|
|
340
|
-
for (let name of
|
|
320
|
+
for (let name of LOGGER_NAMES) {
|
|
341
321
|
files[name] = { total: fileCounts[name].total, scanned: fileCounts[name].scanned };
|
|
342
322
|
}
|
|
343
323
|
|
|
@@ -387,8 +367,13 @@ export class MCPIndexedLogs {
|
|
|
387
367
|
sink: SearchSink;
|
|
388
368
|
}): Promise<void> {
|
|
389
369
|
let { entry, indexBuf, dataBuf, direction, limit, queryBuffer, matchesPattern, columns, startTime, endTime, stats, sink } = scan;
|
|
390
|
-
let {
|
|
370
|
+
let { loggerStats } = sink;
|
|
391
371
|
let p = entry.path;
|
|
372
|
+
// Per-file cap. Blocks are scanned in time order, so anything past the
|
|
373
|
+
// first `limit` rows from this file would lose the caller's final
|
|
374
|
+
// sort+slice anyway. Tracked locally (not against the shared sink)
|
|
375
|
+
// so a noisy earlier file doesn't starve overlapping later files.
|
|
376
|
+
let rowsFromThisFile = 0;
|
|
392
377
|
|
|
393
378
|
// Region 1: the index scan that picks candidate blocks.
|
|
394
379
|
let findStart = Date.now();
|
|
@@ -421,7 +406,7 @@ export class MCPIndexedLogs {
|
|
|
421
406
|
}
|
|
422
407
|
|
|
423
408
|
for (let block of blocks) {
|
|
424
|
-
if (
|
|
409
|
+
if (rowsFromThisFile >= limit) break;
|
|
425
410
|
|
|
426
411
|
// Region 2: decoding the candidate block's buffers.
|
|
427
412
|
let buffers: Buffer[] | undefined;
|
|
@@ -441,8 +426,10 @@ export class MCPIndexedLogs {
|
|
|
441
426
|
|
|
442
427
|
let ordered = direction === "fromStart" ? buffers : [...buffers].reverse();
|
|
443
428
|
for (let buf of ordered) {
|
|
444
|
-
if (
|
|
429
|
+
if (rowsFromThisFile >= limit) break;
|
|
430
|
+
let before = sink.resultRows.length;
|
|
445
431
|
this.appendRow({ buf, matchesPattern, columns, startTime, endTime, sink });
|
|
432
|
+
if (sink.resultRows.length > before) rowsFromThisFile++;
|
|
446
433
|
}
|
|
447
434
|
}
|
|
448
435
|
loggerStats.getBlockBuffersMs += Date.now() - blockStart;
|
|
@@ -485,20 +472,16 @@ export class MCPIndexedLogs {
|
|
|
485
472
|
sink.loggerStats.rows++;
|
|
486
473
|
}
|
|
487
474
|
|
|
488
|
-
// For each
|
|
489
|
-
//
|
|
490
|
-
//
|
|
491
|
-
//
|
|
492
|
-
//
|
|
493
|
-
//
|
|
494
|
-
//
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
let lastMoved = this.movedThroughByMachineLogger.get(`${machineId}|${name}`) ?? 0;
|
|
499
|
-
return lastMoved < endTime;
|
|
500
|
-
});
|
|
501
|
-
if (needed.length === 0) return "cached";
|
|
475
|
+
// For each logger, asks each remote node on the target machine whether it
|
|
476
|
+
// has pending logs overlapping [0, endTime]. The first node that answers
|
|
477
|
+
// without throwing wins; if it says yes, we ask the same node to flush.
|
|
478
|
+
// We iterate because not every node necessarily exposes the new endpoints
|
|
479
|
+
// (e.g. older versions still running). Records moved-through up to
|
|
480
|
+
// now - MOVE_GRACE so we skip this on subsequent calls covering the same
|
|
481
|
+
// window.
|
|
482
|
+
private async ensureMovedThrough(machineId: string, endTime: number): Promise<"cached" | "no-node" | "moved"> {
|
|
483
|
+
let lastMoved = this.movedThroughByMachine.get(machineId) ?? 0;
|
|
484
|
+
if (lastMoved >= endTime) return "cached";
|
|
502
485
|
|
|
503
486
|
let nodeIds = await this.findRemoteNodesOnMachine(machineId);
|
|
504
487
|
if (nodeIds.length === 0) {
|
|
@@ -506,7 +489,7 @@ export class MCPIndexedLogs {
|
|
|
506
489
|
return "no-node";
|
|
507
490
|
}
|
|
508
491
|
|
|
509
|
-
for (let loggerName of
|
|
492
|
+
for (let loggerName of LOGGER_NAMES) {
|
|
510
493
|
let answered = false;
|
|
511
494
|
for (let nodeId of nodeIds) {
|
|
512
495
|
try {
|
|
@@ -534,10 +517,7 @@ export class MCPIndexedLogs {
|
|
|
534
517
|
}
|
|
535
518
|
}
|
|
536
519
|
|
|
537
|
-
|
|
538
|
-
for (let loggerName of needed) {
|
|
539
|
-
this.movedThroughByMachineLogger.set(`${machineId}|${loggerName}`, recordTime);
|
|
540
|
-
}
|
|
520
|
+
this.movedThroughByMachine.set(machineId, Date.now() - MOVE_GRACE);
|
|
541
521
|
return "moved";
|
|
542
522
|
}
|
|
543
523
|
|