@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
package/dist/auth.d.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
// src/lib/evidence-references.ts
|
|
2
|
-
|
|
2
|
+
import { dashboardRouteToHref, hrefForEntity } from "@obsunified/types";
|
|
3
|
+
var analysisRoute = (analysisId) => dashboardRouteToHref({ tab: "investigate", investigationId: analysisId });
|
|
3
4
|
var analysisEvidenceReference = (evidence) => {
|
|
4
5
|
const route = analysisRoute(evidence.analysisId);
|
|
5
6
|
const status = evidence.result?.status ?? "unknown";
|
|
@@ -48,46 +49,53 @@ var valueFor = (row, headerIndex, names) => {
|
|
|
48
49
|
}
|
|
49
50
|
return null;
|
|
50
51
|
};
|
|
51
|
-
var traceRoute = (traceId) =>
|
|
52
|
-
var spanRoute = (traceId, spanId) =>
|
|
52
|
+
var traceRoute = (traceId) => dashboardRouteToHref({ tab: "traces", traceId });
|
|
53
|
+
var spanRoute = (traceId, spanId) => dashboardRouteToHref({ tab: "traces", traceId, spanId });
|
|
53
54
|
var evidenceRouteFor = (entityKind, entityId, traceId) => {
|
|
54
55
|
switch (entityKind) {
|
|
55
56
|
case "analysis":
|
|
56
57
|
return analysisRoute(entityId);
|
|
57
58
|
case "action":
|
|
58
|
-
return
|
|
59
|
+
return dashboardRouteToHref({ tab: "actions", actionId: entityId });
|
|
59
60
|
case "alert":
|
|
60
|
-
return
|
|
61
|
+
return dashboardRouteToHref({ tab: "alerts", alertId: entityId });
|
|
61
62
|
case "agent_run":
|
|
62
|
-
return
|
|
63
|
+
return dashboardRouteToHref({ tab: "agent-runs", agentRunId: entityId });
|
|
63
64
|
case "ai_call":
|
|
64
|
-
return
|
|
65
|
+
return dashboardRouteToHref({ tab: "ai", aiCallId: entityId });
|
|
65
66
|
case "eval":
|
|
66
|
-
return
|
|
67
|
+
return dashboardRouteToHref({
|
|
68
|
+
tab: "evaluations",
|
|
69
|
+
evaluationId: entityId
|
|
70
|
+
});
|
|
67
71
|
case "log":
|
|
68
|
-
return
|
|
72
|
+
return dashboardRouteToHref({ tab: "logs", logId: entityId });
|
|
69
73
|
case "profile":
|
|
70
|
-
return
|
|
74
|
+
return dashboardRouteToHref({
|
|
75
|
+
tab: "profiles",
|
|
76
|
+
profileId: entityId,
|
|
77
|
+
...traceId ? { traceId } : {}
|
|
78
|
+
});
|
|
71
79
|
case "replay":
|
|
72
|
-
return
|
|
80
|
+
return dashboardRouteToHref({ tab: "replay", sessionId: entityId });
|
|
73
81
|
case "service":
|
|
74
|
-
return
|
|
82
|
+
return dashboardRouteToHref({ tab: "service-map", service: entityId });
|
|
75
83
|
case "span": {
|
|
76
84
|
const [tracePart, spanPart] = entityId.split(":");
|
|
77
|
-
return spanPart ? spanRoute(tracePart, spanPart) :
|
|
85
|
+
return spanPart ? spanRoute(tracePart, spanPart) : hrefForEntity("span", entityId, traceId ? { traceId } : void 0);
|
|
78
86
|
}
|
|
79
87
|
case "tool_call":
|
|
80
|
-
return
|
|
88
|
+
return dashboardRouteToHref({ tab: "tool-calls", toolCallId: entityId });
|
|
81
89
|
case "trace":
|
|
82
90
|
return traceRoute(entityId);
|
|
83
91
|
case "docs":
|
|
84
|
-
return
|
|
92
|
+
return hrefForEntity("docs", entityId);
|
|
85
93
|
case "change":
|
|
86
|
-
return
|
|
94
|
+
return dashboardRouteToHref({ tab: "changes", changeId: entityId });
|
|
87
95
|
case "finding":
|
|
88
|
-
return
|
|
96
|
+
return dashboardRouteToHref({ tab: "findings", findingId: entityId });
|
|
89
97
|
case "incident":
|
|
90
|
-
return
|
|
98
|
+
return dashboardRouteToHref({ tab: "incidents", incidentId: entityId });
|
|
91
99
|
}
|
|
92
100
|
const exhaustive = entityKind;
|
|
93
101
|
return exhaustive;
|
|
@@ -140,7 +148,7 @@ var instrumentationGapEvidenceReferences = (result) => {
|
|
|
140
148
|
const durationText = durationMs === null ? "unknown duration" : `${durationMs}ms`;
|
|
141
149
|
const ratioText = ratio === null ? "" : `, ${(ratio * 100).toFixed(1)}% of parent`;
|
|
142
150
|
const thresholdText = thresholdVersion ? ` Thresholds: ${thresholdVersion}.` : "";
|
|
143
|
-
const docsRoute = "
|
|
151
|
+
const docsRoute = hrefForEntity("docs", "howto/ebpf");
|
|
144
152
|
return {
|
|
145
153
|
evidenceId: `analysis:${result.analysisId}:instrumentation-gap:${index}:span:${entityId}`,
|
|
146
154
|
entityKind: "span",
|
|
@@ -169,7 +177,7 @@ var instrumentationGapEvidenceReferences = (result) => {
|
|
|
169
177
|
label: "Open trace gap data",
|
|
170
178
|
entityKind: "trace",
|
|
171
179
|
entityId: traceId,
|
|
172
|
-
route:
|
|
180
|
+
route: traceRoute(traceId),
|
|
173
181
|
reason: "Review all structured instrumentation blindspots for this trace."
|
|
174
182
|
},
|
|
175
183
|
{
|
|
@@ -604,7 +612,7 @@ var LlmCallError = class extends Error {
|
|
|
604
612
|
};
|
|
605
613
|
async function generateNarrative(req, config) {
|
|
606
614
|
if (config.provider === "openai") {
|
|
607
|
-
const { generateNarrativeOpenAI } = await import("./openai-
|
|
615
|
+
const { generateNarrativeOpenAI } = await import("./openai-VDKVVZAV.js");
|
|
608
616
|
return generateNarrativeOpenAI(req, config);
|
|
609
617
|
}
|
|
610
618
|
return generateNarrativeAnthropic(req, config);
|
|
@@ -674,4 +682,4 @@ export {
|
|
|
674
682
|
LlmCallError,
|
|
675
683
|
generateNarrative
|
|
676
684
|
};
|
|
677
|
-
//# sourceMappingURL=chunk-
|
|
685
|
+
//# sourceMappingURL=chunk-RASSTGNB.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/lib/evidence-references.ts","../src/lib/llm.ts"],"sourcesContent":["import type {\n\tAIEvaluationRecord,\n\tAlertEvaluation,\n\tAlertRule,\n\tAnalysisResult,\n\tAskEvidence,\n\tEvidenceCitation,\n\tEvidenceEntityKind,\n\tEvidenceNextPivot,\n\tEvidenceReference,\n\tJsonValue,\n} from \"@obsunified/types\";\nimport { dashboardRouteToHref, hrefForEntity } from \"@obsunified/types\";\n\nconst analysisRoute = (analysisId: string) =>\n\tdashboardRouteToHref({ tab: \"investigate\", investigationId: analysisId });\n\nexport const analysisEvidenceReference = (\n\tevidence: AskEvidence,\n): EvidenceReference => {\n\tconst route = analysisRoute(evidence.analysisId);\n\tconst status = evidence.result?.status ?? \"unknown\";\n\treturn {\n\t\tevidenceId: `ask:analysis:${evidence.analysisId}`,\n\t\tentityKind: \"analysis\",\n\t\tentityId: evidence.analysisId,\n\t\troute,\n\t\tsource: \"ask.run_analysis\",\n\t\tconfidence: evidence.result ? 1 : 0.55,\n\t\treason: evidence.result\n\t\t\t? `Ask consulted analysis \"${evidence.definition.title}\" with latest status ${status}.`\n\t\t\t: `Ask consulted analysis \"${evidence.definition.title}\", but no latest result was available.`,\n\t\tcitations: [\n\t\t\t{\n\t\t\t\tlabel: evidence.definition.title,\n\t\t\t\tentityKind: \"analysis\",\n\t\t\t\tentityId: evidence.analysisId,\n\t\t\t\troute,\n\t\t\t},\n\t\t],\n\t\tsuggestedNextPivots: [\n\t\t\t{\n\t\t\t\tlabel: \"Open investigation\",\n\t\t\t\tentityKind: \"analysis\",\n\t\t\t\tentityId: evidence.analysisId,\n\t\t\t\troute,\n\t\t\t\treason:\n\t\t\t\t\t\"Review the analysis payload, narrative, and connected telemetry rail.\",\n\t\t\t},\n\t\t],\n\t};\n};\n\nexport const askEvidenceReferences = (\n\tevidence: Iterable<AskEvidence>,\n): EvidenceReference[] =>\n\t[...evidence].map((item) => analysisEvidenceReference(item));\n\nconst MAX_ANALYSIS_EVIDENCE_REFERENCES = 50;\n\ninterface EvidenceTable {\n\ttitle?: string | null;\n\theaders: string[];\n\trows: JsonValue[][];\n}\n\ninterface RowEntity {\n\tentityKind: EvidenceEntityKind;\n\tentityId: string;\n\troute: string;\n\tlabel: string;\n\tconfidence: number;\n\tcitations: EvidenceCitation[];\n\tsuggestedNextPivots: EvidenceNextPivot[];\n}\n\nconst isRecord = (value: unknown): value is Record<string, JsonValue> =>\n\t!!value && typeof value === \"object\" && !Array.isArray(value);\n\nconst asString = (value: JsonValue | undefined): string | null => {\n\tif (typeof value === \"string\" && value.trim()) return value.trim();\n\tif (typeof value === \"number\" && Number.isFinite(value)) return String(value);\n\treturn null;\n};\n\nconst normalizeHeader = (header: string) =>\n\theader\n\t\t.trim()\n\t\t.toLowerCase()\n\t\t.replace(/[\\s-]+/g, \"_\");\n\nconst valueFor = (\n\trow: JsonValue[],\n\theaderIndex: Map<string, number>,\n\tnames: string[],\n): string | null => {\n\tfor (const name of names) {\n\t\tconst index = headerIndex.get(name);\n\t\tif (index === undefined) continue;\n\t\tconst value = asString(row[index]);\n\t\tif (value) return value;\n\t}\n\treturn null;\n};\n\nconst traceRoute = (traceId: string) =>\n\tdashboardRouteToHref({ tab: \"traces\", traceId });\n\nconst spanRoute = (traceId: string, spanId: string) =>\n\tdashboardRouteToHref({ tab: \"traces\", traceId, spanId });\n\nexport const evidenceRouteFor = (\n\tentityKind: EvidenceEntityKind,\n\tentityId: string,\n\ttraceId?: string | null,\n) => {\n\tswitch (entityKind) {\n\t\tcase \"analysis\":\n\t\t\treturn analysisRoute(entityId);\n\t\tcase \"action\":\n\t\t\treturn dashboardRouteToHref({ tab: \"actions\", actionId: entityId });\n\t\tcase \"alert\":\n\t\t\treturn dashboardRouteToHref({ tab: \"alerts\", alertId: entityId });\n\t\tcase \"agent_run\":\n\t\t\treturn dashboardRouteToHref({ tab: \"agent-runs\", agentRunId: entityId });\n\t\tcase \"ai_call\":\n\t\t\treturn dashboardRouteToHref({ tab: \"ai\", aiCallId: entityId });\n\t\tcase \"eval\":\n\t\t\treturn dashboardRouteToHref({\n\t\t\t\ttab: \"evaluations\",\n\t\t\t\tevaluationId: entityId,\n\t\t\t});\n\t\tcase \"log\":\n\t\t\treturn dashboardRouteToHref({ tab: \"logs\", logId: entityId });\n\t\tcase \"profile\":\n\t\t\treturn dashboardRouteToHref({\n\t\t\t\ttab: \"profiles\",\n\t\t\t\tprofileId: entityId,\n\t\t\t\t...(traceId ? { traceId } : {}),\n\t\t\t});\n\t\tcase \"replay\":\n\t\t\treturn dashboardRouteToHref({ tab: \"replay\", sessionId: entityId });\n\t\tcase \"service\":\n\t\t\treturn dashboardRouteToHref({ tab: \"service-map\", service: entityId });\n\t\tcase \"span\": {\n\t\t\tconst [tracePart, spanPart] = entityId.split(\":\");\n\t\t\treturn spanPart\n\t\t\t\t? spanRoute(tracePart, spanPart)\n\t\t\t\t: hrefForEntity(\"span\", entityId, traceId ? { traceId } : undefined);\n\t\t}\n\t\tcase \"tool_call\":\n\t\t\treturn dashboardRouteToHref({ tab: \"tool-calls\", toolCallId: entityId });\n\t\tcase \"trace\":\n\t\t\treturn traceRoute(entityId);\n\t\tcase \"docs\":\n\t\t\treturn hrefForEntity(\"docs\", entityId);\n\t\tcase \"change\":\n\t\t\treturn dashboardRouteToHref({ tab: \"changes\", changeId: entityId });\n\t\tcase \"finding\":\n\t\t\treturn dashboardRouteToHref({ tab: \"findings\", findingId: entityId });\n\t\tcase \"incident\":\n\t\t\treturn dashboardRouteToHref({ tab: \"incidents\", incidentId: entityId });\n\t}\n\tconst exhaustive: never = entityKind;\n\treturn exhaustive;\n};\n\nconst parseEvidenceTables = (\n\tpayload: Record<string, unknown>,\n): Array<{ sectionName: string; table: EvidenceTable }> => {\n\tconst evidence = payload.evidence;\n\tif (!isRecord(evidence)) return [];\n\n\tconst tables: Array<{ sectionName: string; table: EvidenceTable }> = [];\n\tfor (const [sectionName, section] of Object.entries(evidence)) {\n\t\tif (!isRecord(section)) continue;\n\t\tconst headers = section.headers;\n\t\tconst rows = section.rows;\n\t\tif (\n\t\t\t!Array.isArray(headers) ||\n\t\t\t!headers.every(\n\t\t\t\t(header): header is string => typeof header === \"string\",\n\t\t\t) ||\n\t\t\t!Array.isArray(rows)\n\t\t) {\n\t\t\tcontinue;\n\t\t}\n\n\t\tconst normalizedRows = rows.filter((row): row is JsonValue[] =>\n\t\t\tArray.isArray(row),\n\t\t);\n\t\ttables.push({\n\t\t\tsectionName,\n\t\t\ttable: {\n\t\t\t\ttitle: asString(section.title),\n\t\t\t\theaders,\n\t\t\t\trows: normalizedRows,\n\t\t\t},\n\t\t});\n\t}\n\treturn tables;\n};\n\nconst instrumentationGapEvidenceReferences = (\n\tresult: Pick<AnalysisResult, \"analysisId\" | \"payload\">,\n): EvidenceReference[] => {\n\tconst gaps = result.payload.instrumentationGaps;\n\tif (!isRecord(gaps)) return [];\n\tconst traceId = asString(gaps.traceId);\n\tconst blindspots = gaps.blindspots;\n\tif (!traceId || !Array.isArray(blindspots)) return [];\n\n\treturn blindspots\n\t\t.filter(isRecord)\n\t\t.map((gap, index): EvidenceReference | null => {\n\t\t\tconst spanId = asString(gap.parentSpanId);\n\t\t\tif (!spanId) return null;\n\t\t\tconst serviceName = asString(gap.parentServiceName);\n\t\t\tconst spanName = asString(gap.parentSpanName);\n\t\t\tconst durationMs =\n\t\t\t\ttypeof gap.durationMs === \"number\" && Number.isFinite(gap.durationMs)\n\t\t\t\t\t? gap.durationMs\n\t\t\t\t\t: null;\n\t\t\tconst ratio =\n\t\t\t\ttypeof gap.ratioOfParent === \"number\" &&\n\t\t\t\tNumber.isFinite(gap.ratioOfParent)\n\t\t\t\t\t? gap.ratioOfParent\n\t\t\t\t\t: null;\n\t\t\tconst recommendation = asString(gap.recommendation);\n\t\t\tconst thresholdVersion =\n\t\t\t\tasString(gap.thresholdVersion) ??\n\t\t\t\t(isRecord(gaps.thresholds) ? asString(gaps.thresholds.version) : null);\n\t\t\tconst entityId = `${traceId}:${spanId}`;\n\t\t\tconst route = spanRoute(traceId, spanId);\n\t\t\tconst label = spanName ? `${spanName} (${spanId})` : `span ${spanId}`;\n\t\t\tconst durationText =\n\t\t\t\tdurationMs === null ? \"unknown duration\" : `${durationMs}ms`;\n\t\t\tconst ratioText =\n\t\t\t\tratio === null ? \"\" : `, ${(ratio * 100).toFixed(1)}% of parent`;\n\t\t\tconst thresholdText = thresholdVersion\n\t\t\t\t? ` Thresholds: ${thresholdVersion}.`\n\t\t\t\t: \"\";\n\t\t\tconst docsRoute = hrefForEntity(\"docs\", \"howto/ebpf\");\n\t\t\treturn {\n\t\t\t\tevidenceId: `analysis:${result.analysisId}:instrumentation-gap:${index}:span:${entityId}`,\n\t\t\t\tentityKind: \"span\",\n\t\t\t\tentityId,\n\t\t\t\troute,\n\t\t\t\tsource: \"analysis.payload.instrumentationGaps\",\n\t\t\t\tconfidence: 0.9,\n\t\t\t\treason: `Analysis \"${result.analysisId}\" found uninstrumented self-time on ${label}: ${durationText}${ratioText}.${thresholdText}${recommendation ? ` ${recommendation}` : \"\"}`,\n\t\t\t\tcitations: [\n\t\t\t\t\t{\n\t\t\t\t\t\tlabel: `trace ${traceId}`,\n\t\t\t\t\t\tentityKind: \"trace\",\n\t\t\t\t\t\tentityId: traceId,\n\t\t\t\t\t\troute: traceRoute(traceId),\n\t\t\t\t\t},\n\t\t\t\t],\n\t\t\t\tsuggestedNextPivots: [\n\t\t\t\t\t{\n\t\t\t\t\t\tlabel: \"Open uninstrumented span\",\n\t\t\t\t\t\tentityKind: \"span\",\n\t\t\t\t\t\tentityId,\n\t\t\t\t\t\troute,\n\t\t\t\t\t\treason:\n\t\t\t\t\t\t\t\"Inspect the parent span whose self-time suggests missing child instrumentation.\",\n\t\t\t\t\t},\n\t\t\t\t\t{\n\t\t\t\t\t\tlabel: \"Open trace gap data\",\n\t\t\t\t\t\tentityKind: \"trace\",\n\t\t\t\t\t\tentityId: traceId,\n\t\t\t\t\t\troute: traceRoute(traceId),\n\t\t\t\t\t\treason:\n\t\t\t\t\t\t\t\"Review all structured instrumentation blindspots for this trace.\",\n\t\t\t\t\t},\n\t\t\t\t\t{\n\t\t\t\t\t\tlabel: \"Open profiler setup docs\",\n\t\t\t\t\t\tentityKind: \"docs\",\n\t\t\t\t\t\tentityId: \"howto/ebpf\",\n\t\t\t\t\t\troute: docsRoute,\n\t\t\t\t\t\treason:\n\t\t\t\t\t\t\tserviceName != null\n\t\t\t\t\t\t\t\t? `Use profiler or eBPF setup guidance to cover missing spans in ${serviceName}.`\n\t\t\t\t\t\t\t\t: \"Use profiler or eBPF setup guidance to cover missing spans.\",\n\t\t\t\t\t},\n\t\t\t\t],\n\t\t\t};\n\t\t})\n\t\t.filter((ref): ref is EvidenceReference => ref !== null)\n\t\t.slice(0, MAX_ANALYSIS_EVIDENCE_REFERENCES);\n};\n\nconst entityFromRow = (\n\trow: JsonValue[],\n\theaders: string[],\n): RowEntity | null => {\n\tconst headerIndex = new Map(headers.map((header, index) => [header, index]));\n\tconst traceId = valueFor(row, headerIndex, [\"trace_id\", \"traceid\", \"trace\"]);\n\tconst spanId = valueFor(row, headerIndex, [\"span_id\", \"spanid\"]);\n\tconst logId = valueFor(row, headerIndex, [\"log_id\", \"logid\"]);\n\tconst profileId = valueFor(row, headerIndex, [\"profile_id\", \"profileid\"]);\n\tconst actionId = valueFor(row, headerIndex, [\"action_id\", \"actionid\"]);\n\tconst agentRunId = valueFor(row, headerIndex, [\n\t\t\"agent_run_id\",\n\t\t\"agentrunid\",\n\t\t\"agent_run\",\n\t]);\n\tconst toolCallId = valueFor(row, headerIndex, [\n\t\t\"tool_call_id\",\n\t\t\"toolcallid\",\n\t\t\"tool_call\",\n\t]);\n\tconst evalId = valueFor(row, headerIndex, [\"eval_id\", \"evalid\"]);\n\tconst service = valueFor(row, headerIndex, [\"service\", \"service_name\"]);\n\n\tconst traceCitation = traceId\n\t\t? [\n\t\t\t\t{\n\t\t\t\t\tlabel: `trace ${traceId}`,\n\t\t\t\t\tentityKind: \"trace\" as const,\n\t\t\t\t\tentityId: traceId,\n\t\t\t\t\troute: traceRoute(traceId),\n\t\t\t\t},\n\t\t\t]\n\t\t: [];\n\n\tif (traceId && spanId) {\n\t\tconst entityId = `${traceId}:${spanId}`;\n\t\treturn {\n\t\t\tentityKind: \"span\",\n\t\t\tentityId,\n\t\t\troute: spanRoute(traceId, spanId),\n\t\t\tlabel: `span ${spanId}`,\n\t\t\tconfidence: 0.95,\n\t\t\tcitations: traceCitation,\n\t\t\tsuggestedNextPivots: [\n\t\t\t\t{\n\t\t\t\t\tlabel: \"Open span\",\n\t\t\t\t\tentityKind: \"span\",\n\t\t\t\t\tentityId,\n\t\t\t\t\troute: spanRoute(traceId, spanId),\n\t\t\t\t\treason: \"Inspect the cited span in its trace context.\",\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\tlabel: \"Open trace\",\n\t\t\t\t\tentityKind: \"trace\",\n\t\t\t\t\tentityId: traceId,\n\t\t\t\t\troute: traceRoute(traceId),\n\t\t\t\t\treason: \"Review the surrounding trace context.\",\n\t\t\t\t},\n\t\t\t],\n\t\t};\n\t}\n\n\tconst candidates: Array<{\n\t\tentityKind: EvidenceEntityKind;\n\t\tentityId: string | null;\n\t\tlabelPrefix: string;\n\t\tconfidence: number;\n\t}> = [\n\t\t{\n\t\t\tentityKind: \"trace\",\n\t\t\tentityId: traceId,\n\t\t\tlabelPrefix: \"trace\",\n\t\t\tconfidence: 0.95,\n\t\t},\n\t\t{\n\t\t\tentityKind: \"action\",\n\t\t\tentityId: actionId,\n\t\t\tlabelPrefix: \"action\",\n\t\t\tconfidence: 0.9,\n\t\t},\n\t\t{\n\t\t\tentityKind: \"tool_call\",\n\t\t\tentityId: toolCallId,\n\t\t\tlabelPrefix: \"tool call\",\n\t\t\tconfidence: 0.9,\n\t\t},\n\t\t{\n\t\t\tentityKind: \"agent_run\",\n\t\t\tentityId: agentRunId,\n\t\t\tlabelPrefix: \"agent run\",\n\t\t\tconfidence: 0.9,\n\t\t},\n\t\t{\n\t\t\tentityKind: \"profile\",\n\t\t\tentityId: profileId,\n\t\t\tlabelPrefix: \"profile\",\n\t\t\tconfidence: 0.9,\n\t\t},\n\t\t{\n\t\t\tentityKind: \"log\",\n\t\t\tentityId: logId,\n\t\t\tlabelPrefix: \"log\",\n\t\t\tconfidence: 0.85,\n\t\t},\n\t\t{\n\t\t\tentityKind: \"eval\",\n\t\t\tentityId: evalId,\n\t\t\tlabelPrefix: \"eval\",\n\t\t\tconfidence: 0.85,\n\t\t},\n\t\t{\n\t\t\tentityKind: \"service\",\n\t\t\tentityId: service,\n\t\t\tlabelPrefix: \"service\",\n\t\t\tconfidence: 0.7,\n\t\t},\n\t];\n\n\tconst candidate = candidates.find((item) => item.entityId);\n\tif (!candidate?.entityId) return null;\n\n\tconst route = evidenceRouteFor(\n\t\tcandidate.entityKind,\n\t\tcandidate.entityId,\n\t\ttraceId,\n\t);\n\treturn {\n\t\tentityKind: candidate.entityKind,\n\t\tentityId: candidate.entityId,\n\t\troute,\n\t\tlabel: `${candidate.labelPrefix} ${candidate.entityId}`,\n\t\tconfidence: candidate.confidence,\n\t\tcitations: traceCitation.filter(\n\t\t\t(citation) =>\n\t\t\t\tcitation.entityKind !== candidate.entityKind ||\n\t\t\t\tcitation.entityId !== candidate.entityId,\n\t\t),\n\t\tsuggestedNextPivots: [\n\t\t\t{\n\t\t\t\tlabel: `Open ${candidate.labelPrefix}`,\n\t\t\t\tentityKind: candidate.entityKind,\n\t\t\t\tentityId: candidate.entityId,\n\t\t\t\troute,\n\t\t\t\treason: \"Inspect the cited evidence row.\",\n\t\t\t},\n\t\t],\n\t};\n};\n\nexport const alertEvidenceReferences = (\n\trule: AlertRule,\n\tevaluation?: AlertEvaluation | null,\n): EvidenceReference[] => {\n\tconst route = evidenceRouteFor(\"alert\", rule.id);\n\tconst references: EvidenceReference[] = [\n\t\t{\n\t\t\tevidenceId: evaluation\n\t\t\t\t? `alert:${rule.id}:evaluation:${evaluation.id}`\n\t\t\t\t: `alert:${rule.id}:rule`,\n\t\t\tentityKind: \"alert\",\n\t\t\tentityId: rule.id,\n\t\t\troute,\n\t\t\tsource: evaluation\n\t\t\t\t? \"alert_evaluations\"\n\t\t\t\t: \"alerts.rule_evaluation_preview\",\n\t\t\tconfidence: evaluation ? 1 : 0.8,\n\t\t\treason: evaluation\n\t\t\t\t? `Alert \"${rule.name}\" evaluated to ${evaluation.value} and state ${evaluation.state}.`\n\t\t\t\t: `Alert \"${rule.name}\" was evaluated against its configured threshold.`,\n\t\t\tcitations: [\n\t\t\t\t{\n\t\t\t\t\tlabel: rule.name,\n\t\t\t\t\tentityKind: \"alert\",\n\t\t\t\t\tentityId: rule.id,\n\t\t\t\t\troute,\n\t\t\t\t},\n\t\t\t],\n\t\t\tsuggestedNextPivots: [\n\t\t\t\t{\n\t\t\t\t\tlabel: \"Open alert\",\n\t\t\t\t\tentityKind: \"alert\",\n\t\t\t\t\tentityId: rule.id,\n\t\t\t\t\troute,\n\t\t\t\t\treason: \"Inspect the alert rule, threshold, and recent evaluations.\",\n\t\t\t\t},\n\t\t\t],\n\t\t},\n\t];\n\n\tif (rule.analysisId) {\n\t\tconst analysisRouteValue = analysisRoute(rule.analysisId);\n\t\treferences.push({\n\t\t\tevidenceId: evaluation\n\t\t\t\t? `alert:${rule.id}:evaluation:${evaluation.id}:analysis:${rule.analysisId}`\n\t\t\t\t: `alert:${rule.id}:analysis:${rule.analysisId}`,\n\t\t\tentityKind: \"analysis\",\n\t\t\tentityId: rule.analysisId,\n\t\t\troute: analysisRouteValue,\n\t\t\tsource: \"alert_rules.analysis_id\",\n\t\t\tconfidence: 0.9,\n\t\t\treason: `Alert \"${rule.name}\" is bound to analysis \"${rule.analysisId}\".`,\n\t\t\tcitations: [\n\t\t\t\t{\n\t\t\t\t\tlabel: rule.name,\n\t\t\t\t\tentityKind: \"alert\",\n\t\t\t\t\tentityId: rule.id,\n\t\t\t\t\troute,\n\t\t\t\t},\n\t\t\t],\n\t\t\tsuggestedNextPivots: [\n\t\t\t\t{\n\t\t\t\t\tlabel: \"Open investigation\",\n\t\t\t\t\tentityKind: \"analysis\",\n\t\t\t\t\tentityId: rule.analysisId,\n\t\t\t\t\troute: analysisRouteValue,\n\t\t\t\t\treason: \"Review the analysis result that drives this alert.\",\n\t\t\t\t},\n\t\t\t],\n\t\t});\n\t}\n\n\treturn references;\n};\n\nexport const aiEvaluationEvidenceReferences = (\n\tevaluation: Pick<\n\t\tAIEvaluationRecord,\n\t\t\"evaluationId\" | \"traceId\" | \"spanId\" | \"name\" | \"source\"\n\t>,\n): EvidenceReference[] => {\n\tconst spanEntityId = `${evaluation.traceId}:${evaluation.spanId}`;\n\tconst spanRouteValue = evidenceRouteFor(\n\t\t\"span\",\n\t\tspanEntityId,\n\t\tevaluation.traceId,\n\t);\n\treturn [\n\t\t{\n\t\t\tevidenceId: `ai-evaluation:${evaluation.evaluationId}`,\n\t\t\tentityKind: \"eval\",\n\t\t\tentityId: evaluation.evaluationId,\n\t\t\troute: evidenceRouteFor(\"eval\", evaluation.evaluationId),\n\t\t\tsource: `ai_span_evaluations.${evaluation.source}`,\n\t\t\tconfidence: 0.95,\n\t\t\treason: `AI evaluation \"${evaluation.name}\" graded span \"${evaluation.spanId}\".`,\n\t\t\tcitations: [\n\t\t\t\t{\n\t\t\t\t\tlabel: `span ${evaluation.spanId}`,\n\t\t\t\t\tentityKind: \"span\",\n\t\t\t\t\tentityId: spanEntityId,\n\t\t\t\t\troute: spanRouteValue,\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\tlabel: `trace ${evaluation.traceId}`,\n\t\t\t\t\tentityKind: \"trace\",\n\t\t\t\t\tentityId: evaluation.traceId,\n\t\t\t\t\troute: evidenceRouteFor(\"trace\", evaluation.traceId),\n\t\t\t\t},\n\t\t\t],\n\t\t\tsuggestedNextPivots: [\n\t\t\t\t{\n\t\t\t\t\tlabel: \"Open span\",\n\t\t\t\t\tentityKind: \"span\",\n\t\t\t\t\tentityId: spanEntityId,\n\t\t\t\t\troute: spanRouteValue,\n\t\t\t\t\treason: \"Inspect the evaluated AI span and its surrounding trace.\",\n\t\t\t\t},\n\t\t\t],\n\t\t},\n\t];\n};\n\nexport interface SourceEvidenceLinks {\n\tsourceAgentRunId?: string | null;\n\tsourceActionId?: string | null;\n\tsourceToolCallId?: string | null;\n\tsourceTraceId?: string | null;\n\tsourceSpanId?: string | null;\n}\n\nexport const sourceLinkEvidenceReferences = (\n\tsource: {\n\t\tsourceLabel: string;\n\t\tsourceId: string;\n\t\tsourceKind: \"eval_case\" | \"eval_case_result\" | \"eval_run\";\n\t\tsourceRoute?: string | null;\n\t\tsourceName?: string | null;\n\t},\n\tlinks: SourceEvidenceLinks,\n): EvidenceReference[] => {\n\tconst citations: EvidenceCitation[] = [];\n\tif (source.sourceRoute) {\n\t\tcitations.push({\n\t\t\tlabel: source.sourceName ?? source.sourceId,\n\t\t\tentityKind: \"eval\",\n\t\t\tentityId: source.sourceId,\n\t\t\troute: source.sourceRoute,\n\t\t});\n\t}\n\n\tconst candidates: Array<{\n\t\tkind: EvidenceEntityKind;\n\t\tid: string | null | undefined;\n\t\tlabel: string;\n\t\tconfidence: number;\n\t}> = [\n\t\t{\n\t\t\tkind: \"action\",\n\t\t\tid: links.sourceActionId,\n\t\t\tlabel: \"source action\",\n\t\t\tconfidence: 0.92,\n\t\t},\n\t\t{\n\t\t\tkind: \"tool_call\",\n\t\t\tid: links.sourceToolCallId,\n\t\t\tlabel: \"source tool call\",\n\t\t\tconfidence: 0.9,\n\t\t},\n\t\t{\n\t\t\tkind: \"agent_run\",\n\t\t\tid: links.sourceAgentRunId,\n\t\t\tlabel: \"source agent run\",\n\t\t\tconfidence: 0.9,\n\t\t},\n\t\t{\n\t\t\tkind: \"span\",\n\t\t\tid:\n\t\t\t\tlinks.sourceTraceId && links.sourceSpanId\n\t\t\t\t\t? `${links.sourceTraceId}:${links.sourceSpanId}`\n\t\t\t\t\t: null,\n\t\t\tlabel: \"source span\",\n\t\t\tconfidence: 0.9,\n\t\t},\n\t\t{\n\t\t\tkind: \"trace\",\n\t\t\tid: links.sourceTraceId,\n\t\t\tlabel: \"source trace\",\n\t\t\tconfidence: 0.88,\n\t\t},\n\t];\n\n\treturn candidates\n\t\t.filter((candidate): candidate is typeof candidate & { id: string } =>\n\t\t\tBoolean(candidate.id),\n\t\t)\n\t\t.map((candidate) => {\n\t\t\tconst route = evidenceRouteFor(\n\t\t\t\tcandidate.kind,\n\t\t\t\tcandidate.id,\n\t\t\t\tlinks.sourceTraceId,\n\t\t\t);\n\t\t\treturn {\n\t\t\t\tevidenceId: `${source.sourceKind}:${source.sourceId}:${candidate.kind}:${candidate.id}`,\n\t\t\t\tentityKind: candidate.kind,\n\t\t\t\tentityId: candidate.id,\n\t\t\t\troute,\n\t\t\t\tsource: `${source.sourceKind}.source_links`,\n\t\t\t\tconfidence: candidate.confidence,\n\t\t\t\treason: `${source.sourceLabel} links to ${candidate.label} \"${candidate.id}\".`,\n\t\t\t\tcitations,\n\t\t\t\tsuggestedNextPivots: [\n\t\t\t\t\t{\n\t\t\t\t\t\tlabel: `Open ${candidate.label}`,\n\t\t\t\t\t\tentityKind: candidate.kind,\n\t\t\t\t\t\tentityId: candidate.id,\n\t\t\t\t\t\troute,\n\t\t\t\t\t\treason:\n\t\t\t\t\t\t\t\"Inspect the production evidence behind this evaluation item.\",\n\t\t\t\t\t},\n\t\t\t\t],\n\t\t\t} satisfies EvidenceReference;\n\t\t});\n};\n\nexport const analysisResultEvidenceReferences = (\n\tresult: Pick<AnalysisResult, \"analysisId\" | \"payload\">,\n): EvidenceReference[] => {\n\tconst references: EvidenceReference[] = [\n\t\t...instrumentationGapEvidenceReferences(result),\n\t];\n\tfor (const { sectionName, table } of parseEvidenceTables(result.payload)) {\n\t\tconst headers = table.headers.map(normalizeHeader);\n\t\tfor (const [rowIndex, row] of table.rows.entries()) {\n\t\t\tif (references.length >= MAX_ANALYSIS_EVIDENCE_REFERENCES) {\n\t\t\t\treturn references;\n\t\t\t}\n\t\t\tconst entity = entityFromRow(row, headers);\n\t\t\tif (!entity) continue;\n\n\t\t\tconst title = table.title ?? sectionName;\n\t\t\treferences.push({\n\t\t\t\tevidenceId: `analysis:${result.analysisId}:${sectionName}:${rowIndex}:${entity.entityKind}:${entity.entityId}`,\n\t\t\t\tentityKind: entity.entityKind,\n\t\t\t\tentityId: entity.entityId,\n\t\t\t\troute: entity.route,\n\t\t\t\tsource: `analysis.payload.evidence.${sectionName}`,\n\t\t\t\tconfidence: entity.confidence,\n\t\t\t\treason: `Analysis \"${result.analysisId}\" cited ${entity.label} in \"${title}\" row ${rowIndex + 1}.`,\n\t\t\t\tcitations: entity.citations,\n\t\t\t\tsuggestedNextPivots: entity.suggestedNextPivots,\n\t\t\t});\n\t\t}\n\t}\n\treturn references;\n};\n","/**\n * RFC 0002 Stage 3 — narrative LLM adapter.\n *\n * Single-provider for v1 (Anthropic). The shape is \"stuff in a system\n * prompt that bakes the rendering rules, ask for one declarative\n * sentence, return text\" — a different provider plugs in here without\n * changing callers.\n *\n * Rendering rules from the RFC's UX section, enforced at the system\n * prompt layer so the model can't accidentally drift:\n *\n * 1. Never start with \"I\" or \"Here's\" — declarative, not chatbot.\n * 2. Always cite — if the prompt mentions \"the trace\", a trace_id\n * must follow inline.\n * 3. Include a time anchor (\"starting 8 min ago\", \"since 14:32\").\n * 4. ≤2 sentences. We're labelling tiles, not writing essays.\n */\n\nimport type { AnalysisDefinition, AnalysisResult } from \"@obsunified/types\";\nimport type { ChildSpanRunner } from \"../framework/logger\";\n\nexport interface NarrativeRequest {\n\tdefinition: AnalysisDefinition;\n\tcurrent: AnalysisResult;\n\tprevious: AnalysisResult | null;\n}\n\n/**\n * Provider discriminator. The collector picks one based on which API key\n * env var is set (OpenAI takes precedence when both are set, since users\n * who configure both presumably did so deliberately for OpenAI).\n *\n * anthropic → POST https://api.anthropic.com/v1/messages\n * openai → POST https://api.openai.com/v1/chat/completions\n * (apiUrl override lets you point at openrouter, vLLM,\n * azure-openai-compat, ollama, etc.)\n */\nexport type LlmProvider = \"anthropic\" | \"openai\";\n\nexport interface LlmConfig {\n\tprovider: LlmProvider;\n\tapiKey: string;\n\t/** Model id. Provider-specific. e.g. \"claude-haiku-4-5\" / \"gpt-4o-mini\". */\n\tmodel: string;\n\t/** Override base URL — Anthropic-compatible or OpenAI-compatible. */\n\tapiUrl?: string;\n\t/** Optional abort signal used to cap provider calls from schedulers. */\n\tsignal?: AbortSignal;\n\t/**\n\t * Optional child-span runner. When set, every LLM API call (and tool\n\t * dispatch in the agent loop) is wrapped in an OpenInference-shaped span\n\t * with `gen_ai.system`, `gen_ai.request.model`, and post-call usage\n\t * attributes. Wired by the worker entrypoint via `runtime.withChildSpan`.\n\t */\n\ttracer?: ChildSpanRunner;\n}\n\nconst DEFAULT_API_URL = \"https://api.anthropic.com/v1/messages\";\n\nconst SYSTEM_PROMPT = `You write one-sentence operational narratives for a self-hosted observability dashboard.\n\nHard rules — violations are unusable:\n1. Never start with \"I\", \"Here's\", \"We\", \"It looks\", or any first-person/chatbot framing. Write declarative statements about the system.\n2. If you reference a trace, span, log, or service, include the supplied identifier inline (e.g. \"trace abc12345\").\n3. Include a time anchor: \"starting Nm ago\", \"for the last Nm\", \"since HH:MM\" — pull from the inputs.\n4. ≤ 2 sentences total, ≤ 220 characters. Numbers belong in the sentence, not in a separate label.\n5. If the inputs are insufficient to write a useful sentence, output exactly the string NO_NARRATIVE (no quotes).\n\nTone: like a senior on-call writing a Slack message. Specific, blunt, no hedging, no advice (\"you should...\"), no questions.`;\n\nconst renderTemplate = (\n\ttemplate: string,\n\tdef: AnalysisDefinition,\n\tr: AnalysisResult,\n): string => {\n\tconst scope = (def.scope ?? {}) as Record<string, unknown>;\n\tconst traceIds = Array.isArray(r.payload?.trace_ids)\n\t\t? (r.payload.trace_ids as unknown[])\n\t\t\t\t.filter((x): x is string => typeof x === \"string\")\n\t\t\t\t.slice(0, 5)\n\t\t\t\t.join(\", \")\n\t\t: \"\";\n\tconst subs: Record<string, string> = {\n\t\ttitle: def.title,\n\t\tstatus: r.status,\n\t\tprimary: r.primaryValue === null ? \"n/a\" : String(r.primaryValue),\n\t\tbaseline: r.baselineValue === null ? \"n/a\" : String(r.baselineValue),\n\t\tdelta_pct: r.deltaPct === null ? \"n/a\" : `${r.deltaPct.toFixed(1)}%`,\n\t\tservice: typeof scope.service === \"string\" ? scope.service : \"\",\n\t\ttrace_ids: traceIds,\n\t};\n\treturn template.replace(/\\{\\{(\\w+)\\}\\}/g, (_, k) => subs[k] ?? \"\");\n};\n\nconst buildUserPrompt = ({\n\tdefinition,\n\tcurrent,\n\tprevious,\n}: NarrativeRequest): string => {\n\tconst lines: string[] = [];\n\tconst tmplRendered = renderTemplate(\n\t\tdefinition.narrate?.prompt ?? \"\",\n\t\tdefinition,\n\t\tcurrent,\n\t).trim();\n\tif (tmplRendered) lines.push(tmplRendered);\n\n\tlines.push(\"\");\n\tlines.push(\"Current state:\");\n\tlines.push(`- panel: ${definition.title}`);\n\tlines.push(`- status: ${current.status}`);\n\tif (current.primaryValue !== null)\n\t\tlines.push(`- primary: ${current.primaryValue}`);\n\tif (current.baselineValue !== null)\n\t\tlines.push(`- baseline (1h): ${current.baselineValue}`);\n\tif (current.deltaPct !== null)\n\t\tlines.push(`- delta vs baseline: ${current.deltaPct.toFixed(1)}%`);\n\n\tconst since = previous\n\t\t? Math.max(\n\t\t\t\t1,\n\t\t\t\tMath.round(\n\t\t\t\t\t(new Date(current.generatedAt).getTime() -\n\t\t\t\t\t\tnew Date(previous.generatedAt).getTime()) /\n\t\t\t\t\t\t60000,\n\t\t\t\t),\n\t\t\t)\n\t\t: null;\n\tif (since !== null) {\n\t\tlines.push(\n\t\t\t`- previous run was ${since} minute${since === 1 ? \"\" : \"s\"} ago, status=${previous?.status ?? \"n/a\"}`,\n\t\t);\n\t}\n\n\tconst payloadKeys = Object.keys(current.payload ?? {}).filter(\n\t\t(k) => k !== \"sparkline\",\n\t);\n\tif (payloadKeys.length > 0) {\n\t\tlines.push(\"\");\n\t\tlines.push(\"Payload details:\");\n\t\tfor (const k of payloadKeys) {\n\t\t\tconst v = current.payload[k];\n\t\t\tconst rendered =\n\t\t\t\ttypeof v === \"string\" || typeof v === \"number\" || typeof v === \"boolean\"\n\t\t\t\t\t? String(v)\n\t\t\t\t\t: JSON.stringify(v);\n\t\t\tif (rendered.length <= 200) lines.push(`- ${k}: ${rendered}`);\n\t\t}\n\t}\n\n\tlines.push(\"\");\n\tlines.push(\n\t\t\"Output: a single sentence (max two), no preamble, no quotes, no Markdown.\",\n\t);\n\treturn lines.join(\"\\n\");\n};\n\nexport class LlmCallError extends Error {\n\treadonly status: number | null;\n\tconstructor(message: string, status: number | null = null) {\n\t\tsuper(message);\n\t\tthis.name = \"LlmCallError\";\n\t\tthis.status = status;\n\t}\n}\n\n/**\n * Provider dispatcher for narrative generation.\n *\n * Returns `null` when:\n * - the model returns the sentinel NO_NARRATIVE\n * - the response is empty\n * Throws `LlmCallError` on non-2xx HTTP responses so the runner can\n * count the failure against the budget without writing a bogus narrative.\n */\nexport async function generateNarrative(\n\treq: NarrativeRequest,\n\tconfig: LlmConfig,\n): Promise<string | null> {\n\tif (config.provider === \"openai\") {\n\t\t// Lazy import to keep the Anthropic-only path zero-cost on the worker\n\t\t// boot and avoid a circular import between llm.ts and openai.ts.\n\t\tconst { generateNarrativeOpenAI } = await import(\"./openai\");\n\t\treturn generateNarrativeOpenAI(req, config);\n\t}\n\treturn generateNarrativeAnthropic(req, config);\n}\n\nasync function generateNarrativeAnthropic(\n\treq: NarrativeRequest,\n\tconfig: LlmConfig,\n): Promise<string | null> {\n\tconst url = config.apiUrl ?? DEFAULT_API_URL;\n\tconst userPrompt = buildUserPrompt(req);\n\n\tconst tracer = config.tracer;\n\tconst callLLM = async (\n\t\tspan: { setAttribute(k: string, v: unknown): void } | null,\n\t): Promise<string | null> => {\n\t\tconst response = await fetch(url, {\n\t\t\tmethod: \"POST\",\n\t\t\theaders: {\n\t\t\t\t\"x-api-key\": config.apiKey,\n\t\t\t\t\"anthropic-version\": \"2023-06-01\",\n\t\t\t\t\"content-type\": \"application/json\",\n\t\t\t},\n\t\t\tbody: JSON.stringify({\n\t\t\t\tmodel: config.model,\n\t\t\t\tmax_tokens: 200,\n\t\t\t\tsystem: SYSTEM_PROMPT,\n\t\t\t\tmessages: [{ role: \"user\", content: userPrompt }],\n\t\t\t}),\n\t\t\tsignal: config.signal,\n\t\t});\n\t\tif (span) {\n\t\t\tspan.setAttribute(\"http.response.status_code\", response.status);\n\t\t}\n\n\t\tif (!response.ok) {\n\t\t\tconst text = await response.text().catch(() => \"\");\n\t\t\tthrow new LlmCallError(\n\t\t\t\t`anthropic ${response.status}: ${text.slice(0, 200)}`,\n\t\t\t\tresponse.status,\n\t\t\t);\n\t\t}\n\n\t\tconst body = (await response.json()) as {\n\t\t\tcontent?: Array<{ type?: string; text?: string }>;\n\t\t\tusage?: { input_tokens?: number; output_tokens?: number };\n\t\t\tstop_reason?: string;\n\t\t};\n\t\tif (body.stop_reason === \"max_tokens\") {\n\t\t\tthrow new LlmCallError(\"anthropic response truncated by max_tokens\");\n\t\t}\n\t\tif (span && body.usage) {\n\t\t\tif (typeof body.usage.input_tokens === \"number\")\n\t\t\t\tspan.setAttribute(\"gen_ai.usage.input_tokens\", body.usage.input_tokens);\n\t\t\tif (typeof body.usage.output_tokens === \"number\")\n\t\t\t\tspan.setAttribute(\n\t\t\t\t\t\"gen_ai.usage.output_tokens\",\n\t\t\t\t\tbody.usage.output_tokens,\n\t\t\t\t);\n\t\t}\n\t\tconst text = (body.content ?? [])\n\t\t\t.filter((block) => block?.type === \"text\")\n\t\t\t.map((block) => block?.text ?? \"\")\n\t\t\t.join(\"\")\n\t\t\t.trim();\n\n\t\tif (!text || text === \"NO_NARRATIVE\") return null;\n\t\t// Strip any accidental wrapping quotes the model might add despite the\n\t\t// system prompt.\n\t\treturn text.replace(/^[\"']|[\"']$/g, \"\").trim();\n\t};\n\n\tif (!tracer) return callLLM(null);\n\treturn tracer(\"llm.anthropic.messages\", async (span) => callLLM(span), {\n\t\t\"openinference.span.kind\": \"LLM\",\n\t\t\"gen_ai.system\": \"anthropic\",\n\t\t\"gen_ai.request.model\": config.model,\n\t\t\"gen_ai.request.max_tokens\": 200,\n\t});\n}\n"],"mappings":"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","names":["text"]}
|
package/dist/index.d.ts
CHANGED
|
@@ -2,9 +2,9 @@ import * as hono from 'hono';
|
|
|
2
2
|
import { MiddlewareHandler, Hono, Context } from 'hono';
|
|
3
3
|
import * as hono_types from 'hono/types';
|
|
4
4
|
import * as _obsunified_types from '@obsunified/types';
|
|
5
|
-
import { StoredSpan, TelemetryOverviewOptions, TelemetryOverviewResponse, TelemetryTraceDetailResponse, TelemetryInstrumentationGapsResponse, TelemetryIssueOptions, TelemetryIssueOverviewResponse, TelemetryIssueDetailResponse, UsageEventRecord, UsageOverviewOptions, UsageOverviewResponse, UsageSessionDetailResponse, UsageSessionSummary, AlertRuleInput, AlertRule, AlertStateRow, AlertState, AlertEvaluationsListResponse, AlertComparison, ChangeBlastRadiusResult, ContentionCandidate, ChangeEvent, ChangeEventInput, InvestigationFinding, InvestigationFindingInput, OntologyNode, OntologyEdge, CanonicalNodeIdentity, OntologyGraphResponse, RelationshipCertainty, CausalTriple, IncidentInvestigation, RemediationProposal, EvidenceEntityKind, RemediationProposalInput, JsonValue, EvidenceReference, EvidenceReferenceContract, Project, IngestKey, IngestKeyWithPlaintext } from '@obsunified/types';
|
|
6
|
-
import {
|
|
7
|
-
export {
|
|
5
|
+
import { StoredSpan, TelemetryOverviewOptions, TelemetryOverviewResponse, TelemetryTraceDetailResponse, TelemetryInstrumentationGapsResponse, TelemetryIssueOptions, TelemetryIssueOverviewResponse, TelemetryIssueDetailResponse, UsageEventRecord, UsageOverviewOptions, UsageOverviewResponse, UsageSessionDetailResponse, UsageSessionSummary, AlertRuleInput, AlertRule, AlertStateRow, AlertState, AlertEvaluationOutcome, AlertEvaluationsListResponse, AlertComparison, ChangeBlastRadiusResult, ContentionCandidate, ChangeEvent, ChangeEventInput, InvestigationFinding, InvestigationFindingInput, OntologyNode, OntologyEdge, CanonicalNodeIdentity, OntologyGraphResponse, RelationshipCertainty, CausalTriple, IncidentInvestigation, RemediationProposal, EvidenceEntityKind, RemediationProposalInput, JsonValue, EvidenceReference, EvidenceReferenceContract, AlertQueryMetrics, AlertMetricAggregation, AlertMetricField, Project, IngestKey, IngestKeyWithPlaintext } from '@obsunified/types';
|
|
6
|
+
import { g as CollectorEnv, L as Logger, e as ChildSpanRunner, l as CollectorRouteContext, a as AlertEmailTransport } from './ingest-auth-BTowieER.js';
|
|
7
|
+
export { A as AlertEmailMessage, b as AlertNotificationPayload, C as COLLECTOR_HOST_MANAGED_ENV_KEYS, c as COLLECTOR_OPTIONAL_STRING_ENV_KEYS, d as COLLECTOR_STRING_ENV_MANIFEST, f as CollectorApp, h as CollectorHostManagedEnvKey, i as CollectorManifestStringEnvKey, j as CollectorObjectBindingKey, k as CollectorOptionalStringEnvKey, m as CollectorStringEnvKey, n as assertCollectorEnvManifestCompleteness, o as consoleLogger, p as createIngestAuth, q as formatAlertEmail, r as isValidEmailAddress } from './ingest-auth-BTowieER.js';
|
|
8
8
|
import { ActionConfidence } from '@obsunified/types/constants';
|
|
9
9
|
import { Pool } from 'pg';
|
|
10
10
|
|
|
@@ -488,15 +488,178 @@ declare const aiReceiverPlugin: CollectorPlugin;
|
|
|
488
488
|
|
|
489
489
|
declare const aiSpanPayloadsProcessorPlugin: CollectorPlugin;
|
|
490
490
|
|
|
491
|
+
/** A rule's value, and whether a metrics rule found no fresh data. */
|
|
492
|
+
interface AlertRuleEvaluation {
|
|
493
|
+
value: number;
|
|
494
|
+
noData: boolean;
|
|
495
|
+
}
|
|
496
|
+
|
|
497
|
+
interface AlertsStoreOptions {
|
|
498
|
+
/** Permit webhook targets on loopback / private / link-local hosts. */
|
|
499
|
+
allowPrivateWebhookTargets?: boolean;
|
|
500
|
+
/**
|
|
501
|
+
* Whether this collector can send email (a transport is bound as
|
|
502
|
+
* `env.ALERT_EMAIL`). Rules with an email channel are refused otherwise.
|
|
503
|
+
*/
|
|
504
|
+
emailDeliveryAvailable?: boolean;
|
|
505
|
+
}
|
|
506
|
+
declare function compareValue(value: number, threshold: number, comparison: AlertComparison): boolean;
|
|
507
|
+
declare class AlertsStore {
|
|
508
|
+
private readonly db;
|
|
509
|
+
private readonly options;
|
|
510
|
+
constructor(db: SqlDb, options?: AlertsStoreOptions);
|
|
511
|
+
validateInput(input: AlertRuleInput): void;
|
|
512
|
+
listRules(projectId: string): Promise<AlertRule[]>;
|
|
513
|
+
/**
|
|
514
|
+
* The newest evaluation of every rule in the project, one query. Falls
|
|
515
|
+
* back to the pre-057/059 column set on older databases.
|
|
516
|
+
*/
|
|
517
|
+
private lastEvaluations;
|
|
518
|
+
getRule(id: string, projectId: string): Promise<AlertRule | null>;
|
|
519
|
+
createRule(projectId: string, input: AlertRuleInput): Promise<AlertRule>;
|
|
520
|
+
updateRule(id: string, projectId: string, patch: Partial<AlertRuleInput>): Promise<AlertRule | null>;
|
|
521
|
+
/**
|
|
522
|
+
* Clears a rule's pending period. Tolerates databases that have not
|
|
523
|
+
* applied 055/058 (there is nothing to clear on them).
|
|
524
|
+
*/
|
|
525
|
+
resetPending(ruleId: string, projectId: string): Promise<void>;
|
|
526
|
+
/**
|
|
527
|
+
* Runs `current`; on a database that has not applied migration 054 (no
|
|
528
|
+
* `alert_rules.pending_secs`) a rule without a pending duration is
|
|
529
|
+
* written with the legacy statement so rolling deploys keep working. A
|
|
530
|
+
* rule that needs the column is refused with a clear message instead of
|
|
531
|
+
* silently losing its pending duration.
|
|
532
|
+
*/
|
|
533
|
+
private withPendingColumnFallback;
|
|
534
|
+
deleteRule(id: string, projectId: string): Promise<boolean>;
|
|
535
|
+
listEnabledRules(): Promise<AlertRule[]>;
|
|
536
|
+
getState(ruleId: string, projectId: string): Promise<AlertStateRow | null>;
|
|
537
|
+
/**
|
|
538
|
+
* Record a state transition with its notification still outstanding
|
|
539
|
+
* (`notified = 0`); `markNotified` flips it once a channel accepts the
|
|
540
|
+
* alert. On a database that has not applied migration 049 the column is
|
|
541
|
+
* missing: the transition is still written (legacy statement) so alerting
|
|
542
|
+
* keeps working through a rolling deploy, and the evaluator treats a row
|
|
543
|
+
* without the column as delivered.
|
|
544
|
+
*/
|
|
545
|
+
transitionState(ruleId: string, projectId: string, newState: AlertState, now: string): Promise<void>;
|
|
546
|
+
/**
|
|
547
|
+
* Mark the transition `(state, lastStateChange)` as delivered. Scoped to
|
|
548
|
+
* the exact transition the caller notified for: if the rule changed state
|
|
549
|
+
* again while the webhook was in flight, the newer transition keeps its
|
|
550
|
+
* own outstanding flag. Returns false when nothing matched (stale or
|
|
551
|
+
* already marked). On a pre-049 schema there is nothing to mark.
|
|
552
|
+
*/
|
|
553
|
+
markNotified(ruleId: string, projectId: string, state: AlertState, lastStateChange: string): Promise<boolean>;
|
|
554
|
+
/**
|
|
555
|
+
* Record (`{ since, seenAt }`) or clear (`null`) the pending period of a
|
|
556
|
+
* rule that has a pending duration (migrations 055, 058): when the
|
|
557
|
+
* condition started holding, and the last evaluation that saw it hold.
|
|
558
|
+
* A rule without a state row gets one in state `ok` since its creation,
|
|
559
|
+
* already delivered (nothing is owed for a rule that never changed
|
|
560
|
+
* state).
|
|
561
|
+
*/
|
|
562
|
+
setPending(ruleId: string, projectId: string, pending: {
|
|
563
|
+
since: string;
|
|
564
|
+
seenAt: string;
|
|
565
|
+
} | null, ruleCreatedAt: string): Promise<void>;
|
|
566
|
+
recordEvaluation(ruleId: string, projectId: string, value: number, state: AlertState, notified: boolean, outcome?: AlertEvaluationOutcome, errorMessage?: string): Promise<void>;
|
|
567
|
+
listEvaluations(params: {
|
|
568
|
+
ruleId: string;
|
|
569
|
+
projectId?: string;
|
|
570
|
+
hours: number;
|
|
571
|
+
limit?: number;
|
|
572
|
+
}): Promise<AlertEvaluationsListResponse>;
|
|
573
|
+
evaluateRule(rule: AlertRule, now?: Date): Promise<number>;
|
|
574
|
+
/** Like `evaluateRule`, plus whether a metrics rule found no fresh data. */
|
|
575
|
+
evaluateRuleWithStatus(rule: AlertRule, now?: Date): Promise<AlertRuleEvaluation>;
|
|
576
|
+
private evaluateCountRule;
|
|
577
|
+
/**
|
|
578
|
+
* Stage 6 — fetch the latest narrative associated with this rule's
|
|
579
|
+
* bound analysis. Used by the evaluator when firing an analysis-bound
|
|
580
|
+
* alert so the webhook payload includes the human-readable
|
|
581
|
+
* narrative, not just a threshold crossing.
|
|
582
|
+
*/
|
|
583
|
+
getAnalysisNarrative(projectId: string, analysisId: string): Promise<{
|
|
584
|
+
narrative: string | null;
|
|
585
|
+
status: string | null;
|
|
586
|
+
} | null>;
|
|
587
|
+
private evaluateSpanRule;
|
|
588
|
+
private evaluateLogRule;
|
|
589
|
+
private evaluateUsageRule;
|
|
590
|
+
private evaluateAIRule;
|
|
591
|
+
}
|
|
592
|
+
|
|
491
593
|
/** Resolve a hostname to its addresses (e.g. `dns.lookup(host, { all: true })`). */
|
|
492
594
|
type HostLookup = (hostname: string) => Promise<string[]>;
|
|
493
595
|
|
|
596
|
+
interface AlertEvaluatorStore {
|
|
597
|
+
listEnabledRules(): Promise<AlertRule[]>;
|
|
598
|
+
evaluateRule(rule: AlertRule): Promise<number>;
|
|
599
|
+
/** Preferred when present: also says whether a metrics rule had no data. */
|
|
600
|
+
evaluateRuleWithStatus?(rule: AlertRule): Promise<AlertRuleEvaluation>;
|
|
601
|
+
getState(ruleId: string, projectId: string): Promise<{
|
|
602
|
+
current_state: AlertState;
|
|
603
|
+
last_state_change: string;
|
|
604
|
+
/** 0 while the transition's notification is owed; absent pre-049 (= delivered). */
|
|
605
|
+
notified?: number;
|
|
606
|
+
/** Start of the current pending period (migration 055); null/absent when not pending. */
|
|
607
|
+
pending_since?: string | null;
|
|
608
|
+
/** Last evaluation that saw the pending condition hold (migration 058). */
|
|
609
|
+
pending_seen_at?: string | null;
|
|
610
|
+
} | null>;
|
|
611
|
+
/** Record (`{ since, seenAt }`) or clear (null) the rule's pending period. */
|
|
612
|
+
setPending(ruleId: string, projectId: string, pending: {
|
|
613
|
+
since: string;
|
|
614
|
+
seenAt: string;
|
|
615
|
+
} | null, ruleCreatedAt: string): Promise<void>;
|
|
616
|
+
/** Flip the exact transition `(state, lastStateChange)` to delivered. */
|
|
617
|
+
markNotified(ruleId: string, projectId: string, state: AlertState, lastStateChange: string): Promise<boolean>;
|
|
618
|
+
getAnalysisNarrative(projectId: string, analysisId: string): Promise<{
|
|
619
|
+
narrative: string | null;
|
|
620
|
+
status: string | null;
|
|
621
|
+
} | null>;
|
|
622
|
+
transitionState(ruleId: string, projectId: string, newState: AlertState, now: string): Promise<void>;
|
|
623
|
+
recordEvaluation(ruleId: string, projectId: string, value: number, state: AlertState, notified: boolean, outcome?: AlertEvaluationOutcome,
|
|
624
|
+
/** With outcome `error`: why (the store sanitizes it). */
|
|
625
|
+
errorMessage?: string): Promise<void>;
|
|
626
|
+
}
|
|
627
|
+
declare function evaluateRuleBatch(store: AlertEvaluatorStore, rules: AlertRule[], options?: {
|
|
628
|
+
logger?: Logger;
|
|
629
|
+
tracer?: ChildSpanRunner;
|
|
630
|
+
concurrency?: number;
|
|
631
|
+
ruleTimeoutMs?: number;
|
|
632
|
+
allowPrivateWebhookTargets?: boolean;
|
|
633
|
+
hostLookup?: HostLookup;
|
|
634
|
+
email?: AlertEmailTransport;
|
|
635
|
+
onAlertFiring?: (rule: AlertRule) => Promise<void>;
|
|
636
|
+
/**
|
|
637
|
+
* Longest gap between two evaluations that still continues a pending
|
|
638
|
+
* hold. Default `DEFAULT_MAX_EVALUATION_GAP_MS` (two 5-minute ticks);
|
|
639
|
+
* pass twice the host's evaluation interval.
|
|
640
|
+
*/
|
|
641
|
+
maxEvaluationGapMs?: number;
|
|
642
|
+
/**
|
|
643
|
+
* ISO start of this collector process. A hold last seen before it
|
|
644
|
+
* starts over, as Prometheus resets `for` on restart. Unset on hosts
|
|
645
|
+
* without a long-lived process (Workers), where the gap rule applies.
|
|
646
|
+
*/
|
|
647
|
+
holdStartedAt?: string;
|
|
648
|
+
}): Promise<{
|
|
649
|
+
evaluated: number;
|
|
650
|
+
fired: number;
|
|
651
|
+
resolved: number;
|
|
652
|
+
/** Notifications for an earlier transition that were delivered on this tick. */
|
|
653
|
+
redelivered: number;
|
|
654
|
+
}>;
|
|
494
655
|
declare function evaluateAllRules(env: CollectorEnv, options?: {
|
|
495
656
|
logger?: Logger;
|
|
496
657
|
tracer?: ChildSpanRunner;
|
|
497
658
|
concurrency?: number;
|
|
498
659
|
ruleTimeoutMs?: number;
|
|
499
660
|
hostLookup?: HostLookup;
|
|
661
|
+
maxEvaluationGapMs?: number;
|
|
662
|
+
holdStartedAt?: string;
|
|
500
663
|
}): Promise<{
|
|
501
664
|
evaluated: number;
|
|
502
665
|
fired: number;
|
|
@@ -1024,64 +1187,6 @@ declare class ActionStore {
|
|
|
1024
1187
|
listArtifactsByAction(projectId: string, actionId: string, limit?: number): Promise<ArtifactRef[]>;
|
|
1025
1188
|
}
|
|
1026
1189
|
|
|
1027
|
-
interface AlertsStoreOptions {
|
|
1028
|
-
/** Permit webhook targets on loopback / private / link-local hosts. */
|
|
1029
|
-
allowPrivateWebhookTargets?: boolean;
|
|
1030
|
-
}
|
|
1031
|
-
declare function compareValue(value: number, threshold: number, comparison: AlertComparison): boolean;
|
|
1032
|
-
declare class AlertsStore {
|
|
1033
|
-
private readonly db;
|
|
1034
|
-
private readonly options;
|
|
1035
|
-
constructor(db: SqlDb, options?: AlertsStoreOptions);
|
|
1036
|
-
validateInput(input: AlertRuleInput): void;
|
|
1037
|
-
listRules(projectId: string): Promise<AlertRule[]>;
|
|
1038
|
-
getRule(id: string, projectId: string): Promise<AlertRule | null>;
|
|
1039
|
-
createRule(projectId: string, input: AlertRuleInput): Promise<AlertRule>;
|
|
1040
|
-
updateRule(id: string, projectId: string, patch: Partial<AlertRuleInput>): Promise<AlertRule | null>;
|
|
1041
|
-
deleteRule(id: string, projectId: string): Promise<boolean>;
|
|
1042
|
-
listEnabledRules(): Promise<AlertRule[]>;
|
|
1043
|
-
getState(ruleId: string, projectId: string): Promise<AlertStateRow | null>;
|
|
1044
|
-
/**
|
|
1045
|
-
* Record a state transition with its notification still outstanding
|
|
1046
|
-
* (`notified = 0`); `markNotified` flips it once a channel accepts the
|
|
1047
|
-
* alert. On a database that has not applied migration 049 the column is
|
|
1048
|
-
* missing: the transition is still written (legacy statement) so alerting
|
|
1049
|
-
* keeps working through a rolling deploy, and the evaluator treats a row
|
|
1050
|
-
* without the column as delivered.
|
|
1051
|
-
*/
|
|
1052
|
-
transitionState(ruleId: string, projectId: string, newState: AlertState, now: string): Promise<void>;
|
|
1053
|
-
/**
|
|
1054
|
-
* Mark the transition `(state, lastStateChange)` as delivered. Scoped to
|
|
1055
|
-
* the exact transition the caller notified for: if the rule changed state
|
|
1056
|
-
* again while the webhook was in flight, the newer transition keeps its
|
|
1057
|
-
* own outstanding flag. Returns false when nothing matched (stale or
|
|
1058
|
-
* already marked). On a pre-049 schema there is nothing to mark.
|
|
1059
|
-
*/
|
|
1060
|
-
markNotified(ruleId: string, projectId: string, state: AlertState, lastStateChange: string): Promise<boolean>;
|
|
1061
|
-
recordEvaluation(ruleId: string, projectId: string, value: number, state: AlertState, notified: boolean): Promise<void>;
|
|
1062
|
-
listEvaluations(params: {
|
|
1063
|
-
ruleId: string;
|
|
1064
|
-
projectId?: string;
|
|
1065
|
-
hours: number;
|
|
1066
|
-
limit?: number;
|
|
1067
|
-
}): Promise<AlertEvaluationsListResponse>;
|
|
1068
|
-
evaluateRule(rule: AlertRule): Promise<number>;
|
|
1069
|
-
/**
|
|
1070
|
-
* Stage 6 — fetch the latest narrative associated with this rule's
|
|
1071
|
-
* bound analysis. Used by the evaluator when firing an analysis-bound
|
|
1072
|
-
* alert so the webhook payload includes the human-readable
|
|
1073
|
-
* narrative, not just a threshold crossing.
|
|
1074
|
-
*/
|
|
1075
|
-
getAnalysisNarrative(projectId: string, analysisId: string): Promise<{
|
|
1076
|
-
narrative: string | null;
|
|
1077
|
-
status: string | null;
|
|
1078
|
-
} | null>;
|
|
1079
|
-
private evaluateSpanRule;
|
|
1080
|
-
private evaluateLogRule;
|
|
1081
|
-
private evaluateUsageRule;
|
|
1082
|
-
private evaluateAIRule;
|
|
1083
|
-
}
|
|
1084
|
-
|
|
1085
1190
|
declare function recordChangeEvent(db: SqlDb, projectId: string, input: ChangeEventInput): Promise<ChangeEvent>;
|
|
1086
1191
|
declare function listChangeEvents(db: SqlDb, options: {
|
|
1087
1192
|
projectId: string;
|
|
@@ -1698,6 +1803,103 @@ declare class EvalCasesStore {
|
|
|
1698
1803
|
private buildMetadata;
|
|
1699
1804
|
}
|
|
1700
1805
|
|
|
1806
|
+
/**
|
|
1807
|
+
* `metrics` alert signal: validation and evaluation of an
|
|
1808
|
+
* `AlertQueryMetrics` against `metric_series` / `metric_point`.
|
|
1809
|
+
*
|
|
1810
|
+
* Evaluation, per selector (numerator, then the optional denominator):
|
|
1811
|
+
*
|
|
1812
|
+
* 1. Series: `metric_series` rows of the project whose name is one of the
|
|
1813
|
+
* selector's names (and whose `service_name` matches, when given), kept
|
|
1814
|
+
* when every listed data-point / resource attribute equals the rule's
|
|
1815
|
+
* value. Attribute values are compared as strings, so `200` matches a
|
|
1816
|
+
* stored `"200"` and vice versa.
|
|
1817
|
+
* 2. Points: the series' points whose `received_at` (the collector's clock,
|
|
1818
|
+
* as for every other alert signal) is inside the rule window, ordered by
|
|
1819
|
+
* producer time (`ts_ns`). The number read is `field` (`value`, or a
|
|
1820
|
+
* histogram/summary `count` / `sum`); points without it are skipped.
|
|
1821
|
+
* 3. Staleness: for `last`, `min`, `max`, `avg` and `sum` a series takes
|
|
1822
|
+
* part only when its newest point is at most `staleSecs` (default 300)
|
|
1823
|
+
* old, so a series that stopped reporting (a renamed host, a replaced
|
|
1824
|
+
* pod) no longer adds its last value. `increase` / `rate` keep every
|
|
1825
|
+
* series with points in the window: growth that happened is real even
|
|
1826
|
+
* if the series stopped afterwards.
|
|
1827
|
+
* 4. Each series is reduced on its own (`reduceSeries`), and the per-series
|
|
1828
|
+
* results are summed. A series with no usable point adds nothing.
|
|
1829
|
+
*
|
|
1830
|
+
* When no series takes part (in the numerator or the denominator) the
|
|
1831
|
+
* result is "no data": the value is 0 and `noData` is true. The rule's
|
|
1832
|
+
* `noData` policy decides what that means (`zero`: evaluate 0 as usual;
|
|
1833
|
+
* `keep`: leave the alert state as it is).
|
|
1834
|
+
*
|
|
1835
|
+
* `age_seconds` ignores the window: it is the seconds since the newest
|
|
1836
|
+
* `received_at` of any matching series. When no matching series has any
|
|
1837
|
+
* stored point (never reported, or everything aged out of retention) it is
|
|
1838
|
+
* the seconds since the rule was created, a finite number that grows until
|
|
1839
|
+
* the threshold is crossed, so "never reported" fires after the same grace
|
|
1840
|
+
* period as "stopped reporting".
|
|
1841
|
+
*
|
|
1842
|
+
* `age_seconds` is never "no data".
|
|
1843
|
+
*
|
|
1844
|
+
* Ratio: with a `denominator`, value = numerator / denominator when the
|
|
1845
|
+
* denominator is > 0 and >= minDenominator (default 0), else 0.
|
|
1846
|
+
*
|
|
1847
|
+
* Cost bounds: at most `MAX_MATCHED_SERIES` series and `MAX_LOADED_POINTS`
|
|
1848
|
+
* points per selector; past either the evaluation fails with an error (the
|
|
1849
|
+
* evaluator records it as an `error` evaluation) rather than returning a
|
|
1850
|
+
* value computed from part of the data. Metric rule windows are at most
|
|
1851
|
+
* `MAX_METRIC_WINDOW_MINS`.
|
|
1852
|
+
*/
|
|
1853
|
+
|
|
1854
|
+
/** Throws an Error with a user-facing message when `query` is malformed. */
|
|
1855
|
+
declare function validateMetricQuery(query: unknown): void;
|
|
1856
|
+
interface MetricPointSample {
|
|
1857
|
+
tsNs: string;
|
|
1858
|
+
startTsNs: string | null;
|
|
1859
|
+
value: number | null;
|
|
1860
|
+
count: number | null;
|
|
1861
|
+
sum: number | null;
|
|
1862
|
+
receivedAt: string;
|
|
1863
|
+
}
|
|
1864
|
+
interface SeriesReduceOptions {
|
|
1865
|
+
/**
|
|
1866
|
+
* The series has no history before the window (its `metric_series` row
|
|
1867
|
+
* was created inside it): a cumulative counter's first point then counts
|
|
1868
|
+
* as growth from 0 instead of being the baseline.
|
|
1869
|
+
*/
|
|
1870
|
+
bornInWindow?: boolean;
|
|
1871
|
+
aggregation: Exclude<AlertMetricAggregation, "age_seconds">;
|
|
1872
|
+
field: AlertMetricField;
|
|
1873
|
+
/** `metric_series.temporality`: 1 delta, 2 cumulative, null for gauges. */
|
|
1874
|
+
temporality: number | null;
|
|
1875
|
+
/** Start of the window in Unix nanoseconds. */
|
|
1876
|
+
windowStartNs: bigint;
|
|
1877
|
+
windowSecs: number;
|
|
1878
|
+
}
|
|
1879
|
+
/**
|
|
1880
|
+
* Reduces one series' window points to a number, or null when the series
|
|
1881
|
+
* has no point carrying `field` in the window.
|
|
1882
|
+
*/
|
|
1883
|
+
declare function reduceSeries(points: MetricPointSample[], opts: SeriesReduceOptions): number | null;
|
|
1884
|
+
interface MetricEvalContext {
|
|
1885
|
+
projectId: string;
|
|
1886
|
+
windowMins: number;
|
|
1887
|
+
/** The rule's `createdAt`; the reference for "never reported". */
|
|
1888
|
+
ruleCreatedAt: string;
|
|
1889
|
+
now: Date;
|
|
1890
|
+
/** Staleness bound for gauge-style aggregations; default 300. */
|
|
1891
|
+
staleSecs?: number;
|
|
1892
|
+
/** Point cap per selector; default MAX_LOADED_POINTS (tests lower it). */
|
|
1893
|
+
maxLoadedPoints?: number;
|
|
1894
|
+
}
|
|
1895
|
+
interface MetricEvaluation {
|
|
1896
|
+
value: number;
|
|
1897
|
+
/** No series took part; see the module comment. Never true for age_seconds. */
|
|
1898
|
+
noData: boolean;
|
|
1899
|
+
}
|
|
1900
|
+
/** Evaluates a validated metrics query to a finite number. */
|
|
1901
|
+
declare function evaluateMetricQuery(db: SqlDb, query: AlertQueryMetrics, ctx: MetricEvalContext): Promise<MetricEvaluation>;
|
|
1902
|
+
|
|
1701
1903
|
type MetricType = "gauge" | "sum" | "histogram" | "exp_histogram" | "summary";
|
|
1702
1904
|
/**
|
|
1703
1905
|
* A metric data point normalized for storage. Series-identifying fields
|
|
@@ -1855,4 +2057,4 @@ declare const _default: hono.Hono<{
|
|
|
1855
2057
|
Bindings: CollectorEnv;
|
|
1856
2058
|
}, hono_types.BlankSchema, "/">;
|
|
1857
2059
|
|
|
1858
|
-
export { type ActionGraphPurgeSummary, type ActionGraphRetentionOptions, type ActionRecord, ActionStore, type AgentRunRecord, AlertsStore, type AnalysesRunSummary, type ArtifactRecord, type BlobListOptions, type BlobListResult, type BlobObject, type BlobPutOptions, type BlobStore, BlobStoreToR2Adapter, type CollectorAuthConfig, type CollectorConfig, CollectorEnv, type CollectorPlugin, CollectorRouteContext, CollectorRuntime, CounterfactualVerificationError, D1Adapter, type DashboardAuthConfig, type EvalCase, type EvalCaseInput, type EvalCaseListOptions, type EvalCaseSourceLinks, EvalCaseSourceNotFoundError, type EvalCaseSourceType, EvalCasesStore, type EvalResultRecord, type EvalRun, type EvalRunCandidate, type EvalRunInput, type EvalRunSourceCase, type EvalRunStatus, type EvalRunsListOptions, type HostLookup, Logger, MetricsStore, PostgresAdapter, type PostgresAdapterOptions, type PostgresTypeParserRegistry, ProjectsStore, R2BlobStore, RemediationGateError, type RetentionCleanupSummary, type RetrievalEventRecord, S3BlobStore, type S3BlobStoreOptions, type ScheduledTaskHandler, type SpanProcessorPlugin, type SqlDb, type SqlDbFactory, type SqlStatement, type TailEvent, TailHub, type TailKind, type ToolCallRecord, type UsageEventProcessorPlugin, type WatchdogEvaluationSummary, acquireIncidentLease, actionRetentionCutoff, actionRoutesPlugin, aiReceiverPlugin, aiSpanPayloadsProcessorPlugin, alertsRoutesPlugin, allPlugins, analysesRoutesPlugin, approveRemediationProposal, askRoutesPlugin, autosreRoutesPlugin, botFilterPlugin, claimRemediationGrant, compareValue, completeRemediationProposal, computeChangeBlastRadius, computeTarjanScc, configurePostgresTypeParsers, connectedRoutesPlugin, createAlertEvaluatorHandler, createAnalysesRunHandler, createDashboardAuth, createDefaultCollectorApp, createRetentionCleanupHandler, createTelemetryCollectorApp, dashboardRoutesPlugin, _default as default, defaultSpanEnrichmentPlugin, dispatchIncidentWebhook, evalCasesRoutesPlugin, evaluateAllRules, evaluateRemediationWatchdogs, evidenceRetrievalRoutesPlugin, extractSpanLineageAndTopology, failRemediationProposal, findCrossTraceContention, genAiNormalizerPlugin, getChangeEvent, getIncidentInvestigation, getInvestigationFinding, getOntologyGraph, getRemediationProposal, identityReceiverPlugin, isEvalCaseSourceType, isEvalRunStatus, issueEnrichmentPlugin, issueInsightsPlugin, listChangeEvents, listIncidentInvestigations, listRemediationProposals, loadTraceFaultLocusFromDb, localizeTraceFaultLocus, logsReceiverPlugin, metricsReceiverPlugin, onboardingRoutesPlugin, openOrDeduplicateIncident, otlpReceiverPlugin, platformRoutesPlugin, profileRoutesPlugin, projectsRoutesPlugin, proposeRemediationAction, purgeExpiredActionGraph, queryRoutesPlugin, recordChangeEvent, recordInvestigationFinding, redactionProcessorPlugin, rejectRemediationProposal, replayQueryRoutesPlugin, replayReceiverPlugin, searchInvestigationFindings, tailRoutesPlugin, timelineRoutesPlugin, uaEnrichmentPlugin, upsertOntologyFromSpans, usageNormalizationPlugin, usagePrivacyPlugin, usageQueryRoutesPlugin, usageReceiverPlugin, usersQueryRoutesPlugin, utmEnrichmentPlugin, verifyPairedCounterfactualProof };
|
|
2060
|
+
export { type ActionGraphPurgeSummary, type ActionGraphRetentionOptions, type ActionRecord, ActionStore, type AgentRunRecord, AlertEmailTransport, AlertsStore, type AnalysesRunSummary, type ArtifactRecord, type BlobListOptions, type BlobListResult, type BlobObject, type BlobPutOptions, type BlobStore, BlobStoreToR2Adapter, type CollectorAuthConfig, type CollectorConfig, CollectorEnv, type CollectorPlugin, CollectorRouteContext, CollectorRuntime, CounterfactualVerificationError, D1Adapter, type DashboardAuthConfig, type EvalCase, type EvalCaseInput, type EvalCaseListOptions, type EvalCaseSourceLinks, EvalCaseSourceNotFoundError, type EvalCaseSourceType, EvalCasesStore, type EvalResultRecord, type EvalRun, type EvalRunCandidate, type EvalRunInput, type EvalRunSourceCase, type EvalRunStatus, type EvalRunsListOptions, type HostLookup, Logger, MetricsStore, PostgresAdapter, type PostgresAdapterOptions, type PostgresTypeParserRegistry, ProjectsStore, R2BlobStore, RemediationGateError, type RetentionCleanupSummary, type RetrievalEventRecord, S3BlobStore, type S3BlobStoreOptions, type ScheduledTaskHandler, type SpanProcessorPlugin, type SqlDb, type SqlDbFactory, type SqlStatement, type TailEvent, TailHub, type TailKind, type ToolCallRecord, type UsageEventProcessorPlugin, type WatchdogEvaluationSummary, acquireIncidentLease, actionRetentionCutoff, actionRoutesPlugin, aiReceiverPlugin, aiSpanPayloadsProcessorPlugin, alertsRoutesPlugin, allPlugins, analysesRoutesPlugin, approveRemediationProposal, askRoutesPlugin, autosreRoutesPlugin, botFilterPlugin, claimRemediationGrant, compareValue, completeRemediationProposal, computeChangeBlastRadius, computeTarjanScc, configurePostgresTypeParsers, connectedRoutesPlugin, createAlertEvaluatorHandler, createAnalysesRunHandler, createDashboardAuth, createDefaultCollectorApp, createRetentionCleanupHandler, createTelemetryCollectorApp, dashboardRoutesPlugin, _default as default, defaultSpanEnrichmentPlugin, dispatchIncidentWebhook, evalCasesRoutesPlugin, evaluateAllRules, evaluateMetricQuery, evaluateRemediationWatchdogs, evaluateRuleBatch, evidenceRetrievalRoutesPlugin, extractSpanLineageAndTopology, failRemediationProposal, findCrossTraceContention, genAiNormalizerPlugin, getChangeEvent, getIncidentInvestigation, getInvestigationFinding, getOntologyGraph, getRemediationProposal, identityReceiverPlugin, isEvalCaseSourceType, isEvalRunStatus, issueEnrichmentPlugin, issueInsightsPlugin, listChangeEvents, listIncidentInvestigations, listRemediationProposals, loadTraceFaultLocusFromDb, localizeTraceFaultLocus, logsReceiverPlugin, metricsReceiverPlugin, onboardingRoutesPlugin, openOrDeduplicateIncident, otlpReceiverPlugin, platformRoutesPlugin, profileRoutesPlugin, projectsRoutesPlugin, proposeRemediationAction, purgeExpiredActionGraph, queryRoutesPlugin, recordChangeEvent, recordInvestigationFinding, redactionProcessorPlugin, reduceSeries, rejectRemediationProposal, replayQueryRoutesPlugin, replayReceiverPlugin, searchInvestigationFindings, tailRoutesPlugin, timelineRoutesPlugin, uaEnrichmentPlugin, upsertOntologyFromSpans, usageNormalizationPlugin, usagePrivacyPlugin, usageQueryRoutesPlugin, usageReceiverPlugin, usersQueryRoutesPlugin, utmEnrichmentPlugin, validateMetricQuery, verifyPairedCounterfactualProof };
|