querysub 0.356.0 → 0.358.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.
Files changed (76) hide show
  1. package/.cursorrules +9 -0
  2. package/bin/movelogs.js +4 -0
  3. package/package.json +13 -6
  4. package/scripts/postinstall.js +23 -0
  5. package/src/-a-archives/archiveCache.ts +10 -12
  6. package/src/-a-archives/archives.ts +29 -0
  7. package/src/-a-archives/archivesBackBlaze.ts +60 -12
  8. package/src/-a-archives/archivesDisk.ts +39 -13
  9. package/src/-a-archives/archivesLimitedCache.ts +21 -0
  10. package/src/-a-archives/archivesMemoryCache.ts +374 -0
  11. package/src/-a-archives/archivesPrivateFileSystem.ts +22 -0
  12. package/src/-g-core-values/NodeCapabilities.ts +3 -0
  13. package/src/0-path-value-core/auditLogs.ts +5 -1
  14. package/src/0-path-value-core/pathValueCore.ts +7 -7
  15. package/src/4-dom/qreact.tsx +1 -0
  16. package/src/4-querysub/Querysub.ts +1 -5
  17. package/src/config.ts +5 -0
  18. package/src/deployManager/components/MachineDetailPage.tsx +43 -2
  19. package/src/deployManager/components/MachinesListPage.tsx +10 -2
  20. package/src/deployManager/machineApplyMainCode.ts +3 -3
  21. package/src/deployManager/machineSchema.ts +39 -0
  22. package/src/diagnostics/MachineThreadInfo.tsx +235 -0
  23. package/src/diagnostics/NodeViewer.tsx +5 -3
  24. package/src/diagnostics/logs/FastArchiveAppendable.ts +79 -42
  25. package/src/diagnostics/logs/FastArchiveController.ts +102 -63
  26. package/src/diagnostics/logs/FastArchiveViewer.tsx +36 -8
  27. package/src/diagnostics/logs/IndexedLogs/BufferIndex.ts +462 -0
  28. package/src/diagnostics/logs/IndexedLogs/BufferIndexCPP.cpp +327 -0
  29. package/src/diagnostics/logs/IndexedLogs/BufferIndexCPP.d.ts +18 -0
  30. package/src/diagnostics/logs/IndexedLogs/BufferIndexCPP.js +1 -0
  31. package/src/diagnostics/logs/IndexedLogs/BufferIndexHelpers.ts +222 -0
  32. package/src/diagnostics/logs/IndexedLogs/BufferIndexLogsOptimizationConstants.ts +22 -0
  33. package/src/diagnostics/logs/IndexedLogs/BufferIndexWAT.wat +1145 -0
  34. package/src/diagnostics/logs/IndexedLogs/BufferIndexWAT.wat.d.ts +178 -0
  35. package/src/diagnostics/logs/IndexedLogs/BufferListStreamer.ts +208 -0
  36. package/src/diagnostics/logs/IndexedLogs/BufferUnitIndex.ts +716 -0
  37. package/src/diagnostics/logs/IndexedLogs/BufferUnitSet.ts +146 -0
  38. package/src/diagnostics/logs/IndexedLogs/FilePathSelector.tsx +569 -0
  39. package/src/diagnostics/logs/IndexedLogs/FindProgressTracker.ts +45 -0
  40. package/src/diagnostics/logs/IndexedLogs/IndexedLogs.ts +685 -0
  41. package/src/diagnostics/logs/IndexedLogs/LogStreamer.ts +47 -0
  42. package/src/diagnostics/logs/IndexedLogs/LogViewer3.tsx +901 -0
  43. package/src/diagnostics/logs/IndexedLogs/TimeFileTree.ts +236 -0
  44. package/src/diagnostics/logs/IndexedLogs/binding.gyp +23 -0
  45. package/src/diagnostics/logs/IndexedLogs/moveIndexLogsToPublic.ts +251 -0
  46. package/src/diagnostics/logs/IndexedLogs/moveLogsEntry.ts +10 -0
  47. package/src/diagnostics/logs/LogViewer2.tsx +120 -55
  48. package/src/diagnostics/logs/TimeRangeSelector.tsx +5 -2
  49. package/src/diagnostics/logs/diskLogger.ts +32 -48
  50. package/src/diagnostics/logs/errorNotifications/ErrorNotificationController.ts +3 -2
  51. package/src/diagnostics/logs/errorNotifications/errorDigests.tsx +1 -0
  52. package/src/diagnostics/logs/errorNotifications2/errorNotifications2.ts +0 -0
  53. package/src/diagnostics/logs/lifeCycleAnalysis/LifeCyclePages.tsx +150 -0
  54. package/src/diagnostics/logs/lifeCycleAnalysis/lifeCycles.tsx +150 -15
  55. package/src/diagnostics/logs/lifeCycleAnalysis/test.ts +0 -0
  56. package/src/diagnostics/logs/lifeCycleAnalysis/test.wat +106 -0
  57. package/src/diagnostics/logs/lifeCycleAnalysis/test.wat.d.ts +2 -0
  58. package/src/diagnostics/logs/lifeCycleAnalysis/testHoist.ts +5 -0
  59. package/src/diagnostics/logs/logViewerExtractField.ts +2 -3
  60. package/src/diagnostics/managementPages.tsx +10 -0
  61. package/src/diagnostics/trackResources.ts +1 -1
  62. package/src/functional/limitProcessing.ts +39 -0
  63. package/src/misc/lz4_wasm_nodejs.d.ts +34 -0
  64. package/src/misc/lz4_wasm_nodejs.js +178 -0
  65. package/src/misc/lz4_wasm_nodejs_bg.js +94 -0
  66. package/src/misc/lz4_wasm_nodejs_bg.wasm +0 -0
  67. package/src/misc/lz4_wasm_nodejs_bg.wasm.d.ts +15 -0
  68. package/src/storage/CompressedStream.ts +13 -0
  69. package/src/storage/LZ4.ts +32 -0
  70. package/src/storage/ZSTD.ts +10 -0
  71. package/src/wat/watCompiler.ts +1716 -0
  72. package/src/wat/watGrammar.pegjs +93 -0
  73. package/src/wat/watHandler.ts +179 -0
  74. package/src/wat/watInstructions.txt +707 -0
  75. package/src/zip.ts +3 -89
  76. package/src/diagnostics/logs/lifeCycleAnalysis/spec.md +0 -125
@@ -0,0 +1,901 @@
1
+ module.hotreload = true;
2
+ import { qreact } from "../../../4-dom/qreact";
3
+ import { css } from "../../../4-dom/css";
4
+ import { t } from "../../../2-proxy/schema2";
5
+ import { Button } from "../../../library-components/Button";
6
+ import { InputLabel, InputLabelURL } from "../../../library-components/InputLabel";
7
+ import { getLoggers2, getLoggers2Async, LogDatum } from "../diskLogger";
8
+ import { list, timeInDay, keyByArray, sort, throttleFunction } from "socket-function/src/misc";
9
+ import { formatDateTime, formatDateTimeDetailed, formatNumber, formatTime, formatPercent } from "socket-function/src/formatting/format";
10
+ import { IndexedLogs, TimeFilePathWithSize } from "./IndexedLogs";
11
+ import { Querysub } from "../../../4-querysub/QuerysubController";
12
+ import { URLParam } from "../../../library-components/URLParam";
13
+ import { getOwnMachineId, getOwnThreadId } from "../../../-f-node-discovery/NodeDiscovery";
14
+ import { FilePathSelector, FilePathsByMachine } from "./FilePathSelector";
15
+ import { MachineThreadInfo } from "../../MachineThreadInfo";
16
+ import { TimeRangeSelector, getTimeRange } from "../TimeRangeSelector";
17
+ import { LZ4 } from "../../../storage/LZ4";
18
+ import { ColumnType, Table, TableType } from "../../../5-diagnostics/Table";
19
+ import { InputPicker } from "../../../library-components/InputPicker";
20
+ import { ObjectDisplay } from "../ObjectDisplay";
21
+ import { formatDateJSX } from "../../../misc/formatJSX";
22
+ import { PUBLIC_MOVE_THRESHOLD } from "./BufferIndexLogsOptimizationConstants";
23
+ import { atomic } from "../../../2-proxy/PathValueProxyWatcher";
24
+ import { errorToUndefined } from "querysub/src/errors";
25
+ import { IndexedLogResults, createEmptyIndexedLogResults } from "./BufferIndexHelpers";
26
+
27
+ let searchText = new URLParam("searchText", "");
28
+ let readLiveData = new URLParam("readLiveData", false);
29
+ let excludePendingResults = new URLParam("excludePendingResults", false);
30
+ let limitURL = new URLParam("limit", 100);
31
+
32
+ let savedPathsURL = new URLParam("savedPaths", "");
33
+ let selectedFieldsURL = new URLParam("selectedFields", {} as Record<string, boolean>);
34
+ let useRelativeTimeURL = new URLParam("useRelativeTime", true);
35
+
36
+ const defaultSelectedFields = {
37
+ param0: true,
38
+ time: true,
39
+ __NAME__: true,
40
+ __machineId: true,
41
+ __threadId: true,
42
+ __entry: true,
43
+ };
44
+
45
+ function mergeIndexedLogResults(existing: IndexedLogResults, incoming: IndexedLogResults): IndexedLogResults {
46
+ let readsByKey = new Map<string, typeof existing.reads[0]>();
47
+
48
+ for (let read of existing.reads) {
49
+ let key = `${read.cached}-${read.remote}`;
50
+ let existingRead = readsByKey.get(key);
51
+ if (existingRead) {
52
+ existingRead.count += read.count;
53
+ existingRead.size += read.size;
54
+ existingRead.totalSize = Math.max(existingRead.totalSize, read.totalSize);
55
+ existingRead.totalCount = Math.max(existingRead.totalCount, read.totalCount);
56
+ } else {
57
+ readsByKey.set(key, { ...read });
58
+ }
59
+ }
60
+
61
+ for (let read of incoming.reads) {
62
+ let key = `${read.cached}-${read.remote}`;
63
+ let existingRead = readsByKey.get(key);
64
+ if (existingRead) {
65
+ existingRead.count += read.count;
66
+ existingRead.size += read.size;
67
+ existingRead.totalSize = Math.max(existingRead.totalSize, read.totalSize);
68
+ existingRead.totalCount = Math.max(existingRead.totalCount, read.totalCount);
69
+ } else {
70
+ readsByKey.set(key, { ...read });
71
+ }
72
+ }
73
+
74
+ return {
75
+ matchCount: existing.matchCount + incoming.matchCount,
76
+ totalLocalFiles: existing.totalLocalFiles + incoming.totalLocalFiles,
77
+ totalBackblazeFiles: existing.totalBackblazeFiles + incoming.totalBackblazeFiles,
78
+ reads: Array.from(readsByKey.values()),
79
+ localFilesSearched: existing.localFilesSearched + incoming.localFilesSearched,
80
+ backblazeFilesSearched: existing.backblazeFilesSearched + incoming.backblazeFilesSearched,
81
+ totalBlockCount: existing.totalBlockCount + incoming.totalBlockCount,
82
+ blockCheckedCount: existing.blockCheckedCount + incoming.blockCheckedCount,
83
+ blocksCheckedCompressedSize: existing.blocksCheckedCompressedSize + incoming.blocksCheckedCompressedSize,
84
+ blocksCheckedDecompressedSize: existing.blocksCheckedDecompressedSize + incoming.blocksCheckedDecompressedSize,
85
+ blockErrors: [...existing.blockErrors, ...incoming.blockErrors],
86
+ fileErrors: [...existing.fileErrors, ...incoming.fileErrors],
87
+ remoteIndexesSearched: existing.remoteIndexesSearched + incoming.remoteIndexesSearched,
88
+ remoteIndexSize: existing.remoteIndexSize + incoming.remoteIndexSize,
89
+ localIndexesSearched: existing.localIndexesSearched + incoming.localIndexesSearched,
90
+ localIndexSize: existing.localIndexSize + incoming.localIndexSize,
91
+ timeToFirstMatch: Math.min(existing.timeToFirstMatch === 0 ? Infinity : existing.timeToFirstMatch, incoming.timeToFirstMatch === 0 ? Infinity : incoming.timeToFirstMatch),
92
+ fileFindTime: existing.fileFindTime + incoming.fileFindTime,
93
+ indexSearchTime: existing.indexSearchTime + incoming.indexSearchTime,
94
+ blockSearchTime: existing.blockSearchTime + incoming.blockSearchTime,
95
+ totalSearchTime: Math.max(existing.totalSearchTime, incoming.totalSearchTime),
96
+ remoteBlockCount: existing.remoteBlockCount + incoming.remoteBlockCount,
97
+ localBlockCount: existing.localBlockCount + incoming.localBlockCount,
98
+ remoteBlockCheckedCount: existing.remoteBlockCheckedCount + incoming.remoteBlockCheckedCount,
99
+ localBlockCheckedCount: existing.localBlockCheckedCount + incoming.localBlockCheckedCount,
100
+ };
101
+ }
102
+
103
+ export class LogViewer3 extends qreact.Component {
104
+ static renderInProgress = true;
105
+ state = t.state({
106
+ enableLogs: t.boolean(true),
107
+ enableInfos: t.boolean(true),
108
+ enableWarnings: t.boolean(true),
109
+ enableErrors: t.boolean(true),
110
+ results: t.atomic<LogDatum[]>([]),
111
+ searching: t.boolean,
112
+ searchingLogs: t.boolean,
113
+ searchingInfos: t.boolean,
114
+ searchingWarnings: t.boolean,
115
+ searchingErrors: t.boolean,
116
+ paths: t.atomic<TimeFilePathWithSize[]>([]),
117
+ loadingPaths: t.boolean,
118
+ stats: t.atomic<IndexedLogResults | undefined>(undefined),
119
+ hasSearched: t.boolean(false),
120
+ forceMoveStartTime: t.atomic<number | undefined>(undefined),
121
+ forceMoveEndTime: t.atomic<number | undefined>(undefined),
122
+ });
123
+
124
+ private searchSequenceNumber = 0;
125
+
126
+ componentDidMount(): void {
127
+ void this.loadPaths();
128
+ }
129
+
130
+ getPaths(): TimeFilePathWithSize[] {
131
+ let paths: TimeFilePathWithSize[] = [];
132
+ if (savedPathsURL.value) {
133
+ const compressed = Buffer.from(savedPathsURL.value, "base64");
134
+ const decompressed = LZ4.decompress(compressed);
135
+ paths = JSON.parse(decompressed.toString("utf8"));
136
+ } else {
137
+ paths = this.state.paths;
138
+ }
139
+ if (excludePendingResults.value) {
140
+ paths = paths.filter(x => x.logCount !== undefined);
141
+ }
142
+ return paths;
143
+ }
144
+
145
+ clearPaths() {
146
+ this.state.paths = [];
147
+ this.state.searchingLogs = false;
148
+ this.state.searchingInfos = false;
149
+ this.state.searchingWarnings = false;
150
+ this.state.searchingErrors = false;
151
+ }
152
+
153
+ freezePaths() {
154
+ const json = JSON.stringify(this.state.paths);
155
+ const buffer = Buffer.from(json, "utf8");
156
+ const compressed = LZ4.compress(buffer);
157
+ savedPathsURL.value = compressed.toString("base64");
158
+ }
159
+
160
+ clearFrozenPaths() {
161
+ savedPathsURL.value = "";
162
+ }
163
+
164
+ renderSearchStats() {
165
+ const stats = this.state.stats;
166
+ if (!stats) return undefined;
167
+
168
+ let totalSizeRead = 0;
169
+ let cachedSize = 0;
170
+ let cachedCount = 0;
171
+ let uncachedSize = 0;
172
+ let uncachedCount = 0;
173
+ let uncachedRemoteSize = 0;
174
+ let uncachedRemoteCount = 0;
175
+ let totalSize = 0;
176
+ let remoteTotalSize = 0;
177
+ let localTotalSize = 0;
178
+
179
+ for (let read of stats.reads) {
180
+ totalSizeRead += read.size;
181
+ if (read.cached) {
182
+ cachedSize += read.size;
183
+ cachedCount += read.count;
184
+ } else {
185
+ uncachedSize += read.size;
186
+ uncachedCount += read.count;
187
+ totalSize += read.totalSize;
188
+ if (read.remote) {
189
+ remoteTotalSize += read.totalSize;
190
+ } else {
191
+ localTotalSize += read.totalSize;
192
+ }
193
+ }
194
+ if (read.remote && !read.cached) {
195
+ uncachedRemoteSize += read.size;
196
+ uncachedRemoteCount += read.count;
197
+ }
198
+ }
199
+
200
+ const renderStage = (title: string, items: preact.ComponentChild[], hue: number | undefined, isFirst: boolean, isLast: boolean) => {
201
+ const triangleWidth = 20;
202
+
203
+ let color = hue ? css.hsl(hue, 70, 80) : css.hsl(0, 0, 100);
204
+ let colorhsl = hue ? css.hslcolor(hue, 80, 30) : css;
205
+
206
+ return (
207
+ <div className={css.vbox(2).relative.minWidth(200).paddingTop(8).paddingBottom(8).paddingRight(10).paddingLeft(isFirst ? 10 : 10 + triangleWidth) + color}>
208
+ <div className={css.fontWeight(600) + colorhsl}>{title}</div>
209
+ {items.map((item, i) => (
210
+ <div key={i}>{item}</div>
211
+ ))}
212
+ {!isLast && (
213
+ <svg width={triangleWidth} height="100" viewBox="0 0 20 100" preserveAspectRatio="none" className={css.absolute.right(-triangleWidth).top(0).fillHeight.zIndex(1)}>
214
+ <polygon points="0,0 20,50 0,100" fill={`hsl(${hue}, 70%, 80%)`} />
215
+ </svg>
216
+ )}
217
+ </div>
218
+ );
219
+ };
220
+
221
+ const fileItems = [
222
+ `${formatTime(stats.fileFindTime)} | ${formatNumber(stats.localFilesSearched + stats.backblazeFilesSearched)} / ${formatNumber(stats.totalLocalFiles + stats.totalBackblazeFiles)} | ${formatNumber(totalSize)}B`,
223
+ `remote ${formatNumber(stats.backblazeFilesSearched)} / ${formatNumber(stats.totalBackblazeFiles)} | ${formatNumber(remoteTotalSize)}B`,
224
+ `pending ${formatNumber(stats.localFilesSearched)} / ${formatNumber(stats.totalLocalFiles)} | ${formatNumber(localTotalSize)}B`,
225
+ ];
226
+
227
+ const totalIndexesSearched = stats.remoteIndexesSearched + stats.localIndexesSearched;
228
+ const totalIndexSize = stats.remoteIndexSize + stats.localIndexSize;
229
+ const indexItems = [
230
+ `${formatTime(stats.indexSearchTime)} | ${formatNumber(totalIndexesSearched)} | ${formatNumber(totalIndexSize)}B | ${formatNumber(totalSize / totalIndexSize)}X`,
231
+ `remote ${formatNumber(stats.remoteIndexesSearched)} | ${formatNumber(stats.remoteIndexSize)}B | ${formatNumber(remoteTotalSize / stats.remoteIndexSize)}X`,
232
+ `local ${formatNumber(stats.localIndexesSearched)} | ${formatNumber(stats.localIndexSize)}B | ${formatNumber(localTotalSize / stats.localIndexSize)}X`,
233
+ ];
234
+
235
+ const blockItems = [
236
+ <div title={`Scanned size = ${formatNumber(stats.blocksCheckedCompressedSize)}B, Decompressed size = ${formatNumber(stats.blocksCheckedDecompressedSize)}B, Compression ratio = ${formatNumber(stats.blocksCheckedDecompressedSize / stats.blocksCheckedCompressedSize)}X`}>{formatTime(stats.blockSearchTime)} | {formatNumber(stats.blockCheckedCount)} | {formatNumber(stats.blocksCheckedCompressedSize)}B | {formatNumber(stats.blocksCheckedDecompressedSize / stats.blocksCheckedCompressedSize)}X</div>,
237
+ `total scanned ${formatNumber(stats.totalBlockCount)} (${formatPercent(stats.blockCheckedCount / stats.totalBlockCount)})`,
238
+ `remote ${formatNumber(stats.remoteBlockCheckedCount)} / ${formatNumber(stats.remoteBlockCount)} (${formatPercent(stats.remoteBlockCheckedCount / stats.remoteBlockCount)}) | local ${formatNumber(stats.localBlockCheckedCount)} / ${formatNumber(stats.localBlockCount)} (${formatPercent(stats.localBlockCheckedCount / stats.localBlockCount)})`,
239
+ ];
240
+ let cacheItems = [
241
+ `disk read ${formatNumber(uncachedSize)}B (${formatNumber(uncachedCount)})`,
242
+ `remote ${formatPercent(uncachedRemoteSize / totalSizeRead)} (${formatNumber(uncachedRemoteCount)})`,
243
+ `cached ${formatNumber(cachedSize)} (${formatNumber(cachedCount)})`,
244
+ ];
245
+
246
+ const resultItems = [
247
+ `${formatTime(stats.timeToFirstMatch)} until first result`,
248
+ `${formatNumber(stats.matchCount)} results / ${limitURL.value} limit`,
249
+ ];
250
+
251
+ const doneItems = [
252
+ `${formatTime(stats.totalSearchTime)} total search time`,
253
+ ];
254
+
255
+ let hasErrors = stats.fileErrors.length > 0 || stats.blockErrors.length > 0;
256
+
257
+ return (
258
+ <div className={css.hbox(0).alignItems("stretch")}>
259
+ {renderStage("Files", fileItems, 290, true, false)}
260
+ {renderStage("Indexes", indexItems, 250, false, false)}
261
+ {renderStage("Blocks", blockItems, 210, false, false)}
262
+ {renderStage("Reads", cacheItems, 160, false, false)}
263
+ {renderStage("Results", resultItems, 120, false, false)}
264
+ {renderStage("Done", doneItems, undefined, false, !hasErrors)}
265
+ {(() => {
266
+ if (!hasErrors) return undefined;
267
+ let errorItems: preact.ComponentChild[] = [];
268
+ if (stats.fileErrors.length > 0) {
269
+ const errorTitle = stats.fileErrors.map(e => `${e.path}:\n${e.error}`).join("\n\n");
270
+ errorItems.push(<div title={errorTitle}>{stats.fileErrors.length} files failed</div>);
271
+ }
272
+ if (stats.blockErrors.length > 0) {
273
+ errorItems.push(<div title={stats.blockErrors.join("\n")}>{stats.blockErrors.length} blocks failed</div>);
274
+ }
275
+ return renderStage("Errors", errorItems, 0, false, true);
276
+ })()}
277
+ </div>
278
+ );
279
+ }
280
+
281
+ renderPendingLogWarnings() {
282
+ const paths = this.getPaths();
283
+ const now = Date.now();
284
+ const threshold = now - (2 * PUBLIC_MOVE_THRESHOLD);
285
+
286
+ const machineWarnings: Record<string, { machineId: string; oldestTime: number; count: number; totalSize: number }> = {};
287
+
288
+ for (const path of paths) {
289
+ if (path.logCount !== undefined) continue;
290
+
291
+ if (path.startTime >= threshold) continue;
292
+
293
+ if (!path.machineId) continue;
294
+
295
+ const existing = machineWarnings[path.machineId];
296
+ if (!existing) {
297
+ machineWarnings[path.machineId] = {
298
+ machineId: path.machineId,
299
+ oldestTime: path.startTime,
300
+ count: 1,
301
+ totalSize: path.size || 0,
302
+ };
303
+ } else {
304
+ if (path.startTime < existing.oldestTime) {
305
+ existing.oldestTime = path.startTime;
306
+ }
307
+ existing.count++;
308
+ existing.totalSize += path.size || 0;
309
+ }
310
+ }
311
+
312
+ const warnings = Object.values(machineWarnings);
313
+ if (warnings.length === 0) return undefined;
314
+
315
+ const isFrozen = !!savedPathsURL.value;
316
+
317
+ return (
318
+ <div className={css.vbox(10).pad2(10).hsl(0, 50, 50).colorhsl(60, 50, 100)}>
319
+ {isFrozen && (
320
+ <>
321
+ <div className={css.fontWeight(600)}>Frozen files are too old:</div>
322
+ {warnings.map((warning) => (
323
+ <div key={warning.machineId} className={css.hbox(10)}>
324
+ <MachineThreadInfo machineId={warning.machineId} />
325
+ <span>has {formatNumber(warning.count)} pending files ({formatNumber(warning.totalSize)}B) that are {formatTime(now - warning.oldestTime)} old</span>
326
+ </div>
327
+ ))}
328
+ <Button
329
+ onClick={() => {
330
+ this.clearFrozenPaths();
331
+ void this.loadPaths();
332
+ }}
333
+ hue={180}
334
+ >
335
+ Clear Frozen Files and Reload
336
+ </Button>
337
+ </>
338
+ )}
339
+ {!isFrozen && (
340
+ <>
341
+ <div className={css.fontWeight(600)}>FIX pending logs not being merged!</div>
342
+ {warnings.map((warning) => (
343
+ <div key={warning.machineId} className={css.hbox(10)}>
344
+ <MachineThreadInfo machineId={warning.machineId} />
345
+ <span>is not moving logs to remote storage. {formatNumber(warning.count)} files ({formatNumber(warning.totalSize)}B) are {formatTime(now - warning.oldestTime)} old</span>
346
+ </div>
347
+ ))}
348
+ <Button
349
+ onClick={async () => {
350
+ Querysub.commitLocal(() => {
351
+ this.state.forceMoveStartTime = Date.now();
352
+ this.state.forceMoveEndTime = undefined;
353
+ });
354
+
355
+ let forceMoveInterval = setInterval(() => {
356
+ this.forceUpdate();
357
+ }, 100);
358
+
359
+ try {
360
+ let loggers = await getLoggers2Async();
361
+ for (let logger of Object.values(loggers)) {
362
+ await logger.clientForceMoveLogsToPublic();
363
+ }
364
+ await this.loadPaths();
365
+ } finally {
366
+ if (forceMoveInterval) {
367
+ clearInterval(forceMoveInterval);
368
+ }
369
+
370
+ Querysub.commitLocal(() => {
371
+ this.state.forceMoveEndTime = Date.now();
372
+ });
373
+ }
374
+ }}
375
+ hue={180}
376
+ >
377
+ Force Move Logs to Public
378
+ </Button>
379
+ {(() => {
380
+ if (this.state.forceMoveStartTime !== undefined && this.state.forceMoveEndTime === undefined) {
381
+ const elapsed = Date.now() - this.state.forceMoveStartTime;
382
+ return <div>Moving logs... {formatTime(elapsed)}</div>;
383
+ }
384
+ if (this.state.forceMoveStartTime !== undefined && this.state.forceMoveEndTime !== undefined) {
385
+ const duration = this.state.forceMoveEndTime - this.state.forceMoveStartTime;
386
+ return <div>Moved logs in {formatTime(duration)}</div>;
387
+ }
388
+ return undefined;
389
+ })()}
390
+ </>
391
+ )}
392
+ </div>
393
+ );
394
+ }
395
+
396
+ async loadPaths() {
397
+ if (savedPathsURL.value) {
398
+ return;
399
+ }
400
+
401
+ Querysub.commitLocal(() => {
402
+ this.state.loadingPaths = true;
403
+ });
404
+
405
+ let loggers = await getLoggers2Async();
406
+ let selectedLoggers: typeof loggers.logLogs[] = [];
407
+ Querysub.localRead(() => {
408
+ if (this.state.enableLogs) selectedLoggers.push(loggers.logLogs);
409
+ if (this.state.enableInfos) selectedLoggers.push(loggers.infoLogs);
410
+ if (this.state.enableWarnings) selectedLoggers.push(loggers.warnLogs);
411
+ if (this.state.enableErrors) selectedLoggers.push(loggers.errorLogs);
412
+ });
413
+
414
+ if (selectedLoggers.length === 0) {
415
+ console.error("No log sources selected");
416
+ Querysub.commitLocal(() => {
417
+ this.state.loadingPaths = false;
418
+ });
419
+ return;
420
+ }
421
+
422
+ let allPaths: TimeFilePathWithSize[] = [];
423
+ let range = Querysub.localRead(() => getTimeRange());
424
+
425
+ for (let logger of selectedLoggers) {
426
+ let paths = await logger.clientGetPaths({
427
+ startTime: range.startTime,
428
+ endTime: range.endTime,
429
+ });
430
+ allPaths.push(...paths);
431
+ }
432
+ sort(allPaths, x => -x.startTime);
433
+
434
+ Querysub.commitLocal(() => {
435
+ this.state.paths = allPaths;
436
+ this.state.loadingPaths = false;
437
+ });
438
+ }
439
+
440
+ cancel = async () => {
441
+ this.searchSequenceNumber++;
442
+ let loggers = await getLoggers2Async();
443
+ for (let logger of Object.values(loggers)) {
444
+ logger.clientCancelAllCallbacks();
445
+ }
446
+ Querysub.commitLocal(() => {
447
+ this.state.searching = false;
448
+ this.state.hasSearched = false;
449
+ });
450
+ };
451
+
452
+
453
+ search = async () => {
454
+ await this.cancel();
455
+ Querysub.commitLocal(() => {
456
+ this.state.searching = true;
457
+ this.state.results = [];
458
+ this.state.stats = undefined;
459
+ this.state.searchingLogs = false;
460
+ this.state.searchingInfos = false;
461
+ this.state.searchingWarnings = false;
462
+ this.state.searchingErrors = false;
463
+ });
464
+
465
+
466
+ let loggers = await getLoggers2Async();
467
+
468
+ let selectedLoggers: typeof loggers.logLogs[] = [];
469
+ Querysub.localRead(() => {
470
+ if (this.state.enableLogs) {
471
+ selectedLoggers.push(loggers.logLogs);
472
+ this.state.searchingLogs = true;
473
+ }
474
+ if (this.state.enableInfos) {
475
+ selectedLoggers.push(loggers.infoLogs);
476
+ this.state.searchingInfos = true;
477
+ }
478
+ if (this.state.enableWarnings) {
479
+ selectedLoggers.push(loggers.warnLogs);
480
+ this.state.searchingWarnings = true;
481
+ }
482
+ if (this.state.enableErrors) {
483
+ selectedLoggers.push(loggers.errorLogs);
484
+ this.state.searchingErrors = true;
485
+ }
486
+ });
487
+
488
+ this.searchSequenceNumber++;
489
+ let currentSequenceNumber = this.searchSequenceNumber;
490
+
491
+ let startTime = Date.now();
492
+
493
+ let hasPaths = Querysub.localRead(() => this.getPaths().length > 0);
494
+ let getFilesTime = 0;
495
+ if (readLiveData.value || !hasPaths) {
496
+ let startTime = Date.now();
497
+ await this.loadPaths();
498
+ getFilesTime = Date.now() - startTime;
499
+ }
500
+
501
+
502
+ if (selectedLoggers.length === 0) {
503
+ console.error("No log sources selected");
504
+ Querysub.commitLocal(() => {
505
+ this.state.searching = false;
506
+ });
507
+ return;
508
+ }
509
+
510
+ let searchBuffer = Querysub.localRead(() => Buffer.from(searchText.value, "utf8"));
511
+ let results: LogDatum[] = [];
512
+ let stats: IndexedLogResults = createEmptyIndexedLogResults();
513
+ stats.fileFindTime += getFilesTime;
514
+
515
+ let paths = Querysub.localRead(() => this.getPaths());
516
+
517
+ let range = Querysub.localRead(() => getTimeRange());
518
+
519
+ let updateResults = throttleFunction(100, () => {
520
+ if (this.searchSequenceNumber !== currentSequenceNumber) return;
521
+ Querysub.commitLocal(() => {
522
+ this.state.results = results;
523
+ });
524
+ });
525
+
526
+ let loggerResults = new Map<typeof loggers.logLogs, IndexedLogResults>();
527
+
528
+ let updateStats = () => {
529
+ if (this.searchSequenceNumber !== currentSequenceNumber) return;
530
+
531
+ let mergedStats: IndexedLogResults = createEmptyIndexedLogResults();
532
+
533
+ for (let loggerResult of loggerResults.values()) {
534
+ mergedStats = mergeIndexedLogResults(mergedStats, loggerResult);
535
+ }
536
+
537
+ mergedStats.totalSearchTime = Date.now() - startTime;
538
+
539
+ Querysub.commitLocal(() => {
540
+ this.state.stats = mergedStats;
541
+ });
542
+ };
543
+
544
+ await Promise.all(selectedLoggers.map(async (logger) => {
545
+ let done = false;
546
+ let result = await errorToUndefined(logger.clientFind({
547
+ params: {
548
+ startTime: range.startTime,
549
+ endTime: range.endTime,
550
+ limit: limitURL.value,
551
+ findBuffer: searchBuffer,
552
+ pathOverrides: paths,
553
+ },
554
+ onResult: (match: LogDatum) => {
555
+ console.log("onResult", match);
556
+ results.push(match);
557
+ void updateResults();
558
+ },
559
+ onResults: (loggerStats: IndexedLogResults) => {
560
+ if (done) return;
561
+ loggerResults.set(logger, loggerStats);
562
+ updateStats();
563
+ },
564
+ }));
565
+ done = true;
566
+
567
+ if (result) {
568
+ loggerResults.set(logger, result);
569
+ updateStats();
570
+ }
571
+
572
+ Querysub.commitLocal(() => {
573
+ if (logger === loggers.logLogs) this.state.searchingLogs = false;
574
+ if (logger === loggers.infoLogs) this.state.searchingInfos = false;
575
+ if (logger === loggers.warnLogs) this.state.searchingWarnings = false;
576
+ if (logger === loggers.errorLogs) this.state.searchingErrors = false;
577
+ });
578
+ }));
579
+
580
+
581
+ Querysub.commitLocal(() => {
582
+ this.state.results = results;
583
+ this.state.searching = false;
584
+ this.state.hasSearched = true;
585
+ });
586
+ };
587
+
588
+ renderResults() {
589
+ let fieldNames = new Set<string>();
590
+ for (let i = 0; i < Math.min(1000, this.state.results.length); i++) {
591
+ let result = this.state.results[i];
592
+ for (let key of Object.keys(result)) {
593
+ fieldNames.add(key);
594
+ }
595
+ }
596
+
597
+ let selectedFields: string[] = [];
598
+ for (let field of Object.keys(defaultSelectedFields)) {
599
+ if (atomic(selectedFieldsURL.value[field]) === undefined) {
600
+ selectedFields.push(field);
601
+ }
602
+ }
603
+ for (let [key, value] of Object.entries(selectedFieldsURL.value)) {
604
+ if (value && !selectedFields.includes(key)) {
605
+ selectedFields.splice(1, 0, key);
606
+ }
607
+ }
608
+
609
+ let columns: TableType<LogDatum>["columns"] = {};
610
+ columns["log"] = {
611
+ title: "Log",
612
+ formatter: (x, context) => {
613
+ return <Button onClick={() => console.log(context?.row)}>
614
+ Log
615
+ </Button>;
616
+ }
617
+ };
618
+
619
+ for (let field of selectedFields) {
620
+ let column: ColumnType<unknown, LogDatum> = {};
621
+ if (field === "time") {
622
+ column.formatter = (x: unknown) => useRelativeTimeURL.value ? formatDateJSX(Number(x)) : <span title={formatDateTimeDetailed(Number(x))}>{formatDateTime(Number(x))}</span>;
623
+ } else if (field === "__machineId") {
624
+ column.formatter = (x: unknown, context) => {
625
+ if (!context?.row || !context.row.__machineId) return <ObjectDisplay value={x} />;
626
+ return <MachineThreadInfo machineId={context.row.__machineId} threadId={context.row.__threadId || undefined} />;
627
+ };
628
+ }
629
+ if (!column.formatter) {
630
+ column.formatter = (x: unknown) => <ObjectDisplay value={x} />;
631
+ }
632
+ columns[field] = column;
633
+ }
634
+
635
+ columns["fields"] = {
636
+ title: "Fields",
637
+ formatter: (x, context) => {
638
+ if (!context?.row) return undefined;
639
+ let datum = context.row;
640
+ let fields = Object.keys(datum);
641
+ fields = fields.filter(x => !(x in columns) && !x.startsWith("_"));
642
+ return <div className={css.hbox(4, 4).wrap}>
643
+ {fields.map(x => <div
644
+ key={x}
645
+ className={css.pad2(4, 0).hsl(0, 0, 80).button}
646
+ onClick={() => {
647
+ let newValues = { ...selectedFieldsURL.value };
648
+ newValues[x] = true;
649
+ selectedFieldsURL.value = newValues;
650
+ }}
651
+ >
652
+ {x}
653
+ </div>)}
654
+ </div>;
655
+ }
656
+ };
657
+
658
+ return <>
659
+ <div className={css.hbox(10)}>
660
+ <InputPicker
661
+ label={<div className={css.hbox(10)}>
662
+ <div className={css.flexShrink0}>
663
+ Selected Fields
664
+ </div>
665
+ <Button onClick={() => {
666
+ let newValues = { ...selectedFieldsURL.value };
667
+ for (let key of Object.keys(newValues)) {
668
+ newValues[key] = key in defaultSelectedFields;
669
+ }
670
+ selectedFieldsURL.value = newValues;
671
+ }}>
672
+ Reset
673
+ </Button>
674
+ </div>}
675
+ picked={selectedFields}
676
+ options={Array.from(fieldNames).map(x => ({ value: x, label: x }))}
677
+ addPicked={x => {
678
+ let newValues = { ...selectedFieldsURL.value };
679
+ newValues[x] = true;
680
+ selectedFieldsURL.value = newValues;
681
+ }}
682
+ removePicked={x => {
683
+ let newValues = { ...selectedFieldsURL.value };
684
+ newValues[x] = false;
685
+ selectedFieldsURL.value = newValues;
686
+ }}
687
+ />
688
+ <InputLabelURL
689
+ label="Use Relative Time"
690
+ checkbox
691
+ url={useRelativeTimeURL}
692
+ />
693
+ </div>
694
+
695
+ {this.state.results.length === 0 && !this.state.searching && this.state.hasSearched && <div className={css.hsl(40, 50, 50).colorhsl(60, 50, 100).boldStyle.pad2(10).ellipsis}>
696
+ No logs matched, try adjusting your search or time range.
697
+ </div>}
698
+
699
+ <Table
700
+ rows={this.state.results}
701
+ columns={columns}
702
+ lineLimit={4}
703
+ initialLimit={10}
704
+ characterLimit={400}
705
+ getRowAttributes={row => {
706
+ let hue = -1;
707
+ if (row.__LOG_TYPE === "warn") hue = 40;
708
+ if (row.__LOG_TYPE === "error") hue = 0;
709
+ if (row.__LOG_TYPE === "info") hue = 200;
710
+ return {
711
+ className: hue !== -1 && css.hsl(hue, 40, 50).colorhsl(hue, 0, 100) || undefined,
712
+ };
713
+ }}
714
+ />
715
+ </>;
716
+ }
717
+
718
+ render() {
719
+ return (
720
+ <div className={css.vbox(20).pad2(20).fillBoth}>
721
+ <div className={css.hbox(20)}>
722
+ <div>Log Viewer 3</div>
723
+ <InputLabelURL
724
+ checkbox
725
+ label="Always Read Live Data"
726
+ url={readLiveData}
727
+ />
728
+ <InputLabelURL
729
+ checkbox
730
+ label="Exclude Pending Results"
731
+ url={excludePendingResults}
732
+ />
733
+ <InputLabelURL
734
+ label="Limit"
735
+ number
736
+ url={limitURL}
737
+ />
738
+ </div>
739
+
740
+ <div className={css.hbox(10)}>
741
+ <Button
742
+ onClick={() => {
743
+ this.state.enableLogs = !this.state.enableLogs;
744
+ this.clearPaths();
745
+ }}
746
+ hue={this.state.enableLogs && 210 || undefined}
747
+ className={this.state.searchingLogs && css.opacity(0.5) || undefined}
748
+ >
749
+ Logs
750
+ </Button>
751
+ <Button
752
+ onClick={() => {
753
+ this.state.enableInfos = !this.state.enableInfos;
754
+ this.clearPaths();
755
+ }}
756
+ hue={this.state.enableInfos && 200 || undefined}
757
+ className={this.state.searchingInfos && css.opacity(0.5) || undefined}
758
+ >
759
+ Infos
760
+ </Button>
761
+ <Button
762
+ onClick={() => {
763
+ this.state.enableWarnings = !this.state.enableWarnings;
764
+ this.clearPaths();
765
+ }}
766
+ hue={this.state.enableWarnings && 40 || undefined}
767
+ className={this.state.searchingWarnings && css.opacity(0.5) || undefined}
768
+ >
769
+ Warnings
770
+ </Button>
771
+ <Button
772
+ onClick={() => {
773
+ this.state.enableErrors = !this.state.enableErrors;
774
+ this.clearPaths();
775
+ }}
776
+ hue={this.state.enableErrors ? 0 : undefined}
777
+ className={this.state.searchingErrors && css.opacity(0.5) || undefined}
778
+ >
779
+ Errors
780
+ </Button>
781
+ <TimeRangeSelector />
782
+ </div>
783
+
784
+ <div className={css.hbox(10).fillWidth}>
785
+ <InputLabelURL
786
+ flavor="large"
787
+ fillWidth
788
+ focusOnMount
789
+ url={searchText}
790
+ onKeyDown={(e) => {
791
+ if (e.key === "Enter") {
792
+ searchText.value = e.currentTarget.value;
793
+ void this.search();
794
+ }
795
+ }}
796
+ />
797
+ {!savedPathsURL.value && (
798
+ <Button
799
+ flavor="large"
800
+ onClick={() => void this.loadPaths()}
801
+ hue={120}
802
+ >
803
+ Preview Files
804
+ </Button>
805
+ )}
806
+ {savedPathsURL.value && (
807
+ <Button
808
+ flavor="large"
809
+ onClick={() => this.clearFrozenPaths()}
810
+ hue={0}
811
+ >
812
+ Clear Frozen Files ({this.getPaths().length})
813
+ </Button>
814
+ )}
815
+ <Button
816
+ flavor="large"
817
+ hotkeys={["enter"]}
818
+ showHotkeys
819
+ onClick={() => void this.search()}
820
+ hue={210}
821
+ >
822
+ Search
823
+ </Button>
824
+ {this.state.searching && (
825
+ <Button
826
+ flavor="large"
827
+ onClick={() => void this.cancel()}
828
+ hue={60}
829
+ >
830
+ Cancel
831
+ </Button>
832
+ )}
833
+ </div>
834
+
835
+ <div className={css.hbox(10).fillWidth}>
836
+ <FilePathSelector
837
+ paths={this.getPaths()}
838
+ onChange={(paths) => {
839
+ this.state.paths = paths;
840
+ this.freezePaths();
841
+ }}
842
+ fileErrors={this.state.stats?.fileErrors}
843
+ />
844
+ <Button
845
+ onClick={() => {
846
+ if (savedPathsURL.value) {
847
+ this.clearFrozenPaths();
848
+ void this.loadPaths();
849
+ } else {
850
+ this.freezePaths();
851
+ }
852
+ }}
853
+ hue={savedPathsURL.value ? 0 : 180}
854
+ >
855
+ {savedPathsURL.value ? `Clear Frozen Files (${this.getPaths().length})` : `Freeze Files (${this.state.paths.length})`}
856
+ </Button>
857
+ {(() => {
858
+ if (!savedPathsURL.value) return undefined;
859
+ const paths = this.getPaths();
860
+ const pendingCount = paths.filter(x => x.logCount === undefined).length;
861
+ if (pendingCount === 0) return undefined;
862
+ return (
863
+ <Button
864
+ onClick={() => {
865
+ const paths = this.getPaths();
866
+ const nonPendingPaths = paths.filter(x => x.logCount !== undefined);
867
+ const json = JSON.stringify(nonPendingPaths);
868
+ const buffer = Buffer.from(json, "utf8");
869
+ const compressed = LZ4.compress(buffer);
870
+ savedPathsURL.value = compressed.toString("base64");
871
+ }}
872
+ hue={40}
873
+ >
874
+ Remove Pending ({pendingCount})
875
+ </Button>
876
+ );
877
+ })()}
878
+
879
+ {this.state.loadingPaths && <div>Loading paths...</div>}
880
+ {this.state.searching && (() => {
881
+ let searchingSources: string[] = [];
882
+ if (this.state.searchingLogs) searchingSources.push("Logs");
883
+ if (this.state.searchingInfos) searchingSources.push("Infos");
884
+ if (this.state.searchingWarnings) searchingSources.push("Warnings");
885
+ if (this.state.searchingErrors) searchingSources.push("Errors");
886
+ return <div className={css.pad2(4, 2).hsl(120, 40, 50).colorhsl(120, 0, 100)}>
887
+ Searching: {searchingSources.join(", ")}
888
+ </div>;
889
+ })()}
890
+ </div>
891
+
892
+ {this.renderPendingLogWarnings()}
893
+
894
+ {this.renderSearchStats()}
895
+
896
+ {this.renderResults()}
897
+ </div>
898
+ );
899
+ }
900
+ }
901
+