querysub 0.312.0 → 0.313.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/.cursorrules +1 -1
- package/costsBenefits.txt +4 -1
- package/package.json +3 -2
- package/spec.txt +23 -18
- package/src/-0-hooks/hooks.ts +1 -1
- package/src/-a-archives/archives.ts +16 -3
- package/src/-a-archives/archivesBackBlaze.ts +51 -3
- package/src/-a-archives/archivesLimitedCache.ts +175 -0
- package/src/-a-archives/archivesPrivateFileSystem.ts +299 -0
- package/src/-a-auth/certs.ts +58 -31
- package/src/-b-authorities/cdnAuthority.ts +2 -2
- package/src/-b-authorities/dnsAuthority.ts +3 -2
- package/src/-c-identity/IdentityController.ts +3 -2
- package/src/-d-trust/NetworkTrust2.ts +17 -19
- package/src/-e-certs/EdgeCertController.ts +3 -4
- package/src/-e-certs/certAuthority.ts +1 -2
- package/src/-f-node-discovery/NodeDiscovery.ts +9 -7
- package/src/-g-core-values/NodeCapabilities.ts +6 -1
- package/src/0-path-value-core/NodePathAuthorities.ts +1 -1
- package/src/0-path-value-core/PathValueCommitter.ts +3 -3
- package/src/0-path-value-core/PathValueController.ts +3 -3
- package/src/0-path-value-core/archiveLocks/ArchiveLocks2.ts +15 -37
- package/src/0-path-value-core/pathValueCore.ts +4 -3
- package/src/3-path-functions/PathFunctionRunner.ts +2 -2
- package/src/4-dom/qreact.tsx +4 -3
- package/src/4-querysub/Querysub.ts +2 -2
- package/src/4-querysub/QuerysubController.ts +2 -2
- package/src/5-diagnostics/GenericFormat.tsx +1 -0
- package/src/5-diagnostics/Table.tsx +3 -0
- package/src/5-diagnostics/diskValueAudit.ts +2 -1
- package/src/5-diagnostics/nodeMetadata.ts +0 -1
- package/src/deployManager/components/MachineDetailPage.tsx +9 -1
- package/src/deployManager/components/ServiceDetailPage.tsx +10 -1
- package/src/diagnostics/NodeViewer.tsx +3 -4
- package/src/diagnostics/logs/FastArchiveAppendable.ts +748 -0
- package/src/diagnostics/logs/FastArchiveController.ts +524 -0
- package/src/diagnostics/logs/FastArchiveViewer.tsx +863 -0
- package/src/diagnostics/logs/LogViewer2.tsx +349 -0
- package/src/diagnostics/logs/TimeRangeSelector.tsx +94 -0
- package/src/diagnostics/logs/diskLogger.ts +135 -305
- package/src/diagnostics/logs/diskShimConsoleLogs.ts +6 -29
- package/src/diagnostics/logs/errorNotifications/ErrorNotificationController.ts +577 -0
- package/src/diagnostics/logs/errorNotifications/ErrorSuppressionUI.tsx +225 -0
- package/src/diagnostics/logs/errorNotifications/ErrorWarning.tsx +207 -0
- package/src/diagnostics/logs/importLogsEntry.ts +38 -0
- package/src/diagnostics/logs/injectFileLocationToConsole.ts +7 -17
- package/src/diagnostics/logs/lifeCycleAnalysis/lifeCycles.tsx +0 -0
- package/src/diagnostics/logs/lifeCycleAnalysis/spec.md +151 -0
- package/src/diagnostics/managementPages.tsx +7 -16
- package/src/diagnostics/misc-pages/ComponentSyncStats.tsx +0 -1
- package/src/diagnostics/periodic.ts +5 -0
- package/src/diagnostics/watchdog.ts +2 -2
- package/src/functional/SocketChannel.ts +67 -0
- package/src/library-components/Input.tsx +1 -1
- package/src/library-components/InputLabel.tsx +5 -2
- package/src/misc.ts +111 -0
- package/src/src.d.ts +34 -1
- package/src/user-implementation/userData.ts +4 -3
- package/test.ts +13 -0
- package/testEntry2.ts +29 -0
- package/src/diagnostics/errorLogs/ErrorLogController.ts +0 -535
- package/src/diagnostics/errorLogs/ErrorLogCore.ts +0 -274
- package/src/diagnostics/errorLogs/LogClassifiers.tsx +0 -308
- package/src/diagnostics/errorLogs/LogFilterUI.tsx +0 -84
- package/src/diagnostics/errorLogs/LogNotify.tsx +0 -101
- package/src/diagnostics/errorLogs/LogTimeSelector.tsx +0 -723
- package/src/diagnostics/errorLogs/LogViewer.tsx +0 -757
- package/src/diagnostics/errorLogs/logFiltering.tsx +0 -149
- package/src/diagnostics/logs/DiskLoggerPage.tsx +0 -613
|
@@ -1,613 +0,0 @@
|
|
|
1
|
-
import { css, isNode } from "typesafecss";
|
|
2
|
-
|
|
3
|
-
import { SocketFunction } from "socket-function/SocketFunction";
|
|
4
|
-
import { getSourceVSCodeLink, qreact } from "../../4-dom/qreact";
|
|
5
|
-
import { assertIsManagementUser } from "../managementPages";
|
|
6
|
-
import { LogFile, LogObj, getLogBuffer, getLogFiles, parseLogBuffer } from "./diskLogger";
|
|
7
|
-
import { Querysub, t } from "../../4-querysub/Querysub";
|
|
8
|
-
import { URLParam } from "../../library-components/URLParam";
|
|
9
|
-
import { getSyncedController } from "../../library-components/SyncedController";
|
|
10
|
-
import { getBrowserUrlNode } from "../../-f-node-discovery/NodeDiscovery";
|
|
11
|
-
import { formatDateTime, formatNiceDateTime, formatNumber, formatTime } from "socket-function/src/formatting/format";
|
|
12
|
-
import { Anchor } from "../../library-components/ATag";
|
|
13
|
-
import { cache, cacheLimited, cacheShallowConfigArgEqual, lazy } from "socket-function/src/caching";
|
|
14
|
-
import { InputLabel, InputLabelURL } from "../../library-components/InputLabel";
|
|
15
|
-
import { DropdownSelector } from "../../library-components/DropdownSelector";
|
|
16
|
-
import { Table, TableType } from "../../5-diagnostics/Table";
|
|
17
|
-
import { LogType } from "../errorLogs/ErrorLogCore";
|
|
18
|
-
import { canHaveChildren } from "socket-function/src/types";
|
|
19
|
-
import { measureBlock, measureWrap } from "socket-function/src/profiling/measure";
|
|
20
|
-
import { binarySearchBasic, list, sort, timeInDay, timeInHour } from "socket-function/src/misc";
|
|
21
|
-
import { NodeViewerController, nodeViewerController } from "../NodeViewer";
|
|
22
|
-
import { red } from "socket-function/src/formatting/logColors";
|
|
23
|
-
import { errorMessage, formatValue, genericFormat, warnMessage } from "../../5-diagnostics/GenericFormat";
|
|
24
|
-
import { hslToRGB } from "socket-function/src/formatting/colors";
|
|
25
|
-
import { InputPicker } from "../../library-components/InputPicker";
|
|
26
|
-
import { ObjectDisplay } from "./ObjectDisplay";
|
|
27
|
-
import { parseAnsiColors, rgbToHsl } from "./ansiFormat";
|
|
28
|
-
import { ShowMore } from "../../library-components/ShowMore";
|
|
29
|
-
import { getNodeIdLocation } from "socket-function/src/nodeCache";
|
|
30
|
-
import { decodeNodeId, encodeNodeId, getMachineId } from "../../-a-auth/certs";
|
|
31
|
-
import { Button } from "../../library-components/Button";
|
|
32
|
-
import { TimeRangeSelector } from "../../library-components/TimeRangeSelector";
|
|
33
|
-
import { BrowserLargeFileCache, cacheCalls } from "./BrowserLargeFileCache";
|
|
34
|
-
import { Zip } from "../../zip";
|
|
35
|
-
|
|
36
|
-
// TODO: Realtime log mode, by reading from the previous length forward, to add buffers
|
|
37
|
-
// to what we already read.
|
|
38
|
-
// - ALSO, correctly handle cut off messages? Or not, if the writes are atomic
|
|
39
|
-
// it might be fine...
|
|
40
|
-
|
|
41
|
-
// TODO: Allow showing context for logs, showing filter logs PLUS +/- a certain number of lines
|
|
42
|
-
// - Also grouping them, so we can see which are part of which.
|
|
43
|
-
// - Maybe only showing +/- within a time range as well, and maybe only the same file
|
|
44
|
-
|
|
45
|
-
// TODO: Parse objects in strings and display them nicely
|
|
46
|
-
// - I don't know if we even want to add filter support, but... making them more readable
|
|
47
|
-
// should be easy enough, and might help with reading objects in error messages?
|
|
48
|
-
|
|
49
|
-
// TODO: Track line locations from diskLog
|
|
50
|
-
// - Probably by injecting ids? Although how would they be persistent? Hmm... Maybe line numbers
|
|
51
|
-
// is fine? I think we map the typescript (I hope we do), so they should be correct?
|
|
52
|
-
|
|
53
|
-
// TODO: Use a binary format, but faster parsing.
|
|
54
|
-
// - If we could batch it, it would probably be very fast. We ARE just dealing with KVPs though,
|
|
55
|
-
// so... maybe it wouldn't be that much faster...
|
|
56
|
-
|
|
57
|
-
module.hotreload = true;
|
|
58
|
-
module.hotreload = true;
|
|
59
|
-
|
|
60
|
-
export let selectedNodeId = new URLParam("nodeId", "");
|
|
61
|
-
|
|
62
|
-
export let filterURL = new URLParam("filter", "");
|
|
63
|
-
let sortOldestFirst = new URLParam("oldestFirst", false);
|
|
64
|
-
let selectedFields = new URLParam("selectedFields", "");
|
|
65
|
-
|
|
66
|
-
let startTimeURL = new URLParam("startTime", undefined as number | undefined);
|
|
67
|
-
let endTimeURL = new URLParam("endTime", undefined as number | undefined);
|
|
68
|
-
|
|
69
|
-
let inspectTimeURL = new URLParam("inspectTime", "");
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
export class DiskLoggerPage extends qreact.Component {
|
|
73
|
-
state = t.state({
|
|
74
|
-
downloadFilesHash: t.string,
|
|
75
|
-
});
|
|
76
|
-
render() {
|
|
77
|
-
let selectedNodesIds = selectedNodeId.value.split("|").filter(x => x);
|
|
78
|
-
const nodeController = nodeViewerController(getBrowserUrlNode());
|
|
79
|
-
let nodeIds = nodeController.getControllerNodeIdList(DiskLoggerController) || [];
|
|
80
|
-
|
|
81
|
-
if (selectedNodesIds.length === 0) {
|
|
82
|
-
// Group by machineId
|
|
83
|
-
let grouped = new Map<string, { nodeId: string; entryPoint: string }[]>();
|
|
84
|
-
for (let { nodeId, entryPoint } of nodeIds) {
|
|
85
|
-
let obj = decodeNodeId(nodeId);
|
|
86
|
-
if (!obj) continue;
|
|
87
|
-
let { machineId } = obj;
|
|
88
|
-
let list = grouped.get(machineId);
|
|
89
|
-
if (!list) {
|
|
90
|
-
grouped.set(machineId, list = []);
|
|
91
|
-
}
|
|
92
|
-
list.push({ nodeId, entryPoint });
|
|
93
|
-
}
|
|
94
|
-
return (
|
|
95
|
-
<div class={css.pad2(10).vbox(10)}>
|
|
96
|
-
<h1>Node Ids ({nodeIds.length})</h1>
|
|
97
|
-
<div class={css.hbox(40).wrap}>
|
|
98
|
-
{Array.from(grouped.entries()).map(([machineId, list]) =>
|
|
99
|
-
|
|
100
|
-
<div class={css.vbox(10).wrap.maxWidth("30vw").pad2(5).hsla(0, 0, 0, 0.1)}>
|
|
101
|
-
<div class={css.fontSize(30)}>{getMachineId(machineId)}</div>
|
|
102
|
-
<div class={css.hbox(20, 5).wrap}>
|
|
103
|
-
{list.map(({ nodeId, entryPoint }) =>
|
|
104
|
-
<Anchor
|
|
105
|
-
class={css.hbox(10)}
|
|
106
|
-
values={[selectedNodeId.getOverride(nodeId)]}
|
|
107
|
-
>
|
|
108
|
-
{nodeId} ({entryPoint.replaceAll("\\", "/").split("/").pop()})
|
|
109
|
-
</Anchor>
|
|
110
|
-
)}
|
|
111
|
-
</div>
|
|
112
|
-
</div>
|
|
113
|
-
)}
|
|
114
|
-
</div>
|
|
115
|
-
</div>
|
|
116
|
-
);
|
|
117
|
-
}
|
|
118
|
-
|
|
119
|
-
let startTime = Number(startTimeURL.value) || (Date.now() - timeInDay);
|
|
120
|
-
let endTime = Number(endTimeURL.value) || (Date.now() + timeInDay);
|
|
121
|
-
const controller = diskLoggerController(getBrowserUrlNode());
|
|
122
|
-
let allFiles: (LogFile & { nodeId: string })[] = [];
|
|
123
|
-
let loadingCount = 0;
|
|
124
|
-
|
|
125
|
-
for (let nodeId of selectedNodesIds) {
|
|
126
|
-
try {
|
|
127
|
-
let newFiles = controller.getRemoteLogFiles(nodeId);
|
|
128
|
-
if (newFiles) {
|
|
129
|
-
allFiles.push(...newFiles.map(x => ({ ...x, nodeId })));
|
|
130
|
-
} else {
|
|
131
|
-
loadingCount++;
|
|
132
|
-
}
|
|
133
|
-
} catch (e) {
|
|
134
|
-
console.log("Error reading files", nodeId, e);
|
|
135
|
-
}
|
|
136
|
-
}
|
|
137
|
-
//files = files.filter(file => file.startTime <= endTime && file.endTime >= startTime);
|
|
138
|
-
// File to fix some broken files
|
|
139
|
-
allFiles = allFiles.filter(file => file.startTime >= +new Date("1980"));
|
|
140
|
-
let buffers: Buffer[] = [];
|
|
141
|
-
|
|
142
|
-
let firstTime = Number.MAX_SAFE_INTEGER;
|
|
143
|
-
let lastTime = Number.MIN_SAFE_INTEGER;
|
|
144
|
-
|
|
145
|
-
let filesHash = JSON.stringify(allFiles.map(x => x.path));
|
|
146
|
-
let fileSizeSum = 0;
|
|
147
|
-
let fileCount = 0;
|
|
148
|
-
|
|
149
|
-
let selectedFiles: (LogFile & { nodeId: string })[] = [];
|
|
150
|
-
|
|
151
|
-
for (let file of allFiles) {
|
|
152
|
-
try {
|
|
153
|
-
if (file.startTime < firstTime) firstTime = file.startTime;
|
|
154
|
-
if (file.endTime > lastTime) lastTime = file.endTime;
|
|
155
|
-
if (file.startTime > endTime) continue;
|
|
156
|
-
if (file.endTime < startTime) continue;
|
|
157
|
-
selectedFiles.push(file);
|
|
158
|
-
fileCount++;
|
|
159
|
-
fileSizeSum += file.size;
|
|
160
|
-
if (filesHash !== this.state.downloadFilesHash) {
|
|
161
|
-
loadingCount++;
|
|
162
|
-
continue;
|
|
163
|
-
}
|
|
164
|
-
let buffer = controller.getRemoteLogBuffer(file.nodeId, file.path);
|
|
165
|
-
if (!buffer) {
|
|
166
|
-
loadingCount++;
|
|
167
|
-
continue;
|
|
168
|
-
}
|
|
169
|
-
buffer = unzipCached(buffer);
|
|
170
|
-
buffers.push(buffer);
|
|
171
|
-
} catch (e) {
|
|
172
|
-
console.log("Error reading buffer", file, e);
|
|
173
|
-
}
|
|
174
|
-
}
|
|
175
|
-
if (firstTime === Number.MAX_SAFE_INTEGER) firstTime = startTime;
|
|
176
|
-
if (lastTime === Number.MIN_SAFE_INTEGER) lastTime = endTime;
|
|
177
|
-
|
|
178
|
-
let logs: LogObj[] = loadingCount ? [] : parseLogBufferCached(JSON.stringify(allFiles), buffers);
|
|
179
|
-
|
|
180
|
-
const defaultFields = ["time", "type", "actions", "param0"];
|
|
181
|
-
let selectedValues = selectedFields.value.split("|").filter(x => x);
|
|
182
|
-
let fullSelectedFields = defaultFields.concat(selectedValues);
|
|
183
|
-
|
|
184
|
-
let { table, allFields, filteredCount, totalCount } = processLogs({
|
|
185
|
-
logs,
|
|
186
|
-
startTime, endTime,
|
|
187
|
-
order: sortOldestFirst.value ? "oldestFirst" : "newestFirst",
|
|
188
|
-
filter: filterURL.value,
|
|
189
|
-
selectedFields: fullSelectedFields,
|
|
190
|
-
inspectTime: inspectTimeURL.value,
|
|
191
|
-
});
|
|
192
|
-
|
|
193
|
-
function addPath(path: string[]) {
|
|
194
|
-
let newSelected = Array.from(new Set([...selectedValues, path.join(".")])).join("|");
|
|
195
|
-
selectedFields.value = newSelected;
|
|
196
|
-
}
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
for (let column of Object.values(table.columns)) {
|
|
200
|
-
if (!column) continue;
|
|
201
|
-
if (!column.formatter) {
|
|
202
|
-
column.formatter = (value: unknown) => <ObjectDisplay
|
|
203
|
-
value={value}
|
|
204
|
-
excludedFields={fullSelectedFields}
|
|
205
|
-
onClickValue={(path, value) => {
|
|
206
|
-
if (!canHaveChildren(value)) {
|
|
207
|
-
// Add to search as well
|
|
208
|
-
if (filterURL.value.trim()) {
|
|
209
|
-
filterURL.value += " & ";
|
|
210
|
-
}
|
|
211
|
-
filterURL.value += String(value);
|
|
212
|
-
}
|
|
213
|
-
}}
|
|
214
|
-
/>;
|
|
215
|
-
}
|
|
216
|
-
}
|
|
217
|
-
|
|
218
|
-
table.columns["actions"] = {
|
|
219
|
-
formatter(value, context) {
|
|
220
|
-
let file = (context as any)?.row.__FILE__;
|
|
221
|
-
if (!file) return undefined;
|
|
222
|
-
let line = (context as any)?.row.__LINE__;
|
|
223
|
-
return (
|
|
224
|
-
<div>
|
|
225
|
-
<button onClick={() => {
|
|
226
|
-
let path = `vscode://file/${file}`;
|
|
227
|
-
if (typeof line === "number") {
|
|
228
|
-
path += `:${line + 1}`;
|
|
229
|
-
}
|
|
230
|
-
window.open(path);
|
|
231
|
-
}}>Open In VS Code</button>
|
|
232
|
-
</div>
|
|
233
|
-
);
|
|
234
|
-
},
|
|
235
|
-
};
|
|
236
|
-
table.columns["time"] = {
|
|
237
|
-
formatter(value, context) {
|
|
238
|
-
return (
|
|
239
|
-
<div
|
|
240
|
-
className={css.button + (css.background("hsla(0, 0%, 100%, 0.2)", "hover"))}
|
|
241
|
-
onClick={() => {
|
|
242
|
-
inspectTimeURL.value = String(value) + "|" + 10 + "|" + 10;
|
|
243
|
-
filterURL.value = "";
|
|
244
|
-
}}
|
|
245
|
-
>
|
|
246
|
-
{genericFormat(value)}
|
|
247
|
-
</div>
|
|
248
|
-
);
|
|
249
|
-
},
|
|
250
|
-
};
|
|
251
|
-
|
|
252
|
-
table.columns["type"] = {
|
|
253
|
-
formatter(value, context) {
|
|
254
|
-
let inner = (() => {
|
|
255
|
-
if (value === "error") return <span className={errorMessage}>{value}</span>;
|
|
256
|
-
if (value === "warn") return <span className={warnMessage}>{value}</span>;
|
|
257
|
-
if (value === "info" || value === "log") return <span className={css.hsl(210, 50, 50).pad2(6, 4).color("white")}>{value}</span>;
|
|
258
|
-
return formatValue(value, undefined, context);
|
|
259
|
-
})();
|
|
260
|
-
return <div><div className={css.width(100).hbox0.center}>{inner}</div></div>;
|
|
261
|
-
}
|
|
262
|
-
};
|
|
263
|
-
table.columns["param0"] = {
|
|
264
|
-
title: "Message",
|
|
265
|
-
formatter(value, context) {
|
|
266
|
-
return <ObjectDisplay value={value} />;
|
|
267
|
-
},
|
|
268
|
-
};
|
|
269
|
-
|
|
270
|
-
table.columns["remaining"] = {
|
|
271
|
-
title: "Object",
|
|
272
|
-
formatter(value, context) {
|
|
273
|
-
return <ObjectDisplay
|
|
274
|
-
value={context?.row}
|
|
275
|
-
onClickKey={path => addPath(path)}
|
|
276
|
-
onClickValue={(path, value) => {
|
|
277
|
-
addPath(path);
|
|
278
|
-
if (!canHaveChildren(value)) {
|
|
279
|
-
// Add to search as well
|
|
280
|
-
if (filterURL.value.trim()) {
|
|
281
|
-
filterURL.value += " & ";
|
|
282
|
-
}
|
|
283
|
-
filterURL.value += String(value);
|
|
284
|
-
}
|
|
285
|
-
}}
|
|
286
|
-
/>;
|
|
287
|
-
},
|
|
288
|
-
};
|
|
289
|
-
|
|
290
|
-
let [inspectTime, inspectBeforeCount, inspectAfterCount] = inspectTimeURL.value.split("|").map(x => Number(x));
|
|
291
|
-
if (inspectTime) {
|
|
292
|
-
startTime = inspectTime;
|
|
293
|
-
endTime = inspectTime;
|
|
294
|
-
}
|
|
295
|
-
|
|
296
|
-
return (
|
|
297
|
-
<div class={css.pad2(10).vbox(10)}>
|
|
298
|
-
<Anchor values={[selectedNodeId.getOverride("")]}><h2>Back</h2></Anchor>
|
|
299
|
-
<InputPicker
|
|
300
|
-
picked={selectedNodesIds}
|
|
301
|
-
addPicked={x => selectedNodeId.value = [...selectedNodesIds, x].join("|")}
|
|
302
|
-
removePicked={x => selectedNodeId.value = selectedNodesIds.filter(y => y !== x).join("|")}
|
|
303
|
-
options={nodeIds.map(({ nodeId, entryPoint }) => ({ value: nodeId, label: `${nodeId} (${entryPoint.replaceAll("\\", "/").split("/").pop()})` }))}
|
|
304
|
-
/>
|
|
305
|
-
<div className={css.fontSize(28).boldStyle}>
|
|
306
|
-
{selectedNodesIds.length} nodes, {fileCount}/{allFiles.length} files, filtered logs {formatNumber(filteredCount)} / {formatNumber(totalCount)}
|
|
307
|
-
</div>
|
|
308
|
-
<ShowMore maxHeight={40}>
|
|
309
|
-
<div class={css.vbox(4)}>
|
|
310
|
-
{selectedFiles.map(file =>
|
|
311
|
-
<div class={css.hbox(4)}>
|
|
312
|
-
<span>{formatNumber(file.size)}B</span>
|
|
313
|
-
<span>{formatDateTime(file.startTime)} - {formatDateTime(file.endTime)}</span>
|
|
314
|
-
<span>({file.path})</span>
|
|
315
|
-
</div>
|
|
316
|
-
)}
|
|
317
|
-
</div>
|
|
318
|
-
</ShowMore>
|
|
319
|
-
{filesHash !== this.state.downloadFilesHash && <div>
|
|
320
|
-
<button onClick={() => {
|
|
321
|
-
this.state.downloadFilesHash = filesHash;
|
|
322
|
-
}}>
|
|
323
|
-
Download Files ({formatNumber(fileSizeSum)}B)
|
|
324
|
-
</button>
|
|
325
|
-
</div>
|
|
326
|
-
|| loadingCount > 0 && <h1 className={css.hsl(210, 75, 75).pad2(10, 2)}>Downloading {loadingCount} file(s)</h1>
|
|
327
|
-
}
|
|
328
|
-
{!inspectTime && <>
|
|
329
|
-
<TimeRangeSelector
|
|
330
|
-
start={startTimeURL} end={endTimeURL}
|
|
331
|
-
defaultStart={startTime} defaultEnd={endTime}
|
|
332
|
-
firstTime={firstTime} lastTime={lastTime}
|
|
333
|
-
/>
|
|
334
|
-
<div class={css.hbox(20)}>
|
|
335
|
-
<Button onClick={() => { startTimeURL.reset(); endTimeURL.reset(); }}>Reset Time Filter</Button>
|
|
336
|
-
<div class={css.hbox(10)}>
|
|
337
|
-
<InputLabelURL label="Start Time" url={startTimeURL} isDatetime valueDefault={startTime} />
|
|
338
|
-
<span>({formatNiceDateTime(startTime)})</span>
|
|
339
|
-
</div>
|
|
340
|
-
<div class={css.hbox(10)}>
|
|
341
|
-
<InputLabelURL label="End Time" url={endTimeURL} isDatetime valueDefault={endTime} />
|
|
342
|
-
<span>({formatNiceDateTime(endTime)})</span>
|
|
343
|
-
</div>
|
|
344
|
-
</div>
|
|
345
|
-
<div className={css.hbox(20)}>
|
|
346
|
-
<h2>{formatTime(Date.now() - startTime)} AGO</h2> to <h2>{formatTime(Date.now() - endTime)} AGO</h2>
|
|
347
|
-
</div>
|
|
348
|
-
</> ||
|
|
349
|
-
<div class={css.hbox(20)}>
|
|
350
|
-
<h2>{formatTime(Date.now() - inspectTime)} AGO</h2>
|
|
351
|
-
<h2>{formatNiceDateTime(inspectTime)}</h2>
|
|
352
|
-
<InputLabel
|
|
353
|
-
label="Inspect Time"
|
|
354
|
-
value={inspectTime}
|
|
355
|
-
isDatetime
|
|
356
|
-
onChange={x => inspectTimeURL.value = x + "|" + inspectBeforeCount + "|" + inspectAfterCount}
|
|
357
|
-
/>
|
|
358
|
-
<button onClick={() => {
|
|
359
|
-
inspectTimeURL.value = "";
|
|
360
|
-
}}>
|
|
361
|
-
Reset Inspect
|
|
362
|
-
</button>
|
|
363
|
-
</div>
|
|
364
|
-
}
|
|
365
|
-
|
|
366
|
-
<InputLabelURL fillWidth label="Filter: ['|' to match any conditions, '&' to match all, '!' for negation]" url={filterURL} />
|
|
367
|
-
<InputLabelURL label="Sort Oldest First" checkbox url={sortOldestFirst} />
|
|
368
|
-
<InputPicker
|
|
369
|
-
label="Selected Fields"
|
|
370
|
-
picked={selectedValues}
|
|
371
|
-
options={Array.from(allFields.keys()).map(x => ({ value: x, label: x }))}
|
|
372
|
-
addPicked={x => selectedFields.value = [...selectedValues, x].join("|")}
|
|
373
|
-
removePicked={x => selectedFields.value = selectedValues.filter(y => y !== x).join("|")}
|
|
374
|
-
/>
|
|
375
|
-
{!!inspectTime && (
|
|
376
|
-
<InputLabel
|
|
377
|
-
label="Before Count"
|
|
378
|
-
value={inspectBeforeCount}
|
|
379
|
-
onChangeValue={x => inspectTimeURL.value = inspectTime + "|" + x + "|" + inspectAfterCount}
|
|
380
|
-
number
|
|
381
|
-
/>
|
|
382
|
-
)}
|
|
383
|
-
<Table {...table} initialLimit={30} />
|
|
384
|
-
{!!inspectTime && (
|
|
385
|
-
<InputLabel
|
|
386
|
-
label="After Count"
|
|
387
|
-
value={inspectAfterCount}
|
|
388
|
-
onChangeValue={x => inspectTimeURL.value = inspectTime + "|" + inspectBeforeCount + "|" + x}
|
|
389
|
-
number
|
|
390
|
-
/>
|
|
391
|
-
)}
|
|
392
|
-
</div>
|
|
393
|
-
);
|
|
394
|
-
}
|
|
395
|
-
}
|
|
396
|
-
|
|
397
|
-
const unzipCached = cacheLimited(100, Zip.gunzipSync);
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
let remainingCacheSize = 1024 * 1024 * 1024 * 2;
|
|
401
|
-
let logBufferCache = new Map<string, {
|
|
402
|
-
size: number;
|
|
403
|
-
logs: LogObj[];
|
|
404
|
-
lastAccess: number;
|
|
405
|
-
}>();
|
|
406
|
-
|
|
407
|
-
const parseLogBufferCached = measureWrap(function parseLogBufferCached(hash: string, buffers: Buffer[]): LogObj[] {
|
|
408
|
-
let cached = logBufferCache.get(hash);
|
|
409
|
-
if (cached) {
|
|
410
|
-
cached.lastAccess = Date.now();
|
|
411
|
-
return cached.logs;
|
|
412
|
-
}
|
|
413
|
-
console.log(`Parsing buffers ${buffers.length}`);
|
|
414
|
-
let logs = buffers.map(x => parseLogBuffer(x)).flat();
|
|
415
|
-
let size = buffers.map(x => x.length).reduce((a, b) => a + b, 0);
|
|
416
|
-
remainingCacheSize -= size;
|
|
417
|
-
while (remainingCacheSize < 0 && logBufferCache.size > 0) {
|
|
418
|
-
let oldest = [...logBufferCache.entries()].sort((a, b) => a[1].lastAccess - b[1].lastAccess)[0];
|
|
419
|
-
logBufferCache.delete(oldest[0]);
|
|
420
|
-
remainingCacheSize += oldest[1].size;
|
|
421
|
-
}
|
|
422
|
-
logBufferCache.set(hash, {
|
|
423
|
-
size,
|
|
424
|
-
logs,
|
|
425
|
-
lastAccess: Date.now()
|
|
426
|
-
});
|
|
427
|
-
console.log(`Parsed buffers ${buffers.length} into logs ${logs.length}`);
|
|
428
|
-
return logs;
|
|
429
|
-
});
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
const processLogs = cacheShallowConfigArgEqual((config: {
|
|
434
|
-
logs: LogObj[];
|
|
435
|
-
startTime: number; endTime: number;
|
|
436
|
-
order: "oldestFirst" | "newestFirst";
|
|
437
|
-
filter: string;
|
|
438
|
-
selectedFields: string[];
|
|
439
|
-
inspectTime: string;
|
|
440
|
-
}): {
|
|
441
|
-
table: TableType<LogObj>;
|
|
442
|
-
allFields: Map<string, number>;
|
|
443
|
-
totalCount: number;
|
|
444
|
-
filteredCount: number;
|
|
445
|
-
} => {
|
|
446
|
-
return measureBlock(function processLogs() {
|
|
447
|
-
console.log(`Processing logs ${config.logs.length}`);
|
|
448
|
-
let { logs, order, filter, startTime, endTime, inspectTime, selectedFields } = config;
|
|
449
|
-
logs = logs.slice();
|
|
450
|
-
let totalCount = logs.length;
|
|
451
|
-
|
|
452
|
-
let logMatchesFilter: (log: LogObj) => boolean = () => true;
|
|
453
|
-
|
|
454
|
-
if (filter) {
|
|
455
|
-
filter = filter.toLowerCase();
|
|
456
|
-
let filterParts = filter.split("|").map(x => x.trim()).map(x => x.split("&").map(x => x.trim()));
|
|
457
|
-
// Extract any negation
|
|
458
|
-
let negationQueries: string[] = [];
|
|
459
|
-
filterParts = filterParts.map(filter => filter.filter(x => {
|
|
460
|
-
if (x.startsWith("!")) {
|
|
461
|
-
negationQueries.push(x.slice(1));
|
|
462
|
-
return false;
|
|
463
|
-
}
|
|
464
|
-
return true;
|
|
465
|
-
})).filter(x => x.length > 0);
|
|
466
|
-
function logMatches(filter: string, log: LogObj) {
|
|
467
|
-
return JSON.stringify(log).toLowerCase().includes(filter);
|
|
468
|
-
}
|
|
469
|
-
logMatchesFilter = function logMatchesFilter(log: LogObj) {
|
|
470
|
-
return (
|
|
471
|
-
(filterParts.length === 0 || filterParts.some(filter => filter.every(filter => logMatches(filter, log))))
|
|
472
|
-
&& !negationQueries.some(filter => logMatches(filter, log))
|
|
473
|
-
);
|
|
474
|
-
};
|
|
475
|
-
}
|
|
476
|
-
|
|
477
|
-
function applySort() {
|
|
478
|
-
measureBlock(function filterSort() {
|
|
479
|
-
if (order === "oldestFirst") {
|
|
480
|
-
sort(logs, x => x.time);
|
|
481
|
-
} else {
|
|
482
|
-
sort(logs, x => -x.time);
|
|
483
|
-
}
|
|
484
|
-
});
|
|
485
|
-
}
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
if (inspectTime) {
|
|
489
|
-
applySort();
|
|
490
|
-
// time|before|after
|
|
491
|
-
let [time, before, after] = inspectTime.split("|").map(x => Number(x));
|
|
492
|
-
applySort();
|
|
493
|
-
measureBlock(function filterLogs() {
|
|
494
|
-
// Binary search to find the closest index
|
|
495
|
-
let sortOrder = order === "oldestFirst" ? 1 : -1;
|
|
496
|
-
let index = binarySearchBasic(logs, x => x.time * sortOrder, time * sortOrder);
|
|
497
|
-
let startIndex: number;
|
|
498
|
-
let endIndex: number;
|
|
499
|
-
|
|
500
|
-
let newLogs: LogObj[] = [];
|
|
501
|
-
if (index < 0) {
|
|
502
|
-
index = ~index;
|
|
503
|
-
startIndex = index - 1;
|
|
504
|
-
endIndex = index;
|
|
505
|
-
} else {
|
|
506
|
-
startIndex = index - 1;
|
|
507
|
-
endIndex = index + 1;
|
|
508
|
-
if (logMatchesFilter(logs[index])) {
|
|
509
|
-
newLogs.push(logs[index]);
|
|
510
|
-
}
|
|
511
|
-
}
|
|
512
|
-
|
|
513
|
-
while (before > 0 && startIndex > 0) {
|
|
514
|
-
startIndex--;
|
|
515
|
-
let logBefore = logs[startIndex];
|
|
516
|
-
if (logMatchesFilter(logBefore)) {
|
|
517
|
-
newLogs.unshift(logBefore);
|
|
518
|
-
before--;
|
|
519
|
-
}
|
|
520
|
-
}
|
|
521
|
-
|
|
522
|
-
while (after > 0 && endIndex < logs.length) {
|
|
523
|
-
let logAfter = logs[endIndex];
|
|
524
|
-
if (logMatchesFilter(logAfter)) {
|
|
525
|
-
newLogs.push(logAfter);
|
|
526
|
-
after--;
|
|
527
|
-
}
|
|
528
|
-
endIndex++;
|
|
529
|
-
}
|
|
530
|
-
|
|
531
|
-
logs = newLogs;
|
|
532
|
-
});
|
|
533
|
-
} else {
|
|
534
|
-
measureBlock(function filterLogs() {
|
|
535
|
-
logs = logs.filter(log =>
|
|
536
|
-
startTime <= log.time && log.time <= endTime &&
|
|
537
|
-
logMatchesFilter(log)
|
|
538
|
-
);
|
|
539
|
-
});
|
|
540
|
-
applySort();
|
|
541
|
-
}
|
|
542
|
-
|
|
543
|
-
let filteredCount = logs.length;
|
|
544
|
-
|
|
545
|
-
let fields = new Map<string, number>();
|
|
546
|
-
for (let log of logs) {
|
|
547
|
-
for (let key in log) {
|
|
548
|
-
fields.set(key, (fields.get(key) || 0) + 1);
|
|
549
|
-
}
|
|
550
|
-
}
|
|
551
|
-
|
|
552
|
-
let table: TableType<LogObj> = {
|
|
553
|
-
rows: logs,
|
|
554
|
-
columns: {},
|
|
555
|
-
};
|
|
556
|
-
|
|
557
|
-
for (let key of selectedFields) {
|
|
558
|
-
table.columns[key] = {};
|
|
559
|
-
}
|
|
560
|
-
|
|
561
|
-
console.log(`Processed logs ${logs.length}`);
|
|
562
|
-
return { table, allFields: fields, totalCount, filteredCount };
|
|
563
|
-
});
|
|
564
|
-
}, 10);
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
class DiskLoggerControllerBase {
|
|
570
|
-
// Have to forward it, as backend nodes don't have real domains, so HTTPS won't
|
|
571
|
-
// work from the browser for them.
|
|
572
|
-
public async getRemoteLogFiles(nodeId: string): Promise<LogFile[]> {
|
|
573
|
-
return await DiskLoggerController.nodes[nodeId].getLogFiles();
|
|
574
|
-
}
|
|
575
|
-
public async getRemoteLogBuffer(nodeId: string, path: string): Promise<Buffer | undefined> {
|
|
576
|
-
return await DiskLoggerController.nodes[nodeId].getLogBuffer(path);
|
|
577
|
-
}
|
|
578
|
-
|
|
579
|
-
public async getLogFiles(): Promise<LogFile[]> {
|
|
580
|
-
return await getLogFiles();
|
|
581
|
-
}
|
|
582
|
-
public async getLogBuffer(path: string): Promise<Buffer | undefined> {
|
|
583
|
-
// Always compress it. If it is in progress it can't be compressed on disk, but
|
|
584
|
-
// the compression ratio is so high it's worth it to compress it dynamically now,
|
|
585
|
-
// to speed up the network transfer.
|
|
586
|
-
let buffer = await getLogBuffer(path);
|
|
587
|
-
if (buffer && path.endsWith(".log")) {
|
|
588
|
-
buffer = await Zip.gzip(buffer);
|
|
589
|
-
}
|
|
590
|
-
return buffer;
|
|
591
|
-
}
|
|
592
|
-
}
|
|
593
|
-
|
|
594
|
-
export const DiskLoggerController = SocketFunction.register(
|
|
595
|
-
"DiskLoggerController-f76a6fdf-3bd5-4bd4-a183-55a8be0a5a32",
|
|
596
|
-
new DiskLoggerControllerBase(),
|
|
597
|
-
() => ({
|
|
598
|
-
getRemoteLogFiles: {},
|
|
599
|
-
getRemoteLogBuffer: {
|
|
600
|
-
clientHooks: [cacheCalls]
|
|
601
|
-
},
|
|
602
|
-
|
|
603
|
-
getLogFiles: {},
|
|
604
|
-
getLogBuffer: {},
|
|
605
|
-
}),
|
|
606
|
-
() => ({
|
|
607
|
-
hooks: [assertIsManagementUser],
|
|
608
|
-
}),
|
|
609
|
-
{
|
|
610
|
-
}
|
|
611
|
-
);
|
|
612
|
-
|
|
613
|
-
let diskLoggerController = getSyncedController(DiskLoggerController);
|