@obsunified/collector 2.2.3 → 2.4.3
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 +2 -1
- package/dist/{chunk-ZSF5XP3B.js → chunk-RASSTGNB.js} +30 -22
- package/dist/chunk-RASSTGNB.js.map +1 -0
- package/dist/index.d.ts +264 -62
- package/dist/index.js +3390 -2350
- package/dist/index.js.map +1 -1
- package/dist/{ingest-auth-C5-8Mly5.d.ts → ingest-auth-BTowieER.d.ts} +96 -1
- package/dist/{openai-GSJZGFGL.js → openai-VDKVVZAV.js} +2 -2
- package/package.json +4 -2
- package/src/__tests__/autosre-closed-loop.test.ts +110 -0
- package/src/__tests__/db-parity.integration.test.ts +961 -0
- package/src/__tests__/internal-contracts-routes.test.ts +451 -0
- package/src/framework/env.ts +113 -0
- package/src/index.ts +27 -4
- package/src/lib/ai-store/evaluations.test.ts +3 -3
- package/src/lib/alert-email.ts +104 -0
- package/src/lib/alerts-store.test.ts +116 -2
- package/src/lib/alerts-store.ts +421 -57
- package/src/lib/analyses-store.test.ts +6 -6
- package/src/lib/db-parity.ts +883 -0
- package/src/lib/eval-cases-store.test.ts +1 -1
- package/src/lib/eval-cases-store.ts +16 -4
- package/src/lib/evidence-references.ts +30 -22
- package/src/lib/metric-alerts.test.ts +769 -0
- package/src/lib/metric-alerts.ts +640 -0
- package/src/lib/metrics-store.ts +9 -2
- package/src/lib/sql-db-postgres.integration.test.ts +247 -0
- package/src/lib/stage6.test.ts +1 -1
- package/src/lib/store/trace-queries.ts +1 -1
- package/src/migrations/054_alert_rules_pending_secs.sql +11 -0
- package/src/migrations/055_alert_state_pending_since.sql +12 -0
- package/src/migrations/056_metric_point_series_received.sql +9 -0
- package/src/migrations/057_alert_evaluations_outcome.sql +11 -0
- package/src/migrations/058_alert_state_pending_seen_at.sql +12 -0
- package/src/migrations/059_alert_evaluations_error.sql +11 -0
- package/src/migrations/parity.test.ts +105 -3
- package/src/migrations-postgres/054_alert_rules_pending_secs.sql +7 -0
- package/src/migrations-postgres/055_alert_state_pending_since.sql +6 -0
- package/src/migrations-postgres/056_metric_point_series_received.sql +7 -0
- package/src/migrations-postgres/057_alert_evaluations_outcome.sql +6 -0
- package/src/migrations-postgres/058_alert_state_pending_seen_at.sql +6 -0
- package/src/migrations-postgres/059_alert_evaluations_error.sql +6 -0
- package/src/migrations-postgres/README.md +13 -0
- package/src/plugins/ai-receiver.ts +6 -1
- package/src/plugins/alerts-evaluator-pending.test.ts +322 -0
- package/src/plugins/alerts-evaluator-retention.test.ts +2 -0
- package/src/plugins/alerts-evaluator.test.ts +10 -2
- package/src/plugins/alerts-evaluator.ts +239 -40
- package/src/plugins/alerts-routes.ts +17 -2
- package/src/plugins/autosre-routes.ts +251 -140
- package/src/plugins/connected-routes/manifest.test.ts +114 -32
- package/src/plugins/connected-routes/manifest.ts +59 -76
- package/src/plugins/connected-routes.test.ts +10 -8
- package/src/plugins/connected-routes.ts +42 -12
- package/src/plugins/eval-cases-routes.ts +33 -10
- package/src/plugins/evidence-retrieval-routes.ts +60 -25
- package/dist/chunk-ZSF5XP3B.js.map +0 -1
- /package/dist/{openai-GSJZGFGL.js.map → openai-VDKVVZAV.js.map} +0 -0
|
@@ -1,4 +1,62 @@
|
|
|
1
1
|
import { Hono, Context, MiddlewareHandler } from 'hono';
|
|
2
|
+
import { AlertState } from '@obsunified/types';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Email alert channel: the runtime-neutral half.
|
|
6
|
+
*
|
|
7
|
+
* The collector package runs on Cloudflare Workers as well as Node, and a
|
|
8
|
+
* Worker cannot open an SMTP connection. So the package only defines what an
|
|
9
|
+
* email transport must do (`AlertEmailTransport`) and how an alert reads as
|
|
10
|
+
* mail (`formatAlertEmail`); a host that can speak SMTP binds a transport as
|
|
11
|
+
* `env.ALERT_EMAIL` (the Node collector does, from `ALERT_SMTP_*`, see
|
|
12
|
+
* apps/collector-node/src/alert-email.ts). Without that binding, rules with
|
|
13
|
+
* an email channel are refused at create/update time and an email channel on
|
|
14
|
+
* an existing rule counts as a failed delivery at fire time.
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
interface AlertEmailMessage {
|
|
18
|
+
to: string[];
|
|
19
|
+
subject: string;
|
|
20
|
+
text: string;
|
|
21
|
+
}
|
|
22
|
+
interface AlertEmailTransport {
|
|
23
|
+
/**
|
|
24
|
+
* Sends one message. Resolves only when the SMTP server accepted it for
|
|
25
|
+
* every recipient; rejects otherwise.
|
|
26
|
+
*/
|
|
27
|
+
send(message: AlertEmailMessage): Promise<void>;
|
|
28
|
+
}
|
|
29
|
+
/** What every channel is told about a transition (the webhook JSON body). */
|
|
30
|
+
interface AlertNotificationPayload {
|
|
31
|
+
rule: {
|
|
32
|
+
id: string;
|
|
33
|
+
name: string;
|
|
34
|
+
signal: string;
|
|
35
|
+
};
|
|
36
|
+
value: number;
|
|
37
|
+
threshold: number;
|
|
38
|
+
comparison: string;
|
|
39
|
+
state: AlertState;
|
|
40
|
+
evaluatedAt: string;
|
|
41
|
+
projectId: string;
|
|
42
|
+
/**
|
|
43
|
+
* RFC 0002 Stage 6 — populated when the rule is bound to an Analysis
|
|
44
|
+
* (`rule.analysisId` set). The narrative is the gated, LLM-generated
|
|
45
|
+
* sentence the dashboard panel was last showing — what's actually
|
|
46
|
+
* happening, in plain English. Webhooks consuming this can put the
|
|
47
|
+
* narrative in the Slack/PagerDuty body directly.
|
|
48
|
+
*/
|
|
49
|
+
analysis?: {
|
|
50
|
+
id: string;
|
|
51
|
+
narrative: string | null;
|
|
52
|
+
status: string | null;
|
|
53
|
+
};
|
|
54
|
+
}
|
|
55
|
+
declare const isValidEmailAddress: (addr: string) => boolean;
|
|
56
|
+
declare function formatAlertEmail(payload: AlertNotificationPayload): {
|
|
57
|
+
subject: string;
|
|
58
|
+
text: string;
|
|
59
|
+
};
|
|
2
60
|
|
|
3
61
|
/**
|
|
4
62
|
* Pluggable logger interface for the collector framework.
|
|
@@ -69,6 +127,12 @@ interface CollectorEnv {
|
|
|
69
127
|
/** Durable Object namespace for live-tail SSE pub/sub. Optional — when
|
|
70
128
|
* unbound the /internal/telemetry/tail endpoint returns 503. */
|
|
71
129
|
TAIL_HUB?: DurableObjectNamespace;
|
|
130
|
+
/**
|
|
131
|
+
* SMTP transport for email alert channels. Bound by hosts that can send
|
|
132
|
+
* mail (the Node collector, from `ALERT_SMTP_*`); never set on Cloudflare
|
|
133
|
+
* Workers. Without it, rules with an email channel are refused.
|
|
134
|
+
*/
|
|
135
|
+
ALERT_EMAIL?: AlertEmailTransport;
|
|
72
136
|
/** Write-only API key for SDK ingest endpoints (/v1/*) */
|
|
73
137
|
INGEST_KEY?: string;
|
|
74
138
|
/** Password for dashboard login */
|
|
@@ -135,6 +199,37 @@ interface CollectorEnv {
|
|
|
135
199
|
/** @deprecated Use DASHBOARD_PASSWORD instead */
|
|
136
200
|
TELEMETRY_QUERY_TOKEN?: string;
|
|
137
201
|
}
|
|
202
|
+
/**
|
|
203
|
+
* Optional string settings on `CollectorEnv` that host adapters (such as
|
|
204
|
+
* `apps/collector-node`) forward directly from the process environment after
|
|
205
|
+
* trimming whitespace and coercing blank strings to `undefined`.
|
|
206
|
+
*/
|
|
207
|
+
declare const COLLECTOR_OPTIONAL_STRING_ENV_KEYS: readonly ["RETENTION_HOURS", "ANTHROPIC_API_KEY", "OPENAI_API_KEY", "OPENAI_BASE_URL", "NARRATIVE_MODEL", "NARRATIVE_BUDGET_PER_HOUR", "ALERT_WEBHOOK_ALLOW_PRIVATE", "MAX_BODY_BYTES", "TELEMETRY_REDACT_FIELDS", "OBS_PAYLOAD_CAPTURE_DEFAULT"];
|
|
208
|
+
/**
|
|
209
|
+
* String settings on `CollectorEnv` that require explicit host handling (e.g.
|
|
210
|
+
* required auth secrets, CORS origin parsing, Worker-only self-instrumentation
|
|
211
|
+
* vars, or deprecated Worker tokens) and must not be blindly forwarded by the
|
|
212
|
+
* generic optional-env loop.
|
|
213
|
+
*/
|
|
214
|
+
declare const COLLECTOR_HOST_MANAGED_ENV_KEYS: readonly ["INGEST_KEY", "DASHBOARD_PASSWORD", "DASHBOARD_TOKEN", "ALLOWED_ORIGINS", "ALLOW_UNAUTHENTICATED", "OBS_DASHBOARD_INGEST_KEY", "OBS_COLLECTOR_SELF_URL", "OBS_SELF_AWAIT_EXPORTS", "TELEMETRY_INGEST_TOKEN", "USAGE_INGEST_TOKEN", "TELEMETRY_QUERY_TOKEN"];
|
|
215
|
+
declare const COLLECTOR_STRING_ENV_MANIFEST: {
|
|
216
|
+
readonly forwarded: readonly ["RETENTION_HOURS", "ANTHROPIC_API_KEY", "OPENAI_API_KEY", "OPENAI_BASE_URL", "NARRATIVE_MODEL", "NARRATIVE_BUDGET_PER_HOUR", "ALERT_WEBHOOK_ALLOW_PRIVATE", "MAX_BODY_BYTES", "TELEMETRY_REDACT_FIELDS", "OBS_PAYLOAD_CAPTURE_DEFAULT"];
|
|
217
|
+
readonly hostManaged: readonly ["INGEST_KEY", "DASHBOARD_PASSWORD", "DASHBOARD_TOKEN", "ALLOWED_ORIGINS", "ALLOW_UNAUTHENTICATED", "OBS_DASHBOARD_INGEST_KEY", "OBS_COLLECTOR_SELF_URL", "OBS_SELF_AWAIT_EXPORTS", "TELEMETRY_INGEST_TOKEN", "USAGE_INGEST_TOKEN", "TELEMETRY_QUERY_TOKEN"];
|
|
218
|
+
};
|
|
219
|
+
type CollectorOptionalStringEnvKey = (typeof COLLECTOR_OPTIONAL_STRING_ENV_KEYS)[number];
|
|
220
|
+
type CollectorHostManagedEnvKey = (typeof COLLECTOR_HOST_MANAGED_ENV_KEYS)[number];
|
|
221
|
+
type CollectorManifestStringEnvKey = CollectorOptionalStringEnvKey | CollectorHostManagedEnvKey;
|
|
222
|
+
type CollectorObjectBindingKey = "DB" | "REPLAYS_BUCKET" | "PROFILES_BUCKET" | "TAIL_HUB" | "ALERT_EMAIL";
|
|
223
|
+
type CollectorStringEnvKey = Exclude<keyof CollectorEnv, CollectorObjectBindingKey>;
|
|
224
|
+
/**
|
|
225
|
+
* Verifies that every string key in `declaredKeys` is categorized in either
|
|
226
|
+
* `COLLECTOR_OPTIONAL_STRING_ENV_KEYS` or `COLLECTOR_HOST_MANAGED_ENV_KEYS`
|
|
227
|
+
* without overlap. Throws if any key is uncategorized or duplicated.
|
|
228
|
+
*/
|
|
229
|
+
declare function assertCollectorEnvManifestCompleteness(declaredKeys: readonly string[], manifest?: {
|
|
230
|
+
readonly forwarded: readonly string[];
|
|
231
|
+
readonly hostManaged: readonly string[];
|
|
232
|
+
}): void;
|
|
138
233
|
interface CollectorRouteContext {
|
|
139
234
|
hono: Context<{
|
|
140
235
|
Bindings: CollectorEnv;
|
|
@@ -171,4 +266,4 @@ declare function createIngestAuth(config?: {
|
|
|
171
266
|
};
|
|
172
267
|
}>;
|
|
173
268
|
|
|
174
|
-
export { type
|
|
269
|
+
export { type AlertEmailMessage as A, COLLECTOR_HOST_MANAGED_ENV_KEYS as C, type Logger as L, type AlertEmailTransport as a, type AlertNotificationPayload as b, COLLECTOR_OPTIONAL_STRING_ENV_KEYS as c, COLLECTOR_STRING_ENV_MANIFEST as d, type ChildSpanRunner as e, type CollectorApp as f, type CollectorEnv as g, type CollectorHostManagedEnvKey as h, type CollectorManifestStringEnvKey as i, type CollectorObjectBindingKey as j, type CollectorOptionalStringEnvKey as k, type CollectorRouteContext as l, type CollectorStringEnvKey as m, assertCollectorEnvManifestCompleteness as n, consoleLogger as o, createIngestAuth as p, formatAlertEmail as q, isValidEmailAddress as r, resetIngestAuthCache as s };
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import {
|
|
2
2
|
LlmCallError,
|
|
3
3
|
askEvidenceReferences
|
|
4
|
-
} from "./chunk-
|
|
4
|
+
} from "./chunk-RASSTGNB.js";
|
|
5
5
|
|
|
6
6
|
// src/lib/openai.ts
|
|
7
7
|
import { EVIDENCE_REFERENCE_CONTRACT } from "@obsunified/types";
|
|
@@ -381,4 +381,4 @@ export {
|
|
|
381
381
|
generateNarrativeOpenAI,
|
|
382
382
|
runAskOpenAI
|
|
383
383
|
};
|
|
384
|
-
//# sourceMappingURL=openai-
|
|
384
|
+
//# sourceMappingURL=openai-VDKVVZAV.js.map
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@obsunified/collector",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.4.3",
|
|
4
4
|
"description": "Plugin-based observability collector runtime with pluggable SQL/blob storage.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"repository": {
|
|
@@ -38,7 +38,7 @@
|
|
|
38
38
|
"dependencies": {
|
|
39
39
|
"@bufbuild/protobuf": "^2.11.0",
|
|
40
40
|
"@obsunified/pprof-decoder": "2.0.2",
|
|
41
|
-
"@obsunified/types": "2.
|
|
41
|
+
"@obsunified/types": "2.4.1"
|
|
42
42
|
},
|
|
43
43
|
"peerDependencies": {
|
|
44
44
|
"hono": "^4.7.0",
|
|
@@ -66,6 +66,8 @@
|
|
|
66
66
|
"build": "tsup",
|
|
67
67
|
"type-check": "tsc --noEmit",
|
|
68
68
|
"test": "vitest run --passWithNoTests",
|
|
69
|
+
"check:migrations": "vitest run src/migrations/parity.test.ts",
|
|
70
|
+
"check:db-parity": "node scripts/check-db-parity.mjs",
|
|
69
71
|
"bench": "vitest bench --run",
|
|
70
72
|
"codegen": "PATH=\"$PWD/node_modules/.bin:$PATH\" buf generate"
|
|
71
73
|
}
|
|
@@ -2423,6 +2423,116 @@ describe("AutoSRE governance hardening", () => {
|
|
|
2423
2423
|
db.close();
|
|
2424
2424
|
});
|
|
2425
2425
|
|
|
2426
|
+
it("POST /internal/findings: schema-backed validation, trimming and defaults", async () => {
|
|
2427
|
+
const db = createMigratedSqliteDb();
|
|
2428
|
+
const { app, env } = createTestApp(db);
|
|
2429
|
+
const post = (body: unknown) =>
|
|
2430
|
+
app.request(
|
|
2431
|
+
"/internal/findings",
|
|
2432
|
+
{
|
|
2433
|
+
method: "POST",
|
|
2434
|
+
headers: { "Content-Type": "application/json" },
|
|
2435
|
+
body: JSON.stringify(body),
|
|
2436
|
+
},
|
|
2437
|
+
env,
|
|
2438
|
+
);
|
|
2439
|
+
const base = {
|
|
2440
|
+
title: "t",
|
|
2441
|
+
summary: "s",
|
|
2442
|
+
symptomEntityKind: "trace",
|
|
2443
|
+
symptomEntityId: "t1",
|
|
2444
|
+
faultLocusEntityKind: "service",
|
|
2445
|
+
faultLocusEntityId: SERVICE_NODE,
|
|
2446
|
+
};
|
|
2447
|
+
const required =
|
|
2448
|
+
"title, summary, symptomEntityKind, symptomEntityId, faultLocusEntityKind, and faultLocusEntityId are required";
|
|
2449
|
+
|
|
2450
|
+
// Combined required message: missing and empty-after-trim.
|
|
2451
|
+
for (const bad of [
|
|
2452
|
+
{ ...base, title: undefined },
|
|
2453
|
+
{ ...base, summary: " " },
|
|
2454
|
+
{ ...base, faultLocusEntityId: 5 },
|
|
2455
|
+
]) {
|
|
2456
|
+
const res = await post(bad);
|
|
2457
|
+
expect(res.status).toBe(400);
|
|
2458
|
+
expect(((await res.json()) as { error: string }).error).toBe(required);
|
|
2459
|
+
}
|
|
2460
|
+
|
|
2461
|
+
// causalChain explicit messages.
|
|
2462
|
+
const notArray = await post({ ...base, causalChain: "nope" });
|
|
2463
|
+
expect(notArray.status).toBe(400);
|
|
2464
|
+
expect(((await notArray.json()) as { error: string }).error).toBe(
|
|
2465
|
+
"causalChain must be an array",
|
|
2466
|
+
);
|
|
2467
|
+
const badPred = await post({
|
|
2468
|
+
...base,
|
|
2469
|
+
causalChain: [
|
|
2470
|
+
{
|
|
2471
|
+
subjectKind: "trace",
|
|
2472
|
+
subjectId: "t1",
|
|
2473
|
+
predicate: "made_up",
|
|
2474
|
+
objectKind: "span",
|
|
2475
|
+
objectId: "s1",
|
|
2476
|
+
},
|
|
2477
|
+
],
|
|
2478
|
+
});
|
|
2479
|
+
expect(badPred.status).toBe(400);
|
|
2480
|
+
expect(((await badPred.json()) as { error: string }).error).toBe(
|
|
2481
|
+
"causalChain[0].predicate is not a known evidence predicate: made_up",
|
|
2482
|
+
);
|
|
2483
|
+
|
|
2484
|
+
// Tightened: wrong-typed optional fields now 400 with path: message.
|
|
2485
|
+
const badConf = await post({ ...base, confidence: "high" });
|
|
2486
|
+
expect(badConf.status).toBe(400);
|
|
2487
|
+
expect(((await badConf.json()) as { error: string }).error).toContain(
|
|
2488
|
+
"confidence:",
|
|
2489
|
+
);
|
|
2490
|
+
const badTier = await post({ ...base, causalProofTier: "bogus" });
|
|
2491
|
+
expect(badTier.status).toBe(400);
|
|
2492
|
+
expect(((await badTier.json()) as { error: string }).error).toContain(
|
|
2493
|
+
"causalProofTier:",
|
|
2494
|
+
);
|
|
2495
|
+
const badKind = await post({ ...base, symptomEntityKind: "not_a_kind" });
|
|
2496
|
+
expect(badKind.status).toBe(400);
|
|
2497
|
+
|
|
2498
|
+
// Trimming + defaults.
|
|
2499
|
+
const ok = await post({
|
|
2500
|
+
title: " Padded title ",
|
|
2501
|
+
summary: " Padded summary ",
|
|
2502
|
+
symptomEntityKind: " trace ",
|
|
2503
|
+
symptomEntityId: " t1 ",
|
|
2504
|
+
faultLocusEntityKind: " service ",
|
|
2505
|
+
faultLocusEntityId: ` ${SERVICE_NODE} `,
|
|
2506
|
+
idempotencyKey: " ",
|
|
2507
|
+
incidentId: null,
|
|
2508
|
+
rootCauseEntityKind: null,
|
|
2509
|
+
cohortVerdict: null,
|
|
2510
|
+
});
|
|
2511
|
+
expect(ok.status).toBe(201);
|
|
2512
|
+
const created = (await ok.json()) as {
|
|
2513
|
+
finding: Record<string, unknown>;
|
|
2514
|
+
};
|
|
2515
|
+
expect(created.finding.title).toBe("Padded title");
|
|
2516
|
+
expect(created.finding.summary).toBe("Padded summary");
|
|
2517
|
+
expect(
|
|
2518
|
+
created.finding.symptom_entity_kind ?? created.finding.symptomEntityKind,
|
|
2519
|
+
).toBe("trace");
|
|
2520
|
+
// Blank idempotencyKey is ignored: two identical posts create two findings.
|
|
2521
|
+
const again = await post({
|
|
2522
|
+
title: " Padded title ",
|
|
2523
|
+
summary: " Padded summary ",
|
|
2524
|
+
symptomEntityKind: "trace",
|
|
2525
|
+
symptomEntityId: "t1",
|
|
2526
|
+
faultLocusEntityKind: "service",
|
|
2527
|
+
faultLocusEntityId: SERVICE_NODE,
|
|
2528
|
+
idempotencyKey: " ",
|
|
2529
|
+
});
|
|
2530
|
+
expect(((await again.json()) as { id: string }).id).not.toBe(
|
|
2531
|
+
(created.finding as { id: string }).id,
|
|
2532
|
+
);
|
|
2533
|
+
db.close();
|
|
2534
|
+
});
|
|
2535
|
+
|
|
2426
2536
|
it("J9: GET /internal/evals/runs/:id reports existence so counterfactual prerequisites can be checked first", async () => {
|
|
2427
2537
|
const db = createMigratedSqliteDb();
|
|
2428
2538
|
const { app, env } = createTestApp(db);
|