@obsunified/collector 2.1.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/{chunk-XKQTVIRE.js → chunk-ZSF5XP3B.js} +8 -2
- package/dist/chunk-ZSF5XP3B.js.map +1 -0
- package/dist/index.d.ts +309 -2
- package/dist/index.js +4159 -256
- package/dist/index.js.map +1 -1
- package/dist/{openai-FJYOQ7WO.js → openai-GSJZGFGL.js} +2 -2
- package/package.json +2 -2
- package/src/__tests__/autosre-closed-loop.test.ts +1275 -0
- package/src/index.ts +43 -0
- package/src/lib/analyses-runner.ts +9 -0
- 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/eval-cases-store.ts +63 -1
- package/src/lib/evidence-references.ts +6 -0
- package/src/lib/housekeeping.test.ts +80 -0
- package/src/lib/housekeeping.ts +101 -1
- package/src/lib/store/ingest.ts +94 -6
- package/src/migrations/050_autosre_causal_ontology.sql +216 -0
- package/src/migrations/alert-state-notified.test.ts +3 -3
- package/src/migrations/parity.test.ts +4 -0
- package/src/migrations/project-scoped-keys.test.ts +1 -1
- package/src/migrations-postgres/050_autosre_causal_ontology.sql +216 -0
- package/src/plugins/alerts-evaluator.ts +17 -1
- package/src/plugins/autosre-routes.ts +1019 -0
- package/src/plugins/connected-routes/manifest.ts +13 -1
- package/src/plugins/connected-routes.ts +271 -0
- package/src/plugins/default-span-enrichment.ts +13 -1
- package/src/plugins/eval-cases-routes.ts +57 -2
- package/src/plugins/evidence-retrieval-routes.ts +300 -4
- package/dist/chunk-XKQTVIRE.js.map +0 -1
- /package/dist/{openai-FJYOQ7WO.js.map → openai-GSJZGFGL.js.map} +0 -0
|
@@ -76,6 +76,10 @@ export interface EvalRunCandidate {
|
|
|
76
76
|
modelProvider: string | null;
|
|
77
77
|
model: string | null;
|
|
78
78
|
modelVersion: string | null;
|
|
79
|
+
environment?: string | null;
|
|
80
|
+
serviceVersion?: string | null;
|
|
81
|
+
commitSha?: string | null;
|
|
82
|
+
configDigest?: string | null;
|
|
79
83
|
}
|
|
80
84
|
|
|
81
85
|
export interface EvalRunSourceCase {
|
|
@@ -99,6 +103,12 @@ export interface EvalRun {
|
|
|
99
103
|
evalCaseId: string | null;
|
|
100
104
|
status: EvalRunStatus;
|
|
101
105
|
candidate: EvalRunCandidate;
|
|
106
|
+
environment?: string;
|
|
107
|
+
serviceVersion?: string | null;
|
|
108
|
+
commitSha?: string | null;
|
|
109
|
+
configDigest?: string | null;
|
|
110
|
+
inputPayloadHash?: string | null;
|
|
111
|
+
failureFingerprint?: string | null;
|
|
102
112
|
startedAt: string | null;
|
|
103
113
|
endedAt: string | null;
|
|
104
114
|
totalCount: number;
|
|
@@ -117,6 +127,12 @@ export interface EvalRunInput {
|
|
|
117
127
|
evalCaseId?: string | null;
|
|
118
128
|
status?: EvalRunStatus;
|
|
119
129
|
candidate?: Partial<EvalRunCandidate>;
|
|
130
|
+
environment?: string | null;
|
|
131
|
+
serviceVersion?: string | null;
|
|
132
|
+
commitSha?: string | null;
|
|
133
|
+
configDigest?: string | null;
|
|
134
|
+
inputPayloadHash?: string | null;
|
|
135
|
+
failureFingerprint?: string | null;
|
|
120
136
|
startedAt?: string | null;
|
|
121
137
|
endedAt?: string | null;
|
|
122
138
|
totalCount?: number;
|
|
@@ -144,6 +160,12 @@ interface EvalRunRow {
|
|
|
144
160
|
candidate_model_provider: string | null;
|
|
145
161
|
candidate_model: string | null;
|
|
146
162
|
candidate_model_version: string | null;
|
|
163
|
+
environment?: string | null;
|
|
164
|
+
service_version?: string | null;
|
|
165
|
+
commit_sha?: string | null;
|
|
166
|
+
config_digest?: string | null;
|
|
167
|
+
input_payload_hash?: string | null;
|
|
168
|
+
failure_fingerprint?: string | null;
|
|
147
169
|
status: string;
|
|
148
170
|
started_at: string | null;
|
|
149
171
|
ended_at: string | null;
|
|
@@ -412,7 +434,17 @@ const rowToEvalRun = (row: EvalRunRow): EvalRun => {
|
|
|
412
434
|
modelProvider: row.candidate_model_provider ?? null,
|
|
413
435
|
model: row.candidate_model ?? null,
|
|
414
436
|
modelVersion: row.candidate_model_version ?? null,
|
|
437
|
+
environment: row.environment ?? "default",
|
|
438
|
+
serviceVersion: row.service_version ?? null,
|
|
439
|
+
commitSha: row.commit_sha ?? null,
|
|
440
|
+
configDigest: row.config_digest ?? null,
|
|
415
441
|
},
|
|
442
|
+
environment: row.environment ?? "default",
|
|
443
|
+
serviceVersion: row.service_version ?? null,
|
|
444
|
+
commitSha: row.commit_sha ?? null,
|
|
445
|
+
configDigest: row.config_digest ?? null,
|
|
446
|
+
inputPayloadHash: row.input_payload_hash ?? null,
|
|
447
|
+
failureFingerprint: row.failure_fingerprint ?? null,
|
|
416
448
|
startedAt: row.started_at ?? null,
|
|
417
449
|
endedAt: row.ended_at ?? null,
|
|
418
450
|
totalCount: row.total_count ?? 0,
|
|
@@ -560,6 +592,16 @@ export class EvalCasesStore {
|
|
|
560
592
|
}
|
|
561
593
|
|
|
562
594
|
const candidate = input.candidate ?? {};
|
|
595
|
+
const environment =
|
|
596
|
+
input.environment?.trim() || candidate.environment?.trim() || "default";
|
|
597
|
+
const serviceVersion =
|
|
598
|
+
input.serviceVersion?.trim() || candidate.serviceVersion?.trim() || null;
|
|
599
|
+
const commitSha =
|
|
600
|
+
input.commitSha?.trim() || candidate.commitSha?.trim() || null;
|
|
601
|
+
const configDigest =
|
|
602
|
+
input.configDigest?.trim() || candidate.configDigest?.trim() || null;
|
|
603
|
+
const inputPayloadHash = input.inputPayloadHash?.trim() || null;
|
|
604
|
+
const failureFingerprint = input.failureFingerprint?.trim() || null;
|
|
563
605
|
const totalCount = nonNegativeInteger(input.totalCount);
|
|
564
606
|
const passCount = nonNegativeInteger(input.passCount);
|
|
565
607
|
const failCount = nonNegativeInteger(input.failCount);
|
|
@@ -580,10 +622,12 @@ export class EvalCasesStore {
|
|
|
580
622
|
candidate_agent_id, candidate_agent_version,
|
|
581
623
|
candidate_prompt_id, candidate_prompt_version,
|
|
582
624
|
candidate_model_provider, candidate_model, candidate_model_version,
|
|
625
|
+
environment, service_version, commit_sha, config_digest,
|
|
626
|
+
input_payload_hash, failure_fingerprint,
|
|
583
627
|
status, started_at, ended_at,
|
|
584
628
|
total_count, pass_count, fail_count, average_score,
|
|
585
629
|
metadata_json, created_at
|
|
586
|
-
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
|
|
630
|
+
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
|
|
587
631
|
)
|
|
588
632
|
.bind(
|
|
589
633
|
id,
|
|
@@ -596,6 +640,12 @@ export class EvalCasesStore {
|
|
|
596
640
|
candidate.modelProvider ?? null,
|
|
597
641
|
candidate.model ?? null,
|
|
598
642
|
candidate.modelVersion ?? null,
|
|
643
|
+
environment,
|
|
644
|
+
serviceVersion,
|
|
645
|
+
commitSha,
|
|
646
|
+
configDigest,
|
|
647
|
+
inputPayloadHash,
|
|
648
|
+
failureFingerprint,
|
|
599
649
|
status,
|
|
600
650
|
startedAt,
|
|
601
651
|
endedAt,
|
|
@@ -679,6 +729,14 @@ export class EvalCasesStore {
|
|
|
679
729
|
average_score = (
|
|
680
730
|
SELECT AVG(score) FROM eval_case_results
|
|
681
731
|
WHERE project_id = ? AND run_id = ? AND score IS NOT NULL
|
|
732
|
+
),
|
|
733
|
+
input_payload_hash = COALESCE(
|
|
734
|
+
input_payload_hash,
|
|
735
|
+
(SELECT input_payload_hash FROM eval_case_results WHERE project_id = ? AND run_id = ? AND input_payload_hash IS NOT NULL LIMIT 1)
|
|
736
|
+
),
|
|
737
|
+
failure_fingerprint = COALESCE(
|
|
738
|
+
failure_fingerprint,
|
|
739
|
+
(SELECT failure_fingerprint FROM eval_case_results WHERE project_id = ? AND run_id = ? AND failure_fingerprint IS NOT NULL LIMIT 1)
|
|
682
740
|
)
|
|
683
741
|
WHERE project_id = ? AND id = ?`,
|
|
684
742
|
)
|
|
@@ -693,6 +751,10 @@ export class EvalCasesStore {
|
|
|
693
751
|
id,
|
|
694
752
|
projectId,
|
|
695
753
|
id,
|
|
754
|
+
projectId,
|
|
755
|
+
id,
|
|
756
|
+
projectId,
|
|
757
|
+
id,
|
|
696
758
|
)
|
|
697
759
|
.run();
|
|
698
760
|
}
|
|
@@ -146,6 +146,12 @@ export const evidenceRouteFor = (
|
|
|
146
146
|
return traceRoute(entityId);
|
|
147
147
|
case "docs":
|
|
148
148
|
return entityId.startsWith("#/") ? entityId : `#/docs/${entityId}`;
|
|
149
|
+
case "change":
|
|
150
|
+
return `#/changes/${encodeURIComponent(entityId)}`;
|
|
151
|
+
case "finding":
|
|
152
|
+
return `#/findings/${encodeURIComponent(entityId)}`;
|
|
153
|
+
case "incident":
|
|
154
|
+
return `#/incidents/${encodeURIComponent(entityId)}`;
|
|
149
155
|
}
|
|
150
156
|
const exhaustive: never = entityKind;
|
|
151
157
|
return exhaustive;
|
|
@@ -58,6 +58,12 @@ describe("purgeSecondaryRows", () => {
|
|
|
58
58
|
metric_series: 1,
|
|
59
59
|
evidence_refs: 1,
|
|
60
60
|
evidence_ref_expansions: 1,
|
|
61
|
+
ontology_edges: 0,
|
|
62
|
+
ontology_nodes: 0,
|
|
63
|
+
change_events: 0,
|
|
64
|
+
incident_investigations: 0,
|
|
65
|
+
remediation_proposals: 0,
|
|
66
|
+
investigation_findings: 0,
|
|
61
67
|
});
|
|
62
68
|
const ids = (sql: string) =>
|
|
63
69
|
(raw.prepare(sql).all() as Array<{ id: string }>).map((r) => r.id).sort();
|
|
@@ -70,4 +76,78 @@ describe("purgeSecondaryRows", () => {
|
|
|
70
76
|
expect(ids("SELECT id FROM evidence_ref_expansions")).toEqual(["e2"]);
|
|
71
77
|
db.close();
|
|
72
78
|
});
|
|
79
|
+
|
|
80
|
+
it("enforces RFC 0013 AutoSRE retention windows while preserving counterfactual_verified and operationally_corroborated findings", async () => {
|
|
81
|
+
const db = createMigratedSqliteDb();
|
|
82
|
+
const raw = db.raw;
|
|
83
|
+
raw.exec(`
|
|
84
|
+
INSERT INTO projects (id, name, slug, created_at) VALUES ('alpha', 'A', 'a', '${iso(3000)}');
|
|
85
|
+
`);
|
|
86
|
+
// Ontology edges/nodes (30d = 720h)
|
|
87
|
+
raw
|
|
88
|
+
.prepare(
|
|
89
|
+
`INSERT INTO ontology_nodes (id, project_id, environment, kind, name, first_seen_at, last_seen_at) VALUES (?, 'alpha', 'prod', 'service', ?, ?, ?)`,
|
|
90
|
+
)
|
|
91
|
+
.run("n-old", "old-svc", iso(800), iso(750));
|
|
92
|
+
raw
|
|
93
|
+
.prepare(
|
|
94
|
+
`INSERT INTO ontology_nodes (id, project_id, environment, kind, name, first_seen_at, last_seen_at) VALUES (?, 'alpha', 'prod', 'service', ?, ?, ?)`,
|
|
95
|
+
)
|
|
96
|
+
.run("n-fresh", "fresh-svc", iso(100), iso(10));
|
|
97
|
+
raw
|
|
98
|
+
.prepare(
|
|
99
|
+
`INSERT INTO ontology_edges (id, project_id, environment, source_node_id, predicate, target_node_id, relationship_certainty, first_seen_at, last_seen_at) VALUES (?, 'alpha', 'prod', 'n-old', 'calls', 'n-fresh', 'exact', ?, ?)`,
|
|
100
|
+
)
|
|
101
|
+
.run("edge-old", iso(800), iso(750));
|
|
102
|
+
|
|
103
|
+
// Change events (90d = 2160h)
|
|
104
|
+
raw
|
|
105
|
+
.prepare(
|
|
106
|
+
`INSERT INTO change_events (id, project_id, environment, kind, source, target_node_id, summary, occurred_at, received_at) VALUES (?, 'alpha', 'prod', 'deploy', 'ci_webhook', 'n-fresh', 'old deploy', ?, ?)`,
|
|
107
|
+
)
|
|
108
|
+
.run("chg-old", iso(2200), iso(2200));
|
|
109
|
+
raw
|
|
110
|
+
.prepare(
|
|
111
|
+
`INSERT INTO change_events (id, project_id, environment, kind, source, target_node_id, summary, occurred_at, received_at) VALUES (?, 'alpha', 'prod', 'deploy', 'ci_webhook', 'n-fresh', 'recent deploy', ?, ?)`,
|
|
112
|
+
)
|
|
113
|
+
.run("chg-recent", iso(100), iso(100));
|
|
114
|
+
|
|
115
|
+
// Findings (90d = 2160h, unless counterfactual_verified or operationally_corroborated)
|
|
116
|
+
const insFinding = raw.prepare(
|
|
117
|
+
`INSERT INTO investigation_findings (id, project_id, environment, fingerprint, fingerprint_input_json, title, summary, causal_proof_tier, causal_verification_status, confidence, confidence_breakdown_json, symptom_entity_kind, symptom_entity_id, fault_locus_entity_kind, fault_locus_entity_id, causal_chain_json, cited_evidence_ids_json, actor_type, actor_id, created_at, updated_at) VALUES (?, 'alpha', 'prod', 'fp_1', '{}', 't', 's', 'corroborated_candidate', ?, 0.8, '{}', 'trace', 't1', 'span', 's1', '[]', '["t1"]', 'agent', 'autosre', ?, ?)`,
|
|
118
|
+
);
|
|
119
|
+
insFinding.run("fnd-old-unverified", "unverified", iso(2200), iso(2200));
|
|
120
|
+
insFinding.run(
|
|
121
|
+
"fnd-old-verified",
|
|
122
|
+
"counterfactual_verified",
|
|
123
|
+
iso(2200),
|
|
124
|
+
iso(2200),
|
|
125
|
+
);
|
|
126
|
+
insFinding.run(
|
|
127
|
+
"fnd-old-corroborated",
|
|
128
|
+
"operationally_corroborated",
|
|
129
|
+
iso(2200),
|
|
130
|
+
iso(2200),
|
|
131
|
+
);
|
|
132
|
+
|
|
133
|
+
const summary = await purgeSecondaryRows(db, {
|
|
134
|
+
retentionHours: 72,
|
|
135
|
+
now: () => NOW,
|
|
136
|
+
});
|
|
137
|
+
expect(summary.ontology_edges).toBe(1);
|
|
138
|
+
expect(summary.ontology_nodes).toBe(1);
|
|
139
|
+
expect(summary.change_events).toBe(1);
|
|
140
|
+
expect(summary.investigation_findings).toBe(1);
|
|
141
|
+
|
|
142
|
+
const remainingFindings = (
|
|
143
|
+
raw
|
|
144
|
+
.prepare(`SELECT id FROM investigation_findings ORDER BY id ASC`)
|
|
145
|
+
.all() as Array<{ id: string }>
|
|
146
|
+
).map((r) => r.id);
|
|
147
|
+
expect(remainingFindings).toEqual([
|
|
148
|
+
"fnd-old-corroborated",
|
|
149
|
+
"fnd-old-verified",
|
|
150
|
+
]);
|
|
151
|
+
db.close();
|
|
152
|
+
});
|
|
73
153
|
});
|
package/src/lib/housekeeping.ts
CHANGED
|
@@ -13,6 +13,7 @@
|
|
|
13
13
|
* the signal they reference); expansions follow their ref.
|
|
14
14
|
*/
|
|
15
15
|
|
|
16
|
+
import { evaluateRemediationWatchdogs } from "./autosre-remediation-gate";
|
|
16
17
|
import type { SqlDb } from "./sql-db";
|
|
17
18
|
|
|
18
19
|
export const ALERT_EVALUATIONS_MAX_AGE_HOURS = 720;
|
|
@@ -24,6 +25,12 @@ export interface HousekeepingSummary {
|
|
|
24
25
|
metric_series: number;
|
|
25
26
|
evidence_refs: number;
|
|
26
27
|
evidence_ref_expansions: number;
|
|
28
|
+
ontology_edges?: number;
|
|
29
|
+
ontology_nodes?: number;
|
|
30
|
+
change_events?: number;
|
|
31
|
+
incident_investigations?: number;
|
|
32
|
+
investigation_findings?: number;
|
|
33
|
+
remediation_proposals?: number;
|
|
27
34
|
}
|
|
28
35
|
|
|
29
36
|
export interface HousekeepingOptions {
|
|
@@ -42,6 +49,7 @@ export async function purgeSecondaryRows(
|
|
|
42
49
|
const nowMs = options.now?.() ?? Date.now();
|
|
43
50
|
const isoBefore = (hours: number) =>
|
|
44
51
|
new Date(nowMs - hours * 3_600_000).toISOString();
|
|
52
|
+
const nowIso = isoBefore(0);
|
|
45
53
|
|
|
46
54
|
const alertEvaluations = await db
|
|
47
55
|
.prepare(`DELETE FROM alert_evaluations WHERE evaluated_at < ?`)
|
|
@@ -71,7 +79,7 @@ export async function purgeSecondaryRows(
|
|
|
71
79
|
`DELETE FROM evidence_retrieval_refs
|
|
72
80
|
WHERE expires_at IS NOT NULL AND expires_at < ?`,
|
|
73
81
|
)
|
|
74
|
-
.bind(
|
|
82
|
+
.bind(nowIso)
|
|
75
83
|
.run();
|
|
76
84
|
const expansions = await db
|
|
77
85
|
.prepare(
|
|
@@ -83,11 +91,103 @@ export async function purgeSecondaryRows(
|
|
|
83
91
|
)
|
|
84
92
|
.run();
|
|
85
93
|
|
|
94
|
+
// RFC 0013 — AutoSRE ontology, change events, incident investigations,
|
|
95
|
+
// investigation findings, and remediation proposals retention + watchdog.
|
|
96
|
+
const thirtyDaysAgoIso = isoBefore(30 * 24);
|
|
97
|
+
const ninetyDaysAgoIso = isoBefore(90 * 24);
|
|
98
|
+
let ontologyEdgesPurged = 0;
|
|
99
|
+
let ontologyNodesPurged = 0;
|
|
100
|
+
let changeEventsPurged = 0;
|
|
101
|
+
let incidentInvestigationsPurged = 0;
|
|
102
|
+
let investigationFindingsPurged = 0;
|
|
103
|
+
let remediationProposalsPurged = 0;
|
|
104
|
+
try {
|
|
105
|
+
const edgesRes = await db
|
|
106
|
+
.prepare(`DELETE FROM ontology_edges WHERE last_seen_at < ?`)
|
|
107
|
+
.bind(thirtyDaysAgoIso)
|
|
108
|
+
.run();
|
|
109
|
+
ontologyEdgesPurged = changes(edgesRes);
|
|
110
|
+
|
|
111
|
+
const nodesRes = await db
|
|
112
|
+
.prepare(
|
|
113
|
+
`DELETE FROM ontology_nodes
|
|
114
|
+
WHERE last_seen_at < ?
|
|
115
|
+
AND NOT EXISTS (
|
|
116
|
+
SELECT 1 FROM ontology_edges e
|
|
117
|
+
WHERE e.project_id = ontology_nodes.project_id
|
|
118
|
+
AND e.environment = ontology_nodes.environment
|
|
119
|
+
AND (e.source_node_id = ontology_nodes.id OR e.target_node_id = ontology_nodes.id)
|
|
120
|
+
)`,
|
|
121
|
+
)
|
|
122
|
+
.bind(thirtyDaysAgoIso)
|
|
123
|
+
.run();
|
|
124
|
+
ontologyNodesPurged = changes(nodesRes);
|
|
125
|
+
|
|
126
|
+
const proposalsRes = await db
|
|
127
|
+
.prepare(
|
|
128
|
+
`DELETE FROM remediation_proposals
|
|
129
|
+
WHERE (expires_at IS NOT NULL AND expires_at < ?)
|
|
130
|
+
OR (expires_at IS NULL AND created_at < ?)`,
|
|
131
|
+
)
|
|
132
|
+
.bind(nowIso, ninetyDaysAgoIso)
|
|
133
|
+
.run();
|
|
134
|
+
remediationProposalsPurged = changes(proposalsRes);
|
|
135
|
+
|
|
136
|
+
const findingsRes = await db
|
|
137
|
+
.prepare(
|
|
138
|
+
`DELETE FROM investigation_findings
|
|
139
|
+
WHERE causal_verification_status NOT IN ('counterfactual_verified', 'operationally_corroborated')
|
|
140
|
+
AND ((expires_at IS NOT NULL AND expires_at < ?)
|
|
141
|
+
OR (expires_at IS NULL AND created_at < ?))`,
|
|
142
|
+
)
|
|
143
|
+
.bind(nowIso, ninetyDaysAgoIso)
|
|
144
|
+
.run();
|
|
145
|
+
investigationFindingsPurged = changes(findingsRes);
|
|
146
|
+
|
|
147
|
+
// Preserve change_events referenced by active investigation_findings
|
|
148
|
+
const changesRes = await db
|
|
149
|
+
.prepare(
|
|
150
|
+
`DELETE FROM change_events
|
|
151
|
+
WHERE ((expires_at IS NOT NULL AND expires_at < ?)
|
|
152
|
+
OR (expires_at IS NULL AND occurred_at < ?))
|
|
153
|
+
AND NOT EXISTS (
|
|
154
|
+
SELECT 1 FROM investigation_findings f
|
|
155
|
+
WHERE f.project_id = change_events.project_id
|
|
156
|
+
AND f.environment = change_events.environment
|
|
157
|
+
AND f.contributing_change_id = change_events.id
|
|
158
|
+
)`,
|
|
159
|
+
)
|
|
160
|
+
.bind(nowIso, ninetyDaysAgoIso)
|
|
161
|
+
.run();
|
|
162
|
+
changeEventsPurged = changes(changesRes);
|
|
163
|
+
|
|
164
|
+
const incidentsRes = await db
|
|
165
|
+
.prepare(
|
|
166
|
+
`DELETE FROM incident_investigations
|
|
167
|
+
WHERE status IN ('resolved', 'escalated')
|
|
168
|
+
AND ((expires_at IS NOT NULL AND expires_at < ?)
|
|
169
|
+
OR (expires_at IS NULL AND updated_at < ?))`,
|
|
170
|
+
)
|
|
171
|
+
.bind(nowIso, ninetyDaysAgoIso)
|
|
172
|
+
.run();
|
|
173
|
+
incidentInvestigationsPurged = changes(incidentsRes);
|
|
174
|
+
|
|
175
|
+
await evaluateRemediationWatchdogs(db, { nowIso });
|
|
176
|
+
} catch {
|
|
177
|
+
// Gracefully ignore when running against partial test schemas without 050
|
|
178
|
+
}
|
|
179
|
+
|
|
86
180
|
return {
|
|
87
181
|
alert_evaluations: changes(alertEvaluations),
|
|
88
182
|
ask_evidence_events: changes(askEvents),
|
|
89
183
|
metric_series: changes(series),
|
|
90
184
|
evidence_refs: changes(refs),
|
|
91
185
|
evidence_ref_expansions: changes(expansions),
|
|
186
|
+
ontology_edges: ontologyEdgesPurged,
|
|
187
|
+
ontology_nodes: ontologyNodesPurged,
|
|
188
|
+
change_events: changeEventsPurged,
|
|
189
|
+
incident_investigations: incidentInvestigationsPurged,
|
|
190
|
+
investigation_findings: investigationFindingsPurged,
|
|
191
|
+
remediation_proposals: remediationProposalsPurged,
|
|
92
192
|
};
|
|
93
193
|
}
|
package/src/lib/store/ingest.ts
CHANGED
|
@@ -1,4 +1,8 @@
|
|
|
1
1
|
import type { StoredSpan } from "@obsunified/types";
|
|
2
|
+
import {
|
|
3
|
+
extractSpanLineageAndTopology,
|
|
4
|
+
upsertOntologyFromSpans,
|
|
5
|
+
} from "../autosre-ontology";
|
|
2
6
|
import type { SqlDb } from "../sql-db";
|
|
3
7
|
|
|
4
8
|
export async function ingestTelemetrySpans(
|
|
@@ -7,10 +11,29 @@ export async function ingestTelemetrySpans(
|
|
|
7
11
|
): Promise<{ inserted: number; traceCount: number }> {
|
|
8
12
|
if (spans.length === 0) return { inserted: 0, traceCount: 0 };
|
|
9
13
|
|
|
10
|
-
const
|
|
14
|
+
const enrichedSpans = spans.map((span) => {
|
|
11
15
|
if (!span.projectId)
|
|
12
16
|
throw new Error("TelemetryStore.ingest: span.projectId is required");
|
|
13
|
-
|
|
17
|
+
if (
|
|
18
|
+
span.environment !== undefined &&
|
|
19
|
+
span.serviceVersion !== undefined &&
|
|
20
|
+
span.targetNodeId !== undefined
|
|
21
|
+
) {
|
|
22
|
+
return span;
|
|
23
|
+
}
|
|
24
|
+
const lineage = extractSpanLineageAndTopology(span);
|
|
25
|
+
return {
|
|
26
|
+
...span,
|
|
27
|
+
environment: span.environment ?? lineage.environment,
|
|
28
|
+
serviceVersion: span.serviceVersion ?? lineage.serviceVersion,
|
|
29
|
+
commitSha: span.commitSha ?? lineage.commitSha,
|
|
30
|
+
targetNodeId: span.targetNodeId ?? lineage.targetNode?.id ?? null,
|
|
31
|
+
errorSignature: span.errorSignature ?? lineage.errorSignature,
|
|
32
|
+
};
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
const statements = enrichedSpans.map((span) =>
|
|
36
|
+
db
|
|
14
37
|
.prepare(`
|
|
15
38
|
INSERT OR IGNORE INTO telemetry_spans (
|
|
16
39
|
project_id, trace_id, span_id, parent_span_id, trace_state,
|
|
@@ -19,8 +42,9 @@ export async function ingestTelemetrySpans(
|
|
|
19
42
|
attributes_json, dropped_attributes_count,
|
|
20
43
|
resource_attributes_json, events_json, dropped_events_count,
|
|
21
44
|
links_json, dropped_links_count, received_at, expires_at,
|
|
22
|
-
session_id, interaction_id, telemetry_sdk_name
|
|
23
|
-
|
|
45
|
+
session_id, interaction_id, telemetry_sdk_name,
|
|
46
|
+
environment, service_version, commit_sha, target_node_id, error_signature
|
|
47
|
+
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
24
48
|
`)
|
|
25
49
|
.bind(
|
|
26
50
|
span.projectId,
|
|
@@ -50,10 +74,74 @@ export async function ingestTelemetrySpans(
|
|
|
50
74
|
span.sessionId ?? null,
|
|
51
75
|
span.interactionId ?? null,
|
|
52
76
|
span.telemetrySdkName ?? null,
|
|
77
|
+
span.environment ?? "default",
|
|
78
|
+
span.serviceVersion ?? null,
|
|
79
|
+
span.commitSha ?? null,
|
|
80
|
+
span.targetNodeId ?? null,
|
|
81
|
+
span.errorSignature ?? null,
|
|
82
|
+
),
|
|
83
|
+
);
|
|
84
|
+
|
|
85
|
+
try {
|
|
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
|
+
),
|
|
53
133
|
);
|
|
54
|
-
|
|
134
|
+
await db.batch(legacyStatements);
|
|
135
|
+
} else {
|
|
136
|
+
throw err;
|
|
137
|
+
}
|
|
138
|
+
}
|
|
55
139
|
|
|
56
|
-
|
|
140
|
+
try {
|
|
141
|
+
await upsertOntologyFromSpans(db, enrichedSpans);
|
|
142
|
+
} catch {
|
|
143
|
+
// Non-fatal on partial-schema unit test DBs
|
|
144
|
+
}
|
|
57
145
|
|
|
58
146
|
// Trace instrumentation gaps are computed lazily at read time (see
|
|
59
147
|
// `getTelemetryTraceGaps`), not materialized on ingest. This keeps the
|