querysub 0.328.0 → 0.329.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/bin/error-email.js +8 -0
- package/bin/error-im.js +8 -0
- package/package.json +5 -3
- package/src/-a-archives/archivesCborT.ts +52 -0
- package/src/-a-archives/archivesJSONT.ts +19 -5
- package/src/2-proxy/PathValueProxyWatcher.ts +4 -0
- package/src/config.ts +15 -3
- package/src/diagnostics/logs/FastArchiveAppendable.ts +15 -6
- package/src/diagnostics/logs/FastArchiveController.ts +6 -0
- package/src/diagnostics/logs/diskLogGlobalContext.ts +2 -1
- package/src/diagnostics/logs/errorNotifications/ErrorDigestPage.tsx +409 -0
- package/src/diagnostics/logs/errorNotifications/ErrorNotificationController.ts +12 -4
- package/src/diagnostics/logs/errorNotifications/ErrorWarning.tsx +9 -4
- package/src/diagnostics/logs/errorNotifications/errorDigestEmail.tsx +174 -0
- package/src/diagnostics/logs/errorNotifications/errorDigests.tsx +290 -7
- package/src/diagnostics/logs/errorNotifications/errorLoopEntry.tsx +7 -0
- package/src/diagnostics/logs/errorNotifications/errorWatchEntry.tsx +3 -28
- package/src/diagnostics/logs/lifeCycleAnalysis/spec.md +8 -18
- package/src/diagnostics/managementPages.tsx +28 -15
- package/src/email_ims_notifications/postmark.tsx +3 -3
- package/src/library-components/SyncedController.ts +3 -3
- package/src/misc.ts +5 -0
- package/src/misc2.ts +52 -0
- package/src/user-implementation/userData.ts +26 -7
- package/testEntry2.ts +1 -1
|
@@ -0,0 +1,409 @@
|
|
|
1
|
+
module.noserverhotreload = false;
|
|
2
|
+
module.hotreload = true;
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
import { SocketFunction } from "socket-function/SocketFunction";
|
|
6
|
+
import { qreact } from "../../../4-dom/qreact";
|
|
7
|
+
import { getSyncedController } from "../../../library-components/SyncedController";
|
|
8
|
+
import { ErrorDigestController, errorDigestHistory, ErrorDigestInfo } from "./errorDigests";
|
|
9
|
+
import { isManagementUser } from "../../../-0-hooks/hooks";
|
|
10
|
+
import { assertIsManagementUser } from "../../managementPages";
|
|
11
|
+
import { URLParam } from "../../../library-components/URLParam";
|
|
12
|
+
import { tabURL } from "../../../library-components/urlResetGroups";
|
|
13
|
+
import { Anchor, ATag } from "../../../library-components/ATag";
|
|
14
|
+
import { css } from "../../../4-dom/css";
|
|
15
|
+
import { list, sort } from "socket-function/src/misc";
|
|
16
|
+
import { formatDateTime, formatVeryNiceDateTime, formatNumber } from "socket-function/src/formatting/format";
|
|
17
|
+
import { Table } from "../../../5-diagnostics/Table";
|
|
18
|
+
import { Histogram } from "../../../library-components/Histogram";
|
|
19
|
+
import { TabbedUI } from "../../../library-components/TabbedUI";
|
|
20
|
+
import { LogDatum } from "../diskLogger";
|
|
21
|
+
import { t } from "../../../2-proxy/schema2";
|
|
22
|
+
import { getErrorLogsLink } from "./ErrorWarning";
|
|
23
|
+
|
|
24
|
+
export const digestKeyURL = new URLParam("digestKey", "");
|
|
25
|
+
export const fileDetailTabURL = new URLParam("fileDetailTab", "errors");
|
|
26
|
+
|
|
27
|
+
export class ErrorDigestPage extends qreact.Component {
|
|
28
|
+
state = t.state({
|
|
29
|
+
selectedFile: t.string("")
|
|
30
|
+
});
|
|
31
|
+
render() {
|
|
32
|
+
// Get the current tab mode
|
|
33
|
+
let currentTab = tabURL.value;
|
|
34
|
+
let selectedDigestKey = digestKeyURL.value;
|
|
35
|
+
|
|
36
|
+
if (currentTab === "detail" && selectedDigestKey) {
|
|
37
|
+
return this.renderDigestDetail(selectedDigestKey);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
return this.renderDigestList();
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
renderDigestList() {
|
|
44
|
+
let controller = ErrorDigestController(SocketFunction.browserNodeId());
|
|
45
|
+
|
|
46
|
+
let keys = controller.getDigestKeys();
|
|
47
|
+
|
|
48
|
+
if (!keys || keys.length === 0) {
|
|
49
|
+
return (
|
|
50
|
+
<div className={css.vbox(16).pad2(24)}>
|
|
51
|
+
<div className={css.fontSize(24).fontWeight(600)}>
|
|
52
|
+
Error Digest History
|
|
53
|
+
</div>
|
|
54
|
+
<div>No digest data available</div>
|
|
55
|
+
</div>
|
|
56
|
+
);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
// Sort lexicographically (highest = newest) and take latest 100
|
|
60
|
+
let sortedKeys = sort(keys as string[], key => key).reverse().slice(0, 100);
|
|
61
|
+
|
|
62
|
+
return (
|
|
63
|
+
<div className={css.vbox(16).paddingTop(24).paddingLeft(24).fillHeight.fillWidth}>
|
|
64
|
+
<div className={css.fontSize(24).fontWeight(600)}>
|
|
65
|
+
Error Digest History
|
|
66
|
+
</div>
|
|
67
|
+
<div className={css.fontSize(14).color("gray")}>
|
|
68
|
+
Showing latest {Math.min(sortedKeys.length, 100)} digests (sorted by creation time, newest first)
|
|
69
|
+
</div>
|
|
70
|
+
|
|
71
|
+
<div className={css.vbox(8).overflowAuto.fillWidth}>
|
|
72
|
+
{sortedKeys.map(key => (
|
|
73
|
+
<ATag
|
|
74
|
+
key={key}
|
|
75
|
+
values={[
|
|
76
|
+
tabURL.getOverride("detail"),
|
|
77
|
+
digestKeyURL.getOverride(key)
|
|
78
|
+
]}
|
|
79
|
+
className={css.pad2(12).hsla(0, 0, 0, 0.1).pointer.hslahover(0, 0, 0, 0.15)}
|
|
80
|
+
>
|
|
81
|
+
{formatVeryNiceDateTime(+key.split(".")[0])}
|
|
82
|
+
</ATag>
|
|
83
|
+
))}
|
|
84
|
+
</div>
|
|
85
|
+
</div>
|
|
86
|
+
);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
renderDigestDetail(digestKey: string) {
|
|
90
|
+
let controller = ErrorDigestController(SocketFunction.browserNodeId());
|
|
91
|
+
|
|
92
|
+
let digestData = controller.getDigest(digestKey);
|
|
93
|
+
|
|
94
|
+
if (!digestData) {
|
|
95
|
+
return (
|
|
96
|
+
<div className={css.vbox(16).pad2(24)}>
|
|
97
|
+
<div className={css.hbox(16)}>
|
|
98
|
+
<ATag
|
|
99
|
+
values={[
|
|
100
|
+
tabURL.getOverride(""),
|
|
101
|
+
digestKeyURL.getOverride("")
|
|
102
|
+
]}
|
|
103
|
+
>
|
|
104
|
+
← Back to Digest List
|
|
105
|
+
</ATag>
|
|
106
|
+
</div>
|
|
107
|
+
<div>Loading digest data...</div>
|
|
108
|
+
</div>
|
|
109
|
+
);
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
// If a file is selected, show file details
|
|
113
|
+
if (this.state.selectedFile) {
|
|
114
|
+
return this.renderFileDetail(digestData, digestKey);
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
return (
|
|
118
|
+
<div className={css.vbox(24).pad2(24).fillHeight.overflowAuto}>
|
|
119
|
+
{/* Header */}
|
|
120
|
+
<div className={css.hbox(16).alignItems("center")}>
|
|
121
|
+
<ATag
|
|
122
|
+
values={[
|
|
123
|
+
tabURL.getOverride(""),
|
|
124
|
+
digestKeyURL.getOverride("")
|
|
125
|
+
]}
|
|
126
|
+
>
|
|
127
|
+
← Back to Digest List
|
|
128
|
+
</ATag>
|
|
129
|
+
<div className={css.fontSize(24).fontWeight(600)}>
|
|
130
|
+
Digest: {formatVeryNiceDateTime(+digestKey.split(".")[0])}
|
|
131
|
+
</div>
|
|
132
|
+
</div>
|
|
133
|
+
|
|
134
|
+
<Anchor
|
|
135
|
+
className={css.fontSize(24)}
|
|
136
|
+
values={getErrorLogsLink({
|
|
137
|
+
startTime: digestData.startTime,
|
|
138
|
+
endTime: digestData.endTime,
|
|
139
|
+
})}
|
|
140
|
+
>
|
|
141
|
+
View live logs
|
|
142
|
+
</Anchor>
|
|
143
|
+
|
|
144
|
+
{/* Metadata Section */}
|
|
145
|
+
{this.renderMetadata(digestData)}
|
|
146
|
+
|
|
147
|
+
{/* Charts Section */}
|
|
148
|
+
{this.renderCharts(digestData)}
|
|
149
|
+
|
|
150
|
+
{/* Files Table */}
|
|
151
|
+
{this.renderFilesTable(digestData)}
|
|
152
|
+
</div>
|
|
153
|
+
);
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
renderMetadata(digestData: ErrorDigestInfo) {
|
|
157
|
+
return (
|
|
158
|
+
<div className={css.vbox(16)}>
|
|
159
|
+
<div className={css.fontSize(20).fontWeight(600)}>Metadata</div>
|
|
160
|
+
<div className={css.hbox(32).wrap}>
|
|
161
|
+
<div className={css.vbox(4)}>
|
|
162
|
+
<div className={css.fontSize(14).colorhsl(0, 0, 60)}>Compressed Bytes</div>
|
|
163
|
+
<div className={css.fontSize(16).fontWeight(500)}>
|
|
164
|
+
{formatNumber(digestData.totalCompressedBytes)}
|
|
165
|
+
</div>
|
|
166
|
+
</div>
|
|
167
|
+
<div className={css.vbox(4)}>
|
|
168
|
+
<div className={css.fontSize(14).colorhsl(0, 0, 60)}>Uncompressed Bytes</div>
|
|
169
|
+
<div className={css.fontSize(16).fontWeight(500)}>
|
|
170
|
+
{formatNumber(digestData.totalUncompressedBytes)}
|
|
171
|
+
</div>
|
|
172
|
+
</div>
|
|
173
|
+
<div className={css.vbox(4)}>
|
|
174
|
+
<div className={css.fontSize(14).colorhsl(0, 0, 60)}>Total Files</div>
|
|
175
|
+
<div className={css.fontSize(16).fontWeight(500)}>
|
|
176
|
+
{formatNumber(digestData.totalFiles)}
|
|
177
|
+
</div>
|
|
178
|
+
</div>
|
|
179
|
+
<div className={css.vbox(4)}>
|
|
180
|
+
<div className={css.fontSize(14).colorhsl(0, 0, 60)}>Scan Duration</div>
|
|
181
|
+
<div className={css.fontSize(16).fontWeight(500)}>
|
|
182
|
+
{formatNumber(digestData.scanDuration)}ms
|
|
183
|
+
</div>
|
|
184
|
+
</div>
|
|
185
|
+
<div className={css.vbox(4)}>
|
|
186
|
+
<div className={css.fontSize(14).colorhsl(0, 0, 60)}>Start Time</div>
|
|
187
|
+
<div className={css.fontSize(16).fontWeight(500)}>
|
|
188
|
+
{formatVeryNiceDateTime(digestData.startTime)}
|
|
189
|
+
</div>
|
|
190
|
+
</div>
|
|
191
|
+
<div className={css.vbox(4)}>
|
|
192
|
+
<div className={css.fontSize(14).colorhsl(0, 0, 60)}>End Time</div>
|
|
193
|
+
<div className={css.fontSize(16).fontWeight(500)}>
|
|
194
|
+
{formatVeryNiceDateTime(digestData.endTime)}
|
|
195
|
+
</div>
|
|
196
|
+
</div>
|
|
197
|
+
</div>
|
|
198
|
+
</div>
|
|
199
|
+
);
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
renderCharts(digestData: ErrorDigestInfo) {
|
|
203
|
+
// Convert histogram data to chart data
|
|
204
|
+
let unsuppressedErrorsData: { x: number; y: number }[] = [];
|
|
205
|
+
let unsuppressedWarningsData: { x: number; y: number }[] = [];
|
|
206
|
+
let corruptData: { x: number; y: number }[] = [];
|
|
207
|
+
let suppressedData: { x: number; y: number }[] = [];
|
|
208
|
+
|
|
209
|
+
for (let [time, data] of digestData.histogram.entries()) {
|
|
210
|
+
unsuppressedErrorsData.push({ x: time, y: data.unsuppressedErrors });
|
|
211
|
+
unsuppressedWarningsData.push({ x: time, y: data.unsuppressedWarnings });
|
|
212
|
+
corruptData.push({ x: time, y: data.corruptErrors + data.corruptWarnings });
|
|
213
|
+
suppressedData.push({ x: time, y: data.suppressedErrors + data.suppressedWarnings });
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
return (
|
|
217
|
+
<div className={css.vbox(16)}>
|
|
218
|
+
<div className={css.fontSize(20).fontWeight(600)}>Error Distribution Over Time</div>
|
|
219
|
+
<div className={css.vbox(16)}>
|
|
220
|
+
<div className={css.hbox(16).wrap}>
|
|
221
|
+
<div className={css.fillWidth.minWidth(400).height(300)}>
|
|
222
|
+
<Histogram
|
|
223
|
+
title="Unsuppressed Errors"
|
|
224
|
+
values={unsuppressedErrorsData}
|
|
225
|
+
xColumn={{
|
|
226
|
+
title: "Time",
|
|
227
|
+
format: (value) => formatDateTime(value)
|
|
228
|
+
}}
|
|
229
|
+
yColumn={{ title: "Count" }}
|
|
230
|
+
/>
|
|
231
|
+
</div>
|
|
232
|
+
<div className={css.fillWidth.minWidth(400).height(300)}>
|
|
233
|
+
<Histogram
|
|
234
|
+
title="Unsuppressed Warnings"
|
|
235
|
+
values={unsuppressedWarningsData}
|
|
236
|
+
xColumn={{
|
|
237
|
+
title: "Time",
|
|
238
|
+
format: (value) => formatDateTime(value)
|
|
239
|
+
}}
|
|
240
|
+
yColumn={{ title: "Count" }}
|
|
241
|
+
/>
|
|
242
|
+
</div>
|
|
243
|
+
</div>
|
|
244
|
+
<div className={css.hbox(16).wrap}>
|
|
245
|
+
<div className={css.fillWidth.minWidth(400).height(300)}>
|
|
246
|
+
<Histogram
|
|
247
|
+
title="Corrupt Errors + Warnings"
|
|
248
|
+
values={corruptData}
|
|
249
|
+
xColumn={{
|
|
250
|
+
title: "Time",
|
|
251
|
+
format: (value) => formatDateTime(value)
|
|
252
|
+
}}
|
|
253
|
+
yColumn={{ title: "Count" }}
|
|
254
|
+
/>
|
|
255
|
+
</div>
|
|
256
|
+
<div className={css.fillWidth.minWidth(400).height(300)}>
|
|
257
|
+
<Histogram
|
|
258
|
+
title="Suppressed Errors + Warnings"
|
|
259
|
+
values={suppressedData}
|
|
260
|
+
xColumn={{
|
|
261
|
+
title: "Time",
|
|
262
|
+
format: (value) => formatDateTime(value)
|
|
263
|
+
}}
|
|
264
|
+
yColumn={{ title: "Count" }}
|
|
265
|
+
/>
|
|
266
|
+
</div>
|
|
267
|
+
</div>
|
|
268
|
+
</div>
|
|
269
|
+
</div>
|
|
270
|
+
);
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
renderFilesTable(digestData: ErrorDigestInfo) {
|
|
274
|
+
// Convert byFile map to table rows
|
|
275
|
+
let fileRows = Array.from(digestData.byFile.entries()).map(([fileName, fileData]) => {
|
|
276
|
+
let latestError = fileData.latestErrors[fileData.latestErrors.length - 1];
|
|
277
|
+
let latestWarning = fileData.latestWarnings[fileData.latestWarnings.length - 1];
|
|
278
|
+
let latestLog = latestError ?? latestWarning;
|
|
279
|
+
|
|
280
|
+
return {
|
|
281
|
+
fileName,
|
|
282
|
+
errorCount: fileData.errors,
|
|
283
|
+
warningCount: fileData.warnings,
|
|
284
|
+
latestMessage: latestLog?.param0 ?? "No messages",
|
|
285
|
+
latestTime: latestLog?.time ?? 0
|
|
286
|
+
};
|
|
287
|
+
});
|
|
288
|
+
|
|
289
|
+
// Sort by error count descending
|
|
290
|
+
fileRows = sort(fileRows, row => row.errorCount).reverse();
|
|
291
|
+
|
|
292
|
+
return (
|
|
293
|
+
<div className={css.vbox(16)}>
|
|
294
|
+
<div className={css.fontSize(20).fontWeight(600)}>Files ({fileRows.length} files)</div>
|
|
295
|
+
<Table
|
|
296
|
+
columns={{
|
|
297
|
+
fileName: { title: "File Name" },
|
|
298
|
+
errorCount: { title: "Errors" },
|
|
299
|
+
warningCount: { title: "Warnings" },
|
|
300
|
+
latestMessage: {
|
|
301
|
+
title: "Latest Message",
|
|
302
|
+
formatter: (value: any) => String(value).slice(0, 100) + (String(value).length > 100 ? "..." : "")
|
|
303
|
+
}
|
|
304
|
+
}}
|
|
305
|
+
rows={fileRows}
|
|
306
|
+
getRowAttributes={(row: any) => ({
|
|
307
|
+
className: css.cursor("pointer").hslhover(210, 20, 95),
|
|
308
|
+
onClick: () => {
|
|
309
|
+
this.state.selectedFile = row.fileName;
|
|
310
|
+
}
|
|
311
|
+
})}
|
|
312
|
+
/>
|
|
313
|
+
</div>
|
|
314
|
+
);
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
renderFileDetail(digestData: ErrorDigestInfo, digestKey: string) {
|
|
318
|
+
let selectedFileName = this.state.selectedFile;
|
|
319
|
+
let fileData = digestData.byFile.get(selectedFileName);
|
|
320
|
+
|
|
321
|
+
if (!fileData) {
|
|
322
|
+
return (
|
|
323
|
+
<div className={css.vbox(16).pad2(24)}>
|
|
324
|
+
<div>File not found</div>
|
|
325
|
+
</div>
|
|
326
|
+
);
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
return (
|
|
330
|
+
<div className={css.vbox(24).pad2(24).fillHeight}>
|
|
331
|
+
{/* Header with back button */}
|
|
332
|
+
<div className={css.hbox(16).alignItems("center")}>
|
|
333
|
+
<ATag
|
|
334
|
+
clickOverride={() => {
|
|
335
|
+
this.state.selectedFile = "";
|
|
336
|
+
}}
|
|
337
|
+
className={css.cursor("pointer")}
|
|
338
|
+
>
|
|
339
|
+
← Back to Files Table
|
|
340
|
+
</ATag>
|
|
341
|
+
<div className={css.fontSize(20).fontWeight(600)}>
|
|
342
|
+
{selectedFileName}
|
|
343
|
+
</div>
|
|
344
|
+
</div>
|
|
345
|
+
|
|
346
|
+
{/* File stats */}
|
|
347
|
+
<div className={css.hbox(32)}>
|
|
348
|
+
<div className={css.vbox(4)}>
|
|
349
|
+
<div className={css.fontSize(14).colorhsl(0, 0, 60)}>Errors</div>
|
|
350
|
+
<div className={css.fontSize(16).fontWeight(500)}>{fileData.errors}</div>
|
|
351
|
+
</div>
|
|
352
|
+
<div className={css.vbox(4)}>
|
|
353
|
+
<div className={css.fontSize(14).colorhsl(0, 0, 60)}>Warnings</div>
|
|
354
|
+
<div className={css.fontSize(16).fontWeight(500)}>{fileData.warnings}</div>
|
|
355
|
+
</div>
|
|
356
|
+
</div>
|
|
357
|
+
|
|
358
|
+
{/* Tabbed interface for errors/warnings */}
|
|
359
|
+
<div className={css.fillHeight.overflowHidden}>
|
|
360
|
+
<TabbedUI
|
|
361
|
+
tab={fileDetailTabURL}
|
|
362
|
+
tabs={[
|
|
363
|
+
{
|
|
364
|
+
value: "errors",
|
|
365
|
+
title: `Errors (${fileData.errors})`,
|
|
366
|
+
contents: this.renderLogEntries(fileData.latestErrors, "errors")
|
|
367
|
+
},
|
|
368
|
+
{
|
|
369
|
+
value: "warnings",
|
|
370
|
+
title: `Warnings (${fileData.warnings})`,
|
|
371
|
+
contents: this.renderLogEntries(fileData.latestWarnings, "warnings")
|
|
372
|
+
}
|
|
373
|
+
]}
|
|
374
|
+
/>
|
|
375
|
+
</div>
|
|
376
|
+
</div>
|
|
377
|
+
);
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
renderLogEntries(logs: LogDatum[], type: "errors" | "warnings") {
|
|
381
|
+
if (logs.length === 0) {
|
|
382
|
+
return <div className={css.colorhsl(0, 0, 60)}>No {type} found</div>;
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
let tableRows = logs.map((log, index) => ({
|
|
386
|
+
index: logs.length - index,
|
|
387
|
+
time: formatVeryNiceDateTime(log.time),
|
|
388
|
+
message: log.param0 ?? "No message",
|
|
389
|
+
threadId: log.__threadId ?? "Unknown",
|
|
390
|
+
name: log.__NAME__ ?? "Unknown"
|
|
391
|
+
}));
|
|
392
|
+
|
|
393
|
+
return (
|
|
394
|
+
<div className={css.fillHeight.overflowAuto}>
|
|
395
|
+
<Table
|
|
396
|
+
columns={{
|
|
397
|
+
index: { title: "#" },
|
|
398
|
+
time: { title: "Time" },
|
|
399
|
+
name: { title: "Source" },
|
|
400
|
+
threadId: { title: "Thread" },
|
|
401
|
+
message: { title: "Message" }
|
|
402
|
+
}}
|
|
403
|
+
rows={tableRows}
|
|
404
|
+
initialLimit={50}
|
|
405
|
+
/>
|
|
406
|
+
</div>
|
|
407
|
+
);
|
|
408
|
+
}
|
|
409
|
+
}
|
|
@@ -62,7 +62,7 @@ type SuppressedChecker = {
|
|
|
62
62
|
fnc: (buffer: Buffer, posStart: number, posEnd: number) => boolean;
|
|
63
63
|
}
|
|
64
64
|
|
|
65
|
-
function
|
|
65
|
+
export function getErrorAppendables() {
|
|
66
66
|
let loggers = getLoggers();
|
|
67
67
|
if (!loggers) throw new Error("Loggers not available?");
|
|
68
68
|
// error, warn
|
|
@@ -222,6 +222,14 @@ const suppressionListArchive = archiveJSONT<SuppressionListBase>(() =>
|
|
|
222
222
|
);
|
|
223
223
|
const suppressionUpdatedChannel = new SocketChannel<boolean>("suppression-updated");
|
|
224
224
|
|
|
225
|
+
export async function getSuppressionListRaw(): Promise<SuppressionListBase> {
|
|
226
|
+
let entries = await suppressionListArchive.get(suppressionListKey);
|
|
227
|
+
if (!entries) {
|
|
228
|
+
entries = { entries: {} };
|
|
229
|
+
}
|
|
230
|
+
return entries;
|
|
231
|
+
}
|
|
232
|
+
|
|
225
233
|
class SuppressionList {
|
|
226
234
|
private init = lazy(async () => {
|
|
227
235
|
suppressionUpdatedChannel.watch(async () => {
|
|
@@ -233,8 +241,8 @@ class SuppressionList {
|
|
|
233
241
|
});
|
|
234
242
|
});
|
|
235
243
|
private cacheEntries: SuppressionListBase | undefined = undefined;
|
|
236
|
-
|
|
237
|
-
let entries = await
|
|
244
|
+
public updateEntriesNow = async () => {
|
|
245
|
+
let entries = await getSuppressionListRaw();
|
|
238
246
|
if (!entries) {
|
|
239
247
|
entries = { entries: {} };
|
|
240
248
|
}
|
|
@@ -616,7 +624,7 @@ export class RecentErrors {
|
|
|
616
624
|
if (!this.lastSuppressionList || !config.noLocalFiles) {
|
|
617
625
|
this.lastSuppressionList = new Map((await suppressionList.getSuppressionList()).map(x => [x.key, x]));
|
|
618
626
|
}
|
|
619
|
-
for (let appendable of
|
|
627
|
+
for (let appendable of getErrorAppendables()) {
|
|
620
628
|
let startTime = Date.now() - VIEW_WINDOW;
|
|
621
629
|
let endTime = Date.now() + timeInHour * 2;
|
|
622
630
|
let result = await new FastArchiveAppendableControllerBase().startSynchronizeInternal({
|
|
@@ -18,7 +18,12 @@ import { endTimeParam, startTimeParam } from "../TimeRangeSelector";
|
|
|
18
18
|
import { formatDateJSX } from "../../../misc/formatJSX";
|
|
19
19
|
import { atomic } from "../../../2-proxy/PathValueProxyWatcher";
|
|
20
20
|
|
|
21
|
-
export function
|
|
21
|
+
export function getErrorLogsLink(config?: {
|
|
22
|
+
startTime?: number;
|
|
23
|
+
endTime?: number;
|
|
24
|
+
}): URLOverride[] {
|
|
25
|
+
let startTime = config?.startTime ?? Date.now() - timeInDay * 1;
|
|
26
|
+
let endTime = config?.endTime ?? Date.now() + timeInHour * 2;
|
|
22
27
|
return [
|
|
23
28
|
showingManagementURL.getOverride(true),
|
|
24
29
|
managementPageURL.getOverride("LogViewer2"),
|
|
@@ -26,8 +31,8 @@ export function getLogsLinkParts(): URLOverride[] {
|
|
|
26
31
|
filterParam.getOverride(""),
|
|
27
32
|
|
|
28
33
|
// NOTE: While loading a weeks worth of logs clientside is a bit slow. Scanning serverside is not nearly as bad, as it can be done over hours, but... we want the page to be snappy, loading in seconds, so... just use a day, and we might reduce it even further if needed...
|
|
29
|
-
startTimeParam.getOverride(
|
|
30
|
-
endTimeParam.getOverride(
|
|
34
|
+
startTimeParam.getOverride(startTime),
|
|
35
|
+
endTimeParam.getOverride(endTime),
|
|
31
36
|
];
|
|
32
37
|
}
|
|
33
38
|
|
|
@@ -99,7 +104,7 @@ export class ErrorWarning extends qreact.Component {
|
|
|
99
104
|
);
|
|
100
105
|
}
|
|
101
106
|
|
|
102
|
-
const logLink =
|
|
107
|
+
const logLink = getErrorLogsLink();
|
|
103
108
|
|
|
104
109
|
if (!recentErrors || recentErrors.length === 0) {
|
|
105
110
|
return (
|
|
@@ -0,0 +1,174 @@
|
|
|
1
|
+
import { formatDate, formatDateTime, formatNumber, formatTime } from "socket-function/src/formatting/format";
|
|
2
|
+
import { getNotifyEmails } from "../../../config";
|
|
3
|
+
import { sendEmail_postmark } from "../../../email_ims_notifications/postmark";
|
|
4
|
+
import { sendEmail_sendgrid } from "../../../email_ims_notifications/sendgrid";
|
|
5
|
+
import { sendEmail } from "../../../user-implementation/userData";
|
|
6
|
+
import { ErrorDigestInfo } from "./errorDigests";
|
|
7
|
+
import { qreact } from "../../../4-dom/qreact";
|
|
8
|
+
import { LogDatum } from "../diskLogger";
|
|
9
|
+
import { sort } from "socket-function/src/misc";
|
|
10
|
+
import { createLink } from "../../../library-components/ATag";
|
|
11
|
+
import { getErrorLogsLink } from "./ErrorWarning";
|
|
12
|
+
import { managementPageURL, showingManagementURL } from "../../managementPages";
|
|
13
|
+
import { digestKeyURL } from "./ErrorDigestPage";
|
|
14
|
+
import { tabURL } from "../../../library-components/urlResetGroups";
|
|
15
|
+
|
|
16
|
+
const MAX_COUNT_PER_FILE = 2;
|
|
17
|
+
const MAX_COUNT = 100;
|
|
18
|
+
|
|
19
|
+
export async function sendErrorDigestEmail(digestInfo: ErrorDigestInfo) {
|
|
20
|
+
let notifyEmails = getNotifyEmails();
|
|
21
|
+
if (notifyEmails.length === 0) {
|
|
22
|
+
throw new Error(`No notify emails set, so we can't send email. Set in querysub.json config file (beside your package.json) with the { "notifyemails": ["email1@example.com", "email2@example.com"] }, or as a command line argument with --notifyemails "email1@example.com"`);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
let errors: {
|
|
26
|
+
errorsInFile: number;
|
|
27
|
+
warningsInFile: number;
|
|
28
|
+
message: string;
|
|
29
|
+
messageTime: string;
|
|
30
|
+
}[] = [];
|
|
31
|
+
let errorCount = 0;
|
|
32
|
+
let warningCount = 0;
|
|
33
|
+
let suppressedErrors = 0;
|
|
34
|
+
let suppressedWarnings = 0;
|
|
35
|
+
let corruptErrors = 0;
|
|
36
|
+
let corruptWarnings = 0;
|
|
37
|
+
|
|
38
|
+
let corruptWarning = "";
|
|
39
|
+
|
|
40
|
+
let failingFiles = 0;
|
|
41
|
+
|
|
42
|
+
for (let value of digestInfo.histogram.values()) {
|
|
43
|
+
errorCount += value.unsuppressedErrors;
|
|
44
|
+
warningCount += value.unsuppressedWarnings;
|
|
45
|
+
suppressedErrors += value.suppressedErrors;
|
|
46
|
+
suppressedWarnings += value.suppressedWarnings;
|
|
47
|
+
corruptErrors += value.corruptErrors;
|
|
48
|
+
corruptWarnings += value.corruptWarnings;
|
|
49
|
+
if (value.firstCorruptError && !corruptWarning) {
|
|
50
|
+
corruptWarning = value.firstCorruptError;
|
|
51
|
+
}
|
|
52
|
+
if (value.firstCorruptWarning && !corruptWarning) {
|
|
53
|
+
corruptWarning = value.firstCorruptWarning;
|
|
54
|
+
}
|
|
55
|
+
if (value.unsuppressedErrors > 0) {
|
|
56
|
+
failingFiles++;
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
for (let value of digestInfo.byFile.values()) {
|
|
61
|
+
for (let error of value.latestErrors.slice(-MAX_COUNT_PER_FILE)) {
|
|
62
|
+
errors.push({
|
|
63
|
+
errorsInFile: value.errors,
|
|
64
|
+
warningsInFile: value.warnings,
|
|
65
|
+
message: `${error.param0} (${error.__NAME__})`,
|
|
66
|
+
messageTime: formatDateTime(error.time),
|
|
67
|
+
});
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
sort(errors, x => -x.errorsInFile);
|
|
72
|
+
errors = errors.slice(0, MAX_COUNT);
|
|
73
|
+
|
|
74
|
+
let link = createLink([
|
|
75
|
+
showingManagementURL.getOverride(true),
|
|
76
|
+
managementPageURL.getOverride("ErrorDigestPage"),
|
|
77
|
+
digestKeyURL.getOverride(digestInfo.key),
|
|
78
|
+
tabURL.getOverride("detail"),
|
|
79
|
+
]);
|
|
80
|
+
|
|
81
|
+
await sendEmail({
|
|
82
|
+
to: notifyEmails,
|
|
83
|
+
fromPrefix: "error-digest",
|
|
84
|
+
subject: `${errorCount} errors | ${formatNumber(failingFiles)} failing files | ${warningCount} warnings${corruptErrors + corruptWarnings > 0 ? ` | ${corruptErrors + corruptWarnings} corrupt` : ""} | ${formatTime(digestInfo.scanDuration)} | ${formatNumber(digestInfo.totalCompressedBytes)} / ${formatNumber(digestInfo.totalUncompressedBytes)} | ${formatNumber(digestInfo.totalFiles)} files`,
|
|
85
|
+
contents: <div>
|
|
86
|
+
<h2>Error Summary</h2>
|
|
87
|
+
<ul style="list-style-type: none; padding-left: 0;">
|
|
88
|
+
<li style="margin-bottom: 8px;">
|
|
89
|
+
<strong style="color: #dc3545;">Errors:</strong>
|
|
90
|
+
<span style="background-color: #dc3545; color: white; padding: 2px 6px; border-radius: 3px; font-weight: bold; margin-left: 8px;">{errorCount}</span>
|
|
91
|
+
<span style="color: #dc3545;"> unsuppressed</span>
|
|
92
|
+
{suppressedErrors > 0 && <span>, <span style="color: #6c757d;">{formatNumber(suppressedErrors)} suppressed</span></span>}
|
|
93
|
+
</li>
|
|
94
|
+
<li>
|
|
95
|
+
<strong style="color: #fd7e14;">Warnings:</strong>
|
|
96
|
+
<span style="background-color: #fd7e14; color: white; padding: 2px 6px; border-radius: 3px; font-weight: bold; margin-left: 8px;">{warningCount}</span>
|
|
97
|
+
<span style="color: #fd7e14;"> unsuppressed</span>
|
|
98
|
+
{suppressedWarnings > 0 && <span>, <span style="color: #6c757d;">{formatNumber(suppressedWarnings)} suppressed</span></span>}
|
|
99
|
+
</li>
|
|
100
|
+
</ul>
|
|
101
|
+
|
|
102
|
+
{corruptWarning && <div>
|
|
103
|
+
<h2 style="color: #dc3545;">
|
|
104
|
+
Corrupt Lines
|
|
105
|
+
<span style="background-color: #dc3545; color: white; padding: 2px 6px; border-radius: 3px; font-weight: bold; margin-left: 8px;">{formatNumber(corruptErrors + corruptWarnings)}</span>
|
|
106
|
+
, example:
|
|
107
|
+
</h2>
|
|
108
|
+
<p style="color: #dc3545; font-weight: bold; background-color: #f8d7da; padding: 10px; border-left: 4px solid #dc3545;">{corruptWarning}</p>
|
|
109
|
+
</div>}
|
|
110
|
+
|
|
111
|
+
<div style="background-color: #f8f9fa; border: 1px solid #dee2e6; border-radius: 8px; padding: 20px; margin-bottom: 20px;">
|
|
112
|
+
<table style="width: 100%; border-collapse: collapse;">
|
|
113
|
+
<tr>
|
|
114
|
+
<td style="text-align: center; padding: 10px;">
|
|
115
|
+
<div style="color: #6c757d; font-size: 12px; text-transform: uppercase; letter-spacing: 0.5px;">Total Files</div>
|
|
116
|
+
<div style="background-color: #17a2b8; color: white; padding: 8px 12px; border-radius: 6px; font-weight: bold; font-size: 18px; margin-top: 4px;">{formatNumber(digestInfo.totalFiles)}</div>
|
|
117
|
+
</td>
|
|
118
|
+
<td style="text-align: center; padding: 10px;">
|
|
119
|
+
<div style="color: #6c757d; font-size: 12px; text-transform: uppercase; letter-spacing: 0.5px;">Compressed Bytes</div>
|
|
120
|
+
<div style="background-color: #ffc107; color: #212529; padding: 8px 12px; border-radius: 6px; font-weight: bold; font-size: 18px; margin-top: 4px;">{formatNumber(digestInfo.totalCompressedBytes)}</div>
|
|
121
|
+
</td>
|
|
122
|
+
<td style="text-align: center; padding: 10px;">
|
|
123
|
+
<div style="color: #6c757d; font-size: 12px; text-transform: uppercase; letter-spacing: 0.5px;">Uncompressed Bytes</div>
|
|
124
|
+
<div style="background-color: #6f42c1; color: white; padding: 8px 12px; border-radius: 6px; font-weight: bold; font-size: 18px; margin-top: 4px;">{formatNumber(digestInfo.totalUncompressedBytes)}</div>
|
|
125
|
+
</td>
|
|
126
|
+
</tr>
|
|
127
|
+
</table>
|
|
128
|
+
<div style="border-top: 1px solid #dee2e6; padding-top: 15px; margin-top: 15px;">
|
|
129
|
+
<table style="width: 100%; border-collapse: collapse;">
|
|
130
|
+
<tr>
|
|
131
|
+
<td style="text-align: center; padding: 10px; width: 200px;">
|
|
132
|
+
<div style="color: #6c757d; font-size: 12px; text-transform: uppercase; letter-spacing: 0.5px;">Scan Duration</div>
|
|
133
|
+
<div style="background-color: #007bff; color: white; padding: 8px 12px; border-radius: 6px; font-weight: bold; font-size: 18px; margin-top: 4px; font-family: monospace;">{formatTime(digestInfo.scanDuration)}</div>
|
|
134
|
+
</td>
|
|
135
|
+
<td style="padding: 10px; vertical-align: top;">
|
|
136
|
+
<div style="color: #6c757d; font-size: 12px; text-transform: uppercase; letter-spacing: 0.5px; margin-bottom: 8px;">Time Range</div>
|
|
137
|
+
<div style="font-size: 14px;">
|
|
138
|
+
<div style="margin-bottom: 8px;">From: <span style="background-color: #28a745; color: white; padding: 4px 8px; border-radius: 4px; font-weight: bold;">{formatDateTime(digestInfo.startTime)}</span></div>
|
|
139
|
+
<div>To: <span style="background-color: #dc3545; color: white; padding: 4px 8px; border-radius: 4px; font-weight: bold;">{formatDateTime(digestInfo.endTime)}</span></div>
|
|
140
|
+
</div>
|
|
141
|
+
</td>
|
|
142
|
+
</tr>
|
|
143
|
+
</table>
|
|
144
|
+
</div>
|
|
145
|
+
</div>
|
|
146
|
+
|
|
147
|
+
<a href={link} style="display: block; margin-bottom: 20px; padding: 10px; background-color: #007bff; color: white; text-decoration: none; border-radius: 4px; text-align: center;">View live logs</a>
|
|
148
|
+
|
|
149
|
+
{errors.length > 0 && <div>
|
|
150
|
+
<h2 style="color: #495057;">Recent Errors (<span style="color: #dc3545; font-weight: bold;">{errors.length}</span> shown)</h2>
|
|
151
|
+
<table style="border-collapse: collapse; width: 100%; margin-top: 10px;">
|
|
152
|
+
<thead>
|
|
153
|
+
<tr style="background-color: #495057; color: white;">
|
|
154
|
+
<th style="border: 1px solid #6c757d; padding: 8px; text-align: left;">Time</th>
|
|
155
|
+
<th style="border: 1px solid #6c757d; padding: 8px; text-align: left;">Message</th>
|
|
156
|
+
<th style="border: 1px solid #6c757d; padding: 8px; text-align: right; background-color: #dc3545;">Errors in File</th>
|
|
157
|
+
<th style="border: 1px solid #6c757d; padding: 8px; text-align: right; background-color: #fd7e14;">Warnings in File</th>
|
|
158
|
+
</tr>
|
|
159
|
+
</thead>
|
|
160
|
+
<tbody>
|
|
161
|
+
{errors.map((error, index) => (
|
|
162
|
+
<tr key={index} style={`background-color: ${index % 2 === 0 ? "#f8f9fa" : "white"}`}>
|
|
163
|
+
<td style="border: 1px solid #ddd; padding: 8px;">{error.messageTime}</td>
|
|
164
|
+
<td style="border: 1px solid #ddd; padding: 8px;">{error.message}</td>
|
|
165
|
+
<td style="border: 1px solid #ddd; padding: 8px; text-align: right; background-color: #f8d7da; color: #721c24; font-weight: bold;">{error.errorsInFile}</td>
|
|
166
|
+
<td style="border: 1px solid #ddd; padding: 8px; text-align: right; background-color: #fff3cd; color: #856404; font-weight: bold;">{error.warningsInFile}</td>
|
|
167
|
+
</tr>
|
|
168
|
+
))}
|
|
169
|
+
</tbody>
|
|
170
|
+
</table>
|
|
171
|
+
</div>}
|
|
172
|
+
</div>,
|
|
173
|
+
});
|
|
174
|
+
}
|