@futurewindai/wotchi 0.1.0-beta.2 → 0.1.0-beta.6
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/README.md +178 -47
- package/SECURITY.md +2 -2
- package/dist/cjs/core/admission.js +22 -0
- package/dist/cjs/core/client.js +416 -21
- package/dist/cjs/core/config.js +193 -18
- package/dist/cjs/core/diagnostics.js +16 -1
- package/dist/cjs/core/group-store.js +22 -9
- package/dist/cjs/core/incident-builder.js +18 -0
- package/dist/cjs/core/incident-policy.js +2 -7
- package/dist/cjs/core/limits.js +17 -0
- package/dist/cjs/core/normalize.js +16 -6
- package/dist/cjs/core/notification-queue.js +92 -6
- package/dist/cjs/core/prometheus.js +129 -0
- package/dist/cjs/core/redact.js +57 -8
- package/dist/cjs/core/runtime-monitor.js +109 -0
- package/dist/cjs/index.js +8 -1
- package/dist/cjs/integrations/express/error-handler.js +21 -4
- package/dist/cjs/integrations/express/index.js +2 -1
- package/dist/cjs/integrations/express/status-observer.js +6 -0
- package/dist/cjs/integrations/nest/capture.js +21 -0
- package/dist/cjs/integrations/nest/exception-filter.js +64 -8
- package/dist/cjs/integrations/nest/filter-wrapper.js +17 -0
- package/dist/cjs/integrations/nest/index.js +7 -1
- package/dist/cjs/integrations/nest/module.js +44 -0
- package/dist/cjs/integrations/nest/register.js +15 -1
- package/dist/cjs/integrations/request-context.js +83 -1
- package/dist/cjs/notifiers/alert-payload.js +20 -0
- package/dist/cjs/notifiers/console.js +85 -15
- package/dist/cjs/notifiers/telegram-format.js +75 -5
- package/dist/cjs/notifiers/webhook-http.js +394 -0
- package/dist/cjs/notifiers/webhook.js +28 -0
- package/dist/esm/core/admission.js +19 -0
- package/dist/esm/core/client.js +416 -21
- package/dist/esm/core/config.js +193 -18
- package/dist/esm/core/diagnostics.js +16 -1
- package/dist/esm/core/group-store.js +22 -9
- package/dist/esm/core/incident-builder.js +18 -0
- package/dist/esm/core/incident-policy.js +2 -7
- package/dist/esm/core/limits.js +14 -0
- package/dist/esm/core/normalize.js +16 -6
- package/dist/esm/core/notification-queue.js +92 -6
- package/dist/esm/core/prometheus.js +125 -0
- package/dist/esm/core/redact.js +57 -8
- package/dist/esm/core/runtime-monitor.js +106 -0
- package/dist/esm/index.js +4 -1
- package/dist/esm/integrations/express/error-handler.js +21 -4
- package/dist/esm/integrations/express/index.js +1 -1
- package/dist/esm/integrations/express/status-observer.js +6 -0
- package/dist/esm/integrations/nest/capture.js +17 -0
- package/dist/esm/integrations/nest/exception-filter.js +65 -9
- package/dist/esm/integrations/nest/filter-wrapper.js +14 -0
- package/dist/esm/integrations/nest/index.js +3 -1
- package/dist/esm/integrations/nest/module.js +41 -0
- package/dist/esm/integrations/nest/register.js +15 -1
- package/dist/esm/integrations/request-context.js +83 -1
- package/dist/esm/notifiers/alert-payload.js +16 -0
- package/dist/esm/notifiers/console.js +85 -15
- package/dist/esm/notifiers/telegram-format.js +75 -5
- package/dist/esm/notifiers/webhook-http.js +387 -0
- package/dist/esm/notifiers/webhook.js +24 -0
- package/dist/types/core/admission.d.ts +10 -0
- package/dist/types/core/config.d.ts +17 -1
- package/dist/types/core/diagnostics.d.ts +7 -1
- package/dist/types/core/incident-policy.d.ts +1 -1
- package/dist/types/core/limits.d.ts +14 -0
- package/dist/types/core/notification-queue.d.ts +14 -1
- package/dist/types/core/prometheus.d.ts +7 -0
- package/dist/types/core/runtime-monitor.d.ts +15 -0
- package/dist/types/core/types.d.ts +111 -2
- package/dist/types/index.d.ts +8 -2
- package/dist/types/integrations/express/index.d.ts +2 -2
- package/dist/types/integrations/nest/capture.d.ts +4 -0
- package/dist/types/integrations/nest/exception-filter.d.ts +6 -2
- package/dist/types/integrations/nest/filter-wrapper.d.ts +7 -0
- package/dist/types/integrations/nest/index.d.ts +6 -2
- package/dist/types/integrations/nest/module.d.ts +9 -0
- package/dist/types/integrations/nest/register.d.ts +3 -0
- package/dist/types/integrations/request-context.d.ts +3 -0
- package/dist/types/notifiers/alert-payload.d.ts +5 -0
- package/dist/types/notifiers/webhook-http.d.ts +37 -0
- package/dist/types/notifiers/webhook.d.ts +4 -0
- package/dist/types-cjs/core/admission.d.cts +10 -0
- package/dist/types-cjs/core/config.d.cts +17 -1
- package/dist/types-cjs/core/diagnostics.d.cts +7 -1
- package/dist/types-cjs/core/incident-policy.d.cts +1 -1
- package/dist/types-cjs/core/limits.d.cts +14 -0
- package/dist/types-cjs/core/notification-queue.d.cts +14 -1
- package/dist/types-cjs/core/prometheus.d.cts +7 -0
- package/dist/types-cjs/core/runtime-monitor.d.cts +15 -0
- package/dist/types-cjs/core/types.d.cts +111 -2
- package/dist/types-cjs/index.d.cts +8 -2
- package/dist/types-cjs/integrations/express/index.d.cts +2 -2
- package/dist/types-cjs/integrations/nest/capture.d.cts +4 -0
- package/dist/types-cjs/integrations/nest/exception-filter.d.cts +6 -2
- package/dist/types-cjs/integrations/nest/filter-wrapper.d.cts +7 -0
- package/dist/types-cjs/integrations/nest/index.d.cts +6 -2
- package/dist/types-cjs/integrations/nest/module.d.cts +9 -0
- package/dist/types-cjs/integrations/nest/register.d.cts +3 -0
- package/dist/types-cjs/integrations/request-context.d.cts +3 -0
- package/dist/types-cjs/notifiers/alert-payload.d.cts +5 -0
- package/dist/types-cjs/notifiers/webhook-http.d.cts +37 -0
- package/dist/types-cjs/notifiers/webhook.d.cts +4 -0
- package/package.json +17 -4
package/dist/cjs/core/client.js
CHANGED
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.createWotchi = createWotchi;
|
|
4
4
|
const config_js_1 = require("./config.js");
|
|
5
|
+
const admission_js_1 = require("./admission.js");
|
|
5
6
|
const diagnostics_js_1 = require("./diagnostics.js");
|
|
6
7
|
const fingerprint_js_1 = require("./fingerprint.js");
|
|
7
8
|
const group_store_js_1 = require("./group-store.js");
|
|
@@ -13,6 +14,34 @@ const redact_js_1 = require("./redact.js");
|
|
|
13
14
|
const isRecord = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
|
|
14
15
|
const asString = (value) => typeof value === "string" ? value : undefined;
|
|
15
16
|
const asStatusCode = (value) => typeof value === "number" && Number.isSafeInteger(value) ? value : undefined;
|
|
17
|
+
const deepFreeze = (value) => {
|
|
18
|
+
if (typeof value !== "object" || value === null || Object.isFrozen(value)) {
|
|
19
|
+
return value;
|
|
20
|
+
}
|
|
21
|
+
Object.freeze(value);
|
|
22
|
+
for (const child of Object.values(value)) {
|
|
23
|
+
deepFreeze(child);
|
|
24
|
+
}
|
|
25
|
+
return value;
|
|
26
|
+
};
|
|
27
|
+
const safeTrace = (value, privacy) => {
|
|
28
|
+
if (value === undefined) {
|
|
29
|
+
return undefined;
|
|
30
|
+
}
|
|
31
|
+
const redacted = (0, redact_js_1.redactValue)(value, privacy);
|
|
32
|
+
if (!isRecord(redacted)) {
|
|
33
|
+
return undefined;
|
|
34
|
+
}
|
|
35
|
+
const traceId = asString(redacted.traceId);
|
|
36
|
+
const spanId = asString(redacted.spanId);
|
|
37
|
+
if (traceId === undefined && spanId === undefined) {
|
|
38
|
+
return undefined;
|
|
39
|
+
}
|
|
40
|
+
return {
|
|
41
|
+
...(traceId === undefined ? {} : { traceId: traceId.slice(0, 128) }),
|
|
42
|
+
...(spanId === undefined ? {} : { spanId: spanId.slice(0, 128) }),
|
|
43
|
+
};
|
|
44
|
+
};
|
|
16
45
|
const safeRecord = (value, privacy) => {
|
|
17
46
|
if (value === undefined) {
|
|
18
47
|
return undefined;
|
|
@@ -42,20 +71,48 @@ const safeRequest = (request, privacy) => {
|
|
|
42
71
|
const method = asString(redacted.method);
|
|
43
72
|
const route = asString(redacted.route);
|
|
44
73
|
const requestId = asString(redacted.requestId);
|
|
74
|
+
const correlationId = asString(redacted.correlationId);
|
|
45
75
|
const statusCode = asStatusCode(redacted.statusCode);
|
|
76
|
+
const trace = safeTrace(redacted.trace, privacy);
|
|
46
77
|
if (method === undefined &&
|
|
47
78
|
route === undefined &&
|
|
48
79
|
requestId === undefined &&
|
|
49
|
-
|
|
80
|
+
correlationId === undefined &&
|
|
81
|
+
statusCode === undefined &&
|
|
82
|
+
trace === undefined) {
|
|
50
83
|
return undefined;
|
|
51
84
|
}
|
|
52
85
|
return {
|
|
53
86
|
...(method === undefined ? {} : { method }),
|
|
54
87
|
...(route === undefined ? {} : { route }),
|
|
55
88
|
...(requestId === undefined ? {} : { requestId }),
|
|
89
|
+
...(correlationId === undefined ? {} : { correlationId }),
|
|
56
90
|
...(statusCode === undefined ? {} : { statusCode }),
|
|
91
|
+
...(trace === undefined ? {} : { trace }),
|
|
57
92
|
};
|
|
58
93
|
};
|
|
94
|
+
const safeTags = (value, privacy) => {
|
|
95
|
+
const redacted = (0, redact_js_1.redactValue)(value, { ...privacy, maxKeys: Math.min(privacy.maxKeys, 50) });
|
|
96
|
+
if (!isRecord(redacted)) {
|
|
97
|
+
return undefined;
|
|
98
|
+
}
|
|
99
|
+
const tags = {};
|
|
100
|
+
for (const [key, tagValue] of Object.entries(redacted)) {
|
|
101
|
+
if (key.length > 64 || typeof tagValue !== "string") {
|
|
102
|
+
continue;
|
|
103
|
+
}
|
|
104
|
+
tags[key] = tagValue.slice(0, 200);
|
|
105
|
+
}
|
|
106
|
+
return Object.keys(tags).length === 0 ? undefined : tags;
|
|
107
|
+
};
|
|
108
|
+
const safeShortString = (value, privacy, maxLength) => {
|
|
109
|
+
if (value === undefined) {
|
|
110
|
+
return undefined;
|
|
111
|
+
}
|
|
112
|
+
const redacted = (0, redact_js_1.redactValue)(value, { ...privacy, maxStringLength: maxLength });
|
|
113
|
+
const text = asString(redacted);
|
|
114
|
+
return text === undefined ? undefined : text.slice(0, maxLength);
|
|
115
|
+
};
|
|
59
116
|
const safeMessage = (value, privacy) => {
|
|
60
117
|
const normalized = (0, normalize_js_1.normalizeUnknown)(value, privacy);
|
|
61
118
|
return typeof normalized === "string" ? normalized : "[unreadable value]";
|
|
@@ -69,6 +126,150 @@ const normalizeEventAlertThreshold = (value) => {
|
|
|
69
126
|
}
|
|
70
127
|
return value;
|
|
71
128
|
};
|
|
129
|
+
const safeErrorFromValue = (value, privacy) => {
|
|
130
|
+
const redacted = (0, redact_js_1.redactValue)(value, privacy);
|
|
131
|
+
if (!isRecord(redacted)) {
|
|
132
|
+
return undefined;
|
|
133
|
+
}
|
|
134
|
+
const name = asString(redacted.name);
|
|
135
|
+
const message = asString(redacted.message);
|
|
136
|
+
if (name === undefined || message === undefined) {
|
|
137
|
+
return undefined;
|
|
138
|
+
}
|
|
139
|
+
const stack = asString(redacted.stack);
|
|
140
|
+
const code = asString(redacted.code);
|
|
141
|
+
const applicationFrame = asString(redacted.applicationFrame);
|
|
142
|
+
return {
|
|
143
|
+
name,
|
|
144
|
+
message,
|
|
145
|
+
...(stack === undefined ? {} : { stack }),
|
|
146
|
+
...(code === undefined ? {} : { code }),
|
|
147
|
+
...(applicationFrame === undefined
|
|
148
|
+
? {}
|
|
149
|
+
: { applicationFrame: applicationFrame.slice(0, 1_000) }),
|
|
150
|
+
};
|
|
151
|
+
};
|
|
152
|
+
const safeAlertFromValue = (value, fallback, privacy) => {
|
|
153
|
+
const redacted = (0, redact_js_1.redactValue)(value, privacy);
|
|
154
|
+
if (!isRecord(redacted)) {
|
|
155
|
+
return undefined;
|
|
156
|
+
}
|
|
157
|
+
const title = asString(redacted.title);
|
|
158
|
+
const fingerprint = asString(redacted.fingerprint);
|
|
159
|
+
const summary = asString(redacted.summary);
|
|
160
|
+
const severity = asString(redacted.severity);
|
|
161
|
+
const firstSeenAt = asString(redacted.firstSeenAt);
|
|
162
|
+
const lastSeenAt = asString(redacted.lastSeenAt);
|
|
163
|
+
const service = asString(redacted.service);
|
|
164
|
+
const environment = asString(redacted.environment);
|
|
165
|
+
const occurrences = asStatusCode(redacted.occurrences);
|
|
166
|
+
if (title === undefined ||
|
|
167
|
+
fingerprint === undefined ||
|
|
168
|
+
summary === undefined ||
|
|
169
|
+
firstSeenAt === undefined ||
|
|
170
|
+
lastSeenAt === undefined ||
|
|
171
|
+
service === undefined ||
|
|
172
|
+
environment === undefined ||
|
|
173
|
+
occurrences === undefined ||
|
|
174
|
+
(severity !== "low" && severity !== "medium" && severity !== "high" && severity !== "critical")) {
|
|
175
|
+
return undefined;
|
|
176
|
+
}
|
|
177
|
+
const rawActions = redacted.suggestedActions;
|
|
178
|
+
if (!Array.isArray(rawActions)) {
|
|
179
|
+
return undefined;
|
|
180
|
+
}
|
|
181
|
+
const suggestedActions = rawActions
|
|
182
|
+
.filter((action) => typeof action === "string")
|
|
183
|
+
.slice(0, 5)
|
|
184
|
+
.map((action) => action.slice(0, 600));
|
|
185
|
+
const error = safeErrorFromValue(redacted.error, privacy);
|
|
186
|
+
const request = safeRequest(redacted.request, privacy);
|
|
187
|
+
const trace = safeTrace(redacted.trace, privacy);
|
|
188
|
+
const context = safeRecord(redacted.context, privacy);
|
|
189
|
+
const tags = safeTags(redacted.tags, privacy);
|
|
190
|
+
const linksValue = safeRecord(redacted.links, privacy);
|
|
191
|
+
const logLink = linksValue === undefined ? undefined : asString(linksValue.log);
|
|
192
|
+
const traceLink = linksValue === undefined ? undefined : asString(linksValue.trace);
|
|
193
|
+
const links = linksValue
|
|
194
|
+
? {
|
|
195
|
+
...(logLink === undefined ? {} : { log: logLink.slice(0, 2_048) }),
|
|
196
|
+
...(traceLink === undefined ? {} : { trace: traceLink.slice(0, 2_048) }),
|
|
197
|
+
}
|
|
198
|
+
: undefined;
|
|
199
|
+
const instance = asString(redacted.instance);
|
|
200
|
+
const release = asString(redacted.release);
|
|
201
|
+
const correlationId = asString(redacted.correlationId);
|
|
202
|
+
const operation = asString(redacted.operation);
|
|
203
|
+
const job = asString(redacted.job);
|
|
204
|
+
return deepFreeze({
|
|
205
|
+
id: asString(redacted.id) ?? fallback.id,
|
|
206
|
+
fingerprint: fingerprint.slice(0, 200),
|
|
207
|
+
title: title.slice(0, 300),
|
|
208
|
+
severity,
|
|
209
|
+
summary: summary.slice(0, 2_000),
|
|
210
|
+
suggestedActions,
|
|
211
|
+
firstSeenAt: firstSeenAt.slice(0, 100),
|
|
212
|
+
lastSeenAt: lastSeenAt.slice(0, 100),
|
|
213
|
+
occurrences: Math.max(0, Math.min(occurrences, Number.MAX_SAFE_INTEGER)),
|
|
214
|
+
service: service.slice(0, 300),
|
|
215
|
+
environment: environment.slice(0, 300),
|
|
216
|
+
...(instance === undefined ? {} : { instance: instance.slice(0, 200) }),
|
|
217
|
+
...(release === undefined ? {} : { release: release.slice(0, 200) }),
|
|
218
|
+
...(correlationId === undefined ? {} : { correlationId: correlationId.slice(0, 200) }),
|
|
219
|
+
...(operation === undefined ? {} : { operation: operation.slice(0, 200) }),
|
|
220
|
+
...(job === undefined ? {} : { job: job.slice(0, 200) }),
|
|
221
|
+
...(tags === undefined ? {} : { tags }),
|
|
222
|
+
...(error === undefined ? {} : { error }),
|
|
223
|
+
...(request === undefined ? {} : { request }),
|
|
224
|
+
...(trace === undefined ? {} : { trace }),
|
|
225
|
+
...(context === undefined ? {} : { context }),
|
|
226
|
+
...(links === undefined || Object.keys(links).length === 0 ? {} : { links }),
|
|
227
|
+
});
|
|
228
|
+
};
|
|
229
|
+
const normalizeFingerprintOverride = (override, event) => {
|
|
230
|
+
if (override === undefined) {
|
|
231
|
+
return undefined;
|
|
232
|
+
}
|
|
233
|
+
const value = typeof override === "function" ? override(event) : override;
|
|
234
|
+
if (value === undefined) {
|
|
235
|
+
return undefined;
|
|
236
|
+
}
|
|
237
|
+
if (typeof value !== "string" || value.trim().length === 0 || value.length > 200) {
|
|
238
|
+
throw new TypeError("fingerprint must be a non-empty string no longer than 200 characters");
|
|
239
|
+
}
|
|
240
|
+
return value.trim();
|
|
241
|
+
};
|
|
242
|
+
const renderLinks = (templates, event, fingerprint) => {
|
|
243
|
+
if (templates === undefined) {
|
|
244
|
+
return undefined;
|
|
245
|
+
}
|
|
246
|
+
const values = {
|
|
247
|
+
service: event.service,
|
|
248
|
+
environment: event.environment,
|
|
249
|
+
release: event.release ?? "",
|
|
250
|
+
instance: event.instance ?? "",
|
|
251
|
+
fingerprint,
|
|
252
|
+
requestId: event.request?.requestId ?? "",
|
|
253
|
+
correlationId: event.correlationId ?? event.request?.correlationId ?? "",
|
|
254
|
+
traceId: event.trace?.traceId ?? event.request?.trace?.traceId ?? "",
|
|
255
|
+
spanId: event.trace?.spanId ?? event.request?.trace?.spanId ?? "",
|
|
256
|
+
route: event.request?.route ?? "",
|
|
257
|
+
statusCode: event.request?.statusCode === undefined ? "" : String(event.request.statusCode),
|
|
258
|
+
};
|
|
259
|
+
const render = (template) => {
|
|
260
|
+
const expanded = template.replace(/\{\{([^{}]+)\}\}/g, (_match, key) => encodeURIComponent(values[key] ?? ""));
|
|
261
|
+
const redacted = (0, redact_js_1.redactValue)(expanded, { maxStringLength: 2_048 });
|
|
262
|
+
return typeof redacted === "string" ? redacted.slice(0, 2_048) : "";
|
|
263
|
+
};
|
|
264
|
+
const result = {};
|
|
265
|
+
if (templates.log !== undefined) {
|
|
266
|
+
result.log = render(templates.log);
|
|
267
|
+
}
|
|
268
|
+
if (templates.trace !== undefined && (values.traceId !== "" || values.spanId !== "")) {
|
|
269
|
+
result.trace = render(templates.trace);
|
|
270
|
+
}
|
|
271
|
+
return Object.keys(result).length === 0 ? undefined : result;
|
|
272
|
+
};
|
|
72
273
|
function createWotchi(config) {
|
|
73
274
|
const normalized = (0, config_js_1.validateConfig)(config);
|
|
74
275
|
const now = Date.now;
|
|
@@ -82,48 +283,162 @@ function createWotchi(config) {
|
|
|
82
283
|
const queue = (0, notification_queue_js_1.createNotificationQueue)({
|
|
83
284
|
maxPendingAlerts: normalized.queue.maxPendingAlerts,
|
|
84
285
|
concurrency: normalized.queue.concurrency,
|
|
286
|
+
notifierTimeoutMs: normalized.queue.notifierTimeoutMs,
|
|
287
|
+
notifierCircuitBreaker: normalized.queue.notifierCircuitBreaker,
|
|
85
288
|
});
|
|
289
|
+
const admission = normalized.overload === undefined
|
|
290
|
+
? undefined
|
|
291
|
+
: (0, admission_js_1.createCaptureAdmission)({
|
|
292
|
+
maxEventsPerSecond: normalized.overload.maxEventsPerSecond,
|
|
293
|
+
burst: normalized.overload.burst,
|
|
294
|
+
now,
|
|
295
|
+
});
|
|
86
296
|
const privacy = normalized.privacy;
|
|
87
297
|
let eventSequence = 0;
|
|
88
|
-
|
|
298
|
+
let overloadLastAlertAt = 0;
|
|
299
|
+
const matchingRule = (event) => normalized.rules.find((rule) => (rule.environment === undefined || rule.environment === event.environment) &&
|
|
300
|
+
(rule.route === undefined || rule.route === event.request?.route));
|
|
301
|
+
const captureSafeEvent = (error, metadata, context, request, eventKind = "error", alertThreshold = normalized.grouping.alertThreshold, fingerprintOverride, severityOverride, trace, correlationId, operation, job, tags) => {
|
|
89
302
|
const timestamp = now();
|
|
90
303
|
const safeError = (0, redact_js_1.redactError)((0, normalize_js_1.normalizeError)(error, privacy), privacy);
|
|
91
304
|
const requestContext = safeRequest(request, privacy);
|
|
92
305
|
const eventContext = mergeContext(metadata, context, privacy);
|
|
306
|
+
const requestTrace = safeTrace(trace ?? requestContext?.trace, privacy);
|
|
307
|
+
const eventCorrelationId = safeShortString(correlationId ?? requestContext?.correlationId, privacy, 200);
|
|
308
|
+
const operationValue = safeShortString(operation ?? eventContext?.operation, privacy, 200);
|
|
309
|
+
const jobValue = safeShortString(job ?? eventContext?.job ?? eventContext?.queue, privacy, 200);
|
|
310
|
+
const eventTags = safeTags(tags, privacy);
|
|
93
311
|
const safeEvent = {
|
|
94
312
|
id: `event-${eventSequence + 1}`,
|
|
95
313
|
timestamp: new Date(timestamp).toISOString(),
|
|
96
314
|
service: normalized.service,
|
|
97
315
|
environment: normalized.environment,
|
|
316
|
+
...(normalized.instance === undefined ? {} : { instance: normalized.instance }),
|
|
98
317
|
...(normalized.release === undefined ? {} : { release: normalized.release }),
|
|
318
|
+
...(eventCorrelationId === undefined ? {} : { correlationId: eventCorrelationId }),
|
|
319
|
+
...(operationValue === undefined ? {} : { operation: operationValue.slice(0, 200) }),
|
|
320
|
+
...(jobValue === undefined ? {} : { job: jobValue.slice(0, 200) }),
|
|
321
|
+
...(eventTags === undefined ? {} : { tags: eventTags }),
|
|
99
322
|
error: safeError,
|
|
100
323
|
...(requestContext === undefined ? {} : { request: requestContext }),
|
|
324
|
+
...(requestTrace === undefined ? {} : { trace: requestTrace }),
|
|
101
325
|
...(eventContext === undefined ? {} : { context: eventContext }),
|
|
102
326
|
};
|
|
103
327
|
eventSequence += 1;
|
|
104
|
-
const
|
|
105
|
-
|
|
106
|
-
|
|
328
|
+
const filteredEvent = deepFreeze(safeEvent);
|
|
329
|
+
if (normalized.filter !== undefined) {
|
|
330
|
+
try {
|
|
331
|
+
const accepted = normalized.filter(filteredEvent);
|
|
332
|
+
if (typeof accepted !== "boolean") {
|
|
333
|
+
diagnostics.filterFailures += 1;
|
|
334
|
+
diagnostics.eventsSuppressed += 1;
|
|
335
|
+
return;
|
|
336
|
+
}
|
|
337
|
+
if (!accepted) {
|
|
338
|
+
diagnostics.eventsSuppressed += 1;
|
|
339
|
+
return;
|
|
340
|
+
}
|
|
341
|
+
}
|
|
342
|
+
catch {
|
|
343
|
+
diagnostics.filterFailures += 1;
|
|
344
|
+
diagnostics.eventsSuppressed += 1;
|
|
345
|
+
return;
|
|
346
|
+
}
|
|
347
|
+
}
|
|
348
|
+
const rule = matchingRule(filteredEvent);
|
|
349
|
+
if (rule?.ignore === true) {
|
|
350
|
+
diagnostics.eventsSuppressed += 1;
|
|
351
|
+
return;
|
|
352
|
+
}
|
|
353
|
+
let fingerprint;
|
|
354
|
+
try {
|
|
355
|
+
fingerprint = normalizeFingerprintOverride(fingerprintOverride, filteredEvent);
|
|
356
|
+
}
|
|
357
|
+
catch {
|
|
358
|
+
diagnostics.fingerprintCallbackFailures += 1;
|
|
359
|
+
}
|
|
360
|
+
if (fingerprint === undefined && normalized.fingerprint !== undefined) {
|
|
361
|
+
try {
|
|
362
|
+
fingerprint = normalizeFingerprintOverride(normalized.fingerprint, filteredEvent);
|
|
363
|
+
}
|
|
364
|
+
catch {
|
|
365
|
+
diagnostics.fingerprintCallbackFailures += 1;
|
|
366
|
+
}
|
|
367
|
+
}
|
|
368
|
+
fingerprint ??= (0, fingerprint_js_1.fingerprintSafeErrorEvent)(filteredEvent);
|
|
369
|
+
const group = groupStore.record(fingerprint, filteredEvent, rule?.severity ?? severityOverride ?? "medium");
|
|
370
|
+
const policyInput = {
|
|
107
371
|
group,
|
|
108
372
|
now: timestamp,
|
|
109
|
-
alertThreshold,
|
|
373
|
+
alertThreshold: rule?.alertThreshold ?? alertThreshold,
|
|
110
374
|
cooldownMs: normalized.grouping.cooldownMs,
|
|
111
375
|
eventKind,
|
|
112
|
-
}
|
|
376
|
+
};
|
|
377
|
+
const manualSeverity = rule?.severity ?? severityOverride;
|
|
378
|
+
const decision = manualSeverity === undefined
|
|
379
|
+
? (0, incident_policy_js_1.evaluateIncidentPolicy)(policyInput)
|
|
380
|
+
: (0, incident_policy_js_1.evaluateIncidentPolicy)({ ...policyInput, manualSeverity });
|
|
113
381
|
if (decision.shouldAlert) {
|
|
114
|
-
const
|
|
115
|
-
|
|
382
|
+
const builtAlert = (0, incident_builder_js_1.buildIncidentAlert)(group, decision);
|
|
383
|
+
const links = renderLinks(normalized.links, filteredEvent, fingerprint);
|
|
384
|
+
const alert = deepFreeze({
|
|
385
|
+
...builtAlert,
|
|
386
|
+
...(links === undefined ? {} : { links }),
|
|
387
|
+
});
|
|
388
|
+
let finalAlert = alert;
|
|
389
|
+
if (normalized.beforeSend !== undefined) {
|
|
390
|
+
let transformed;
|
|
391
|
+
try {
|
|
392
|
+
transformed = normalized.beforeSend(alert);
|
|
393
|
+
}
|
|
394
|
+
catch {
|
|
395
|
+
diagnostics.beforeSendFailures += 1;
|
|
396
|
+
diagnostics.eventsSuppressed += 1;
|
|
397
|
+
return;
|
|
398
|
+
}
|
|
399
|
+
if (transformed === null) {
|
|
400
|
+
diagnostics.eventsSuppressed += 1;
|
|
401
|
+
return;
|
|
402
|
+
}
|
|
403
|
+
if (transformed !== undefined) {
|
|
404
|
+
const sanitized = safeAlertFromValue(transformed, alert, privacy);
|
|
405
|
+
if (sanitized === undefined) {
|
|
406
|
+
diagnostics.beforeSendFailures += 1;
|
|
407
|
+
diagnostics.eventsSuppressed += 1;
|
|
408
|
+
return;
|
|
409
|
+
}
|
|
410
|
+
finalAlert = sanitized;
|
|
411
|
+
}
|
|
412
|
+
}
|
|
413
|
+
if (queue.enqueue(finalAlert, normalized.notifiers)) {
|
|
116
414
|
groupStore.markAlerted(fingerprint, timestamp);
|
|
117
415
|
}
|
|
118
416
|
}
|
|
119
417
|
diagnostics.capturedEvents += 1;
|
|
120
418
|
};
|
|
121
|
-
const
|
|
419
|
+
const emitOverloadSignal = (timestamp) => {
|
|
420
|
+
const cooldownMs = normalized.overload?.alertCooldownMs ?? 0;
|
|
421
|
+
if (cooldownMs > 0 && timestamp - overloadLastAlertAt < cooldownMs) {
|
|
422
|
+
return;
|
|
423
|
+
}
|
|
424
|
+
overloadLastAlertAt = timestamp;
|
|
425
|
+
captureSafeEvent(new Error("Wotchi capture overload: admission limit reached"), undefined, { droppedEvents: diagnostics.eventsDroppedOverload }, undefined, "runtime-monitor", 1, "wotchi.capture.overload", "high", undefined, undefined, "wotchi.overload", undefined, undefined);
|
|
426
|
+
};
|
|
427
|
+
const captureException = (error, context, options) => {
|
|
122
428
|
if (!normalized.enabled) {
|
|
123
429
|
return;
|
|
124
430
|
}
|
|
431
|
+
if (queue.isClosed()) {
|
|
432
|
+
diagnostics.capturesAfterShutdown += 1;
|
|
433
|
+
return;
|
|
434
|
+
}
|
|
125
435
|
try {
|
|
126
|
-
|
|
436
|
+
if (admission !== undefined && !admission.tryAcquire()) {
|
|
437
|
+
diagnostics.eventsDroppedOverload += 1;
|
|
438
|
+
emitOverloadSignal(now());
|
|
439
|
+
return;
|
|
440
|
+
}
|
|
441
|
+
captureSafeEvent(error, undefined, context, options?.request, "error", normalizeEventAlertThreshold(options?.alertThreshold) ?? normalized.grouping.alertThreshold, options?.fingerprint, options?.severity, options?.trace, options?.correlationId, options?.operation, options?.job, options?.tags);
|
|
127
442
|
}
|
|
128
443
|
catch {
|
|
129
444
|
diagnostics.captureFailures += 1;
|
|
@@ -133,6 +448,10 @@ function createWotchi(config) {
|
|
|
133
448
|
if (!normalized.enabled) {
|
|
134
449
|
return;
|
|
135
450
|
}
|
|
451
|
+
if (queue.isClosed()) {
|
|
452
|
+
diagnostics.capturesAfterShutdown += 1;
|
|
453
|
+
return;
|
|
454
|
+
}
|
|
136
455
|
try {
|
|
137
456
|
if (event === null || typeof event !== "object" || event.level !== "error") {
|
|
138
457
|
throw new TypeError("event.level must be error");
|
|
@@ -141,24 +460,100 @@ function createWotchi(config) {
|
|
|
141
460
|
throw new TypeError("event.message must be a string");
|
|
142
461
|
}
|
|
143
462
|
const alertThreshold = normalizeEventAlertThreshold(event.alertThreshold);
|
|
144
|
-
|
|
463
|
+
if (admission !== undefined &&
|
|
464
|
+
event.kind !== "process-monitor" &&
|
|
465
|
+
event.kind !== "runtime-monitor" &&
|
|
466
|
+
!admission.tryAcquire()) {
|
|
467
|
+
diagnostics.eventsDroppedOverload += 1;
|
|
468
|
+
emitOverloadSignal(now());
|
|
469
|
+
return;
|
|
470
|
+
}
|
|
471
|
+
captureSafeEvent(event.error ?? safeMessage(event.message, privacy), event.metadata, event.context, event.request, event.kind ?? "error", alertThreshold ?? normalized.grouping.alertThreshold, event.fingerprint, event.severity, event.trace, event.correlationId, event.operation, event.job, event.tags);
|
|
145
472
|
}
|
|
146
473
|
catch {
|
|
147
474
|
diagnostics.captureFailures += 1;
|
|
148
475
|
}
|
|
149
476
|
};
|
|
477
|
+
const readDiagnostics = () => (0, diagnostics_js_1.snapshotDiagnostics)(diagnostics, {
|
|
478
|
+
groupsEvicted: groupStore.groupsEvicted(),
|
|
479
|
+
alertsQueued: queue.alertsQueued(),
|
|
480
|
+
alertsDropped: queue.alertsDropped(),
|
|
481
|
+
alertsSent: queue.alertsSent(),
|
|
482
|
+
notifierFailures: queue.notifierFailures(),
|
|
483
|
+
notifierTimeouts: queue.notifierTimeouts(),
|
|
484
|
+
notifierCircuitOpenSkips: queue.notifierCircuitOpenSkips(),
|
|
485
|
+
activeGroups: groupStore.size(),
|
|
486
|
+
pendingAlerts: queue.pending(),
|
|
487
|
+
});
|
|
488
|
+
const testAlert = async () => {
|
|
489
|
+
const timestamp = new Date(now()).toISOString();
|
|
490
|
+
const alert = {
|
|
491
|
+
id: `wotchi-test-${timestamp}`,
|
|
492
|
+
fingerprint: "wotchi-test-alert",
|
|
493
|
+
title: "Wotchi — Test alert",
|
|
494
|
+
severity: "low",
|
|
495
|
+
summary: "This is a notifier configuration test alert.",
|
|
496
|
+
suggestedActions: ["Confirm this destination received the test alert."],
|
|
497
|
+
firstSeenAt: timestamp,
|
|
498
|
+
lastSeenAt: timestamp,
|
|
499
|
+
occurrences: 1,
|
|
500
|
+
service: normalized.service,
|
|
501
|
+
environment: normalized.environment,
|
|
502
|
+
...(normalized.instance === undefined ? {} : { instance: normalized.instance }),
|
|
503
|
+
...(normalized.release === undefined ? {} : { release: normalized.release }),
|
|
504
|
+
};
|
|
505
|
+
let jobResult;
|
|
506
|
+
if (!queue.enqueue(alert, normalized.notifiers, (result) => {
|
|
507
|
+
jobResult = result;
|
|
508
|
+
})) {
|
|
509
|
+
return {
|
|
510
|
+
status: "queue-full",
|
|
511
|
+
configurationAccepted: true,
|
|
512
|
+
queued: false,
|
|
513
|
+
flushed: false,
|
|
514
|
+
delivered: false,
|
|
515
|
+
notifierFailures: 0,
|
|
516
|
+
diagnostics: readDiagnostics(),
|
|
517
|
+
error: "Notification queue is full; test alert was not queued.",
|
|
518
|
+
};
|
|
519
|
+
}
|
|
520
|
+
try {
|
|
521
|
+
await queue.flush();
|
|
522
|
+
}
|
|
523
|
+
catch {
|
|
524
|
+
return {
|
|
525
|
+
status: "timeout",
|
|
526
|
+
configurationAccepted: true,
|
|
527
|
+
queued: true,
|
|
528
|
+
flushed: false,
|
|
529
|
+
delivered: false,
|
|
530
|
+
notifierFailures: jobResult?.notifierFailures ?? 0,
|
|
531
|
+
diagnostics: readDiagnostics(),
|
|
532
|
+
error: "Notification queue flush timed out.",
|
|
533
|
+
};
|
|
534
|
+
}
|
|
535
|
+
const notifierFailures = jobResult?.notifierFailures ?? 0;
|
|
536
|
+
const status = notifierFailures > 0 ? "notifier-failed" : "sent";
|
|
537
|
+
return {
|
|
538
|
+
status,
|
|
539
|
+
configurationAccepted: true,
|
|
540
|
+
queued: true,
|
|
541
|
+
flushed: true,
|
|
542
|
+
delivered: status === "sent",
|
|
543
|
+
notifierFailures,
|
|
544
|
+
diagnostics: readDiagnostics(),
|
|
545
|
+
...(status === "notifier-failed" ? { error: "One or more notifiers failed." } : {}),
|
|
546
|
+
};
|
|
547
|
+
};
|
|
548
|
+
const shutdown = async (timeoutMs) => {
|
|
549
|
+
await queue.close(timeoutMs);
|
|
550
|
+
};
|
|
150
551
|
return {
|
|
151
552
|
captureException,
|
|
152
553
|
captureEvent,
|
|
554
|
+
testAlert,
|
|
153
555
|
flush: (timeoutMs) => queue.flush(timeoutMs),
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
alertsQueued: queue.alertsQueued(),
|
|
157
|
-
alertsDropped: queue.alertsDropped(),
|
|
158
|
-
alertsSent: queue.alertsSent(),
|
|
159
|
-
notifierFailures: queue.notifierFailures(),
|
|
160
|
-
activeGroups: groupStore.size(),
|
|
161
|
-
pendingAlerts: queue.pending(),
|
|
162
|
-
}),
|
|
556
|
+
shutdown,
|
|
557
|
+
getDiagnostics: readDiagnostics,
|
|
163
558
|
};
|
|
164
559
|
}
|