@obsunified/collector 2.2.0 → 2.2.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.js +1 -1
- package/dist/{chunk-KMEZKGSO.js → chunk-2FALTDCE.js} +12 -4
- package/dist/chunk-2FALTDCE.js.map +1 -0
- package/dist/index.d.ts +61 -3
- package/dist/index.js +10933 -9769
- package/dist/index.js.map +1 -1
- package/package.json +2 -3
- package/src/__tests__/autosre-closed-loop.test.ts +1554 -17
- package/src/auth/dashboard-auth.test.ts +61 -0
- package/src/auth/dashboard-auth.ts +17 -2
- package/src/lib/ai-store/calls.ts +43 -23
- package/src/lib/ai-store.ts +12 -1
- package/src/lib/analyses-runner.ts +44 -7
- package/src/lib/autosre-change-correlation.ts +39 -12
- package/src/lib/autosre-findings.ts +237 -44
- package/src/lib/autosre-ontology-cache.test.ts +78 -0
- package/src/lib/autosre-ontology.ts +52 -28
- package/src/lib/autosre-remediation-gate.ts +942 -348
- package/src/lib/housekeeping.test.ts +70 -0
- package/src/lib/housekeeping.ts +91 -83
- package/src/lib/projects-store.test.ts +38 -1
- package/src/lib/projects-store.ts +23 -4
- package/src/lib/sql-db-postgres.integration.test.ts +75 -2
- package/src/lib/store/ingest.ts +8 -56
- package/src/lib/usage-store.ts +9 -3
- package/src/migrations/051_incident_active_dedup_unique.sql +31 -0
- package/src/migrations/052_remediation_callback.sql +10 -0
- package/src/migrations/053_remediation_callback_delivery.sql +7 -0
- package/src/migrations/alert-state-notified.test.ts +3 -3
- package/src/migrations-postgres/051_incident_active_dedup_unique.sql +31 -0
- package/src/migrations-postgres/052_remediation_callback.sql +10 -0
- package/src/migrations-postgres/053_remediation_callback_delivery.sql +7 -0
- package/src/plugins/ai-call-by-id.test.ts +73 -0
- package/src/plugins/ai-receiver.ts +10 -0
- package/src/plugins/alerts-evaluator.ts +15 -3
- package/src/plugins/autosre-routes.ts +374 -108
- package/src/plugins/connected-routes/deep-link-shapes.test.ts +44 -0
- package/src/plugins/connected-routes/manifest.test.ts +142 -0
- package/src/plugins/connected-routes/manifest.ts +123 -6
- package/src/plugins/connected-routes-links.test.ts +96 -0
- package/src/plugins/connected-routes.test.ts +3 -3
- package/src/plugins/connected-routes.ts +56 -83
- package/src/plugins/evidence-retrieval-routes.test.ts +32 -0
- package/src/plugins/evidence-retrieval-routes.ts +397 -25
- package/dist/chunk-KMEZKGSO.js.map +0 -1
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
-- (PostgreSQL mirror) Make active-incident deduplication race-free (RFC 0013 § 6.A, governance review B4).
|
|
2
|
+
--
|
|
3
|
+
-- `openOrDeduplicateIncident` used to SELECT-then-INSERT, so two concurrent
|
|
4
|
+
-- alerts for the same symptom could each open an incident. A partial UNIQUE
|
|
5
|
+
-- index lets the database arbitrate; the application inserts with
|
|
6
|
+
-- ON CONFLICT DO NOTHING and re-reads the winner.
|
|
7
|
+
|
|
8
|
+
-- Collapse any pre-existing duplicate active rows (keep the newest per key) so
|
|
9
|
+
-- the index can be built. Older duplicates are closed as escalated, not deleted.
|
|
10
|
+
UPDATE incident_investigations
|
|
11
|
+
SET status = 'escalated'
|
|
12
|
+
WHERE status IN ('open', 'dispatched', 'leased', 'remediating', 'verifying')
|
|
13
|
+
AND EXISTS (
|
|
14
|
+
SELECT 1 FROM incident_investigations o
|
|
15
|
+
WHERE o.project_id = incident_investigations.project_id
|
|
16
|
+
AND o.environment = incident_investigations.environment
|
|
17
|
+
AND o.dedup_key = incident_investigations.dedup_key
|
|
18
|
+
AND o.status IN ('open', 'dispatched', 'leased', 'remediating', 'verifying')
|
|
19
|
+
AND (
|
|
20
|
+
o.opened_at > incident_investigations.opened_at
|
|
21
|
+
OR (o.opened_at = incident_investigations.opened_at AND o.id > incident_investigations.id)
|
|
22
|
+
)
|
|
23
|
+
);
|
|
24
|
+
|
|
25
|
+
CREATE UNIQUE INDEX IF NOT EXISTS uq_incidents_active_dedup
|
|
26
|
+
ON incident_investigations (project_id, environment, dedup_key)
|
|
27
|
+
WHERE status IN ('open', 'dispatched', 'leased', 'remediating', 'verifying');
|
|
28
|
+
|
|
29
|
+
-- Watchdog (governance review B11): a finding is only `recovered` after two
|
|
30
|
+
-- consecutive checkpoints with sufficient traffic and a healthy error rate.
|
|
31
|
+
ALTER TABLE investigation_findings ADD COLUMN IF NOT EXISTS consecutive_ok_checks BIGINT NOT NULL DEFAULT 0;
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
-- (PostgreSQL mirror) Persisted grant-handoff callback (RFC 0013 § 6.B, journey review J17).
|
|
2
|
+
--
|
|
3
|
+
-- A proposal parked for human approval may carry a webhook that receives the
|
|
4
|
+
-- one-time grant token when the approval happens later. `callback_url` is the
|
|
5
|
+
-- target (SSRF-guarded again at delivery time); `callback_secret` is the HMAC
|
|
6
|
+
-- signing secret, stored as supplied (the collector has no at-rest encryption
|
|
7
|
+
-- helper) and never returned by any GET. Both are cleared once the grant is
|
|
8
|
+
-- delivered, rejected or expired.
|
|
9
|
+
ALTER TABLE remediation_proposals ADD COLUMN IF NOT EXISTS callback_url TEXT;
|
|
10
|
+
ALTER TABLE remediation_proposals ADD COLUMN IF NOT EXISTS callback_secret TEXT;
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
-- (PostgreSQL mirror) Last grant-handoff webhook delivery outcome (RFC 0013 § 6.B).
|
|
2
|
+
--
|
|
3
|
+
-- JSON `{status: 'delivered'|'failed', attempts, lastError, at}` recorded after
|
|
4
|
+
-- the grant webhook is attempted (up to 3 attempts). It never contains the
|
|
5
|
+
-- callback URL or secret, so unlike callback_url it is kept after approval so
|
|
6
|
+
-- the dashboard can flag a failed handoff.
|
|
7
|
+
ALTER TABLE remediation_proposals ADD COLUMN IF NOT EXISTS callback_delivery TEXT;
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import type { MiddlewareHandler } from "hono";
|
|
2
|
+
import { describe, expect, it } from "vitest";
|
|
3
|
+
import { createTelemetryCollectorApp } from "../framework/collector";
|
|
4
|
+
import type { CollectorEnv } from "../framework/env";
|
|
5
|
+
import { MemSqlDb } from "../lib/test-utils/mem-sql-db";
|
|
6
|
+
import { aiReceiverPlugin } from "./ai-receiver";
|
|
7
|
+
|
|
8
|
+
const passthrough: MiddlewareHandler<{
|
|
9
|
+
Bindings: CollectorEnv;
|
|
10
|
+
Variables: { projectId: string };
|
|
11
|
+
}> = async (c, next) => {
|
|
12
|
+
c.set("projectId", "default");
|
|
13
|
+
await next();
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
const app = createTelemetryCollectorApp({
|
|
17
|
+
plugins: [aiReceiverPlugin],
|
|
18
|
+
auth: { middleware: passthrough },
|
|
19
|
+
dashboardAuth: { middleware: passthrough, registerRoutes: () => {} },
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
const row = {
|
|
23
|
+
project_id: "default",
|
|
24
|
+
call_id: "call-1",
|
|
25
|
+
trace_id: "trace-1",
|
|
26
|
+
span_id: "span-1",
|
|
27
|
+
service_name: "svc",
|
|
28
|
+
model_name: "gpt-4.1",
|
|
29
|
+
provider: "openai",
|
|
30
|
+
call_type: "chat",
|
|
31
|
+
request_json: null,
|
|
32
|
+
response_json: null,
|
|
33
|
+
prompt_tokens: 10,
|
|
34
|
+
completion_tokens: 5,
|
|
35
|
+
total_cost_usd: 0.01,
|
|
36
|
+
latency_ms: 120,
|
|
37
|
+
is_error: 0,
|
|
38
|
+
error_message: null,
|
|
39
|
+
occurred_at: "2026-05-01T00:00:00.000Z",
|
|
40
|
+
received_at: "2026-05-01T00:00:01.000Z",
|
|
41
|
+
expires_at: "2026-05-02T00:00:00.000Z",
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
describe("GET /internal/ai/calls/:callId", () => {
|
|
45
|
+
it("returns the call with its trace and span ids", async () => {
|
|
46
|
+
const db = new MemSqlDb({
|
|
47
|
+
first: (sql, binds) =>
|
|
48
|
+
sql.includes("FROM ai_calls") && binds[1] === "call-1" ? row : null,
|
|
49
|
+
});
|
|
50
|
+
const res = await app.request("/internal/ai/calls/call-1", {}, {
|
|
51
|
+
DB: db,
|
|
52
|
+
} as unknown as CollectorEnv);
|
|
53
|
+
expect(res.status).toBe(200);
|
|
54
|
+
const body = (await res.json()) as {
|
|
55
|
+
call: { callId: string; modelName: string };
|
|
56
|
+
traceId: string;
|
|
57
|
+
spanId: string;
|
|
58
|
+
};
|
|
59
|
+
expect(body.call.callId).toBe("call-1");
|
|
60
|
+
expect(body.call.modelName).toBe("gpt-4.1");
|
|
61
|
+
expect(body.traceId).toBe("trace-1");
|
|
62
|
+
expect(body.spanId).toBe("span-1");
|
|
63
|
+
expect(db.calls[0].binds).toEqual(["default", "call-1"]);
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
it("answers 404 for an unknown call id", async () => {
|
|
67
|
+
const db = new MemSqlDb({ first: () => null });
|
|
68
|
+
const res = await app.request("/internal/ai/calls/nope", {}, {
|
|
69
|
+
DB: db,
|
|
70
|
+
} as unknown as CollectorEnv);
|
|
71
|
+
expect(res.status).toBe(404);
|
|
72
|
+
});
|
|
73
|
+
});
|
|
@@ -97,6 +97,16 @@ export const aiReceiverPlugin: CollectorPlugin = {
|
|
|
97
97
|
return c.json(response);
|
|
98
98
|
});
|
|
99
99
|
|
|
100
|
+
// One AI call by its call id, with the trace/span it belongs to. Backs
|
|
101
|
+
// the `#/ai/calls/<id>` deep link when the call is outside the loaded list.
|
|
102
|
+
app.get("/internal/ai/calls/:callId", async (c) => {
|
|
103
|
+
const projectId = getProjectId(c);
|
|
104
|
+
const store = new AIStore(sqlDbFor(c.env));
|
|
105
|
+
const call = await store.getAICall(projectId, c.req.param("callId"));
|
|
106
|
+
if (!call) return c.json({ error: "AI call not found" }, 404);
|
|
107
|
+
return c.json({ call, traceId: call.traceId, spanId: call.spanId });
|
|
108
|
+
});
|
|
109
|
+
|
|
100
110
|
// OpenInference-kind spans (LLM/TOOL/RETRIEVER/CHAIN/AGENT/...) joined
|
|
101
111
|
// with their payload side-table. This is the replacement for the flat
|
|
102
112
|
// ai_calls endpoint — it returns typed spans that live inside the
|
|
@@ -11,7 +11,10 @@ import {
|
|
|
11
11
|
type Logger,
|
|
12
12
|
} from "../framework/logger";
|
|
13
13
|
import { AlertsStore, compareValue } from "../lib/alerts-store";
|
|
14
|
-
import {
|
|
14
|
+
import {
|
|
15
|
+
openOrDeduplicateIncident,
|
|
16
|
+
resolveServiceIncidentTarget,
|
|
17
|
+
} from "../lib/autosre-remediation-gate";
|
|
15
18
|
import { sqlDbFor } from "../lib/sql-db";
|
|
16
19
|
import {
|
|
17
20
|
assertSafeWebhookUrl,
|
|
@@ -439,9 +442,18 @@ export async function evaluateAllRules(
|
|
|
439
442
|
allowPrivateWebhookTargets,
|
|
440
443
|
hostLookup: options?.hostLookup,
|
|
441
444
|
onAlertFiring: async (rule) => {
|
|
445
|
+
// Target the rule's service when it names one (environment taken from
|
|
446
|
+
// that service's latest spans); otherwise fall back to the rule itself.
|
|
447
|
+
const target = await resolveServiceIncidentTarget(
|
|
448
|
+
db,
|
|
449
|
+
rule.projectId,
|
|
450
|
+
(rule.query as { serviceName?: string }).serviceName,
|
|
451
|
+
`alert:${rule.id}`,
|
|
452
|
+
logger,
|
|
453
|
+
);
|
|
442
454
|
await openOrDeduplicateIncident(db, rule.projectId, {
|
|
443
|
-
environment:
|
|
444
|
-
targetNodeId:
|
|
455
|
+
environment: target.environment,
|
|
456
|
+
targetNodeId: target.targetNodeId,
|
|
445
457
|
symptomAnchorKind: "alert",
|
|
446
458
|
symptomAnchorId: rule.id,
|
|
447
459
|
});
|