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,757 +0,0 @@
|
|
|
1
|
-
|
|
2
|
-
import preact from "preact";
|
|
3
|
-
import { qreact } from "../../../src/4-dom/qreact";
|
|
4
|
-
import { SocketFunction } from "socket-function/SocketFunction";
|
|
5
|
-
import { ErrorLogControllerBase } from "../../../src/diagnostics/errorLogs/ErrorLogController";
|
|
6
|
-
import { getSyncedController } from "../../../src/library-components/SyncedController";
|
|
7
|
-
import { getBrowserUrlNode } from "../../../src/-f-node-discovery/NodeDiscovery";
|
|
8
|
-
import { LogBlock, LogBlockInfo, LogClass, LogClassSummary, LogRaw, LogType, getLogClassCategorizer, isClassifierMatch, isUngroupedClass, logBlockFromBuffer, sanitizeMessageForMatch, ungroupedPrefix } from "../../../src/diagnostics/errorLogs/ErrorLogCore";
|
|
9
|
-
import { css } from "typesafecss";
|
|
10
|
-
import { cacheShallowConfigArgEqual } from "socket-function/src/caching";
|
|
11
|
-
import { nextId, sort } from "socket-function/src/misc";
|
|
12
|
-
import { URLParam } from "../../../src/library-components/URLParam";
|
|
13
|
-
import { ATag } from "../../../src/library-components/ATag";
|
|
14
|
-
import { Button } from "../../../src/library-components/Button";
|
|
15
|
-
import { doAtomicWrites } from "../../../src/2-proxy/PathValueProxyWatcher";
|
|
16
|
-
import { formatNumber } from "socket-function/src/formatting/format";
|
|
17
|
-
import { LogTimeSelector, TimelineInfo, getLogFilterRange } from "./LogTimeSelector";
|
|
18
|
-
import { errorColor, fatalColor, filterMachineIdsURL, filterThreadIdsURL, getLogFilterClasses, getLogFilterMachineIds, getLogFilterThreadIds, getPathArrayLax, infoColor, showErrorURL, showFatalURL, showInfoURL, showLiveLogsURL, showOldestLogsFirstURL, showWarningURL, warnColor } from "./logFiltering";
|
|
19
|
-
import { logErrors } from "../../../src/errors";
|
|
20
|
-
import { Querysub } from "../../../src/4-querysub/Querysub";
|
|
21
|
-
import { InputLabel } from "../../../src/library-components/InputLabel";
|
|
22
|
-
import { LogFilterUI } from "./LogFilterUI";
|
|
23
|
-
import { Table } from "../../../src/5-diagnostics/Table";
|
|
24
|
-
import { JSXFormatter, toSpaceCase } from "../../../src/5-diagnostics/GenericFormat";
|
|
25
|
-
import { SuppressUntil, logClassIdURL } from "./LogClassifiers";
|
|
26
|
-
import { measureFnc } from "socket-function/src/profiling/measure";
|
|
27
|
-
import { Icon } from "../../library-components/icons";
|
|
28
|
-
import { assertIsManagementUser, managementPageURL } from "../managementPages";
|
|
29
|
-
import { NodeViewerController } from "../NodeViewer";
|
|
30
|
-
|
|
31
|
-
export const downloadLogsOnceURL = new URLParam("downloadLogsOnce", false);
|
|
32
|
-
|
|
33
|
-
export type LogsParsed = {
|
|
34
|
-
class: LogClass;
|
|
35
|
-
count: number;
|
|
36
|
-
firstTime: number;
|
|
37
|
-
lastTime: number;
|
|
38
|
-
};
|
|
39
|
-
export type LogExample = {
|
|
40
|
-
message: string;
|
|
41
|
-
time: number;
|
|
42
|
-
threadId: string;
|
|
43
|
-
machineId: string;
|
|
44
|
-
class: LogClass;
|
|
45
|
-
};
|
|
46
|
-
export type LogProcessedData = {
|
|
47
|
-
examples: LogExample[];
|
|
48
|
-
parsed: LogsParsed[];
|
|
49
|
-
timeline: TimelineInfo[];
|
|
50
|
-
};
|
|
51
|
-
|
|
52
|
-
export class LogViewer extends qreact.Component {
|
|
53
|
-
state = {
|
|
54
|
-
allData: null as LogProcessedData | null,
|
|
55
|
-
ungroupedData: null as LogProcessedData | null,
|
|
56
|
-
unsuppressedData: null as LogProcessedData | null,
|
|
57
|
-
|
|
58
|
-
allMachineIds: null as Set<string> | null,
|
|
59
|
-
allThreadIds: null as Set<string> | null,
|
|
60
|
-
downloading: false,
|
|
61
|
-
};
|
|
62
|
-
blockBuffers: Buffer[] = [];
|
|
63
|
-
|
|
64
|
-
componentDidMount(): void {
|
|
65
|
-
if (downloadLogsOnceURL.value) {
|
|
66
|
-
downloadLogsOnceURL.value = false;
|
|
67
|
-
logErrors(this.download());
|
|
68
|
-
}
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
@measureFnc
|
|
72
|
-
async download() {
|
|
73
|
-
Querysub.localCommit(() => {
|
|
74
|
-
this.state.downloading = true;
|
|
75
|
-
});
|
|
76
|
-
try {
|
|
77
|
-
|
|
78
|
-
const logController = LogViewerController.nodes[getBrowserUrlNode()];
|
|
79
|
-
|
|
80
|
-
let timeFilter = getLogFilterRange();
|
|
81
|
-
|
|
82
|
-
let blockInfos = await logController.scanBlocks();
|
|
83
|
-
|
|
84
|
-
// Only blocks in the time range
|
|
85
|
-
blockInfos = blockInfos.filter(info =>
|
|
86
|
-
!(info.endTime <= timeFilter.startTime || info.startTime >= timeFilter.endTime)
|
|
87
|
-
);
|
|
88
|
-
// NOTE: We COULD filter blocks to only blocks which contain the types we want, but...
|
|
89
|
-
// 1) We might have reclassified the examples
|
|
90
|
-
// 2) Blocks are fairly small (for now), so downloading them isn't that big of a deal.
|
|
91
|
-
|
|
92
|
-
let blockBuffers = await logController.readBlocks({ files: blockInfos.map(info => info.fileName) });
|
|
93
|
-
|
|
94
|
-
if (showLiveLogsURL.value) {
|
|
95
|
-
let liveBlocks = await logController.readAllLiveBlocks();
|
|
96
|
-
blockBuffers.push(...liveBlocks);
|
|
97
|
-
}
|
|
98
|
-
this.blockBuffers = blockBuffers;
|
|
99
|
-
await this.parse();
|
|
100
|
-
} finally {
|
|
101
|
-
Querysub.localCommit(() => {
|
|
102
|
-
this.state.downloading = false;
|
|
103
|
-
});
|
|
104
|
-
}
|
|
105
|
-
}
|
|
106
|
-
@measureFnc
|
|
107
|
-
async parse() {
|
|
108
|
-
let timeFilter = getLogFilterRange();
|
|
109
|
-
|
|
110
|
-
let byClass = new Map<string, LogsParsed>();
|
|
111
|
-
function getByClass(classObj: LogClass) {
|
|
112
|
-
let obj = byClass.get(classObj.id);
|
|
113
|
-
if (!obj) {
|
|
114
|
-
obj = { class: classObj, count: 0, firstTime: Number.MAX_SAFE_INTEGER, lastTime: 0, };
|
|
115
|
-
byClass.set(classObj.id, obj);
|
|
116
|
-
}
|
|
117
|
-
return obj;
|
|
118
|
-
}
|
|
119
|
-
let examples: LogExample[] = [];
|
|
120
|
-
let timeline: (TimelineInfo & { class: LogClass })[] = [];
|
|
121
|
-
|
|
122
|
-
let byClassUnsuppressed = new Map<string, LogsParsed>();
|
|
123
|
-
function getByClassUnsuppressed(classObj: LogClass) {
|
|
124
|
-
let obj = byClassUnsuppressed.get(classObj.id);
|
|
125
|
-
if (!obj) {
|
|
126
|
-
obj = { class: classObj, count: 0, firstTime: Number.MAX_SAFE_INTEGER, lastTime: 0, };
|
|
127
|
-
byClassUnsuppressed.set(classObj.id, obj);
|
|
128
|
-
}
|
|
129
|
-
return obj;
|
|
130
|
-
}
|
|
131
|
-
let examplesUnsuppressed: LogExample[] = [];
|
|
132
|
-
let timelineUnsuppressed: (TimelineInfo & { class: LogClass })[] = [];
|
|
133
|
-
|
|
134
|
-
const blockBuffers = this.blockBuffers;
|
|
135
|
-
const logController = LogViewerController.nodes[getBrowserUrlNode()];
|
|
136
|
-
|
|
137
|
-
let classObjs = await logController.getClasses();
|
|
138
|
-
let classObjLookup = new Map(classObjs.map(obj => [obj.id, obj] as const));
|
|
139
|
-
|
|
140
|
-
let classIds = new Set(getLogFilterClasses());
|
|
141
|
-
let machineIds = new Set(getLogFilterMachineIds());
|
|
142
|
-
let threadIds = new Set(getLogFilterThreadIds());
|
|
143
|
-
|
|
144
|
-
let showFatal = showFatalURL.value;
|
|
145
|
-
let showError = showErrorURL.value;
|
|
146
|
-
let showWarning = showWarningURL.value;
|
|
147
|
-
let showInfo = showInfoURL.value;
|
|
148
|
-
function isClassInclude(classObj: LogClass) {
|
|
149
|
-
if (classIds.size > 0) {
|
|
150
|
-
if (!classIds.has(classObj.id)) {
|
|
151
|
-
return false;
|
|
152
|
-
}
|
|
153
|
-
}
|
|
154
|
-
if (!showFatal && classObj.type === "fatal") return false;
|
|
155
|
-
if (!showError && classObj.type === "error") return false;
|
|
156
|
-
if (!showWarning && classObj.type === "warn") return false;
|
|
157
|
-
if (!showInfo && classObj.type === "info") return false;
|
|
158
|
-
return true;
|
|
159
|
-
}
|
|
160
|
-
|
|
161
|
-
let allMachineIds = new Set<string>();
|
|
162
|
-
let allThreadIds = new Set<string>();
|
|
163
|
-
|
|
164
|
-
let blocks: LogBlock[] = [];
|
|
165
|
-
for (let buffer of blockBuffers) {
|
|
166
|
-
let block = await logBlockFromBuffer(buffer);
|
|
167
|
-
allMachineIds.add(block.machineId);
|
|
168
|
-
allThreadIds.add(block.threadId);
|
|
169
|
-
|
|
170
|
-
if (machineIds.size > 0 && !machineIds.has(block.machineId)) {
|
|
171
|
-
continue;
|
|
172
|
-
}
|
|
173
|
-
if (threadIds.size > 0 && !threadIds.has(block.threadId)) {
|
|
174
|
-
continue;
|
|
175
|
-
}
|
|
176
|
-
|
|
177
|
-
blocks.push(block);
|
|
178
|
-
}
|
|
179
|
-
|
|
180
|
-
let classifier = getLogClassCategorizer(classObjs);
|
|
181
|
-
|
|
182
|
-
for (let block of blocks) {
|
|
183
|
-
// Only look at classes, ignore summary data (except for threadId/machineId)
|
|
184
|
-
const threadId = block.threadId;
|
|
185
|
-
const machineId = block.machineId;
|
|
186
|
-
function addToClass(classSummary: LogsParsed, config: {
|
|
187
|
-
count: number;
|
|
188
|
-
time: number;
|
|
189
|
-
}) {
|
|
190
|
-
classSummary.count += config.count;
|
|
191
|
-
classSummary.firstTime = Math.min(classSummary.firstTime, config.time);
|
|
192
|
-
classSummary.lastTime = Math.max(classSummary.lastTime, config.time);
|
|
193
|
-
}
|
|
194
|
-
|
|
195
|
-
function addRaw(classObj: LogClass, log: LogRaw) {
|
|
196
|
-
if (!isClassInclude(classObj)) return;
|
|
197
|
-
let example: LogExample = {
|
|
198
|
-
class: classObj,
|
|
199
|
-
message: log.message,
|
|
200
|
-
threadId,
|
|
201
|
-
machineId,
|
|
202
|
-
time: log.time,
|
|
203
|
-
};
|
|
204
|
-
let timelineObj: TimelineInfo & { class: LogClass } = {
|
|
205
|
-
count: 1,
|
|
206
|
-
startTime: log.time,
|
|
207
|
-
endTime: log.time,
|
|
208
|
-
fatalCount: classObj.type === "fatal" ? 1 : 0,
|
|
209
|
-
errorCount: classObj.type === "error" ? 1 : 0,
|
|
210
|
-
warnCount: classObj.type === "warn" ? 1 : 0,
|
|
211
|
-
infoCount: classObj.type === "info" ? 1 : 0,
|
|
212
|
-
class: classObj,
|
|
213
|
-
};
|
|
214
|
-
examples.push(example);
|
|
215
|
-
timeline.push(timelineObj);
|
|
216
|
-
addToClass(getByClass(classObj), { count: 1, time: example.time });
|
|
217
|
-
|
|
218
|
-
if (example.time > classObj.suppressUntil) {
|
|
219
|
-
examplesUnsuppressed.push(example);
|
|
220
|
-
timelineUnsuppressed.push(timelineObj);
|
|
221
|
-
addToClass(getByClassUnsuppressed(classObj), { count: 1, time: example.time });
|
|
222
|
-
}
|
|
223
|
-
}
|
|
224
|
-
function addClassSummary(classObj: LogClass, summary: LogClassSummary) {
|
|
225
|
-
if (!isClassInclude(classObj)) return;
|
|
226
|
-
let timelineObj: TimelineInfo & { class: LogClass } = {
|
|
227
|
-
count: summary.count,
|
|
228
|
-
startTime: summary.firstTime,
|
|
229
|
-
endTime: summary.lastTime,
|
|
230
|
-
fatalCount: classObj.type === "fatal" ? summary.count : 0,
|
|
231
|
-
errorCount: classObj.type === "error" ? summary.count : 0,
|
|
232
|
-
warnCount: classObj.type === "warn" ? summary.count : 0,
|
|
233
|
-
infoCount: classObj.type === "info" ? summary.count : 0,
|
|
234
|
-
class: classObj,
|
|
235
|
-
};
|
|
236
|
-
timeline.push(timelineObj);
|
|
237
|
-
addToClass(getByClass(classObj), { count: summary.count, time: summary.firstTime });
|
|
238
|
-
addToClass(getByClass(classObj), { count: 0, time: summary.lastTime });
|
|
239
|
-
// NOTE: If the user suppressed to the current moment, pending blocks will not be caught,
|
|
240
|
-
// as their last time will keep increasing. BUT, users should suppress for at least a day,
|
|
241
|
-
// so this shouldn't be an issue.
|
|
242
|
-
if (summary.lastTime > classObj.suppressUntil) {
|
|
243
|
-
timelineUnsuppressed.push(timelineObj);
|
|
244
|
-
addToClass(getByClassUnsuppressed(classObj), { count: summary.count, time: summary.firstTime });
|
|
245
|
-
addToClass(getByClassUnsuppressed(classObj), { count: 0, time: summary.lastTime });
|
|
246
|
-
}
|
|
247
|
-
}
|
|
248
|
-
|
|
249
|
-
for (let classSummary of Object.values(block.classes)) {
|
|
250
|
-
let classObj: LogClass = classObjLookup.get(classSummary.logClassId) || {
|
|
251
|
-
id: ungroupedPrefix + classSummary.logClassType,
|
|
252
|
-
title: "missing Class",
|
|
253
|
-
description: "missing class",
|
|
254
|
-
createTime: 0,
|
|
255
|
-
type: "fatal",
|
|
256
|
-
match: [],
|
|
257
|
-
suppressUntil: 0
|
|
258
|
-
};
|
|
259
|
-
// ALWAYS reclassify examples
|
|
260
|
-
{
|
|
261
|
-
classSummary = { ...classSummary };
|
|
262
|
-
for (let example of classSummary.examples) {
|
|
263
|
-
if (!(timeFilter.startTime <= example.time && example.time < timeFilter.endTime)) {
|
|
264
|
-
continue;
|
|
265
|
-
}
|
|
266
|
-
let newClass = classifier(example, classObj.type);
|
|
267
|
-
addRaw(newClass, example);
|
|
268
|
-
}
|
|
269
|
-
|
|
270
|
-
classSummary.count -= classSummary.examples.length;
|
|
271
|
-
if (classSummary.count <= 0) continue;
|
|
272
|
-
}
|
|
273
|
-
addClassSummary(classObj, classSummary);
|
|
274
|
-
}
|
|
275
|
-
}
|
|
276
|
-
|
|
277
|
-
let classList = Array.from(byClass.values());
|
|
278
|
-
let classListUnsuppressed = Array.from(byClassUnsuppressed.values());
|
|
279
|
-
if (showOldestLogsFirstURL.value) {
|
|
280
|
-
sort(classList, x => x.count);
|
|
281
|
-
sort(examples, x => x.time);
|
|
282
|
-
sort(timeline, x => x.startTime);
|
|
283
|
-
|
|
284
|
-
sort(classListUnsuppressed, x => x.firstTime);
|
|
285
|
-
sort(examplesUnsuppressed, x => x.time);
|
|
286
|
-
sort(timelineUnsuppressed, x => x.startTime);
|
|
287
|
-
} else {
|
|
288
|
-
sort(classList, x => -x.count);
|
|
289
|
-
sort(examples, x => -x.time);
|
|
290
|
-
sort(timeline, x => -x.startTime);
|
|
291
|
-
|
|
292
|
-
sort(classListUnsuppressed, x => -x.firstTime);
|
|
293
|
-
sort(examplesUnsuppressed, x => -x.time);
|
|
294
|
-
sort(timelineUnsuppressed, x => -x.startTime);
|
|
295
|
-
}
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
Querysub.localCommit(() => {
|
|
299
|
-
doAtomicWrites(() => {
|
|
300
|
-
this.state.allMachineIds = allMachineIds;
|
|
301
|
-
this.state.allThreadIds = allThreadIds;
|
|
302
|
-
|
|
303
|
-
this.state.allData = {
|
|
304
|
-
examples,
|
|
305
|
-
parsed: classList,
|
|
306
|
-
timeline,
|
|
307
|
-
};
|
|
308
|
-
this.state.ungroupedData = {
|
|
309
|
-
examples: examples.filter(a => isUngroupedClass(a.class)),
|
|
310
|
-
parsed: classList.filter(a => isUngroupedClass(a.class)),
|
|
311
|
-
timeline: timeline.filter(a => isUngroupedClass(a.class)),
|
|
312
|
-
};
|
|
313
|
-
this.state.unsuppressedData = {
|
|
314
|
-
examples: examplesUnsuppressed.filter(a => !isUngroupedClass(a.class)),
|
|
315
|
-
parsed: classListUnsuppressed.filter(a => !isUngroupedClass(a.class)),
|
|
316
|
-
timeline: timelineUnsuppressed.filter(a => !isUngroupedClass(a.class)),
|
|
317
|
-
};
|
|
318
|
-
});
|
|
319
|
-
});
|
|
320
|
-
}
|
|
321
|
-
render() {
|
|
322
|
-
let blockInfos = LogViewerSynced(getBrowserUrlNode()).scanBlocks();
|
|
323
|
-
blockInfos = filterBlocks({
|
|
324
|
-
blockInfos,
|
|
325
|
-
machineIdsURL: filterMachineIdsURL.value,
|
|
326
|
-
threadIdsURL: filterThreadIdsURL.value,
|
|
327
|
-
});
|
|
328
|
-
|
|
329
|
-
const reparse = () => this.parse();
|
|
330
|
-
|
|
331
|
-
function divBorder(
|
|
332
|
-
outer: string,
|
|
333
|
-
borderColor: string,
|
|
334
|
-
content: preact.ComponentChild
|
|
335
|
-
) {
|
|
336
|
-
let border = 10;
|
|
337
|
-
const fullMinusBorder = `calc(100% - ${border}px)` as const;
|
|
338
|
-
let createBorder = () => <div
|
|
339
|
-
class={
|
|
340
|
-
css.fillBoth
|
|
341
|
-
.backgroundImage(`
|
|
342
|
-
repeating-linear-gradient(
|
|
343
|
-
-45deg,
|
|
344
|
-
${borderColor},
|
|
345
|
-
${borderColor} 12px,
|
|
346
|
-
transparent 10px,
|
|
347
|
-
transparent 23px
|
|
348
|
-
)
|
|
349
|
-
`)
|
|
350
|
-
}
|
|
351
|
-
/>;
|
|
352
|
-
return (
|
|
353
|
-
<div
|
|
354
|
-
class={css.pad2(border).relative + outer}
|
|
355
|
-
>
|
|
356
|
-
<div class={css.absolute.pos(0, 0).size("100%", border)}>
|
|
357
|
-
{createBorder()}
|
|
358
|
-
</div>
|
|
359
|
-
<div class={css.absolute.pos(0, border).size(border, fullMinusBorder)}>
|
|
360
|
-
{createBorder()}
|
|
361
|
-
</div>
|
|
362
|
-
<div class={css.absolute.pos(border, fullMinusBorder).size(fullMinusBorder, border)}>
|
|
363
|
-
{createBorder()}
|
|
364
|
-
</div>
|
|
365
|
-
<div class={css.absolute.pos(fullMinusBorder, border).size(border, fullMinusBorder)}>
|
|
366
|
-
{createBorder()}
|
|
367
|
-
</div>
|
|
368
|
-
<div class={css.pad2(5)}>
|
|
369
|
-
{content}
|
|
370
|
-
</div>
|
|
371
|
-
</div>
|
|
372
|
-
);
|
|
373
|
-
}
|
|
374
|
-
|
|
375
|
-
return (
|
|
376
|
-
<div class={css.pad2(10).vbox(10).fillWidth.center}>
|
|
377
|
-
<div class={css.fillWidth.hbox(10)}>
|
|
378
|
-
<div class={css.marginAuto} />
|
|
379
|
-
|
|
380
|
-
<h1>File Overview ({blockInfos?.length} files)</h1>
|
|
381
|
-
|
|
382
|
-
<div class={css.marginAuto} />
|
|
383
|
-
</div>
|
|
384
|
-
<LogTimeSelector infos={blockInfos} hotkeys />
|
|
385
|
-
|
|
386
|
-
<div class={css.fillWidth.hbox(10).alignItems("start")}>
|
|
387
|
-
<Button
|
|
388
|
-
flavor="large"
|
|
389
|
-
class={css.fontSize(40, "important").flexShrink0}
|
|
390
|
-
showHotkeys
|
|
391
|
-
hotkeys={["Enter", "Space"]}
|
|
392
|
-
onClick={async () => this.download()}
|
|
393
|
-
>
|
|
394
|
-
Download
|
|
395
|
-
</Button>
|
|
396
|
-
<LogFilterUI
|
|
397
|
-
allMachineIds={this.state.allMachineIds}
|
|
398
|
-
allThreadIds={this.state.allThreadIds}
|
|
399
|
-
/>
|
|
400
|
-
</div>
|
|
401
|
-
|
|
402
|
-
<style>
|
|
403
|
-
{`
|
|
404
|
-
.loadingSpinner {
|
|
405
|
-
animation: spin 3s linear infinite;
|
|
406
|
-
}
|
|
407
|
-
@keyframes spin {
|
|
408
|
-
0% { transform: rotate(0deg); }
|
|
409
|
-
100% { transform: rotate(360deg); }
|
|
410
|
-
}
|
|
411
|
-
`}
|
|
412
|
-
</style>
|
|
413
|
-
{this.state.downloading &&
|
|
414
|
-
<div class={css.fillWidth.center.paddingTop(200) + " loadingSpinner"}>
|
|
415
|
-
{Icon.loadingSymbol({ size: 300 })}
|
|
416
|
-
</div>
|
|
417
|
-
}
|
|
418
|
-
|
|
419
|
-
{!!this.state.ungroupedData?.examples.length && (
|
|
420
|
-
divBorder(
|
|
421
|
-
css.hsl(-5, 40, 60),
|
|
422
|
-
"hsl(-5, 60%, 50%)",
|
|
423
|
-
<LogSection
|
|
424
|
-
key="ungroupedData"
|
|
425
|
-
reparse={reparse}
|
|
426
|
-
title="Add accurate and specific classifiers for these immediately, don't worry about fixing them yet"
|
|
427
|
-
data={this.state.ungroupedData}
|
|
428
|
-
showExamplesByDefault
|
|
429
|
-
/>
|
|
430
|
-
)
|
|
431
|
-
)}
|
|
432
|
-
{!!this.state.unsuppressedData?.examples.length && (
|
|
433
|
-
divBorder(
|
|
434
|
-
css.hsl(40, 40, 60),
|
|
435
|
-
"hsl(40, 60%, 50%)",
|
|
436
|
-
<LogSection
|
|
437
|
-
key="unsuppressedData"
|
|
438
|
-
reparse={reparse}
|
|
439
|
-
title="Create tickets for these logs, and then suppress them"
|
|
440
|
-
addSuppressColumns
|
|
441
|
-
data={this.state.unsuppressedData}
|
|
442
|
-
/>
|
|
443
|
-
)
|
|
444
|
-
)}
|
|
445
|
-
{this.state.allData && (
|
|
446
|
-
divBorder(
|
|
447
|
-
//css.hsl(210, 40, 60),
|
|
448
|
-
"",
|
|
449
|
-
"hsl(210, 60%, 50%)",
|
|
450
|
-
<LogSection
|
|
451
|
-
key="allData"
|
|
452
|
-
reparse={reparse}
|
|
453
|
-
title="All Data"
|
|
454
|
-
data={this.state.allData}
|
|
455
|
-
/>
|
|
456
|
-
)
|
|
457
|
-
)}
|
|
458
|
-
</div>
|
|
459
|
-
);
|
|
460
|
-
}
|
|
461
|
-
}
|
|
462
|
-
|
|
463
|
-
const filterBlocks = cacheShallowConfigArgEqual((
|
|
464
|
-
config: {
|
|
465
|
-
blockInfos: LogBlockInfo[] | undefined;
|
|
466
|
-
machineIdsURL: string;
|
|
467
|
-
threadIdsURL: string;
|
|
468
|
-
}
|
|
469
|
-
): LogBlockInfo[] => {
|
|
470
|
-
const { blockInfos, machineIdsURL, threadIdsURL } = config;
|
|
471
|
-
let machineIds = new Set(getPathArrayLax(machineIdsURL));
|
|
472
|
-
let threadIds = new Set(getPathArrayLax(threadIdsURL));
|
|
473
|
-
|
|
474
|
-
return blockInfos?.filter(info =>
|
|
475
|
-
(!machineIds.size || machineIds.has(info.machineId)) &&
|
|
476
|
-
(!threadIds.size || threadIds.has(info.threadId))
|
|
477
|
-
) || [];
|
|
478
|
-
});
|
|
479
|
-
|
|
480
|
-
let lastClassifierUpdated: LogClass | undefined;
|
|
481
|
-
class LogSection extends qreact.Component<{
|
|
482
|
-
title: string;
|
|
483
|
-
data: LogProcessedData;
|
|
484
|
-
showExamplesByDefault?: boolean;
|
|
485
|
-
addSuppressColumns?: boolean;
|
|
486
|
-
reparse: () => void;
|
|
487
|
-
}> {
|
|
488
|
-
state = {
|
|
489
|
-
previewFilter: "",
|
|
490
|
-
summaryExpanded: { value: true },
|
|
491
|
-
examplesExpanded: { value: !!this.props.showExamplesByDefault },
|
|
492
|
-
};
|
|
493
|
-
render() {
|
|
494
|
-
let { title, data } = this.props;
|
|
495
|
-
let previewFilter = this.state.previewFilter;
|
|
496
|
-
if (previewFilter) {
|
|
497
|
-
data = { ...data };
|
|
498
|
-
let tempClassifier: LogClass = {
|
|
499
|
-
id: "",
|
|
500
|
-
title: "",
|
|
501
|
-
createTime: 0,
|
|
502
|
-
description: "",
|
|
503
|
-
match: [{ type: "includes", value: previewFilter }],
|
|
504
|
-
suppressUntil: 0,
|
|
505
|
-
type: "info",
|
|
506
|
-
};
|
|
507
|
-
data.examples = data.examples.filter(example =>
|
|
508
|
-
isClassifierMatch(tempClassifier, example)
|
|
509
|
-
);
|
|
510
|
-
}
|
|
511
|
-
let { reparse } = this.props;
|
|
512
|
-
const createClassifier = async (type: LogType) => {
|
|
513
|
-
let previewFilter = this.state.previewFilter;
|
|
514
|
-
// Wait for the blur to finish before resetting previewFilter
|
|
515
|
-
Querysub.onCommitFinished(() => {
|
|
516
|
-
setImmediate(() => {
|
|
517
|
-
Querysub.localCommit(() => {
|
|
518
|
-
this.state.previewFilter = "";
|
|
519
|
-
});
|
|
520
|
-
});
|
|
521
|
-
});
|
|
522
|
-
let newClass: LogClass = {
|
|
523
|
-
id: nextId(),
|
|
524
|
-
title: toSpaceCase(previewFilter),
|
|
525
|
-
createTime: Date.now(),
|
|
526
|
-
description: previewFilter,
|
|
527
|
-
match: [{ type: "includes", value: previewFilter }],
|
|
528
|
-
suppressUntil: 0,
|
|
529
|
-
type,
|
|
530
|
-
};
|
|
531
|
-
lastClassifierUpdated = newClass;
|
|
532
|
-
await LogViewerController.nodes[getBrowserUrlNode()].updateClass(newClass);
|
|
533
|
-
await reparse();
|
|
534
|
-
};
|
|
535
|
-
const classFormatter: JSXFormatter<LogClass> = (x) => {
|
|
536
|
-
return (
|
|
537
|
-
<div class={css.hbox(4).wrap}>
|
|
538
|
-
{this.props.addSuppressColumns && (
|
|
539
|
-
<SuppressUntil
|
|
540
|
-
type={x.type}
|
|
541
|
-
time={x.suppressUntil}
|
|
542
|
-
updateClass={async newValue => {
|
|
543
|
-
let newClass = { ...x, ...newValue };
|
|
544
|
-
lastClassifierUpdated = newClass;
|
|
545
|
-
await LogViewerController.nodes[getBrowserUrlNode()].updateClass(newClass);
|
|
546
|
-
reparse();
|
|
547
|
-
}}
|
|
548
|
-
/>
|
|
549
|
-
)}
|
|
550
|
-
<ATag
|
|
551
|
-
target="_blank"
|
|
552
|
-
class={css.button.textDecoration("none", "important").hbox(0)}
|
|
553
|
-
values={[
|
|
554
|
-
{ param: logClassIdURL, value: x.id },
|
|
555
|
-
{ param: managementPageURL, value: "LogClassifiers" },
|
|
556
|
-
]}
|
|
557
|
-
>
|
|
558
|
-
(Edit {formatTypeColor(x.type, x.title)})
|
|
559
|
-
</ATag>
|
|
560
|
-
</div>
|
|
561
|
-
);
|
|
562
|
-
};
|
|
563
|
-
let totalCount = data.parsed.reduce((sum, x) => sum + x.count, 0);
|
|
564
|
-
return (
|
|
565
|
-
<div class={css.fillWidth.vbox(10).maxHeight("80vh")}>
|
|
566
|
-
<h1>{title}</h1>
|
|
567
|
-
<LogTimeSelector infos={data.timeline} />
|
|
568
|
-
<SideCollapsible
|
|
569
|
-
expanded={this.state.summaryExpanded}
|
|
570
|
-
collapsedTitle={`Summary (${formatNumber(data.parsed.length)})`}
|
|
571
|
-
>
|
|
572
|
-
<Table
|
|
573
|
-
columns={{
|
|
574
|
-
class: { formatter: classFormatter },
|
|
575
|
-
count: {
|
|
576
|
-
formatter: (x) => {
|
|
577
|
-
return (
|
|
578
|
-
<div class={css.relative}>
|
|
579
|
-
<div class={
|
|
580
|
-
css.absolute.pos(0, 0).size(`${x / totalCount * 100}%`, "100%")
|
|
581
|
-
.hsla(0, 0, 100, 0.75)
|
|
582
|
-
} />
|
|
583
|
-
<span class={css.relative}>{formatNumber(x)}</span>
|
|
584
|
-
</div>
|
|
585
|
-
);
|
|
586
|
-
}
|
|
587
|
-
},
|
|
588
|
-
firstTime: {},
|
|
589
|
-
lastTime: {},
|
|
590
|
-
}}
|
|
591
|
-
rows={data.parsed}
|
|
592
|
-
/>
|
|
593
|
-
</SideCollapsible>
|
|
594
|
-
{lastClassifierUpdated &&
|
|
595
|
-
<ATag
|
|
596
|
-
// NOTE: Not only is this nice, but... without it we would need
|
|
597
|
-
// to explicitly invalidate our cache, which is annoying to do with ATag.
|
|
598
|
-
target="_blank"
|
|
599
|
-
values={[
|
|
600
|
-
{ param: logClassIdURL, value: lastClassifierUpdated.id },
|
|
601
|
-
{ param: managementPageURL, value: "LogClassifiers" },
|
|
602
|
-
]}
|
|
603
|
-
class={css.hbox(4).marginLeft(10).hsla(0, 0, 100, 0.5).pad2(4, 1)}
|
|
604
|
-
>
|
|
605
|
-
View last updated classifier: {formatTypeColor(lastClassifierUpdated.type, lastClassifierUpdated.title)}
|
|
606
|
-
</ATag>
|
|
607
|
-
}
|
|
608
|
-
{/* NOTE: We show classifiers for all views, as you might want to reclassify even classified data,
|
|
609
|
-
by using a higher priority. */}
|
|
610
|
-
{this.state.examplesExpanded.value && <div class={css.hbox(10)}>
|
|
611
|
-
<InputLabel
|
|
612
|
-
label="Create Classifier"
|
|
613
|
-
value={previewFilter}
|
|
614
|
-
hot
|
|
615
|
-
onChangeValue={(value) => {
|
|
616
|
-
this.state.previewFilter = value;
|
|
617
|
-
}}
|
|
618
|
-
onKeyDown={async e => {
|
|
619
|
-
if (e.key === "Enter") {
|
|
620
|
-
await createClassifier(data.examples[0]?.class.type || "error");
|
|
621
|
-
}
|
|
622
|
-
}}
|
|
623
|
-
/>
|
|
624
|
-
{previewFilter && (
|
|
625
|
-
<div class={css.hbox(4).hsla(0, 0, 100, 0.5).pad2(4, 1)}>
|
|
626
|
-
<span>Matches</span>
|
|
627
|
-
<span>
|
|
628
|
-
{formatNumber(data.examples.length)} / {formatNumber(this.props.data.examples.length)}
|
|
629
|
-
</span>
|
|
630
|
-
<Button class={css.hbox(6).hsl(fatalColor.h, fatalColor.s, fatalColor.l)} onClick={() => createClassifier("fatal")}>
|
|
631
|
-
Fatal
|
|
632
|
-
</Button>
|
|
633
|
-
<Button class={css.hbox(6).hsl(errorColor.h, errorColor.s, errorColor.l)} onClick={() => createClassifier("error")}>
|
|
634
|
-
Error
|
|
635
|
-
</Button>
|
|
636
|
-
<Button class={css.hbox(6).hsl(warnColor.h, warnColor.s, warnColor.l)} onClick={() => createClassifier("warn")}>
|
|
637
|
-
Warning
|
|
638
|
-
</Button>
|
|
639
|
-
<Button class={css.hbox(6).hsl(infoColor.h, infoColor.s, infoColor.l)} onClick={() => createClassifier("info")}>
|
|
640
|
-
Info
|
|
641
|
-
</Button>
|
|
642
|
-
</div>
|
|
643
|
-
)}
|
|
644
|
-
</div>}
|
|
645
|
-
|
|
646
|
-
<SideCollapsible
|
|
647
|
-
expanded={this.state.examplesExpanded}
|
|
648
|
-
collapsedTitle={`Examples (${formatNumber(data.examples.length)})`}
|
|
649
|
-
defaultCollapsed={!this.props.showExamplesByDefault}
|
|
650
|
-
>
|
|
651
|
-
<Table
|
|
652
|
-
columns={{
|
|
653
|
-
class: {
|
|
654
|
-
formatter: classFormatter,
|
|
655
|
-
},
|
|
656
|
-
time: {},
|
|
657
|
-
machineId: {},
|
|
658
|
-
threadId: {},
|
|
659
|
-
message: {
|
|
660
|
-
formatter: (x) => {
|
|
661
|
-
if (!previewFilter) return x;
|
|
662
|
-
let matchIndex = sanitizeMessageForMatch(x).indexOf(previewFilter.toLowerCase());
|
|
663
|
-
if (matchIndex === -1) return x;
|
|
664
|
-
let before = x.slice(0, matchIndex);
|
|
665
|
-
let match = x.slice(matchIndex, matchIndex + previewFilter.length);
|
|
666
|
-
let after = x.slice(matchIndex + previewFilter.length);
|
|
667
|
-
return (
|
|
668
|
-
<div>
|
|
669
|
-
{before}
|
|
670
|
-
<span class={css.hsl(60, 75, 50)}>{match}</span>
|
|
671
|
-
{after}
|
|
672
|
-
</div>
|
|
673
|
-
);
|
|
674
|
-
},
|
|
675
|
-
},
|
|
676
|
-
}}
|
|
677
|
-
rows={data.examples}
|
|
678
|
-
/>
|
|
679
|
-
</SideCollapsible>
|
|
680
|
-
</div>
|
|
681
|
-
);
|
|
682
|
-
}
|
|
683
|
-
}
|
|
684
|
-
class SideCollapsible extends qreact.Component<{
|
|
685
|
-
defaultCollapsed?: boolean;
|
|
686
|
-
collapsedTitle?: string;
|
|
687
|
-
expanded: { value: boolean }
|
|
688
|
-
}> {
|
|
689
|
-
render() {
|
|
690
|
-
let expanded = this.props.expanded.value;
|
|
691
|
-
return (
|
|
692
|
-
<div class={
|
|
693
|
-
css.fillWidth.hbox(10).alignItems("start").overflowAuto
|
|
694
|
-
//+ css.minHeight(expanded ? 100 : 50)
|
|
695
|
-
+ (expanded && css.minHeight(100).maxHeight("60%") || css.minHeight(50))
|
|
696
|
-
}>
|
|
697
|
-
<Button
|
|
698
|
-
square
|
|
699
|
-
flavor="noui"
|
|
700
|
-
onClick={() => this.props.expanded.value = !expanded}
|
|
701
|
-
class={
|
|
702
|
-
(expanded && css.hsla(0, 0, 100, 0.5) || css.hsla(0, 0, 100, 0.25))
|
|
703
|
-
+ css.background(expanded ? "hsla(0, 0%, 100%, 0.25)" : "hsla(0, 0%, 100%, 0.5)", "hover")
|
|
704
|
-
+ css.button.borderWidth(0).flex
|
|
705
|
-
+ css.outline("none", "important")
|
|
706
|
-
}
|
|
707
|
-
>
|
|
708
|
-
{(expanded && Icon.chevronDown || Icon.chevronRight)({
|
|
709
|
-
size: 30,
|
|
710
|
-
style: {
|
|
711
|
-
position: "relative",
|
|
712
|
-
top: expanded ? -3 : 0,
|
|
713
|
-
left: expanded ? 0 : 4,
|
|
714
|
-
}
|
|
715
|
-
})}
|
|
716
|
-
{!expanded && <div class={css.fontSize(20).pad2(8, 2)}>{this.props.collapsedTitle}</div> || ""}
|
|
717
|
-
</Button>
|
|
718
|
-
|
|
719
|
-
{expanded && this.props.children}
|
|
720
|
-
</div>
|
|
721
|
-
);
|
|
722
|
-
}
|
|
723
|
-
}
|
|
724
|
-
|
|
725
|
-
export function formatTypeColor(type: LogType, contents: preact.ComponentChild) {
|
|
726
|
-
let color = (
|
|
727
|
-
(type === "fatal" && fatalColor) ||
|
|
728
|
-
(type === "error" && errorColor) ||
|
|
729
|
-
(type === "warn" && warnColor) ||
|
|
730
|
-
(type === "info" && infoColor) ||
|
|
731
|
-
fatalColor
|
|
732
|
-
);
|
|
733
|
-
return (
|
|
734
|
-
<div class={css.pad2(4, 1).color("white").hsl(color.h, color.s, color.l)}>
|
|
735
|
-
{contents}
|
|
736
|
-
</div>
|
|
737
|
-
);
|
|
738
|
-
}
|
|
739
|
-
|
|
740
|
-
export const LogViewerController = SocketFunction.register(
|
|
741
|
-
"LogViewerController-257d81a9-a170-441e-9ff3-7d9d86b6e80a",
|
|
742
|
-
new ErrorLogControllerBase(),
|
|
743
|
-
() => ({
|
|
744
|
-
scanBlocks: {},
|
|
745
|
-
readBlocks: {},
|
|
746
|
-
readAllLiveBlocks: {},
|
|
747
|
-
getClasses: {},
|
|
748
|
-
updateClass: {},
|
|
749
|
-
onClassesUpdated: {},
|
|
750
|
-
}),
|
|
751
|
-
() => ({
|
|
752
|
-
hooks: [assertIsManagementUser],
|
|
753
|
-
}),
|
|
754
|
-
);
|
|
755
|
-
|
|
756
|
-
export const LogViewerSynced = getSyncedController(LogViewerController);
|
|
757
|
-
const NodeViewerSynced = getSyncedController(NodeViewerController);
|