@obsunified/collector 2.0.1 → 2.2.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/dist/auth.d.ts +1 -1
- package/dist/auth.js +1 -1
- package/dist/{chunk-UGWDOGGH.js → chunk-KMEZKGSO.js} +22 -10
- package/dist/chunk-KMEZKGSO.js.map +1 -0
- package/dist/{chunk-XKQTVIRE.js → chunk-ZSF5XP3B.js} +8 -2
- package/dist/chunk-ZSF5XP3B.js.map +1 -0
- package/dist/index.d.ts +630 -27
- package/dist/index.js +5819 -853
- package/dist/index.js.map +1 -1
- package/dist/{ingest-auth-HFVO1zIp.d.ts → ingest-auth-C5-8Mly5.d.ts} +9 -0
- package/dist/{openai-FJYOQ7WO.js → openai-GSJZGFGL.js} +2 -2
- package/package.json +5 -5
- package/src/__tests__/autosre-closed-loop.test.ts +1275 -0
- package/src/analyses/derive.ts +26 -25
- package/src/analyses/investigations.ts +8 -8
- package/src/analyses/tier0.ts +22 -22
- package/src/auth/dashboard-auth.test.ts +791 -0
- package/src/auth/dashboard-auth.ts +187 -38
- package/src/auth/login-rate-limit.test.ts +129 -0
- package/src/auth/login-rate-limit.ts +143 -0
- package/src/framework/body-limit.test.ts +165 -0
- package/src/framework/collector.ts +355 -115
- package/src/framework/env.ts +9 -0
- package/src/framework/limits.ts +31 -0
- package/src/framework/scheduled-handlers.test.ts +342 -0
- package/src/index.ts +59 -1
- package/src/lib/action-retention.test.ts +472 -0
- package/src/lib/action-retention.ts +420 -0
- package/src/lib/actions-store.ts +6 -12
- package/src/lib/ai-store/calls.ts +2 -2
- package/src/lib/ai-store/sessions.ts +3 -3
- package/src/lib/ai-store/spans.ts +1 -1
- package/src/lib/ai-store/types.ts +2 -1
- package/src/lib/alerts-store.test.ts +153 -0
- package/src/lib/alerts-store.ts +122 -21
- package/src/lib/analyses-runner.test.ts +75 -0
- package/src/lib/analyses-runner.ts +69 -3
- package/src/lib/analyses-store-latest.test.ts +61 -0
- package/src/lib/analyses-store.ts +17 -10
- package/src/lib/autosre-change-correlation.ts +780 -0
- package/src/lib/autosre-findings.ts +725 -0
- package/src/lib/autosre-ontology.ts +914 -0
- package/src/lib/autosre-remediation-gate.ts +1663 -0
- package/src/lib/blob-store.ts +1 -1
- package/src/lib/eval-cases-store.ts +63 -1
- package/src/lib/evidence-references.ts +6 -0
- package/src/lib/housekeeping.test.ts +153 -0
- package/src/lib/housekeeping.ts +193 -0
- package/src/lib/identity-index/action-lookups.ts +6 -6
- package/src/lib/identity-index/key-lookups.ts +2 -2
- package/src/lib/identity-index/user-lookup.ts +8 -5
- package/src/lib/identity-index.test.ts +2 -3
- package/src/lib/metrics-store-series.test.ts +99 -0
- package/src/lib/metrics-store.test.ts +23 -0
- package/src/lib/metrics-store.ts +27 -3
- package/src/lib/projects-store.test.ts +25 -0
- package/src/lib/projects-store.ts +4 -3
- package/src/lib/sql-db-dialect.test.ts +12 -3
- package/src/lib/sql-db-postgres.integration.test.ts +1225 -0
- package/src/lib/sql-db-postgres.test.ts +314 -0
- package/src/lib/sql-db-postgres.ts +392 -54
- package/src/lib/sql-db-sqlite-time-windows.test.ts +194 -0
- package/src/lib/sql-db.ts +40 -6
- package/src/lib/store/ingest.ts +94 -6
- package/src/lib/test-utils/action-graph-fixtures.ts +307 -0
- package/src/lib/test-utils/node-sqlite-db.ts +188 -0
- package/src/lib/webhook-url.test.ts +122 -0
- package/src/lib/webhook-url.ts +155 -0
- package/src/migrations/041_ingest_key_hash_text.sql +5 -0
- package/src/migrations/042_session_replay_project_composite_pk.sql +52 -0
- package/src/migrations/043_metric_exemplars_text_timestamps.sql +5 -0
- package/src/migrations/044_actions_started_at_index.sql +10 -0
- package/src/migrations/045_user_profiles_project_composite_pk.sql +40 -0
- package/src/migrations/046_action_graph_project_composite_pk.sql +255 -0
- package/src/migrations/047_ai_span_payloads_project_composite_pk.sql +61 -0
- package/src/migrations/048_telemetry_spans_project_composite_pk.sql +92 -0
- package/src/migrations/049_alert_state_notified.sql +17 -0
- package/src/migrations/050_autosre_causal_ontology.sql +216 -0
- package/src/migrations/alert-state-notified.test.ts +111 -0
- package/src/migrations/parity.test.ts +68 -0
- package/src/migrations/project-scoped-keys.test.ts +432 -0
- package/src/migrations-postgres/009_projects_and_keys.sql +4 -4
- package/src/migrations-postgres/026_obs_dashboard_project_seed.sql +1 -1
- package/src/migrations-postgres/035_metric_exemplars.sql +5 -2
- package/src/migrations-postgres/041_ingest_key_hash_text.sql +32 -0
- package/src/migrations-postgres/042_session_replay_project_composite_pk.sql +16 -0
- package/src/migrations-postgres/043_metric_exemplars_text_timestamps.sql +47 -0
- package/src/migrations-postgres/044_actions_started_at_index.sql +8 -0
- package/src/migrations-postgres/045_user_profiles_project_composite_pk.sql +33 -0
- package/src/migrations-postgres/046_action_graph_project_composite_pk.sql +39 -0
- package/src/migrations-postgres/047_ai_span_payloads_project_composite_pk.sql +30 -0
- package/src/migrations-postgres/048_telemetry_spans_project_composite_pk.sql +37 -0
- package/src/migrations-postgres/049_alert_state_notified.sql +10 -0
- package/src/migrations-postgres/050_autosre_causal_ontology.sql +216 -0
- package/src/migrations-postgres/README.md +58 -5
- package/src/otlp/decode/body.test.ts +76 -0
- package/src/otlp/decode/body.ts +67 -6
- package/src/plugins/_json.ts +21 -0
- package/src/plugins/action-graph-processor.ts +6 -12
- package/src/plugins/ai-receiver.ts +5 -12
- package/src/plugins/ai-span-payloads-processor.ts +1 -2
- package/src/plugins/alerts-evaluator-retention.test.ts +170 -0
- package/src/plugins/alerts-evaluator.test.ts +464 -3
- package/src/plugins/alerts-evaluator.ts +181 -41
- package/src/plugins/alerts-routes.ts +10 -6
- package/src/plugins/ask-routes.ts +4 -5
- package/src/plugins/autosre-routes.ts +1019 -0
- package/src/plugins/connected-routes/manifest.ts +13 -1
- package/src/plugins/connected-routes.test.ts +106 -0
- package/src/plugins/connected-routes.ts +285 -5
- package/src/plugins/dashboard-routes.test.ts +120 -0
- package/src/plugins/dashboard-routes.ts +11 -2
- package/src/plugins/default-span-enrichment.ts +13 -1
- package/src/plugins/eval-cases-routes.ts +65 -19
- package/src/plugins/evidence-retrieval-routes.ts +300 -4
- package/src/plugins/identity-receiver.ts +9 -11
- package/src/plugins/json-body.test.ts +91 -0
- package/src/plugins/onboarding-routes.test.ts +105 -0
- package/src/plugins/onboarding-routes.ts +26 -11
- package/src/plugins/projects-routes.ts +2 -1
- package/src/plugins/replay-isolation.test.ts +922 -0
- package/src/plugins/replay-receiver.ts +8 -8
- package/src/plugins/usage-receiver.ts +3 -6
- package/dist/chunk-UGWDOGGH.js.map +0 -1
- package/dist/chunk-XKQTVIRE.js.map +0 -1
- /package/dist/{openai-FJYOQ7WO.js.map → openai-GSJZGFGL.js.map} +0 -0
|
@@ -11,7 +11,14 @@ import {
|
|
|
11
11
|
type Logger,
|
|
12
12
|
} from "../framework/logger";
|
|
13
13
|
import { AlertsStore, compareValue } from "../lib/alerts-store";
|
|
14
|
+
import { openOrDeduplicateIncident } from "../lib/autosre-remediation-gate";
|
|
14
15
|
import { sqlDbFor } from "../lib/sql-db";
|
|
16
|
+
import {
|
|
17
|
+
assertSafeWebhookUrl,
|
|
18
|
+
type HostLookup,
|
|
19
|
+
redactWebhookUrl,
|
|
20
|
+
resolveWebhookHost,
|
|
21
|
+
} from "../lib/webhook-url";
|
|
15
22
|
|
|
16
23
|
const DEFAULT_RULE_CONCURRENCY = 5;
|
|
17
24
|
const DEFAULT_RULE_TIMEOUT_MS = 10_000;
|
|
@@ -19,7 +26,22 @@ const DEFAULT_RULE_TIMEOUT_MS = 10_000;
|
|
|
19
26
|
interface AlertEvaluatorStore {
|
|
20
27
|
listEnabledRules(): Promise<AlertRule[]>;
|
|
21
28
|
evaluateRule(rule: AlertRule): Promise<number>;
|
|
22
|
-
getState(
|
|
29
|
+
getState(
|
|
30
|
+
ruleId: string,
|
|
31
|
+
projectId: string,
|
|
32
|
+
): Promise<{
|
|
33
|
+
current_state: AlertState;
|
|
34
|
+
last_state_change: string;
|
|
35
|
+
/** 0 while the transition's notification is owed; absent pre-049 (= delivered). */
|
|
36
|
+
notified?: number;
|
|
37
|
+
} | null>;
|
|
38
|
+
/** Flip the exact transition `(state, lastStateChange)` to delivered. */
|
|
39
|
+
markNotified(
|
|
40
|
+
ruleId: string,
|
|
41
|
+
projectId: string,
|
|
42
|
+
state: AlertState,
|
|
43
|
+
lastStateChange: string,
|
|
44
|
+
): Promise<boolean>;
|
|
23
45
|
getAnalysisNarrative(
|
|
24
46
|
projectId: string,
|
|
25
47
|
analysisId: string,
|
|
@@ -65,12 +87,56 @@ interface WebhookPayload {
|
|
|
65
87
|
};
|
|
66
88
|
}
|
|
67
89
|
|
|
90
|
+
export interface WebhookDeliveryOptions {
|
|
91
|
+
/** Permit targets on loopback / private / link-local hosts. */
|
|
92
|
+
allowPrivateWebhookTargets?: boolean;
|
|
93
|
+
/**
|
|
94
|
+
* Resolver used to refuse public names that point at private addresses.
|
|
95
|
+
* Hosts that can resolve (Node: `dns.lookup`) pass one; Workers cannot
|
|
96
|
+
* reach private networks from `fetch` and leave it unset.
|
|
97
|
+
*/
|
|
98
|
+
hostLookup?: HostLookup;
|
|
99
|
+
}
|
|
100
|
+
|
|
68
101
|
async function fireWebhook(
|
|
69
102
|
channel: AlertWebhookChannel,
|
|
70
103
|
payload: WebhookPayload,
|
|
71
104
|
logger: Logger,
|
|
72
105
|
tracer?: ChildSpanRunner,
|
|
106
|
+
delivery: WebhookDeliveryOptions = {},
|
|
73
107
|
): Promise<boolean> {
|
|
108
|
+
// Hook URLs carry secrets in the path (Slack, PagerDuty…): logs and span
|
|
109
|
+
// attributes only ever see the redacted form.
|
|
110
|
+
const target = redactWebhookUrl(channel.url);
|
|
111
|
+
// Re-check at fire time: rules created before the host guard existed
|
|
112
|
+
// (or with the opt-in later removed) must not reach internal addresses.
|
|
113
|
+
try {
|
|
114
|
+
const url = assertSafeWebhookUrl(channel.url, {
|
|
115
|
+
allowPrivateNetworks: delivery.allowPrivateWebhookTargets,
|
|
116
|
+
});
|
|
117
|
+
if (!delivery.allowPrivateWebhookTargets && delivery.hostLookup) {
|
|
118
|
+
const resolution = await resolveWebhookHost(
|
|
119
|
+
url.hostname,
|
|
120
|
+
delivery.hostLookup,
|
|
121
|
+
);
|
|
122
|
+
if (resolution === "private") {
|
|
123
|
+
throw new Error(
|
|
124
|
+
`channel.url host "${url.hostname}" resolves to a private address`,
|
|
125
|
+
);
|
|
126
|
+
}
|
|
127
|
+
if (resolution === "unresolvable") {
|
|
128
|
+
throw new Error(
|
|
129
|
+
`channel.url host "${url.hostname}" could not be resolved; refusing to deliver blind`,
|
|
130
|
+
);
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
} catch (err) {
|
|
134
|
+
logger.error("[alerts-evaluator] webhook target refused", {
|
|
135
|
+
url: target,
|
|
136
|
+
error: err instanceof Error ? err.message : String(err),
|
|
137
|
+
});
|
|
138
|
+
return false;
|
|
139
|
+
}
|
|
74
140
|
const exec = async (
|
|
75
141
|
span: { setAttribute(k: string, v: unknown): void } | null,
|
|
76
142
|
): Promise<boolean> => {
|
|
@@ -82,13 +148,24 @@ async function fireWebhook(
|
|
|
82
148
|
...(channel.headers ?? {}),
|
|
83
149
|
},
|
|
84
150
|
body: JSON.stringify(payload),
|
|
151
|
+
// Never follow redirects: a 3xx could re-aim the POST (and its
|
|
152
|
+
// headers) at an arbitrary host. A redirecting hook is a
|
|
153
|
+
// misconfiguration and counts as a failed delivery.
|
|
154
|
+
redirect: "manual",
|
|
85
155
|
signal: AbortSignal.timeout(5000),
|
|
86
156
|
});
|
|
87
157
|
if (span) span.setAttribute("http.response.status_code", response.status);
|
|
158
|
+
if (response.status >= 300 && response.status < 400) {
|
|
159
|
+
logger.error("[alerts-evaluator] webhook responded with a redirect", {
|
|
160
|
+
url: target,
|
|
161
|
+
status: response.status,
|
|
162
|
+
});
|
|
163
|
+
return false;
|
|
164
|
+
}
|
|
88
165
|
return response.ok;
|
|
89
166
|
} catch (err) {
|
|
90
167
|
logger.error("[alerts-evaluator] webhook delivery failed", {
|
|
91
|
-
url:
|
|
168
|
+
url: target,
|
|
92
169
|
error: err instanceof Error ? err.message : String(err),
|
|
93
170
|
});
|
|
94
171
|
return false;
|
|
@@ -97,7 +174,7 @@ async function fireWebhook(
|
|
|
97
174
|
if (!tracer) return exec(null);
|
|
98
175
|
return tracer("webhook.alert", async (span) => exec(span), {
|
|
99
176
|
"http.method": "POST",
|
|
100
|
-
"http.url":
|
|
177
|
+
"http.url": target,
|
|
101
178
|
"alert.state": payload.state,
|
|
102
179
|
});
|
|
103
180
|
}
|
|
@@ -107,12 +184,13 @@ async function fireChannels(
|
|
|
107
184
|
payload: WebhookPayload,
|
|
108
185
|
logger: Logger,
|
|
109
186
|
tracer?: ChildSpanRunner,
|
|
187
|
+
delivery: WebhookDeliveryOptions = {},
|
|
110
188
|
): Promise<boolean> {
|
|
111
189
|
if (channels.length === 0) return true;
|
|
112
190
|
const results = await Promise.all(
|
|
113
191
|
channels
|
|
114
192
|
.filter((ch): ch is AlertWebhookChannel => ch.type === "webhook")
|
|
115
|
-
.map((ch) => fireWebhook(ch, payload, logger, tracer)),
|
|
193
|
+
.map((ch) => fireWebhook(ch, payload, logger, tracer, delivery)),
|
|
116
194
|
);
|
|
117
195
|
return results.every(Boolean);
|
|
118
196
|
}
|
|
@@ -160,7 +238,10 @@ async function evaluateOneRule(
|
|
|
160
238
|
logger: Logger,
|
|
161
239
|
tracer: ChildSpanRunner | undefined,
|
|
162
240
|
ruleTimeoutMs: number,
|
|
163
|
-
|
|
241
|
+
delivery: WebhookDeliveryOptions,
|
|
242
|
+
onAlertFiring?: (rule: AlertRule) => Promise<void>,
|
|
243
|
+
): Promise<{ fired: number; resolved: number; redelivered: number }> {
|
|
244
|
+
const none = { fired: 0, resolved: 0, redelivered: 0 };
|
|
164
245
|
try {
|
|
165
246
|
const value = await withTimeout(
|
|
166
247
|
store.evaluateRule(rule),
|
|
@@ -168,7 +249,7 @@ async function evaluateOneRule(
|
|
|
168
249
|
`rule evaluation timed out after ${ruleTimeoutMs}ms`,
|
|
169
250
|
);
|
|
170
251
|
const shouldFire = compareValue(value, rule.threshold, rule.comparison);
|
|
171
|
-
const priorState = await store.getState(rule.id);
|
|
252
|
+
const priorState = await store.getState(rule.id, rule.projectId);
|
|
172
253
|
const previous: AlertState = priorState?.current_state ?? "ok";
|
|
173
254
|
const next: AlertState = shouldFire ? "firing" : "ok";
|
|
174
255
|
const now = new Date().toISOString();
|
|
@@ -191,54 +272,81 @@ async function evaluateOneRule(
|
|
|
191
272
|
.catch(() => undefined)
|
|
192
273
|
: undefined;
|
|
193
274
|
|
|
194
|
-
|
|
195
|
-
|
|
275
|
+
const deliver = (state: AlertState) =>
|
|
276
|
+
fireChannels(
|
|
196
277
|
rule.channels,
|
|
197
278
|
{
|
|
198
279
|
rule: { id: rule.id, name: rule.name, signal: rule.signal },
|
|
199
280
|
value,
|
|
200
281
|
threshold: rule.threshold,
|
|
201
282
|
comparison: rule.comparison,
|
|
202
|
-
state
|
|
283
|
+
state,
|
|
203
284
|
evaluatedAt: now,
|
|
204
285
|
projectId: rule.projectId,
|
|
205
286
|
analysis: analysisAttachment,
|
|
206
287
|
},
|
|
207
288
|
logger,
|
|
208
289
|
tracer,
|
|
290
|
+
delivery,
|
|
209
291
|
);
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
292
|
+
|
|
293
|
+
if (previous !== next) {
|
|
294
|
+
// The stored state is what the dashboard shows and must follow the
|
|
295
|
+
// signal, not the webhook: record the transition first (with its
|
|
296
|
+
// notification outstanding, `alert_state.notified = 0`), then
|
|
297
|
+
// notify. On accepted delivery the flag is flipped for exactly this
|
|
298
|
+
// transition; otherwise the same-state branch below retries on the
|
|
299
|
+
// next tick without a second transition.
|
|
300
|
+
await store.transitionState(rule.id, rule.projectId, next, now);
|
|
301
|
+
if (next === "firing" && onAlertFiring) {
|
|
302
|
+
await onAlertFiring(rule).catch(() => undefined);
|
|
303
|
+
}
|
|
304
|
+
const ok = await deliver(next);
|
|
305
|
+
// Acknowledge before writing history: if the evaluation insert
|
|
306
|
+
// fails, the delivered flag is already durable and the next tick
|
|
307
|
+
// will not send the same alert again.
|
|
308
|
+
if (ok) {
|
|
309
|
+
await store.markNotified(rule.id, rule.projectId, next, now);
|
|
310
|
+
}
|
|
311
|
+
await store.recordEvaluation(rule.id, rule.projectId, value, next, ok);
|
|
312
|
+
if (!ok) {
|
|
313
|
+
logger.warn(
|
|
314
|
+
"[alerts-evaluator] state recorded but delivery failed; will retry",
|
|
315
|
+
{ rule_id: rule.id, project_id: rule.projectId, state: next },
|
|
316
|
+
);
|
|
317
|
+
}
|
|
318
|
+
return next === "firing"
|
|
319
|
+
? { fired: 1, resolved: 0, redelivered: 0 }
|
|
320
|
+
: { fired: 0, resolved: 1, redelivered: 0 };
|
|
219
321
|
}
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
);
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
322
|
+
|
|
323
|
+
// Same state. While the transition into it is still marked undelivered,
|
|
324
|
+
// retry the notification; the state itself is already correct. A row
|
|
325
|
+
// without the flag (schema before migration 049) counts as delivered.
|
|
326
|
+
const owed = priorState !== null && priorState.notified === 0;
|
|
327
|
+
if (owed) {
|
|
328
|
+
const ok = await deliver(previous);
|
|
329
|
+
if (ok) {
|
|
330
|
+
await store.markNotified(
|
|
331
|
+
rule.id,
|
|
332
|
+
rule.projectId,
|
|
333
|
+
previous,
|
|
334
|
+
priorState.last_state_change,
|
|
335
|
+
);
|
|
336
|
+
}
|
|
337
|
+
await store.recordEvaluation(rule.id, rule.projectId, value, next, ok);
|
|
338
|
+
if (!ok) {
|
|
339
|
+
logger.warn("[alerts-evaluator] delivery retry failed; will retry", {
|
|
340
|
+
rule_id: rule.id,
|
|
341
|
+
project_id: rule.projectId,
|
|
342
|
+
state: previous,
|
|
343
|
+
});
|
|
344
|
+
return none;
|
|
345
|
+
}
|
|
346
|
+
return { fired: 0, resolved: 0, redelivered: 1 };
|
|
239
347
|
}
|
|
240
348
|
|
|
241
|
-
// Same state — write an evaluation
|
|
349
|
+
// Same state, already notified — write an evaluation, do not fire.
|
|
242
350
|
await store.recordEvaluation(rule.id, rule.projectId, value, next, false);
|
|
243
351
|
} catch (err) {
|
|
244
352
|
logger.error("[alerts-evaluator] rule evaluation failed", {
|
|
@@ -248,7 +356,7 @@ async function evaluateOneRule(
|
|
|
248
356
|
});
|
|
249
357
|
}
|
|
250
358
|
|
|
251
|
-
return
|
|
359
|
+
return none;
|
|
252
360
|
}
|
|
253
361
|
|
|
254
362
|
export async function evaluateRuleBatch(
|
|
@@ -259,13 +367,22 @@ export async function evaluateRuleBatch(
|
|
|
259
367
|
tracer?: ChildSpanRunner;
|
|
260
368
|
concurrency?: number;
|
|
261
369
|
ruleTimeoutMs?: number;
|
|
370
|
+
allowPrivateWebhookTargets?: boolean;
|
|
371
|
+
hostLookup?: HostLookup;
|
|
372
|
+
onAlertFiring?: (rule: AlertRule) => Promise<void>;
|
|
262
373
|
},
|
|
263
374
|
): Promise<{
|
|
264
375
|
evaluated: number;
|
|
265
376
|
fired: number;
|
|
266
377
|
resolved: number;
|
|
378
|
+
/** Notifications for an earlier transition that were delivered on this tick. */
|
|
379
|
+
redelivered: number;
|
|
267
380
|
}> {
|
|
268
381
|
const logger = options?.logger ?? consoleLogger;
|
|
382
|
+
const delivery: WebhookDeliveryOptions = {
|
|
383
|
+
allowPrivateWebhookTargets: options?.allowPrivateWebhookTargets,
|
|
384
|
+
hostLookup: options?.hostLookup,
|
|
385
|
+
};
|
|
269
386
|
const concurrency = Math.max(
|
|
270
387
|
1,
|
|
271
388
|
Math.floor(options?.concurrency ?? DEFAULT_RULE_CONCURRENCY),
|
|
@@ -275,12 +392,21 @@ export async function evaluateRuleBatch(
|
|
|
275
392
|
Math.floor(options?.ruleTimeoutMs ?? DEFAULT_RULE_TIMEOUT_MS),
|
|
276
393
|
);
|
|
277
394
|
const results = await mapWithConcurrency(rules, concurrency, (rule) =>
|
|
278
|
-
evaluateOneRule(
|
|
395
|
+
evaluateOneRule(
|
|
396
|
+
store,
|
|
397
|
+
rule,
|
|
398
|
+
logger,
|
|
399
|
+
options?.tracer,
|
|
400
|
+
ruleTimeoutMs,
|
|
401
|
+
delivery,
|
|
402
|
+
options?.onAlertFiring,
|
|
403
|
+
),
|
|
279
404
|
);
|
|
280
405
|
return {
|
|
281
406
|
evaluated: rules.length,
|
|
282
407
|
fired: results.reduce((sum, result) => sum + result.fired, 0),
|
|
283
408
|
resolved: results.reduce((sum, result) => sum + result.resolved, 0),
|
|
409
|
+
redelivered: results.reduce((sum, result) => sum + result.redelivered, 0),
|
|
284
410
|
};
|
|
285
411
|
}
|
|
286
412
|
|
|
@@ -291,21 +417,35 @@ export async function evaluateAllRules(
|
|
|
291
417
|
tracer?: ChildSpanRunner;
|
|
292
418
|
concurrency?: number;
|
|
293
419
|
ruleTimeoutMs?: number;
|
|
420
|
+
hostLookup?: HostLookup;
|
|
294
421
|
},
|
|
295
422
|
): Promise<{
|
|
296
423
|
evaluated: number;
|
|
297
424
|
fired: number;
|
|
298
425
|
resolved: number;
|
|
426
|
+
redelivered: number;
|
|
299
427
|
}> {
|
|
300
428
|
const logger = options?.logger ?? consoleLogger;
|
|
301
429
|
const tracer = options?.tracer;
|
|
302
|
-
const
|
|
430
|
+
const allowPrivateWebhookTargets = env.ALERT_WEBHOOK_ALLOW_PRIVATE === "true";
|
|
431
|
+
const db = sqlDbFor(env);
|
|
432
|
+
const store = new AlertsStore(db, { allowPrivateWebhookTargets });
|
|
303
433
|
const rules = await store.listEnabledRules();
|
|
304
434
|
return evaluateRuleBatch(store, rules, {
|
|
305
435
|
logger,
|
|
306
436
|
tracer,
|
|
307
437
|
concurrency: options?.concurrency,
|
|
308
438
|
ruleTimeoutMs: options?.ruleTimeoutMs,
|
|
439
|
+
allowPrivateWebhookTargets,
|
|
440
|
+
hostLookup: options?.hostLookup,
|
|
441
|
+
onAlertFiring: async (rule) => {
|
|
442
|
+
await openOrDeduplicateIncident(db, rule.projectId, {
|
|
443
|
+
environment: "default",
|
|
444
|
+
targetNodeId: `alert:${rule.id}`,
|
|
445
|
+
symptomAnchorKind: "alert",
|
|
446
|
+
symptomAnchorId: rule.id,
|
|
447
|
+
});
|
|
448
|
+
},
|
|
309
449
|
});
|
|
310
450
|
}
|
|
311
451
|
|
|
@@ -6,12 +6,16 @@ import { alertEvidenceReferences } from "../lib/evidence-references";
|
|
|
6
6
|
import { sqlDbFor } from "../lib/sql-db";
|
|
7
7
|
import { getProjectId } from "./_context";
|
|
8
8
|
|
|
9
|
+
const storeOptions = (env: { ALERT_WEBHOOK_ALLOW_PRIVATE?: string }) => ({
|
|
10
|
+
allowPrivateWebhookTargets: env.ALERT_WEBHOOK_ALLOW_PRIVATE === "true",
|
|
11
|
+
});
|
|
12
|
+
|
|
9
13
|
export const alertsRoutesPlugin: CollectorPlugin = {
|
|
10
14
|
name: "alerts-routes",
|
|
11
15
|
register(app) {
|
|
12
16
|
app.get("/internal/alerts/rules", async (c) => {
|
|
13
17
|
const projectId = getProjectId(c);
|
|
14
|
-
const store = new AlertsStore(sqlDbFor(c.env));
|
|
18
|
+
const store = new AlertsStore(sqlDbFor(c.env), storeOptions(c.env));
|
|
15
19
|
const rules = await store.listRules(projectId);
|
|
16
20
|
return c.json({ rules });
|
|
17
21
|
});
|
|
@@ -24,7 +28,7 @@ export const alertsRoutesPlugin: CollectorPlugin = {
|
|
|
24
28
|
} catch {
|
|
25
29
|
return c.json({ error: "Invalid JSON body" }, 400);
|
|
26
30
|
}
|
|
27
|
-
const store = new AlertsStore(sqlDbFor(c.env));
|
|
31
|
+
const store = new AlertsStore(sqlDbFor(c.env), storeOptions(c.env));
|
|
28
32
|
try {
|
|
29
33
|
const rule = await store.createRule(projectId, input);
|
|
30
34
|
return c.json({ rule }, 201);
|
|
@@ -43,7 +47,7 @@ export const alertsRoutesPlugin: CollectorPlugin = {
|
|
|
43
47
|
} catch {
|
|
44
48
|
return c.json({ error: "Invalid JSON body" }, 400);
|
|
45
49
|
}
|
|
46
|
-
const store = new AlertsStore(sqlDbFor(c.env));
|
|
50
|
+
const store = new AlertsStore(sqlDbFor(c.env), storeOptions(c.env));
|
|
47
51
|
try {
|
|
48
52
|
const rule = await store.updateRule(id, projectId, patch);
|
|
49
53
|
if (!rule) return c.json({ error: "Rule not found" }, 404);
|
|
@@ -57,7 +61,7 @@ export const alertsRoutesPlugin: CollectorPlugin = {
|
|
|
57
61
|
app.delete("/internal/alerts/rules/:id", async (c) => {
|
|
58
62
|
const projectId = getProjectId(c);
|
|
59
63
|
const id = c.req.param("id");
|
|
60
|
-
const store = new AlertsStore(sqlDbFor(c.env));
|
|
64
|
+
const store = new AlertsStore(sqlDbFor(c.env), storeOptions(c.env));
|
|
61
65
|
const deleted = await store.deleteRule(id, projectId);
|
|
62
66
|
if (!deleted) return c.json({ error: "Rule not found" }, 404);
|
|
63
67
|
return c.json({ success: true });
|
|
@@ -71,7 +75,7 @@ export const alertsRoutesPlugin: CollectorPlugin = {
|
|
|
71
75
|
1,
|
|
72
76
|
Math.min(720, Number.parseInt(c.req.query("hours") || "24", 10) || 24),
|
|
73
77
|
);
|
|
74
|
-
const store = new AlertsStore(sqlDbFor(c.env));
|
|
78
|
+
const store = new AlertsStore(sqlDbFor(c.env), storeOptions(c.env));
|
|
75
79
|
const response = await store.listEvaluations({
|
|
76
80
|
ruleId,
|
|
77
81
|
projectId,
|
|
@@ -83,7 +87,7 @@ export const alertsRoutesPlugin: CollectorPlugin = {
|
|
|
83
87
|
app.post("/internal/alerts/rules/:id/test", async (c) => {
|
|
84
88
|
const projectId = getProjectId(c);
|
|
85
89
|
const id = c.req.param("id");
|
|
86
|
-
const store = new AlertsStore(sqlDbFor(c.env));
|
|
90
|
+
const store = new AlertsStore(sqlDbFor(c.env), storeOptions(c.env));
|
|
87
91
|
const rule = await store.getRule(id, projectId);
|
|
88
92
|
if (!rule) return c.json({ error: "Rule not found" }, 404);
|
|
89
93
|
const value = await store.evaluateRule(rule);
|
|
@@ -22,6 +22,7 @@ import { runAsk } from "../lib/ask";
|
|
|
22
22
|
import type { LlmConfig } from "../lib/llm";
|
|
23
23
|
import { sqlDbFor } from "../lib/sql-db";
|
|
24
24
|
import { getProjectId } from "./_context";
|
|
25
|
+
import { readJsonObject } from "./_json";
|
|
25
26
|
|
|
26
27
|
const MAX_QUESTION_CHARS = 1000;
|
|
27
28
|
|
|
@@ -29,12 +30,10 @@ export const askRoutesPlugin: CollectorPlugin = {
|
|
|
29
30
|
name: "ask-routes",
|
|
30
31
|
register(app, runtime) {
|
|
31
32
|
app.post("/internal/ask", async (c) => {
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
body = (await c.req.json()) as Partial<AskRequest>;
|
|
35
|
-
} catch {
|
|
33
|
+
const body = await readJsonObject<Partial<AskRequest>>(c);
|
|
34
|
+
if (!body) {
|
|
36
35
|
return c.json(
|
|
37
|
-
{ error: "Bad Request", message: "body must be JSON" },
|
|
36
|
+
{ error: "Bad Request", message: "body must be a JSON object" },
|
|
38
37
|
400,
|
|
39
38
|
);
|
|
40
39
|
}
|