querysub 0.327.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.
Files changed (50) hide show
  1. package/bin/error-email.js +8 -0
  2. package/bin/error-im.js +8 -0
  3. package/package.json +4 -3
  4. package/src/-a-archives/archivesBackBlaze.ts +20 -0
  5. package/src/-a-archives/archivesCborT.ts +52 -0
  6. package/src/-a-archives/archivesDisk.ts +5 -5
  7. package/src/-a-archives/archivesJSONT.ts +19 -5
  8. package/src/-a-archives/archivesLimitedCache.ts +118 -7
  9. package/src/-a-archives/archivesPrivateFileSystem.ts +3 -0
  10. package/src/-g-core-values/NodeCapabilities.ts +26 -11
  11. package/src/0-path-value-core/auditLogs.ts +4 -2
  12. package/src/2-proxy/PathValueProxyWatcher.ts +7 -0
  13. package/src/3-path-functions/PathFunctionRunner.ts +2 -2
  14. package/src/4-querysub/Querysub.ts +1 -1
  15. package/src/5-diagnostics/GenericFormat.tsx +2 -2
  16. package/src/config.ts +15 -3
  17. package/src/deployManager/machineApplyMainCode.ts +10 -8
  18. package/src/deployManager/machineSchema.ts +4 -3
  19. package/src/deployManager/setupMachineMain.ts +3 -2
  20. package/src/diagnostics/logs/FastArchiveAppendable.ts +86 -53
  21. package/src/diagnostics/logs/FastArchiveController.ts +11 -2
  22. package/src/diagnostics/logs/FastArchiveViewer.tsx +205 -48
  23. package/src/diagnostics/logs/LogViewer2.tsx +78 -34
  24. package/src/diagnostics/logs/TimeRangeSelector.tsx +8 -0
  25. package/src/diagnostics/logs/diskLogGlobalContext.ts +5 -4
  26. package/src/diagnostics/logs/diskLogger.ts +70 -23
  27. package/src/diagnostics/logs/errorNotifications/ErrorDigestPage.tsx +409 -0
  28. package/src/diagnostics/logs/errorNotifications/ErrorNotificationController.ts +94 -67
  29. package/src/diagnostics/logs/errorNotifications/ErrorSuppressionUI.tsx +37 -3
  30. package/src/diagnostics/logs/errorNotifications/ErrorWarning.tsx +50 -16
  31. package/src/diagnostics/logs/errorNotifications/errorDigestEmail.tsx +174 -0
  32. package/src/diagnostics/logs/errorNotifications/errorDigests.tsx +291 -0
  33. package/src/diagnostics/logs/errorNotifications/errorLoopEntry.tsx +7 -0
  34. package/src/diagnostics/logs/errorNotifications/errorWatchEntry.tsx +185 -68
  35. package/src/diagnostics/logs/lifeCycleAnalysis/spec.md +10 -19
  36. package/src/diagnostics/managementPages.tsx +33 -15
  37. package/src/email_ims_notifications/discord.tsx +203 -0
  38. package/src/{email → email_ims_notifications}/postmark.tsx +3 -3
  39. package/src/fs.ts +9 -0
  40. package/src/functional/SocketChannel.ts +9 -0
  41. package/src/functional/throttleRender.ts +134 -0
  42. package/src/library-components/ATag.tsx +2 -2
  43. package/src/library-components/SyncedController.ts +3 -3
  44. package/src/misc.ts +18 -0
  45. package/src/misc2.ts +106 -0
  46. package/src/user-implementation/SecurityPage.tsx +11 -5
  47. package/src/user-implementation/userData.ts +57 -23
  48. package/testEntry2.ts +14 -5
  49. package/src/user-implementation/setEmailKey.ts +0 -25
  50. /package/src/{email → email_ims_notifications}/sendgrid.tsx +0 -0
@@ -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:&nbsp;&nbsp;<span style="background-color: #28a745; color: white; padding: 4px 8px; border-radius: 4px; font-weight: bold;">{formatDateTime(digestInfo.startTime)}</span></div>
139
+ <div>To:&nbsp;&nbsp;<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
+ }
@@ -0,0 +1,291 @@
1
+ import { insertIntoSortedList, list, timeInDay, timeInHour } from "socket-function/src/misc";
2
+ import { runScheduler } from "../../../misc2";
3
+ import { getErrorAppendables, getSuppressionFull, getSuppressionListRaw, suppressionList } from "./ErrorNotificationController";
4
+ import { createLogScanner } from "../FastArchiveAppendable";
5
+ import { LogDatum, getLogFile, getLoggers } from "../diskLogger";
6
+ import { FastArchiveAppendableControllerBase, FileMetadata } from "../FastArchiveController";
7
+ import { httpsRequest } from "socket-function/src/https";
8
+ import { Zip } from "../../../zip";
9
+ import { encodeCborx } from "../../../misc/cloneHelpers";
10
+ import { archiveJSONT } from "../../../-a-archives/archivesJSONT";
11
+ import { nestArchives } from "../../../-a-archives/archives";
12
+ import { getArchivesBackblaze } from "../../../-a-archives/archivesBackBlaze";
13
+ import { getDomain } from "../../../config";
14
+ import { getOwnMachineId, getOwnThreadId } from "../../../-a-auth/certs";
15
+ import { sendErrorDigestEmail } from "./errorDigestEmail";
16
+ import { archiveCborT } from "../../../-a-archives/archivesCborT";
17
+ import { Querysub } from "../../../4-querysub/Querysub";
18
+ import { sendEmail } from "../../../user-implementation/userData";
19
+ import { qreact } from "../../../4-dom/qreact";
20
+ import { getSyncedController } from "../../../library-components/SyncedController";
21
+ import { SocketFunction } from "socket-function/SocketFunction";
22
+ import { assertIsManagementUser } from "../../managementPages";
23
+
24
+ const LATEST_ERRORS_COUNT_PER_FILE = 20;
25
+
26
+ export type ErrorDigestInfo = {
27
+ key: string;
28
+ // timeGroupEnd =>
29
+ histogram: Map<number, {
30
+ suppressedErrors: number;
31
+ unsuppressedErrors: number;
32
+ suppressedWarnings: number;
33
+ unsuppressedWarnings: number;
34
+
35
+ corruptErrors: number;
36
+ corruptWarnings: number;
37
+
38
+ firstCorruptError?: string;
39
+ firstCorruptWarning?: string;
40
+ }>;
41
+ // file =>
42
+ byFile: Map<string, {
43
+ errors: number;
44
+ warnings: number;
45
+ // Sorted from oldest to newest
46
+ latestErrors: LogDatum[];
47
+ latestWarnings: LogDatum[];
48
+ }>;
49
+ totalCompressedBytes: number;
50
+ totalUncompressedBytes: number;
51
+ totalFiles: number;
52
+
53
+ scanDuration: number;
54
+ scanStartTime: number;
55
+ scanEndTime: number;
56
+ startTime: number;
57
+ endTime: number;
58
+ };
59
+ class ErrorDigest {
60
+ public async getDigestKeys() {
61
+ return errorDigestHistory.keys();
62
+ }
63
+ public async getDigest(key: string) {
64
+ return errorDigestHistory.get(key);
65
+ }
66
+ }
67
+
68
+ export const ErrorDigestController = getSyncedController(SocketFunction.register(
69
+ "ErrorDigestController-e5996b95-dcfc-412e-a104-80ed2c2d5933",
70
+ new ErrorDigest(),
71
+ () => ({
72
+ getDigestKeys: {},
73
+ getDigest: {},
74
+ }),
75
+ () => ({
76
+ hooks: [assertIsManagementUser],
77
+ })
78
+ ));
79
+
80
+ export const errorDigestHistory = archiveCborT<ErrorDigestInfo>(() => nestArchives("error-digests/", getArchivesBackblaze(getDomain())));
81
+
82
+ function getClosest(value: number, choices: number[]) {
83
+ let dist = Number.POSITIVE_INFINITY;
84
+ let closest: number = choices[0];
85
+ for (let choice of choices) {
86
+ let curDist = Math.abs(value - choice);
87
+ if (curDist < dist) {
88
+ dist = curDist;
89
+ closest = choice;
90
+ }
91
+ }
92
+ return closest;
93
+ }
94
+
95
+ async function runDigest() {
96
+ console.log("Running error digest gathering");
97
+ // Find the previous day
98
+ let endTime = getClosest(
99
+ Date.now(),
100
+ [
101
+ new Date().setHours(11),
102
+ new Date(Date.now() - timeInDay).setHours(11),
103
+ ]
104
+ );
105
+ let endDate = new Date(endTime);
106
+ endDate.setMinutes(0);
107
+ endDate.setSeconds(0);
108
+ endDate.setMilliseconds(0);
109
+ endTime = endDate.getTime();
110
+ let startTime = new Date(endTime - timeInDay).getTime();
111
+ let scanStartTime = Date.now();
112
+
113
+ let digestInfo: ErrorDigestInfo = {
114
+ key: `${Date.now()}_${getOwnMachineId()}_${getOwnThreadId()}`,
115
+ histogram: new Map(),
116
+ byFile: new Map(),
117
+ scanDuration: 0,
118
+ scanStartTime,
119
+ scanEndTime: Date.now(),
120
+ startTime,
121
+ endTime,
122
+ totalCompressedBytes: 0,
123
+ totalUncompressedBytes: 0,
124
+ totalFiles: 0,
125
+ };
126
+
127
+ let entries = await getSuppressionListRaw();
128
+ let suppressionFull = getSuppressionFull({
129
+ entries: Object.values(entries.entries),
130
+ blockTimeRange: {
131
+ startTime,
132
+ endTime,
133
+ },
134
+ });
135
+
136
+ function getTimeGroup(time: number) {
137
+ return Math.floor((time - startTime) / timeInHour) * timeInHour + startTime;
138
+ }
139
+
140
+
141
+ let appendables = getErrorAppendables();
142
+ for (let appendable of appendables) {
143
+ let isError = true;
144
+ if (appendable.rootPath.includes("warn")) {
145
+ isError = false;
146
+ } else if (appendable.rootPath.includes("error")) {
147
+ isError = true;
148
+ } else {
149
+ throw new Error(`Unhandled appendable root path: ${appendable.rootPath}`);
150
+ }
151
+ function registerCount(time: number, isSuppressed: boolean, corruptError?: string) {
152
+ let timeGroup = getTimeGroup(time);
153
+ let obj = digestInfo.histogram.get(timeGroup);
154
+ if (!obj) {
155
+ obj = {
156
+ suppressedErrors: 0,
157
+ unsuppressedErrors: 0,
158
+ suppressedWarnings: 0,
159
+ unsuppressedWarnings: 0,
160
+ corruptErrors: 0,
161
+ corruptWarnings: 0,
162
+ };
163
+ digestInfo.histogram.set(timeGroup, obj);
164
+ }
165
+ if (isError) {
166
+ if (isSuppressed) {
167
+ obj.suppressedErrors++;
168
+ } else {
169
+ obj.unsuppressedErrors++;
170
+ }
171
+ if (corruptError) {
172
+ obj.corruptErrors++;
173
+ if (!obj.firstCorruptError) {
174
+ obj.firstCorruptError = corruptError;
175
+ }
176
+ }
177
+ } else {
178
+ if (isSuppressed) {
179
+ obj.suppressedWarnings++;
180
+ } else {
181
+ obj.unsuppressedWarnings++;
182
+ }
183
+ if (corruptError) {
184
+ obj.corruptWarnings++;
185
+ if (!obj.firstCorruptWarning) {
186
+ obj.firstCorruptWarning = corruptError;
187
+ }
188
+ }
189
+ }
190
+ }
191
+ console.log(`Gathering files for ${appendable.rootPath}`);
192
+ let result = await new FastArchiveAppendableControllerBase().startSynchronizeInternal({
193
+ range: {
194
+ startTime,
195
+ endTime,
196
+ },
197
+ rootPath: appendable.rootPath,
198
+ });
199
+ let filesLeft = result.files.slice();
200
+ await Promise.all(list(32).map(() => runThread()));
201
+ async function runThread() {
202
+ while (true) {
203
+ let file = filesLeft.shift();
204
+ if (!file) {
205
+ return;
206
+ }
207
+ await processFile(file);
208
+ }
209
+ }
210
+ async function processFile(file: FileMetadata) {
211
+ try {
212
+ console.log(`Processing file ${file.path}`);
213
+ let compressed = await httpsRequest(file.url);
214
+ let data = await Zip.gunzip(compressed);
215
+ digestInfo.totalCompressedBytes += compressed.length;
216
+ digestInfo.totalUncompressedBytes += data.length;
217
+ digestInfo.totalFiles++;
218
+
219
+ let callback = createLogScanner({
220
+ debugName: "digestScanner",
221
+ onParsedData: (posStart, posEnd, buffer) => {
222
+ if (buffer === "done") {
223
+ return;
224
+ }
225
+ let result = suppressionFull(posStart, posEnd, buffer);
226
+ if (!result) {
227
+ registerCount(file.endTime, true);
228
+ return;
229
+ }
230
+
231
+ let datum: LogDatum;
232
+ try {
233
+ datum = JSON.parse(buffer.slice(posStart, posEnd).toString()) as LogDatum;
234
+ } catch (e: any) {
235
+ let message = `Failed to parse log datum in around ${buffer.slice(posStart, posEnd).slice(0, 100).toString("hex")}, error is:\n${e.stack}`;
236
+ process.stderr.write(message);
237
+ registerCount(file.endTime, false, message);
238
+ return;
239
+ }
240
+ registerCount(datum.time, false);
241
+
242
+ let fileGroup = getLogFile(datum);
243
+ let obj = digestInfo.byFile.get(fileGroup);
244
+ if (!obj) {
245
+ obj = {
246
+ errors: 0,
247
+ warnings: 0,
248
+ latestErrors: [],
249
+ latestWarnings: [],
250
+ };
251
+ digestInfo.byFile.set(fileGroup, obj);
252
+ }
253
+ if (isError) {
254
+ obj.errors++;
255
+ } else {
256
+ obj.warnings++;
257
+ }
258
+ let list = isError ? obj.latestErrors : obj.latestWarnings;
259
+ if (list.length === 0 || datum.time >= list[0].time) {
260
+ // NOTE: This should almost never trigger, so the search, and even worse, the splice, should almost never happen
261
+ insertIntoSortedList(list, x => x.time, datum);
262
+ if (list.length > LATEST_ERRORS_COUNT_PER_FILE) {
263
+ list.splice(0, list.length - LATEST_ERRORS_COUNT_PER_FILE);
264
+ }
265
+ }
266
+ },
267
+ });
268
+ await callback(data);
269
+ await callback("done");
270
+ } catch (e: any) {
271
+ console.warn(`Failed to process file ${file.path}, error: ${e.stack}`);
272
+ }
273
+ let progress = result.files.length - filesLeft.length + 1;
274
+ console.log(`Processed file ${file.path} (${progress} / ${result.files.length}) in ${appendable.rootPath}`);
275
+ }
276
+ }
277
+
278
+
279
+ let scanEndTime = Date.now();
280
+ digestInfo.scanDuration = scanEndTime - scanStartTime;
281
+ digestInfo.scanEndTime = scanEndTime;
282
+ await errorDigestHistory.set(digestInfo.key, digestInfo);
283
+ await sendErrorDigestEmail(digestInfo);
284
+ }
285
+
286
+ export async function runDigestLoop() {
287
+ await Querysub.hostService("error-digests");
288
+
289
+ // TODO: We might want to change the scheduler to run only on some days, adding support to weekday filtering as well (ex, just monday, wednesday, friday)
290
+ await runScheduler([12], runDigest);
291
+ }
@@ -0,0 +1,7 @@
1
+ import { runDigestLoop } from "./errorDigests";
2
+
3
+ async function main() {
4
+ await runDigestLoop();
5
+ }
6
+ // The digest loop should never exit, and if it does, we probably want to terminate ourselves so that the service manager will restart us, hopefully putting us back in a good state.
7
+ main().catch(console.error).finally(() => process.exit());