querysub 0.374.0 → 0.376.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/package.json +2 -4
- package/src/deployManager/components/MachineDetailPage.tsx +2 -5
- package/src/deployManager/components/ServiceDetailPage.tsx +2 -5
- package/src/deployManager/machineApplyMainCode.ts +7 -0
- package/src/diagnostics/NodeViewer.tsx +4 -5
- package/src/diagnostics/logs/IndexedLogs/BufferIndex.ts +10 -5
- package/src/diagnostics/logs/IndexedLogs/BufferIndexCPP.cpp +20 -0
- package/src/diagnostics/logs/IndexedLogs/BufferIndexHelpers.ts +29 -2
- package/src/diagnostics/logs/IndexedLogs/BufferUnitIndex.ts +61 -20
- package/src/diagnostics/logs/IndexedLogs/BufferUnitSet.ts +2 -2
- package/src/diagnostics/logs/IndexedLogs/IndexedLogs.ts +7 -7
- package/src/diagnostics/logs/IndexedLogs/LogViewer3.tsx +250 -243
- package/src/diagnostics/logs/IndexedLogs/LogViewerParams.ts +21 -0
- package/src/diagnostics/logs/IndexedLogs/{bufferMatcher.ts → bufferSearchFindMatcher.ts} +9 -4
- package/src/diagnostics/logs/IndexedLogs/moveIndexLogsToPublic.ts +3 -3
- package/src/diagnostics/logs/diskLogger.ts +0 -38
- package/src/diagnostics/logs/errorNotifications2/errorNotifications2.ts +9 -0
- package/src/diagnostics/logs/injectFileLocationToConsole.ts +3 -0
- package/src/diagnostics/logs/lifeCycleAnalysis/lifeCycles.tsx +24 -22
- package/src/diagnostics/managementPages.tsx +0 -18
- package/test.ts +0 -5
- package/bin/error-email.js +0 -8
- package/bin/error-im.js +0 -8
- package/src/diagnostics/logs/FastArchiveAppendable.ts +0 -843
- package/src/diagnostics/logs/FastArchiveController.ts +0 -573
- package/src/diagnostics/logs/FastArchiveViewer.tsx +0 -1090
- package/src/diagnostics/logs/LogViewer2.tsx +0 -552
- package/src/diagnostics/logs/errorNotifications/ErrorDigestPage.tsx +0 -409
- package/src/diagnostics/logs/errorNotifications/ErrorNotificationController.ts +0 -756
- package/src/diagnostics/logs/errorNotifications/ErrorSuppressionUI.tsx +0 -280
- package/src/diagnostics/logs/errorNotifications/ErrorWarning.tsx +0 -254
- package/src/diagnostics/logs/errorNotifications/errorDigestEmail.tsx +0 -233
- package/src/diagnostics/logs/errorNotifications/errorDigestEntry.tsx +0 -14
- package/src/diagnostics/logs/errorNotifications/errorDigests.tsx +0 -292
- package/src/diagnostics/logs/errorNotifications/errorWatchEntry.tsx +0 -209
- package/src/diagnostics/logs/importLogsEntry.ts +0 -38
- package/src/diagnostics/logs/lifeCycleAnalysis/LifeCyclePages.tsx +0 -150
- package/src/diagnostics/logs/logViewerExtractField.ts +0 -36
|
@@ -1,756 +0,0 @@
|
|
|
1
|
-
import { isNode } from "typesafecss";
|
|
2
|
-
import { getArchives } from "../../../-a-archives/archives";
|
|
3
|
-
import { SizeLimiter } from "../../SizeLimiter";
|
|
4
|
-
import { FastArchiveAppendable, createLogScanner, objectDelimitterBuffer } from "../FastArchiveAppendable";
|
|
5
|
-
import { LogDatum, getLogHash, getLoggers } from "../diskLogger";
|
|
6
|
-
import os from "os";
|
|
7
|
-
import { SocketFunction } from "socket-function/SocketFunction";
|
|
8
|
-
import { cache, cacheLimited, lazy } from "socket-function/src/caching";
|
|
9
|
-
import { getAllNodeIds } from "../../../-f-node-discovery/NodeDiscovery";
|
|
10
|
-
import { archiveJSONT } from "../../../-a-archives/archivesJSONT";
|
|
11
|
-
import { sort, throttleFunction, timeInDay, timeInHour, timeInMinute } from "socket-function/src/misc";
|
|
12
|
-
import { formatNumber } from "socket-function/src/formatting/format";
|
|
13
|
-
import { batchFunction, delay, runInSerial, runInfinitePoll, runInfinitePollCallAtStart } from "socket-function/src/batching";
|
|
14
|
-
import { SocketChannel } from "../../../functional/SocketChannel";
|
|
15
|
-
import { measureFnc, measureWrap } from "socket-function/src/profiling/measure";
|
|
16
|
-
import { FastArchiveAppendableControllerBase, getFileMetadataHash } from "../FastArchiveController";
|
|
17
|
-
import fs from "fs";
|
|
18
|
-
import zlib from "zlib";
|
|
19
|
-
import { getSyncedController } from "../../../library-components/SyncedController";
|
|
20
|
-
import { qreact } from "../../../4-dom/qreact";
|
|
21
|
-
import { requiresNetworkTrustHook } from "../../../-d-trust/NetworkTrust2";
|
|
22
|
-
import { assertIsManagementUser } from "../../managementPages";
|
|
23
|
-
import { streamToIteratable } from "../../../misc";
|
|
24
|
-
import { fsExistsAsync } from "../../../fs";
|
|
25
|
-
import { getPathStr2 } from "../../../path";
|
|
26
|
-
|
|
27
|
-
export const MAX_RECENT_ERRORS = 20;
|
|
28
|
-
const MAX_RECENT_ERRORS_PER_FILE = 3;
|
|
29
|
-
|
|
30
|
-
const BACKBLAZE_POLL_INTERVAL = timeInMinute * 30;
|
|
31
|
-
// The higher we turn this up the less bandwidth and processing time is spent on errors. But also, the longer delay between an error happening and getting an error notification
|
|
32
|
-
const LOCAL_BROADCAST_BATCH = 5000;
|
|
33
|
-
const NOTIFICATION_BROADCAST_BATCH = 5000;
|
|
34
|
-
const SELF_THROTTLE_INTERVAL = 100;
|
|
35
|
-
const SELF_THROTTLE_DELAY = 50;
|
|
36
|
-
|
|
37
|
-
const VIEW_WINDOW = timeInDay * 7;
|
|
38
|
-
const SUPPRESSION_POLL_INTERVAL = timeInMinute * 15;
|
|
39
|
-
const READ_CHUNK_SIZE = 1024 * 1024 * 10;
|
|
40
|
-
|
|
41
|
-
const LOCAL_CACHE_LIMIT_BATCH = timeInMinute * 30;
|
|
42
|
-
|
|
43
|
-
export const NOT_AN_ERROR_EXPIRE_TIME = 2524608000000;
|
|
44
|
-
|
|
45
|
-
export type SuppressionEntry = {
|
|
46
|
-
key: string;
|
|
47
|
-
// Includes, exact case
|
|
48
|
-
// - Supports "*" as a wildcard, which matches slower (converts it to a regex)
|
|
49
|
-
match: string;
|
|
50
|
-
comment: string;
|
|
51
|
-
lastUpdateTime: number;
|
|
52
|
-
expiresAt: number;
|
|
53
|
-
};
|
|
54
|
-
type SuppressionListBase = {
|
|
55
|
-
entries: {
|
|
56
|
-
[key: string]: SuppressionEntry;
|
|
57
|
-
};
|
|
58
|
-
};
|
|
59
|
-
|
|
60
|
-
type SuppressedChecker = {
|
|
61
|
-
entry: SuppressionEntry;
|
|
62
|
-
fnc: (buffer: Buffer, posStart: number, posEnd: number) => boolean;
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
export function getErrorAppendables() {
|
|
66
|
-
let loggers = getLoggers();
|
|
67
|
-
if (!loggers) throw new Error("Loggers not available?");
|
|
68
|
-
// error, warn
|
|
69
|
-
return [loggers.errorLogs, loggers.warnLogs];
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
// NOTE: This cache resets eventually, due to it being no longer used every time we update the suppression list. But that's probably fine...
|
|
73
|
-
export const getSuppressEntryChecker = cacheLimited(
|
|
74
|
-
1000 * 10,
|
|
75
|
-
function getSuppressEntryChecker(entry: SuppressionEntry): SuppressedChecker {
|
|
76
|
-
if (entry.match.includes("*")) {
|
|
77
|
-
try {
|
|
78
|
-
let regex = new RegExp(entry.match.replaceAll("*", ".*"));
|
|
79
|
-
return {
|
|
80
|
-
entry,
|
|
81
|
-
fnc: (buffer: Buffer, posStart: number, posEnd: number) => {
|
|
82
|
-
return regex.test(buffer.slice(posStart, posEnd).toString());
|
|
83
|
-
}
|
|
84
|
-
};
|
|
85
|
-
} catch (error) {
|
|
86
|
-
console.error(`Failed to create regex for ${JSON.stringify(entry.match)}, ignoring wildcard in it`, { error });
|
|
87
|
-
}
|
|
88
|
-
}
|
|
89
|
-
let matchBuffer = Buffer.from(entry.match);
|
|
90
|
-
let char0 = matchBuffer[0];
|
|
91
|
-
return {
|
|
92
|
-
entry,
|
|
93
|
-
fnc: (buffer: Buffer, posStart: number, posEnd: number) => {
|
|
94
|
-
for (let i = posStart; i < posEnd; i++) {
|
|
95
|
-
if (matchBuffer.length === 1) return true;
|
|
96
|
-
if (buffer[i] === char0) {
|
|
97
|
-
for (let j = 1; j < matchBuffer.length; j++) {
|
|
98
|
-
let ch2 = buffer[i + j];
|
|
99
|
-
if (ch2 !== matchBuffer[j]) {
|
|
100
|
-
break;
|
|
101
|
-
}
|
|
102
|
-
if (j === matchBuffer.length - 1) {
|
|
103
|
-
return true;
|
|
104
|
-
}
|
|
105
|
-
}
|
|
106
|
-
}
|
|
107
|
-
}
|
|
108
|
-
return false;
|
|
109
|
-
},
|
|
110
|
-
};
|
|
111
|
-
}
|
|
112
|
-
);
|
|
113
|
-
|
|
114
|
-
export const getSuppressionFull = measureWrap(function getSuppressionFull(config: {
|
|
115
|
-
entries: SuppressionEntry[];
|
|
116
|
-
blockTimeRange: {
|
|
117
|
-
startTime: number;
|
|
118
|
-
endTime: number;
|
|
119
|
-
};
|
|
120
|
-
suppressionCounts?: Map<string, number>;
|
|
121
|
-
expiredSuppressionCounts?: Map<string, number>;
|
|
122
|
-
// => wants data
|
|
123
|
-
}): ((posStart: number, posEnd: number, data: Buffer, obj?: { outdatedSuppressionKey?: string }) => boolean) {
|
|
124
|
-
let { entries, blockTimeRange } = config;
|
|
125
|
-
const { suppressionCounts, expiredSuppressionCounts } = config;
|
|
126
|
-
// Add some buffer, just in case entries get added a bit later, or early.
|
|
127
|
-
let startTime = blockTimeRange.startTime - timeInHour;
|
|
128
|
-
let endTime = blockTimeRange.endTime + timeInHour;
|
|
129
|
-
|
|
130
|
-
sort(entries, x => -x.lastUpdateTime);
|
|
131
|
-
|
|
132
|
-
let checkers = entries.map(x => getSuppressEntryChecker(x));
|
|
133
|
-
|
|
134
|
-
let definitelyNotExpired = checkers.filter(x => x.entry.expiresAt > endTime);
|
|
135
|
-
let definitelyExpired = checkers.filter(x => x.entry.expiresAt < startTime);
|
|
136
|
-
let maybeExpired = checkers.filter(x => x.entry.expiresAt >= startTime && x.entry.expiresAt <= endTime);
|
|
137
|
-
|
|
138
|
-
return (posStart, posEnd, data, obj) => {
|
|
139
|
-
let suppressed = false;
|
|
140
|
-
for (let checker of definitelyNotExpired) {
|
|
141
|
-
if (checker.fnc(data, posStart, posEnd)) {
|
|
142
|
-
if (!suppressionCounts && !expiredSuppressionCounts && !obj) {
|
|
143
|
-
return false;
|
|
144
|
-
}
|
|
145
|
-
suppressed = true;
|
|
146
|
-
if (!suppressionCounts) break;
|
|
147
|
-
|
|
148
|
-
let count = suppressionCounts.get(checker.entry.key) || 0;
|
|
149
|
-
count++;
|
|
150
|
-
suppressionCounts.set(checker.entry.key, count);
|
|
151
|
-
}
|
|
152
|
-
}
|
|
153
|
-
|
|
154
|
-
// Handle definitelyExpired - these are outdated suppressions
|
|
155
|
-
let mostRecentOutdatedSuppressionKey: string | undefined = undefined;
|
|
156
|
-
let mostRecentOutdatedSuppressionTime = 0;
|
|
157
|
-
|
|
158
|
-
// Handle maybeExpired - need to parse timestamp to check if suppression was active
|
|
159
|
-
if (maybeExpired.length > 0 && (suppressionCounts || expiredSuppressionCounts || obj)) {
|
|
160
|
-
let logTime = 0;
|
|
161
|
-
try {
|
|
162
|
-
let logEntry = JSON.parse(data.slice(posStart, posEnd).toString()) as LogDatum;
|
|
163
|
-
if (typeof logEntry.time === "number") {
|
|
164
|
-
logTime = logEntry.time;
|
|
165
|
-
}
|
|
166
|
-
} catch { }
|
|
167
|
-
|
|
168
|
-
for (let checker of maybeExpired) {
|
|
169
|
-
if (checker.fnc(data, posStart, posEnd)) {
|
|
170
|
-
if (checker.entry.expiresAt >= logTime) {
|
|
171
|
-
suppressed = true;
|
|
172
|
-
if (suppressionCounts) {
|
|
173
|
-
let count = suppressionCounts.get(checker.entry.key) || 0;
|
|
174
|
-
count++;
|
|
175
|
-
suppressionCounts.set(checker.entry.key, count);
|
|
176
|
-
}
|
|
177
|
-
} else {
|
|
178
|
-
|
|
179
|
-
if (checker.entry.expiresAt > mostRecentOutdatedSuppressionTime) {
|
|
180
|
-
mostRecentOutdatedSuppressionKey = checker.entry.key;
|
|
181
|
-
mostRecentOutdatedSuppressionTime = checker.entry.expiresAt;
|
|
182
|
-
}
|
|
183
|
-
// Even if we don't want the expired suppression counts, we might want the normal suppression counts, so we have to keep going.
|
|
184
|
-
if (expiredSuppressionCounts) {
|
|
185
|
-
let count = expiredSuppressionCounts.get(checker.entry.key) || 0;
|
|
186
|
-
count++;
|
|
187
|
-
expiredSuppressionCounts.set(checker.entry.key, count);
|
|
188
|
-
}
|
|
189
|
-
}
|
|
190
|
-
}
|
|
191
|
-
}
|
|
192
|
-
}
|
|
193
|
-
|
|
194
|
-
if (expiredSuppressionCounts || obj) {
|
|
195
|
-
for (let checker of definitelyExpired) {
|
|
196
|
-
if (checker.fnc(data, posStart, posEnd)) {
|
|
197
|
-
// First match is the most recent (entries are sorted by lastUpdateTime desc)
|
|
198
|
-
if (checker.entry.expiresAt > mostRecentOutdatedSuppressionTime) {
|
|
199
|
-
mostRecentOutdatedSuppressionKey = checker.entry.key;
|
|
200
|
-
}
|
|
201
|
-
if (!expiredSuppressionCounts) break;
|
|
202
|
-
let count = expiredSuppressionCounts.get(checker.entry.key) || 0;
|
|
203
|
-
count++;
|
|
204
|
-
expiredSuppressionCounts.set(checker.entry.key, count);
|
|
205
|
-
}
|
|
206
|
-
}
|
|
207
|
-
}
|
|
208
|
-
|
|
209
|
-
// Set the most recent outdated suppression key if we found any and weren't suppressed
|
|
210
|
-
if (obj && mostRecentOutdatedSuppressionKey) {
|
|
211
|
-
obj.outdatedSuppressionKey = mostRecentOutdatedSuppressionKey;
|
|
212
|
-
}
|
|
213
|
-
|
|
214
|
-
return !suppressed;
|
|
215
|
-
};
|
|
216
|
-
});
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
const suppressionListKey = "suppression-list.json";
|
|
220
|
-
const suppressionListArchive = archiveJSONT<SuppressionListBase>(() =>
|
|
221
|
-
getArchives("suppression-list"),
|
|
222
|
-
);
|
|
223
|
-
const suppressionUpdatedChannel = new SocketChannel<boolean>("suppression-updated");
|
|
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
|
-
|
|
233
|
-
class SuppressionList {
|
|
234
|
-
private init = lazy(async () => {
|
|
235
|
-
suppressionUpdatedChannel.watch(async () => {
|
|
236
|
-
await this.updateEntriesNow();
|
|
237
|
-
await recentErrors.onSuppressionChanged();
|
|
238
|
-
});
|
|
239
|
-
await runInfinitePollCallAtStart(SUPPRESSION_POLL_INTERVAL, async () => {
|
|
240
|
-
await this.updateEntriesNow();
|
|
241
|
-
});
|
|
242
|
-
});
|
|
243
|
-
private cacheEntries: SuppressionListBase | undefined = undefined;
|
|
244
|
-
public updateEntriesNow = async () => {
|
|
245
|
-
let entries = await getSuppressionListRaw();
|
|
246
|
-
if (!entries) {
|
|
247
|
-
entries = { entries: {} };
|
|
248
|
-
}
|
|
249
|
-
this.cacheEntries = entries;
|
|
250
|
-
};
|
|
251
|
-
private async getEntries(): Promise<SuppressionListBase> {
|
|
252
|
-
await this.init();
|
|
253
|
-
if (!this.cacheEntries) {
|
|
254
|
-
throw new Error("Cache entries not set? Should be impossible.");
|
|
255
|
-
}
|
|
256
|
-
// Infinite poll will have set this, so we don't infinitely loop
|
|
257
|
-
return this.cacheEntries;
|
|
258
|
-
}
|
|
259
|
-
|
|
260
|
-
public async filterObjsToNonSuppressed(objs: LogDatum[]): Promise<LogDatum[]> {
|
|
261
|
-
// NOTE: Streamed data should be rare enough, that handling this inefficiently is okay.
|
|
262
|
-
if (objs.length === 0) return [];
|
|
263
|
-
let startTime = objs[0].time;
|
|
264
|
-
let endTime = objs[objs.length - 1].time;
|
|
265
|
-
let parts: Buffer[] = [];
|
|
266
|
-
for (let obj of objs) {
|
|
267
|
-
parts.push(Buffer.from(JSON.stringify(obj)));
|
|
268
|
-
parts.push(objectDelimitterBuffer);
|
|
269
|
-
if (obj.time < startTime) {
|
|
270
|
-
startTime = obj.time;
|
|
271
|
-
}
|
|
272
|
-
if (obj.time > endTime) {
|
|
273
|
-
endTime = obj.time;
|
|
274
|
-
}
|
|
275
|
-
}
|
|
276
|
-
let buffer = Buffer.concat(parts);
|
|
277
|
-
let scanner = await this.scanForRecentErrors({
|
|
278
|
-
debugName: "filterObjsToNonSuppressed",
|
|
279
|
-
startTime,
|
|
280
|
-
endTime,
|
|
281
|
-
});
|
|
282
|
-
await scanner.onData(buffer);
|
|
283
|
-
return await scanner.finish();
|
|
284
|
-
}
|
|
285
|
-
public async scanForRecentErrors(config: {
|
|
286
|
-
debugName: string;
|
|
287
|
-
startTime: number;
|
|
288
|
-
endTime: number;
|
|
289
|
-
}): Promise<{
|
|
290
|
-
onData: (data: Buffer) => void;
|
|
291
|
-
finish: () => Promise<LogDatum[]>;
|
|
292
|
-
}> {
|
|
293
|
-
let entries = await this.getEntries();
|
|
294
|
-
let suppressionFull = getSuppressionFull({
|
|
295
|
-
entries: Object.values(entries.entries),
|
|
296
|
-
blockTimeRange: {
|
|
297
|
-
startTime: config.startTime,
|
|
298
|
-
endTime: config.endTime,
|
|
299
|
-
},
|
|
300
|
-
});
|
|
301
|
-
let datums: LogDatum[] = [];
|
|
302
|
-
// Create an object which we'll reuse that will be the output object for the suppression key.
|
|
303
|
-
// for the suppression key.
|
|
304
|
-
let obj: { outdatedSuppressionKey?: string } = {};
|
|
305
|
-
let callback = createLogScanner({
|
|
306
|
-
debugName: config.debugName,
|
|
307
|
-
onParsedData: (posStart, posEnd, buffer) => {
|
|
308
|
-
if (buffer === "done") {
|
|
309
|
-
return;
|
|
310
|
-
}
|
|
311
|
-
let result = suppressionFull(posStart, posEnd, buffer, obj);
|
|
312
|
-
|
|
313
|
-
if (!result) return;
|
|
314
|
-
|
|
315
|
-
let datum: LogDatum;
|
|
316
|
-
try {
|
|
317
|
-
datum = JSON.parse(buffer.slice(posStart, posEnd).toString()) as LogDatum;
|
|
318
|
-
} catch (e: any) {
|
|
319
|
-
process.stderr.write(`Failed to parse log datum in around ${buffer.slice(posStart, posEnd).slice(0, 100).toString("hex")}, in source ${config.debugName}, error is:\n${e.stack}`);
|
|
320
|
-
return;
|
|
321
|
-
}
|
|
322
|
-
if (obj.outdatedSuppressionKey) {
|
|
323
|
-
datum.__matchedOutdatedSuppressionKey = obj.outdatedSuppressionKey;
|
|
324
|
-
}
|
|
325
|
-
datums.push(datum);
|
|
326
|
-
},
|
|
327
|
-
});
|
|
328
|
-
let lastWaitTime = Date.now();
|
|
329
|
-
const stream = runInSerial(async (buffer: Buffer | "done") => {
|
|
330
|
-
// TODO: Maybe we should add this pattern to batching.ts? Basically, if we get called fast, we allow the calls through. BUT, if we called slowly OR we are doing a lot of processing (and so we are working for all of SELF_THROTTLE_INTERVAL), then we wait. This prevents this from taking over the machine. The back off is steep though, and if the machine is lagging we might reduce to a trickle, just getting 1 call in per SELF_THROTTLE_DELAY + synchronous lag from work in other parts of the program.
|
|
331
|
-
let now = Date.now();
|
|
332
|
-
if (now - lastWaitTime > SELF_THROTTLE_INTERVAL) {
|
|
333
|
-
await delay(SELF_THROTTLE_DELAY);
|
|
334
|
-
lastWaitTime = now;
|
|
335
|
-
}
|
|
336
|
-
await callback(buffer);
|
|
337
|
-
});
|
|
338
|
-
return {
|
|
339
|
-
onData: stream,
|
|
340
|
-
finish: async () => {
|
|
341
|
-
await stream("done");
|
|
342
|
-
// NOTE: We COULD limit as we run, however... how many errors are we really going to encounter that AREN'T suppressed? Suppression is supposed to prevent overload anyways. I guess worst case scenario, yes, we could get overloaded, but... if we are logging more NEW errors than we can store in memory, we have bigger problems...
|
|
343
|
-
return limitRecentErrors(datums);
|
|
344
|
-
},
|
|
345
|
-
};
|
|
346
|
-
}
|
|
347
|
-
public async setSuppressionEntry(entry: SuppressionEntry) {
|
|
348
|
-
let entries = await this.getEntries();
|
|
349
|
-
entry.lastUpdateTime = Date.now();
|
|
350
|
-
entries.entries[entry.key] = entry;
|
|
351
|
-
await suppressionListArchive.set(suppressionListKey, entries);
|
|
352
|
-
suppressionUpdatedChannel.broadcast(true);
|
|
353
|
-
await recentErrors.onSuppressionChanged();
|
|
354
|
-
}
|
|
355
|
-
public async removeSuppressionEntry(key: string) {
|
|
356
|
-
let entries = await this.getEntries();
|
|
357
|
-
delete entries.entries[key];
|
|
358
|
-
await suppressionListArchive.set(suppressionListKey, entries);
|
|
359
|
-
suppressionUpdatedChannel.broadcast(true);
|
|
360
|
-
await recentErrors.onSuppressionChanged();
|
|
361
|
-
}
|
|
362
|
-
|
|
363
|
-
public async getSuppressionList(): Promise<SuppressionEntry[]> {
|
|
364
|
-
let entries = Object.values((await this.getEntries()).entries);
|
|
365
|
-
return entries;
|
|
366
|
-
}
|
|
367
|
-
}
|
|
368
|
-
export const suppressionList = new SuppressionList();
|
|
369
|
-
export const SuppressionListController = getSyncedController(SocketFunction.register(
|
|
370
|
-
"SuppressionListController-08f985d8-8d06-4041-ac4b-44566c54615d",
|
|
371
|
-
suppressionList,
|
|
372
|
-
() => ({
|
|
373
|
-
setSuppressionEntry: {},
|
|
374
|
-
removeSuppressionEntry: {},
|
|
375
|
-
getSuppressionList: {},
|
|
376
|
-
}),
|
|
377
|
-
() => ({
|
|
378
|
-
hooks: [assertIsManagementUser],
|
|
379
|
-
}),
|
|
380
|
-
{
|
|
381
|
-
noFunctionMeasure: true,
|
|
382
|
-
}
|
|
383
|
-
), {
|
|
384
|
-
reads: {
|
|
385
|
-
getSuppressionList: ["suppression-list"],
|
|
386
|
-
},
|
|
387
|
-
writes: {
|
|
388
|
-
setSuppressionEntry: ["suppression-list", "recent-errors"],
|
|
389
|
-
removeSuppressionEntry: ["suppression-list", "recent-errors"],
|
|
390
|
-
},
|
|
391
|
-
});
|
|
392
|
-
|
|
393
|
-
class URLCache {
|
|
394
|
-
private root = isNode() && os.homedir() + "/backblaze-cache/" || "";
|
|
395
|
-
private sizeLimiter = new SizeLimiter({
|
|
396
|
-
diskRoot: this.root,
|
|
397
|
-
maxBytes: 1024 * 1024 * 1024 * 10,
|
|
398
|
-
// Basically... enough cache for a week of hourly data, for 1000 servers. At which point, this whole system will suck (too many servers to check), and we might have to change it
|
|
399
|
-
// - Ex, by only using backblaze, and aggregating logs there, to reduce file counts
|
|
400
|
-
// - Or... just by using elasticsearch...
|
|
401
|
-
// - Or... just have dedicated search servers, which store the data in memory. We will have few people querying, so having 100 servers all execute a 60s search query is fine.
|
|
402
|
-
maxFiles: 24 * 7 * 1000,
|
|
403
|
-
maxDiskFraction: 0.1,
|
|
404
|
-
maxTotalDiskFraction: 0.96,
|
|
405
|
-
minBytes: 0,
|
|
406
|
-
});
|
|
407
|
-
// Returns the PATH to it on disk, so you can then parse it via a stream, so you never have to store it all at once in memory.
|
|
408
|
-
public async getURLLocalPath(url: string, hash: string): Promise<string | undefined> {
|
|
409
|
-
if (!isNode()) return undefined;
|
|
410
|
-
|
|
411
|
-
// Create cache directory if it doesn't exist
|
|
412
|
-
if (!await fsExistsAsync(this.root)) {
|
|
413
|
-
await fs.promises.mkdir(this.root, { recursive: true });
|
|
414
|
-
}
|
|
415
|
-
|
|
416
|
-
const filePath = this.root + hash;
|
|
417
|
-
|
|
418
|
-
try {
|
|
419
|
-
// Check if file already exists
|
|
420
|
-
const stats = await fs.promises.stat(filePath);
|
|
421
|
-
if (stats.isFile()) {
|
|
422
|
-
void this.applyLimitting([]);
|
|
423
|
-
return filePath;
|
|
424
|
-
}
|
|
425
|
-
} catch (e: any) {
|
|
426
|
-
// File doesn't exist, need to download it
|
|
427
|
-
if (e.code !== "ENOENT") {
|
|
428
|
-
throw e;
|
|
429
|
-
}
|
|
430
|
-
}
|
|
431
|
-
|
|
432
|
-
try {
|
|
433
|
-
// Download and stream to disk
|
|
434
|
-
const response = await fetch(url);
|
|
435
|
-
if (!response.ok) {
|
|
436
|
-
throw new Error(`Failed to fetch ${url}: ${response.status} ${response.statusText}`);
|
|
437
|
-
}
|
|
438
|
-
|
|
439
|
-
if (!response.body) {
|
|
440
|
-
throw new Error(`Response body is undefined for ${url}`);
|
|
441
|
-
}
|
|
442
|
-
|
|
443
|
-
// Create write stream
|
|
444
|
-
let tempPath = filePath + ".temp";
|
|
445
|
-
const writeStream = fs.createWriteStream(tempPath);
|
|
446
|
-
const reader = response.body.getReader();
|
|
447
|
-
|
|
448
|
-
try {
|
|
449
|
-
for await (const chunk of streamToIteratable(reader)) {
|
|
450
|
-
if (!chunk) continue;
|
|
451
|
-
let result = writeStream.write(Buffer.from(chunk));
|
|
452
|
-
if (!result) {
|
|
453
|
-
await new Promise<void>(resolve => writeStream.once("drain", resolve));
|
|
454
|
-
}
|
|
455
|
-
}
|
|
456
|
-
|
|
457
|
-
await new Promise<void>((resolve, reject) => {
|
|
458
|
-
writeStream.end((err: any) => {
|
|
459
|
-
if (err) reject(err);
|
|
460
|
-
else resolve();
|
|
461
|
-
});
|
|
462
|
-
});
|
|
463
|
-
|
|
464
|
-
// Trigger cache cleanup in the background
|
|
465
|
-
void this.applyLimitting([]);
|
|
466
|
-
|
|
467
|
-
await fs.promises.rename(tempPath, filePath);
|
|
468
|
-
return filePath;
|
|
469
|
-
} catch (error) {
|
|
470
|
-
// Clean up partial file on error
|
|
471
|
-
try {
|
|
472
|
-
await fs.promises.unlink(tempPath);
|
|
473
|
-
} catch {
|
|
474
|
-
// Ignore cleanup errors
|
|
475
|
-
}
|
|
476
|
-
throw error;
|
|
477
|
-
} finally {
|
|
478
|
-
reader.releaseLock();
|
|
479
|
-
}
|
|
480
|
-
} catch (error) {
|
|
481
|
-
console.error(`Failed to download and cache file from ${url}:`, error);
|
|
482
|
-
return undefined;
|
|
483
|
-
}
|
|
484
|
-
}
|
|
485
|
-
private applyLimitting = batchFunction({ delay: LOCAL_CACHE_LIMIT_BATCH }, async () => {
|
|
486
|
-
const files = await fs.promises.readdir(this.root);
|
|
487
|
-
const fileInfos: { time: number; bytes: number; path: string; }[] = [];
|
|
488
|
-
|
|
489
|
-
for (const file of files) {
|
|
490
|
-
const filePath = this.root + file;
|
|
491
|
-
const stats = await fs.promises.stat(filePath);
|
|
492
|
-
if (stats.isFile()) {
|
|
493
|
-
fileInfos.push({
|
|
494
|
-
time: stats.atimeMs,
|
|
495
|
-
bytes: stats.size,
|
|
496
|
-
path: filePath,
|
|
497
|
-
});
|
|
498
|
-
}
|
|
499
|
-
}
|
|
500
|
-
|
|
501
|
-
const result = await this.sizeLimiter.limit(fileInfos);
|
|
502
|
-
|
|
503
|
-
for (const fileToRemove of result.remove) {
|
|
504
|
-
await fs.promises.unlink(fileToRemove.path);
|
|
505
|
-
}
|
|
506
|
-
});
|
|
507
|
-
}
|
|
508
|
-
export const urlCache = new URLCache();
|
|
509
|
-
|
|
510
|
-
const limitRecentErrors = measureWrap(function limitRecentErrors(objs: LogDatum[]) {
|
|
511
|
-
sort(objs, x => x.time);
|
|
512
|
-
let recent: LogDatum[] = [];
|
|
513
|
-
let foundHashes = new Set<string>();
|
|
514
|
-
let countByFile = new Map<string, number>();
|
|
515
|
-
// NOTE: We iterate backwards, because... usually new logs come in at the end, and are pushed, so we want to sort by time (that way we often don't have to resort by much). And if we sort by time, the newest at at the end!
|
|
516
|
-
for (let i = objs.length - 1; i >= 0; i--) {
|
|
517
|
-
let obj = objs[i];
|
|
518
|
-
let file = String(obj.__FILE__) || "";
|
|
519
|
-
let count = countByFile.get(file) || 0;
|
|
520
|
-
if (count > MAX_RECENT_ERRORS_PER_FILE) continue;
|
|
521
|
-
count++;
|
|
522
|
-
let hash = getLogHash(obj);
|
|
523
|
-
if (foundHashes.has(hash)) continue;
|
|
524
|
-
foundHashes.add(hash);
|
|
525
|
-
if (count > MAX_RECENT_ERRORS_PER_FILE) continue;
|
|
526
|
-
countByFile.set(file, count);
|
|
527
|
-
recent.push(obj);
|
|
528
|
-
if (recent.length >= MAX_RECENT_ERRORS) break;
|
|
529
|
-
}
|
|
530
|
-
return recent;
|
|
531
|
-
});
|
|
532
|
-
|
|
533
|
-
export class RecentErrors {
|
|
534
|
-
|
|
535
|
-
constructor(private addErrorsCallback?: (objs: LogDatum[]) => void | Promise<void>) {
|
|
536
|
-
this.addErrorsCallback = addErrorsCallback;
|
|
537
|
-
}
|
|
538
|
-
|
|
539
|
-
// TODO: Uninitialize (stopping the infinite polling), if all of our recent errors watchers go away.
|
|
540
|
-
private initialize = lazy(async () => {
|
|
541
|
-
errorWatcherBase.watch(x => {
|
|
542
|
-
void this.addErrors(x);
|
|
543
|
-
});
|
|
544
|
-
await this.scanNow({});
|
|
545
|
-
runInfinitePoll(BACKBLAZE_POLL_INTERVAL, async () => {
|
|
546
|
-
await this.scanNow({ noLocalFiles: true });
|
|
547
|
-
});
|
|
548
|
-
});
|
|
549
|
-
|
|
550
|
-
private _recentErrors: LogDatum[] = [];
|
|
551
|
-
private _lastSentErrors: LogDatum[] = [];
|
|
552
|
-
private updateRecentErrors = runInSerial(async () => {
|
|
553
|
-
let newList = this._recentErrors;
|
|
554
|
-
newList = await suppressionList.filterObjsToNonSuppressed(newList);
|
|
555
|
-
newList = limitRecentErrors(newList);
|
|
556
|
-
this._recentErrors = newList;
|
|
557
|
-
// If any changed
|
|
558
|
-
let prev = new Set(this._lastSentErrors);
|
|
559
|
-
let newErrors = new Set(newList);
|
|
560
|
-
function hasAnyChanged() {
|
|
561
|
-
for (let obj of newList) {
|
|
562
|
-
if (!prev.has(obj)) {
|
|
563
|
-
return true;
|
|
564
|
-
}
|
|
565
|
-
}
|
|
566
|
-
for (let obj of prev) {
|
|
567
|
-
if (!newErrors.has(obj)) {
|
|
568
|
-
return true;
|
|
569
|
-
}
|
|
570
|
-
}
|
|
571
|
-
return false;
|
|
572
|
-
}
|
|
573
|
-
this._lastSentErrors = newList;
|
|
574
|
-
if (hasAnyChanged()) {
|
|
575
|
-
void this.broadcastUpdate(undefined);
|
|
576
|
-
}
|
|
577
|
-
});
|
|
578
|
-
private broadcastUpdate = batchFunction({ delay: NOTIFICATION_BROADCAST_BATCH, noMeasure: true }, () => {
|
|
579
|
-
recentErrorsChannel.broadcast(true);
|
|
580
|
-
});
|
|
581
|
-
|
|
582
|
-
private addErrors = runInSerial(async (objs: LogDatum[]) => {
|
|
583
|
-
if (objs.length === 0) return;
|
|
584
|
-
|
|
585
|
-
if (this.addErrorsCallback) {
|
|
586
|
-
await this.addErrorsCallback(objs);
|
|
587
|
-
return;
|
|
588
|
-
}
|
|
589
|
-
for (let obj of objs) {
|
|
590
|
-
this._recentErrors.push(obj);
|
|
591
|
-
}
|
|
592
|
-
await this.updateRecentErrors();
|
|
593
|
-
});
|
|
594
|
-
|
|
595
|
-
private lastSuppressionList = new Map<string, SuppressionEntry>();
|
|
596
|
-
public onSuppressionChanged = runInSerial(async () => {
|
|
597
|
-
let newSuppressionList = new Map((await suppressionList.getSuppressionList()).map(x => [x.key, x]));
|
|
598
|
-
let prev = this.lastSuppressionList;
|
|
599
|
-
function anyReduced() {
|
|
600
|
-
for (let newEntry of newSuppressionList.values()) {
|
|
601
|
-
let oldEntry = prev.get(newEntry.key);
|
|
602
|
-
if (oldEntry && newEntry.expiresAt < oldEntry.expiresAt) {
|
|
603
|
-
return true;
|
|
604
|
-
}
|
|
605
|
-
}
|
|
606
|
-
for (let oldEntry of prev.values()) {
|
|
607
|
-
if (!newSuppressionList.has(oldEntry.key)) {
|
|
608
|
-
return true;
|
|
609
|
-
}
|
|
610
|
-
}
|
|
611
|
-
return false;
|
|
612
|
-
}
|
|
613
|
-
if (anyReduced()) {
|
|
614
|
-
console.info("Suppression has been reduced (entries removed or expiry times decreased), performing full rescan to find any revealed values.");
|
|
615
|
-
this.scannedHashes.clear();
|
|
616
|
-
void this.scanNow({});
|
|
617
|
-
}
|
|
618
|
-
this.lastSuppressionList = newSuppressionList;
|
|
619
|
-
await this.updateRecentErrors();
|
|
620
|
-
});
|
|
621
|
-
|
|
622
|
-
private scannedHashes = new Set<string>();
|
|
623
|
-
private scanNow = runInSerial(async (config: {
|
|
624
|
-
noLocalFiles?: boolean;
|
|
625
|
-
}) => {
|
|
626
|
-
// If we're scanning everything, we should update the suppression list, because it might have been changed remotely, and we might be scanning everything because the user clicked refresh.
|
|
627
|
-
if (!this.lastSuppressionList || !config.noLocalFiles) {
|
|
628
|
-
this.lastSuppressionList = new Map((await suppressionList.getSuppressionList()).map(x => [x.key, x]));
|
|
629
|
-
}
|
|
630
|
-
for (let appendable of getErrorAppendables()) {
|
|
631
|
-
let startTime = Date.now() - VIEW_WINDOW;
|
|
632
|
-
let endTime = Date.now() + timeInHour * 2;
|
|
633
|
-
let result = await new FastArchiveAppendableControllerBase().startSynchronizeInternal({
|
|
634
|
-
range: {
|
|
635
|
-
startTime,
|
|
636
|
-
endTime,
|
|
637
|
-
},
|
|
638
|
-
rootPath: appendable.rootPath,
|
|
639
|
-
noLocalFiles: config.noLocalFiles,
|
|
640
|
-
forceGetPublic: true,
|
|
641
|
-
});
|
|
642
|
-
// Filter again, as new suppressions change the errors
|
|
643
|
-
await this.updateRecentErrors();
|
|
644
|
-
let recentLimit = 0;
|
|
645
|
-
const applyRecentLimit = () => {
|
|
646
|
-
if (this._recentErrors.length < MAX_RECENT_ERRORS) return;
|
|
647
|
-
recentLimit = this._recentErrors[0].time - timeInHour * 2;
|
|
648
|
-
};
|
|
649
|
-
applyRecentLimit();
|
|
650
|
-
for (let file of result.files) {
|
|
651
|
-
let path: string | undefined = undefined;
|
|
652
|
-
let size: number | undefined = undefined;
|
|
653
|
-
try {
|
|
654
|
-
// Skip if it is older than even our throttled values!
|
|
655
|
-
if (file.startTime < recentLimit) continue;
|
|
656
|
-
// Don't skip local files, we only do them once at the start, or on demand when we want to recalc all anyways!
|
|
657
|
-
if (!file.nodeId) {
|
|
658
|
-
let hash = getFileMetadataHash(file);
|
|
659
|
-
if (this.scannedHashes.has(hash)) continue;
|
|
660
|
-
this.scannedHashes.add(hash);
|
|
661
|
-
}
|
|
662
|
-
|
|
663
|
-
let hash = getFileMetadataHash(file);
|
|
664
|
-
path = await urlCache.getURLLocalPath(file.url, hash);
|
|
665
|
-
if (!path) continue;
|
|
666
|
-
let scanner = await suppressionList.scanForRecentErrors({
|
|
667
|
-
debugName: file.url,
|
|
668
|
-
startTime: file.startTime,
|
|
669
|
-
endTime: file.endTime,
|
|
670
|
-
});
|
|
671
|
-
|
|
672
|
-
// Stream decompress the file while reading it
|
|
673
|
-
size = await fs.promises.stat(path).then(x => x.size);
|
|
674
|
-
if (!size) {
|
|
675
|
-
console.error(`Deleting empty cached file ${path} for ${file.url} `);
|
|
676
|
-
// NOTE: This means we will repeatedly download empty files, but... that should be fairly fast...
|
|
677
|
-
await fs.promises.unlink(path);
|
|
678
|
-
continue;
|
|
679
|
-
}
|
|
680
|
-
const fileStream = fs.createReadStream(path);
|
|
681
|
-
const gunzip = zlib.createGunzip();
|
|
682
|
-
const decompressedStream = fileStream.pipe(gunzip);
|
|
683
|
-
for await (const chunk of decompressedStream) {
|
|
684
|
-
scanner.onData(chunk);
|
|
685
|
-
}
|
|
686
|
-
let newErrors = await scanner.finish();
|
|
687
|
-
await this.addErrors(newErrors);
|
|
688
|
-
applyRecentLimit();
|
|
689
|
-
} catch (e: any) {
|
|
690
|
-
console.error(`Failed to scan file ${file.url}, size is ${size}, error is:\n${e.stack}`);
|
|
691
|
-
}
|
|
692
|
-
}
|
|
693
|
-
}
|
|
694
|
-
});
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
public async getRecentErrors(): Promise<LogDatum[]> {
|
|
698
|
-
await this.initialize();
|
|
699
|
-
return this._recentErrors;
|
|
700
|
-
}
|
|
701
|
-
|
|
702
|
-
// Rescans all local, and new backblaze
|
|
703
|
-
public async rescanAllErrorsNow() {
|
|
704
|
-
await this.scanNow({});
|
|
705
|
-
return this._recentErrors;
|
|
706
|
-
}
|
|
707
|
-
|
|
708
|
-
public async raiseTestError(...params: unknown[]) {
|
|
709
|
-
console.error(...params);
|
|
710
|
-
}
|
|
711
|
-
}
|
|
712
|
-
const recentErrors = new RecentErrors();
|
|
713
|
-
export const RecentErrorsController = getSyncedController(SocketFunction.register(
|
|
714
|
-
"RecentErrorsController-8450c626-2a06-4eee-81cb-67d4c2fa8155",
|
|
715
|
-
recentErrors,
|
|
716
|
-
() => ({
|
|
717
|
-
getRecentErrors: {},
|
|
718
|
-
rescanAllErrorsNow: {},
|
|
719
|
-
raiseTestError: {},
|
|
720
|
-
}),
|
|
721
|
-
() => ({
|
|
722
|
-
hooks: [assertIsManagementUser],
|
|
723
|
-
}),
|
|
724
|
-
), {
|
|
725
|
-
reads: {
|
|
726
|
-
getRecentErrors: ["recent-errors"],
|
|
727
|
-
},
|
|
728
|
-
writes: {
|
|
729
|
-
rescanAllErrorsNow: ["recent-errors"],
|
|
730
|
-
},
|
|
731
|
-
});
|
|
732
|
-
|
|
733
|
-
export const recentErrorsChannel = new SocketChannel<true>("recent-errors-eeceb0c8-4086-4ab3-b3ff-fa9fd5282e14");
|
|
734
|
-
|
|
735
|
-
export const watchRecentErrors = lazy(function watchRecentErrors() {
|
|
736
|
-
recentErrorsChannel.watch(async () => {
|
|
737
|
-
// Only 1 function, so just refresh all...
|
|
738
|
-
RecentErrorsController.refreshAll();
|
|
739
|
-
});
|
|
740
|
-
});
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
export const notifyWatchersOfError = batchFunction({
|
|
746
|
-
delay: LOCAL_BROADCAST_BATCH,
|
|
747
|
-
},
|
|
748
|
-
async (objs: LogDatum[]) => {
|
|
749
|
-
objs = await suppressionList.filterObjsToNonSuppressed(objs);
|
|
750
|
-
if (objs.length > 0) {
|
|
751
|
-
errorWatcherBase.broadcast(objs);
|
|
752
|
-
}
|
|
753
|
-
}
|
|
754
|
-
);
|
|
755
|
-
|
|
756
|
-
export const errorWatcherBase = new SocketChannel<LogDatum[]>("error-watcher-38de08cd-3247-4f75-9ac0-7919b240607d");
|