@obsunified/collector 2.2.0 → 2.2.1
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/index.d.ts +53 -3
- package/dist/index.js +10459 -9696
- package/dist/index.js.map +1 -1
- package/package.json +2 -2
- package/src/__tests__/autosre-closed-loop.test.ts +964 -17
- package/src/lib/analyses-runner.ts +44 -7
- package/src/lib/autosre-change-correlation.ts +39 -12
- package/src/lib/autosre-findings.ts +68 -30
- 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 +799 -335
- package/src/lib/housekeeping.test.ts +70 -0
- package/src/lib/housekeeping.ts +91 -83
- package/src/lib/store/ingest.ts +8 -56
- package/src/migrations/051_incident_active_dedup_unique.sql +31 -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/plugins/alerts-evaluator.ts +15 -3
- package/src/plugins/autosre-routes.ts +223 -108
- package/src/plugins/connected-routes/manifest.ts +1 -4
- package/src/plugins/connected-routes.ts +48 -78
- package/src/plugins/evidence-retrieval-routes.test.ts +32 -0
- package/src/plugins/evidence-retrieval-routes.ts +372 -30
|
@@ -62,6 +62,7 @@ describe("purgeSecondaryRows", () => {
|
|
|
62
62
|
ontology_nodes: 0,
|
|
63
63
|
change_events: 0,
|
|
64
64
|
incident_investigations: 0,
|
|
65
|
+
incident_leases_reaped: 0,
|
|
65
66
|
remediation_proposals: 0,
|
|
66
67
|
investigation_findings: 0,
|
|
67
68
|
});
|
|
@@ -150,4 +151,73 @@ describe("purgeSecondaryRows", () => {
|
|
|
150
151
|
]);
|
|
151
152
|
db.close();
|
|
152
153
|
});
|
|
154
|
+
|
|
155
|
+
it("reaps expired leases back to open without touching live leases or non-leased incidents (RFC 0013 § 11)", async () => {
|
|
156
|
+
const db = createMigratedSqliteDb();
|
|
157
|
+
const ins = db.raw.prepare(
|
|
158
|
+
`INSERT INTO incident_investigations (project_id, environment, id, dedup_key, status, target_node_id, symptom_anchor_kind, symptom_anchor_id, lease_owner_id, lease_expires_at, opened_at, updated_at, expires_at)
|
|
159
|
+
VALUES ('alpha', 'prod', ?, ?, ?, 'n', 'trace', 't', ?, ?, ?, ?, ?)`,
|
|
160
|
+
);
|
|
161
|
+
const farFuture = iso(-24 * 365);
|
|
162
|
+
ins.run(
|
|
163
|
+
"lease-expired",
|
|
164
|
+
"k1",
|
|
165
|
+
"leased",
|
|
166
|
+
"runner-a",
|
|
167
|
+
iso(1),
|
|
168
|
+
iso(5),
|
|
169
|
+
iso(5),
|
|
170
|
+
farFuture,
|
|
171
|
+
);
|
|
172
|
+
ins.run(
|
|
173
|
+
"lease-live",
|
|
174
|
+
"k2",
|
|
175
|
+
"leased",
|
|
176
|
+
"runner-b",
|
|
177
|
+
iso(-1),
|
|
178
|
+
iso(5),
|
|
179
|
+
iso(5),
|
|
180
|
+
farFuture,
|
|
181
|
+
);
|
|
182
|
+
ins.run(
|
|
183
|
+
"remediating",
|
|
184
|
+
"k3",
|
|
185
|
+
"remediating",
|
|
186
|
+
"runner-c",
|
|
187
|
+
iso(1),
|
|
188
|
+
iso(5),
|
|
189
|
+
iso(5),
|
|
190
|
+
farFuture,
|
|
191
|
+
);
|
|
192
|
+
|
|
193
|
+
const summary = await purgeSecondaryRows(db, {
|
|
194
|
+
retentionHours: 72,
|
|
195
|
+
now: () => NOW,
|
|
196
|
+
});
|
|
197
|
+
expect(summary.incident_leases_reaped).toBe(1);
|
|
198
|
+
const rows = db.raw
|
|
199
|
+
.prepare(
|
|
200
|
+
`SELECT id, status, lease_owner_id FROM incident_investigations ORDER BY id`,
|
|
201
|
+
)
|
|
202
|
+
.all() as Array<{
|
|
203
|
+
id: string;
|
|
204
|
+
status: string;
|
|
205
|
+
lease_owner_id: string | null;
|
|
206
|
+
}>;
|
|
207
|
+
expect(rows).toEqual([
|
|
208
|
+
{ id: "lease-expired", status: "open", lease_owner_id: null },
|
|
209
|
+
{ id: "lease-live", status: "leased", lease_owner_id: "runner-b" },
|
|
210
|
+
{ id: "remediating", status: "remediating", lease_owner_id: "runner-c" },
|
|
211
|
+
]);
|
|
212
|
+
db.close();
|
|
213
|
+
});
|
|
214
|
+
|
|
215
|
+
it("propagates storage errors instead of silently swallowing them", async () => {
|
|
216
|
+
const db = createMigratedSqliteDb();
|
|
217
|
+
db.raw.exec("DROP TABLE remediation_proposals");
|
|
218
|
+
await expect(
|
|
219
|
+
purgeSecondaryRows(db, { retentionHours: 72, now: () => NOW }),
|
|
220
|
+
).rejects.toThrow(/remediation_proposals/);
|
|
221
|
+
db.close();
|
|
222
|
+
});
|
|
153
223
|
});
|
package/src/lib/housekeeping.ts
CHANGED
|
@@ -13,7 +13,6 @@
|
|
|
13
13
|
* the signal they reference); expansions follow their ref.
|
|
14
14
|
*/
|
|
15
15
|
|
|
16
|
-
import { evaluateRemediationWatchdogs } from "./autosre-remediation-gate";
|
|
17
16
|
import type { SqlDb } from "./sql-db";
|
|
18
17
|
|
|
19
18
|
export const ALERT_EVALUATIONS_MAX_AGE_HOURS = 720;
|
|
@@ -29,6 +28,8 @@ export interface HousekeepingSummary {
|
|
|
29
28
|
ontology_nodes?: number;
|
|
30
29
|
change_events?: number;
|
|
31
30
|
incident_investigations?: number;
|
|
31
|
+
/** `leased` incidents whose lease expired and were returned to `open` (RFC 0013 § 11). */
|
|
32
|
+
incident_leases_reaped?: number;
|
|
32
33
|
investigation_findings?: number;
|
|
33
34
|
remediation_proposals?: number;
|
|
34
35
|
}
|
|
@@ -92,90 +93,96 @@ export async function purgeSecondaryRows(
|
|
|
92
93
|
.run();
|
|
93
94
|
|
|
94
95
|
// RFC 0013 — AutoSRE ontology, change events, incident investigations,
|
|
95
|
-
// investigation findings, and remediation proposals retention
|
|
96
|
+
// investigation findings, and remediation proposals retention, plus lease
|
|
97
|
+
// reaping. Errors propagate (the retention caller logs them); the
|
|
98
|
+
// post-remediation watchdog runs on the 1-minute analyses tick instead.
|
|
96
99
|
const thirtyDaysAgoIso = isoBefore(30 * 24);
|
|
97
100
|
const ninetyDaysAgoIso = isoBefore(90 * 24);
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
101
|
+
const edgesRes = await db
|
|
102
|
+
.prepare(`DELETE FROM ontology_edges WHERE last_seen_at < ?`)
|
|
103
|
+
.bind(thirtyDaysAgoIso)
|
|
104
|
+
.run();
|
|
105
|
+
const ontologyEdgesPurged = changes(edgesRes);
|
|
106
|
+
|
|
107
|
+
const nodesRes = await db
|
|
108
|
+
.prepare(
|
|
109
|
+
`DELETE FROM ontology_nodes
|
|
110
|
+
WHERE last_seen_at < ?
|
|
111
|
+
AND NOT EXISTS (
|
|
112
|
+
SELECT 1 FROM ontology_edges e
|
|
113
|
+
WHERE e.project_id = ontology_nodes.project_id
|
|
114
|
+
AND e.environment = ontology_nodes.environment
|
|
115
|
+
AND (e.source_node_id = ontology_nodes.id OR e.target_node_id = ontology_nodes.id)
|
|
116
|
+
)`,
|
|
117
|
+
)
|
|
118
|
+
.bind(thirtyDaysAgoIso)
|
|
119
|
+
.run();
|
|
120
|
+
const ontologyNodesPurged = changes(nodesRes);
|
|
121
|
+
|
|
122
|
+
const proposalsRes = await db
|
|
123
|
+
.prepare(
|
|
124
|
+
`DELETE FROM remediation_proposals
|
|
125
|
+
WHERE (expires_at IS NOT NULL AND expires_at < ?)
|
|
126
|
+
OR (expires_at IS NULL AND created_at < ?)`,
|
|
127
|
+
)
|
|
128
|
+
.bind(nowIso, ninetyDaysAgoIso)
|
|
129
|
+
.run();
|
|
130
|
+
const remediationProposalsPurged = changes(proposalsRes);
|
|
131
|
+
|
|
132
|
+
const findingsRes = await db
|
|
133
|
+
.prepare(
|
|
134
|
+
`DELETE FROM investigation_findings
|
|
135
|
+
WHERE causal_verification_status NOT IN ('counterfactual_verified', 'operationally_corroborated')
|
|
136
|
+
AND ((expires_at IS NOT NULL AND expires_at < ?)
|
|
137
|
+
OR (expires_at IS NULL AND created_at < ?))`,
|
|
138
|
+
)
|
|
139
|
+
.bind(nowIso, ninetyDaysAgoIso)
|
|
140
|
+
.run();
|
|
141
|
+
const investigationFindingsPurged = changes(findingsRes);
|
|
142
|
+
|
|
143
|
+
// Preserve change_events referenced by active investigation_findings
|
|
144
|
+
const changesRes = await db
|
|
145
|
+
.prepare(
|
|
146
|
+
`DELETE FROM change_events
|
|
147
|
+
WHERE ((expires_at IS NOT NULL AND expires_at < ?)
|
|
148
|
+
OR (expires_at IS NULL AND occurred_at < ?))
|
|
149
|
+
AND NOT EXISTS (
|
|
150
|
+
SELECT 1 FROM investigation_findings f
|
|
151
|
+
WHERE f.project_id = change_events.project_id
|
|
152
|
+
AND f.environment = change_events.environment
|
|
153
|
+
AND f.contributing_change_id = change_events.id
|
|
154
|
+
)`,
|
|
155
|
+
)
|
|
156
|
+
.bind(nowIso, ninetyDaysAgoIso)
|
|
157
|
+
.run();
|
|
158
|
+
const changeEventsPurged = changes(changesRes);
|
|
159
|
+
|
|
160
|
+
const incidentsRes = await db
|
|
161
|
+
.prepare(
|
|
162
|
+
`DELETE FROM incident_investigations
|
|
163
|
+
WHERE status IN ('resolved', 'escalated')
|
|
164
|
+
AND ((expires_at IS NOT NULL AND expires_at < ?)
|
|
165
|
+
OR (expires_at IS NULL AND updated_at < ?))`,
|
|
166
|
+
)
|
|
167
|
+
.bind(nowIso, ninetyDaysAgoIso)
|
|
168
|
+
.run();
|
|
169
|
+
const incidentInvestigationsPurged = changes(incidentsRes);
|
|
170
|
+
|
|
171
|
+
// Lease reaping (RFC 0013 § 11): a runner that died holding a lease must not
|
|
172
|
+
// strand the incident. Expired `leased` incidents go back to `open` so the
|
|
173
|
+
// next runner can pick them up. `remediating`/`verifying` are owned by the
|
|
174
|
+
// proposal claim/watchdog timeouts, not by this reaper.
|
|
175
|
+
const leasesRes = await db
|
|
176
|
+
.prepare(
|
|
177
|
+
`UPDATE incident_investigations
|
|
178
|
+
SET status = 'open', lease_owner_id = NULL, lease_expires_at = NULL, updated_at = ?
|
|
179
|
+
WHERE status = 'leased'
|
|
180
|
+
AND lease_expires_at IS NOT NULL
|
|
181
|
+
AND lease_expires_at < ?`,
|
|
182
|
+
)
|
|
183
|
+
.bind(nowIso, nowIso)
|
|
184
|
+
.run();
|
|
185
|
+
const incidentLeasesReaped = changes(leasesRes);
|
|
179
186
|
|
|
180
187
|
return {
|
|
181
188
|
alert_evaluations: changes(alertEvaluations),
|
|
@@ -187,6 +194,7 @@ export async function purgeSecondaryRows(
|
|
|
187
194
|
ontology_nodes: ontologyNodesPurged,
|
|
188
195
|
change_events: changeEventsPurged,
|
|
189
196
|
incident_investigations: incidentInvestigationsPurged,
|
|
197
|
+
incident_leases_reaped: incidentLeasesReaped,
|
|
190
198
|
investigation_findings: investigationFindingsPurged,
|
|
191
199
|
remediation_proposals: remediationProposalsPurged,
|
|
192
200
|
};
|
package/src/lib/store/ingest.ts
CHANGED
|
@@ -8,6 +8,7 @@ import type { SqlDb } from "../sql-db";
|
|
|
8
8
|
export async function ingestTelemetrySpans(
|
|
9
9
|
db: SqlDb,
|
|
10
10
|
spans: StoredSpan[],
|
|
11
|
+
logger: { warn: (message: string) => void } = console,
|
|
11
12
|
): Promise<{ inserted: number; traceCount: number }> {
|
|
12
13
|
if (spans.length === 0) return { inserted: 0, traceCount: 0 };
|
|
13
14
|
|
|
@@ -82,65 +83,16 @@ export async function ingestTelemetrySpans(
|
|
|
82
83
|
),
|
|
83
84
|
);
|
|
84
85
|
|
|
85
|
-
|
|
86
|
-
await db.batch(statements);
|
|
87
|
-
} catch (err) {
|
|
88
|
-
const msg = err instanceof Error ? err.message : String(err);
|
|
89
|
-
if (msg.includes("no such column: environment")) {
|
|
90
|
-
// Fallback for unit tests running against pre-050 partial schemas
|
|
91
|
-
const legacyStatements = enrichedSpans.map((span) =>
|
|
92
|
-
db
|
|
93
|
-
.prepare(`
|
|
94
|
-
INSERT OR IGNORE INTO telemetry_spans (
|
|
95
|
-
project_id, trace_id, span_id, parent_span_id, trace_state,
|
|
96
|
-
service_name, scope_name, scope_version, span_name, span_kind,
|
|
97
|
-
status_code, status_message, start_time, end_time, duration_ms,
|
|
98
|
-
attributes_json, dropped_attributes_count,
|
|
99
|
-
resource_attributes_json, events_json, dropped_events_count,
|
|
100
|
-
links_json, dropped_links_count, received_at, expires_at,
|
|
101
|
-
session_id, interaction_id, telemetry_sdk_name
|
|
102
|
-
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
103
|
-
`)
|
|
104
|
-
.bind(
|
|
105
|
-
span.projectId,
|
|
106
|
-
span.traceId,
|
|
107
|
-
span.spanId,
|
|
108
|
-
span.parentSpanId,
|
|
109
|
-
span.traceState,
|
|
110
|
-
span.serviceName,
|
|
111
|
-
span.scopeName,
|
|
112
|
-
span.scopeVersion,
|
|
113
|
-
span.spanName,
|
|
114
|
-
span.spanKind,
|
|
115
|
-
span.statusCode,
|
|
116
|
-
span.statusMessage,
|
|
117
|
-
span.startTime,
|
|
118
|
-
span.endTime,
|
|
119
|
-
span.durationMs,
|
|
120
|
-
span.attributesJson,
|
|
121
|
-
span.droppedAttributesCount,
|
|
122
|
-
span.resourceAttributesJson,
|
|
123
|
-
span.eventsJson,
|
|
124
|
-
span.droppedEventsCount,
|
|
125
|
-
span.linksJson,
|
|
126
|
-
span.droppedLinksCount,
|
|
127
|
-
span.receivedAt,
|
|
128
|
-
span.expiresAt,
|
|
129
|
-
span.sessionId ?? null,
|
|
130
|
-
span.interactionId ?? null,
|
|
131
|
-
span.telemetrySdkName ?? null,
|
|
132
|
-
),
|
|
133
|
-
);
|
|
134
|
-
await db.batch(legacyStatements);
|
|
135
|
-
} else {
|
|
136
|
-
throw err;
|
|
137
|
-
}
|
|
138
|
-
}
|
|
86
|
+
await db.batch(statements);
|
|
139
87
|
|
|
140
88
|
try {
|
|
141
89
|
await upsertOntologyFromSpans(db, enrichedSpans);
|
|
142
|
-
} catch {
|
|
143
|
-
//
|
|
90
|
+
} catch (err) {
|
|
91
|
+
// Ontology enrichment must never block span ingest, but a failure here
|
|
92
|
+
// means the causal graph is silently going stale, so surface it.
|
|
93
|
+
logger.warn(
|
|
94
|
+
`upsertOntologyFromSpans failed: ${err instanceof Error ? err.message : String(err)}`,
|
|
95
|
+
);
|
|
144
96
|
}
|
|
145
97
|
|
|
146
98
|
// Trace instrumentation gaps are computed lazily at read time (see
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
-- 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 consecutive_ok_checks INTEGER NOT NULL DEFAULT 0;
|
|
@@ -44,9 +44,9 @@ describe("migration 049 (alert_state.notified)", () => {
|
|
|
44
44
|
.map((c) => (c as { name: string }).name);
|
|
45
45
|
expect(columnsBefore).not.toContain("notified");
|
|
46
46
|
|
|
47
|
-
expect(
|
|
48
|
-
|
|
49
|
-
)
|
|
47
|
+
expect(applySqliteMigrations(db.raw, { from: "049", upTo: "049" })).toEqual(
|
|
48
|
+
["049_alert_state_notified.sql"],
|
|
49
|
+
);
|
|
50
50
|
|
|
51
51
|
const notified = db.raw
|
|
52
52
|
.prepare("PRAGMA table_info(alert_state)")
|
|
@@ -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;
|
|
@@ -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
|
});
|