@junando/core 0.10.1 → 0.11.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/index.d.ts +1456 -5
- package/dist/index.js +512 -47
- package/package.json +2 -1
package/dist/index.js
CHANGED
|
@@ -230,6 +230,7 @@ var AlertmanagerPayloadSchema = z.object({
|
|
|
230
230
|
|
|
231
231
|
// src/domain/entities/cluster.ts
|
|
232
232
|
import { z as z2 } from "zod";
|
|
233
|
+
var SEVERITY_VALUES = ["critical", "high", "medium", "low"];
|
|
233
234
|
var AlertClusterSchema = z2.object({
|
|
234
235
|
fingerprint: z2.string(),
|
|
235
236
|
serviceName: z2.string(),
|
|
@@ -239,7 +240,11 @@ var AlertClusterSchema = z2.object({
|
|
|
239
240
|
alertCount: z2.number().int().positive(),
|
|
240
241
|
representativeTraceIds: z2.array(z2.string()).max(2),
|
|
241
242
|
firstSeenAt: z2.string().datetime(),
|
|
242
|
-
latencyP99Ms: z2.number().optional()
|
|
243
|
+
latencyP99Ms: z2.number().optional(),
|
|
244
|
+
/** Severity level — can be derived from ALERT_TYPE_LABELS[alertType].severity */
|
|
245
|
+
severity: z2.enum(SEVERITY_VALUES).optional(),
|
|
246
|
+
/** Arbitrary key-value labels passed through from alerts */
|
|
247
|
+
labels: z2.record(z2.string()).optional()
|
|
243
248
|
});
|
|
244
249
|
|
|
245
250
|
// src/domain/entities/incident.ts
|
|
@@ -260,6 +265,73 @@ var IncidentSchema = z3.object({
|
|
|
260
265
|
processedAt: z3.string().datetime()
|
|
261
266
|
});
|
|
262
267
|
|
|
268
|
+
// src/domain/entities/rule.ts
|
|
269
|
+
import { z as z4 } from "zod";
|
|
270
|
+
var RuleActionType = /* @__PURE__ */ ((RuleActionType2) => {
|
|
271
|
+
RuleActionType2["Suppress"] = "suppress";
|
|
272
|
+
RuleActionType2["Route"] = "route";
|
|
273
|
+
RuleActionType2["Escalate"] = "escalate";
|
|
274
|
+
RuleActionType2["Tag"] = "tag";
|
|
275
|
+
return RuleActionType2;
|
|
276
|
+
})(RuleActionType || {});
|
|
277
|
+
var SeverityLevel = /* @__PURE__ */ ((SeverityLevel2) => {
|
|
278
|
+
SeverityLevel2["Critical"] = "critical";
|
|
279
|
+
SeverityLevel2["High"] = "high";
|
|
280
|
+
SeverityLevel2["Medium"] = "medium";
|
|
281
|
+
SeverityLevel2["Low"] = "low";
|
|
282
|
+
return SeverityLevel2;
|
|
283
|
+
})(SeverityLevel || {});
|
|
284
|
+
var RuleEvaluationPhase = /* @__PURE__ */ ((RuleEvaluationPhase2) => {
|
|
285
|
+
RuleEvaluationPhase2["PreLlm"] = "pre-llm";
|
|
286
|
+
RuleEvaluationPhase2["PostLlm"] = "post-llm";
|
|
287
|
+
return RuleEvaluationPhase2;
|
|
288
|
+
})(RuleEvaluationPhase || {});
|
|
289
|
+
var AlertCountSchema = z4.object({
|
|
290
|
+
min: z4.number().optional(),
|
|
291
|
+
max: z4.number().optional()
|
|
292
|
+
});
|
|
293
|
+
var LatencySchema = z4.object({
|
|
294
|
+
min: z4.number().optional(),
|
|
295
|
+
max: z4.number().optional()
|
|
296
|
+
});
|
|
297
|
+
var RuleConditionSchema = z4.object({
|
|
298
|
+
serviceName: z4.string().optional(),
|
|
299
|
+
alertType: z4.nativeEnum(AlertType).optional(),
|
|
300
|
+
severity: z4.nativeEnum(SeverityLevel).optional(),
|
|
301
|
+
labels: z4.record(z4.string()).optional(),
|
|
302
|
+
endpointPath: z4.string().optional(),
|
|
303
|
+
alertCount: AlertCountSchema.optional(),
|
|
304
|
+
latencyP99Ms: LatencySchema.optional(),
|
|
305
|
+
urgencyLevel: UrgencyLevelSchema.optional(),
|
|
306
|
+
requiresRollback: z4.boolean().optional(),
|
|
307
|
+
impactedServices: z4.array(z4.string()).optional()
|
|
308
|
+
});
|
|
309
|
+
var SUPPRESS_SCHEMA = z4.object({ type: z4.literal("suppress" /* Suppress */) });
|
|
310
|
+
var ROUTE_SCHEMA = z4.object({ type: z4.literal("route" /* Route */), channel: z4.string().min(1) });
|
|
311
|
+
var ESCALATE_SCHEMA = z4.object({ type: z4.literal("escalate" /* Escalate */), channel: z4.string().min(1) });
|
|
312
|
+
var TAG_SCHEMA = z4.object({ type: z4.literal("tag" /* Tag */), key: z4.string().min(1), value: z4.string().min(1) });
|
|
313
|
+
var RuleActionSchema = z4.discriminatedUnion("type", [
|
|
314
|
+
SUPPRESS_SCHEMA,
|
|
315
|
+
ROUTE_SCHEMA,
|
|
316
|
+
ESCALATE_SCHEMA,
|
|
317
|
+
TAG_SCHEMA
|
|
318
|
+
]);
|
|
319
|
+
var RuleSchema = z4.object({
|
|
320
|
+
id: z4.string().min(1),
|
|
321
|
+
name: z4.string().optional(),
|
|
322
|
+
condition: RuleConditionSchema,
|
|
323
|
+
actions: z4.array(RuleActionSchema).min(1),
|
|
324
|
+
urgencyLevel: UrgencyLevelSchema.optional(),
|
|
325
|
+
requiresRollback: z4.boolean().optional()
|
|
326
|
+
});
|
|
327
|
+
var RuleSectionSchema = z4.object({
|
|
328
|
+
rules: z4.array(RuleSchema).default([])
|
|
329
|
+
});
|
|
330
|
+
var RuleConfigurationSchema = z4.object({
|
|
331
|
+
["pre-llm" /* PreLlm */]: RuleSectionSchema,
|
|
332
|
+
["post-llm" /* PostLlm */]: RuleSectionSchema
|
|
333
|
+
});
|
|
334
|
+
|
|
263
335
|
// src/domain/value-objects/fingerprint.ts
|
|
264
336
|
import { createHash } from "crypto";
|
|
265
337
|
var Fingerprint = class _Fingerprint {
|
|
@@ -383,6 +455,7 @@ __export(metrics_exports, {
|
|
|
383
455
|
pipelineInlineFailuresTotal: () => pipelineInlineFailuresTotal,
|
|
384
456
|
registry: () => registry,
|
|
385
457
|
sqsQueueLag: () => sqsQueueLag,
|
|
458
|
+
suppressedClusters: () => suppressedClusters,
|
|
386
459
|
webhookRequestsTotal: () => webhookRequestsTotal
|
|
387
460
|
});
|
|
388
461
|
import { Registry, Counter, Gauge, Histogram } from "prom-client";
|
|
@@ -465,6 +538,12 @@ var sqsQueueLag = new Gauge({
|
|
|
465
538
|
labelNames: ["queue_name"],
|
|
466
539
|
registers: [registry]
|
|
467
540
|
});
|
|
541
|
+
var suppressedClusters = new Gauge({
|
|
542
|
+
name: "junando_suppressed_clusters",
|
|
543
|
+
help: "Current number of clusters suppressed by the rule engine",
|
|
544
|
+
labelNames: ["rule_id"],
|
|
545
|
+
registers: [registry]
|
|
546
|
+
});
|
|
468
547
|
|
|
469
548
|
// src/application/use-cases/process-incident.use-case.ts
|
|
470
549
|
var ProcessIncidentUseCase = class {
|
|
@@ -475,7 +554,7 @@ var ProcessIncidentUseCase = class {
|
|
|
475
554
|
deps;
|
|
476
555
|
clustering;
|
|
477
556
|
async execute(alerts, correlationId) {
|
|
478
|
-
const { dedup, traces, llm, notifier, logger: logger5, dedupTtlSeconds } = this.deps;
|
|
557
|
+
const { dedup, traces, llm, notifier, logger: logger5, dedupTtlSeconds, ruleEngine } = this.deps;
|
|
479
558
|
const log = logger5.child({ correlationId, useCase: "ProcessIncident" });
|
|
480
559
|
log.info({ alertCount: alerts.length }, "Processing alert batch");
|
|
481
560
|
const clusters = this.clustering.buildClusters(alerts);
|
|
@@ -490,6 +569,26 @@ var ProcessIncidentUseCase = class {
|
|
|
490
569
|
continue;
|
|
491
570
|
}
|
|
492
571
|
dedupNew.inc({ source: "alertmanager" });
|
|
572
|
+
let preLlmRouteChannels = [];
|
|
573
|
+
let preLlmEscalateChannels = [];
|
|
574
|
+
if (ruleEngine) {
|
|
575
|
+
const preResult = ruleEngine.evaluatePreLlm(cluster);
|
|
576
|
+
if (preResult.suppressed) {
|
|
577
|
+
log2.info({ matchedRuleId: preResult.matchedRuleId }, "Cluster suppressed by rule engine");
|
|
578
|
+
if (preResult.matchedRuleId) {
|
|
579
|
+
suppressedClusters.inc({ rule_id: preResult.matchedRuleId });
|
|
580
|
+
}
|
|
581
|
+
continue;
|
|
582
|
+
}
|
|
583
|
+
for (const action of preResult.actions) {
|
|
584
|
+
if (action.type === "route" && "channel" in action) {
|
|
585
|
+
preLlmRouteChannels.push(action.channel);
|
|
586
|
+
}
|
|
587
|
+
if (action.type === "escalate" && "channel" in action) {
|
|
588
|
+
preLlmEscalateChannels.push(action.channel);
|
|
589
|
+
}
|
|
590
|
+
}
|
|
591
|
+
}
|
|
493
592
|
const spanLists = await Promise.all(
|
|
494
593
|
cluster.representativeTraceIds.map(
|
|
495
594
|
(id) => traces.findByTraceId(id).catch((err) => {
|
|
@@ -507,8 +606,31 @@ var ProcessIncidentUseCase = class {
|
|
|
507
606
|
} catch (err) {
|
|
508
607
|
log2.warn({ err }, "LLM inference failed \u2014 notifying without diagnosis");
|
|
509
608
|
}
|
|
609
|
+
let postLlmEscalateChannels = [];
|
|
610
|
+
if (ruleEngine && analysis) {
|
|
611
|
+
const postResult = ruleEngine.evaluatePostLlm(cluster, analysis);
|
|
612
|
+
for (const action of postResult.actions) {
|
|
613
|
+
if (action.type === "escalate" && "channel" in action) {
|
|
614
|
+
postLlmEscalateChannels.push(action.channel);
|
|
615
|
+
}
|
|
616
|
+
if (action.type === "tag" && "key" in action) {
|
|
617
|
+
log2.info({ tagKey: action.key, tagValue: action.value }, "Tag attached to cluster");
|
|
618
|
+
}
|
|
619
|
+
}
|
|
620
|
+
if (postResult.tags && Object.keys(postResult.tags).length > 0) {
|
|
621
|
+
cluster.labels = { ...cluster.labels, ...postResult.tags };
|
|
622
|
+
}
|
|
623
|
+
}
|
|
510
624
|
try {
|
|
511
|
-
|
|
625
|
+
const primaryChannel = preLlmRouteChannels[0];
|
|
626
|
+
const escalateChannels = [
|
|
627
|
+
...preLlmEscalateChannels,
|
|
628
|
+
...postLlmEscalateChannels
|
|
629
|
+
];
|
|
630
|
+
await notifier.send(cluster, analysis, primaryChannel);
|
|
631
|
+
for (const channel of escalateChannels) {
|
|
632
|
+
await notifier.send(cluster, analysis, channel);
|
|
633
|
+
}
|
|
512
634
|
log2.info("Notification sent");
|
|
513
635
|
} catch (err) {
|
|
514
636
|
log2.error({ err }, "Notification failed");
|
|
@@ -727,24 +849,24 @@ var InMemoryIndexer = class {
|
|
|
727
849
|
|
|
728
850
|
// src/infrastructure/llm/llm.adapter.ts
|
|
729
851
|
import * as Breaker from "opossum";
|
|
730
|
-
import { z as
|
|
852
|
+
import { z as z5 } from "zod";
|
|
731
853
|
var logger2 = createLogger();
|
|
732
|
-
var OpenRouterResponseSchema =
|
|
733
|
-
id:
|
|
734
|
-
choices:
|
|
735
|
-
|
|
736
|
-
index:
|
|
737
|
-
message:
|
|
738
|
-
role:
|
|
739
|
-
content:
|
|
854
|
+
var OpenRouterResponseSchema = z5.object({
|
|
855
|
+
id: z5.string().optional(),
|
|
856
|
+
choices: z5.array(
|
|
857
|
+
z5.object({
|
|
858
|
+
index: z5.number(),
|
|
859
|
+
message: z5.object({
|
|
860
|
+
role: z5.string(),
|
|
861
|
+
content: z5.string().optional()
|
|
740
862
|
}),
|
|
741
|
-
finish_reason:
|
|
863
|
+
finish_reason: z5.string().optional()
|
|
742
864
|
})
|
|
743
865
|
),
|
|
744
|
-
usage:
|
|
745
|
-
prompt_tokens:
|
|
746
|
-
completion_tokens:
|
|
747
|
-
total_tokens:
|
|
866
|
+
usage: z5.object({
|
|
867
|
+
prompt_tokens: z5.number().optional(),
|
|
868
|
+
completion_tokens: z5.number().optional(),
|
|
869
|
+
total_tokens: z5.number().optional()
|
|
748
870
|
}).optional()
|
|
749
871
|
});
|
|
750
872
|
var SYSTEM_PROMPT = `You are a senior Site Reliability Engineer.
|
|
@@ -1005,6 +1127,9 @@ function createLLMProvider(provider, apiKey, model, options) {
|
|
|
1005
1127
|
return factory(apiKey, model, options);
|
|
1006
1128
|
}
|
|
1007
1129
|
|
|
1130
|
+
// src/infrastructure/notifier/factory.ts
|
|
1131
|
+
import { readFileSync } from "fs";
|
|
1132
|
+
|
|
1008
1133
|
// src/shared/factory-registry.ts
|
|
1009
1134
|
var FactoryRegistry = class {
|
|
1010
1135
|
_factories = /* @__PURE__ */ new Map();
|
|
@@ -1062,7 +1187,7 @@ var SlackNotifier = class {
|
|
|
1062
1187
|
}
|
|
1063
1188
|
botToken;
|
|
1064
1189
|
channel;
|
|
1065
|
-
async send(cluster, analysis) {
|
|
1190
|
+
async send(cluster, analysis, _channel) {
|
|
1066
1191
|
const payload = analysis ? this.buildAnalysisMessage(cluster, analysis) : this.buildFallbackMessage(cluster);
|
|
1067
1192
|
try {
|
|
1068
1193
|
const res = await fetch(SLACK_API_URL, {
|
|
@@ -1184,7 +1309,7 @@ LLM analysis unavailable \u2014 manual investigation required.`
|
|
|
1184
1309
|
};
|
|
1185
1310
|
var ConsoleNotifier = class {
|
|
1186
1311
|
sent = [];
|
|
1187
|
-
async send(cluster, analysis) {
|
|
1312
|
+
async send(cluster, analysis, _channel) {
|
|
1188
1313
|
try {
|
|
1189
1314
|
this.sent.push({ cluster, analysis });
|
|
1190
1315
|
logger3.info(
|
|
@@ -1341,7 +1466,7 @@ var TeamsNotifier = class {
|
|
|
1341
1466
|
// If parsing happens inside the catch block and throws, the original error
|
|
1342
1467
|
// context (timeout, network failure, etc.) would be lost.
|
|
1343
1468
|
hostForErrors;
|
|
1344
|
-
async send(cluster, analysis) {
|
|
1469
|
+
async send(cluster, analysis, _channel) {
|
|
1345
1470
|
const card = analysis ? buildAnalysisCard(cluster, analysis) : buildFallbackCard(cluster);
|
|
1346
1471
|
const payload = buildAdaptiveCardPayload(card);
|
|
1347
1472
|
const controller = new AbortController();
|
|
@@ -1379,6 +1504,320 @@ var TeamsNotifier = class {
|
|
|
1379
1504
|
}
|
|
1380
1505
|
};
|
|
1381
1506
|
|
|
1507
|
+
// src/infrastructure/notifier/routing-notifier.ts
|
|
1508
|
+
var ACTION_DISPATCH = {
|
|
1509
|
+
["suppress" /* Suppress */]: async (_action, ctx) => {
|
|
1510
|
+
ctx.suppressed = true;
|
|
1511
|
+
},
|
|
1512
|
+
["route" /* Route */]: async (action, ctx) => {
|
|
1513
|
+
const routeAction = action;
|
|
1514
|
+
ctx.routeChannels.add(routeAction.channel);
|
|
1515
|
+
},
|
|
1516
|
+
["escalate" /* Escalate */]: async (action, ctx) => {
|
|
1517
|
+
const escalateAction = action;
|
|
1518
|
+
ctx.escalationChannels.add(escalateAction.channel);
|
|
1519
|
+
},
|
|
1520
|
+
["tag" /* Tag */]: async (_action, _ctx) => {
|
|
1521
|
+
}
|
|
1522
|
+
};
|
|
1523
|
+
var RoutingNotifier = class {
|
|
1524
|
+
constructor(registry2, defaultNotifier) {
|
|
1525
|
+
this.registry = registry2;
|
|
1526
|
+
this.defaultNotifier = defaultNotifier;
|
|
1527
|
+
}
|
|
1528
|
+
registry;
|
|
1529
|
+
defaultNotifier;
|
|
1530
|
+
/**
|
|
1531
|
+
* Implements INotifier.send — sends via default notifier.
|
|
1532
|
+
* Backward-compatible with existing call sites that don't use rule actions.
|
|
1533
|
+
*/
|
|
1534
|
+
async send(cluster, analysis) {
|
|
1535
|
+
await this.defaultNotifier.send(cluster, analysis);
|
|
1536
|
+
}
|
|
1537
|
+
/**
|
|
1538
|
+
* Dispatch notifications based on rule engine actions.
|
|
1539
|
+
*
|
|
1540
|
+
* - Route actions: send to the specified channel instead of default.
|
|
1541
|
+
* - Escalate actions: send additional notifications to escalation channels.
|
|
1542
|
+
* - Tag actions: metadata-only, no notification side effect.
|
|
1543
|
+
* - Suppress actions: skip all notification (defensive — caller should have already skipped).
|
|
1544
|
+
* - Unknown channels: fall back to default notifier.
|
|
1545
|
+
* - Empty actions or no Route/Escalate: send via default notifier.
|
|
1546
|
+
*/
|
|
1547
|
+
async sendWithActions(cluster, analysis, actions) {
|
|
1548
|
+
const ctx = {
|
|
1549
|
+
cluster,
|
|
1550
|
+
analysis,
|
|
1551
|
+
registry: this.registry,
|
|
1552
|
+
defaultNotifier: this.defaultNotifier,
|
|
1553
|
+
routeChannels: /* @__PURE__ */ new Set(),
|
|
1554
|
+
escalationChannels: /* @__PURE__ */ new Set(),
|
|
1555
|
+
suppressed: false
|
|
1556
|
+
};
|
|
1557
|
+
for (const action of actions) {
|
|
1558
|
+
const handler = ACTION_DISPATCH[action.type];
|
|
1559
|
+
if (handler) {
|
|
1560
|
+
await handler(action, ctx);
|
|
1561
|
+
}
|
|
1562
|
+
}
|
|
1563
|
+
if (ctx.suppressed) {
|
|
1564
|
+
return;
|
|
1565
|
+
}
|
|
1566
|
+
const notifications = [];
|
|
1567
|
+
const hasRoute = ctx.routeChannels.size > 0;
|
|
1568
|
+
if (hasRoute) {
|
|
1569
|
+
for (const channel of ctx.routeChannels) {
|
|
1570
|
+
notifications.push(this.tryResolveAndSend(channel, cluster, analysis));
|
|
1571
|
+
}
|
|
1572
|
+
} else {
|
|
1573
|
+
notifications.push(this.defaultNotifier.send(cluster, analysis));
|
|
1574
|
+
}
|
|
1575
|
+
for (const channel of ctx.escalationChannels) {
|
|
1576
|
+
notifications.push(this.tryResolveAndSend(channel, cluster, analysis));
|
|
1577
|
+
}
|
|
1578
|
+
await Promise.all(notifications);
|
|
1579
|
+
}
|
|
1580
|
+
// ── Private helpers ──────────────────────────────────────────────────────
|
|
1581
|
+
/**
|
|
1582
|
+
* Resolve a channel name to its notifier and send.
|
|
1583
|
+
* Falls back to default notifier if channel is unknown.
|
|
1584
|
+
*/
|
|
1585
|
+
async tryResolveAndSend(channel, cluster, analysis) {
|
|
1586
|
+
try {
|
|
1587
|
+
const notifier = this.registry.resolve(channel);
|
|
1588
|
+
await notifier.send(cluster, analysis);
|
|
1589
|
+
} catch {
|
|
1590
|
+
await this.defaultNotifier.send(cluster, analysis);
|
|
1591
|
+
}
|
|
1592
|
+
}
|
|
1593
|
+
};
|
|
1594
|
+
|
|
1595
|
+
// src/infrastructure/rules/yaml-rule-loader.ts
|
|
1596
|
+
import { parse as parseYaml, YAMLParseError } from "yaml";
|
|
1597
|
+
function parseRuleConfig(yamlString) {
|
|
1598
|
+
let raw;
|
|
1599
|
+
try {
|
|
1600
|
+
raw = parseYaml(yamlString);
|
|
1601
|
+
} catch (err) {
|
|
1602
|
+
if (err instanceof YAMLParseError) {
|
|
1603
|
+
throw new Error(`Invalid YAML in rules config: ${err.message}`);
|
|
1604
|
+
}
|
|
1605
|
+
throw err;
|
|
1606
|
+
}
|
|
1607
|
+
const result = RuleConfigurationSchema.safeParse(raw);
|
|
1608
|
+
if (!result.success) {
|
|
1609
|
+
const issues = result.error.issues.map((i3) => ` - ${i3.path.join(".")}: ${i3.message}`).join("\n");
|
|
1610
|
+
throw new Error(`Invalid rules configuration:
|
|
1611
|
+
${issues}`);
|
|
1612
|
+
}
|
|
1613
|
+
return result.data;
|
|
1614
|
+
}
|
|
1615
|
+
|
|
1616
|
+
// src/infrastructure/rules/channel-registry.ts
|
|
1617
|
+
var ChannelRegistry = class {
|
|
1618
|
+
_channels = /* @__PURE__ */ new Map();
|
|
1619
|
+
_default = null;
|
|
1620
|
+
/**
|
|
1621
|
+
* Register a notifier for a given channel name.
|
|
1622
|
+
* Overwrites any existing registration for that name.
|
|
1623
|
+
*/
|
|
1624
|
+
register(channel, notifier) {
|
|
1625
|
+
this._channels.set(channel, notifier);
|
|
1626
|
+
}
|
|
1627
|
+
/**
|
|
1628
|
+
* Set the default notifier to use when a channel is not found.
|
|
1629
|
+
*/
|
|
1630
|
+
setDefault(notifier) {
|
|
1631
|
+
this._default = notifier;
|
|
1632
|
+
}
|
|
1633
|
+
/**
|
|
1634
|
+
* Resolve a channel name to its notifier instance.
|
|
1635
|
+
* Falls back to the default notifier if the channel is unknown.
|
|
1636
|
+
*
|
|
1637
|
+
* @throws {Error} if the channel is unknown and no default is set
|
|
1638
|
+
*/
|
|
1639
|
+
resolve(channel) {
|
|
1640
|
+
const instance = this._channels.get(channel);
|
|
1641
|
+
if (instance) {
|
|
1642
|
+
return instance;
|
|
1643
|
+
}
|
|
1644
|
+
if (this._default) {
|
|
1645
|
+
return this._default;
|
|
1646
|
+
}
|
|
1647
|
+
throw new Error(
|
|
1648
|
+
`Unknown channel "${channel}" and no default notifier configured`
|
|
1649
|
+
);
|
|
1650
|
+
}
|
|
1651
|
+
/**
|
|
1652
|
+
* Check if a channel is registered.
|
|
1653
|
+
*/
|
|
1654
|
+
has(channel) {
|
|
1655
|
+
return this._channels.has(channel);
|
|
1656
|
+
}
|
|
1657
|
+
};
|
|
1658
|
+
|
|
1659
|
+
// src/infrastructure/rules/condition-evaluator.ts
|
|
1660
|
+
var MATCHER_MAP = {
|
|
1661
|
+
serviceName: (value) => {
|
|
1662
|
+
const target = value.toLowerCase();
|
|
1663
|
+
return (cluster) => cluster.serviceName.toLowerCase() === target;
|
|
1664
|
+
},
|
|
1665
|
+
alertType: (value) => {
|
|
1666
|
+
return (cluster) => cluster.alertType === value;
|
|
1667
|
+
},
|
|
1668
|
+
severity: (value) => {
|
|
1669
|
+
return (cluster) => {
|
|
1670
|
+
const config = ALERT_TYPE_LABELS[cluster.alertType];
|
|
1671
|
+
return config?.severity === value;
|
|
1672
|
+
};
|
|
1673
|
+
},
|
|
1674
|
+
endpointPath: (value) => {
|
|
1675
|
+
return (cluster) => cluster.endpointPath === value;
|
|
1676
|
+
},
|
|
1677
|
+
alertCount: (value) => {
|
|
1678
|
+
const range = value;
|
|
1679
|
+
return (cluster) => {
|
|
1680
|
+
const count = cluster.alertCount;
|
|
1681
|
+
if (range.min !== void 0 && count < range.min) return false;
|
|
1682
|
+
if (range.max !== void 0 && count > range.max) return false;
|
|
1683
|
+
return true;
|
|
1684
|
+
};
|
|
1685
|
+
},
|
|
1686
|
+
latencyP99Ms: (value) => {
|
|
1687
|
+
const range = value;
|
|
1688
|
+
return (cluster) => {
|
|
1689
|
+
const latency2 = cluster.latencyP99Ms;
|
|
1690
|
+
if (latency2 === void 0) return false;
|
|
1691
|
+
if (range.min !== void 0 && latency2 < range.min) return false;
|
|
1692
|
+
if (range.max !== void 0 && latency2 > range.max) return false;
|
|
1693
|
+
return true;
|
|
1694
|
+
};
|
|
1695
|
+
},
|
|
1696
|
+
labels: (value) => {
|
|
1697
|
+
const expected = value;
|
|
1698
|
+
return (cluster) => {
|
|
1699
|
+
const clusterLabels = cluster.labels;
|
|
1700
|
+
if (!clusterLabels) return false;
|
|
1701
|
+
return Object.entries(expected).every(
|
|
1702
|
+
([key, val]) => clusterLabels[key] === val
|
|
1703
|
+
);
|
|
1704
|
+
};
|
|
1705
|
+
},
|
|
1706
|
+
urgencyLevel: (value) => {
|
|
1707
|
+
return (_cluster, analysis) => {
|
|
1708
|
+
if (!analysis) return false;
|
|
1709
|
+
return analysis.urgency_level === value;
|
|
1710
|
+
};
|
|
1711
|
+
},
|
|
1712
|
+
requiresRollback: (value) => {
|
|
1713
|
+
return (_cluster, analysis) => {
|
|
1714
|
+
if (!analysis) return false;
|
|
1715
|
+
return analysis.requires_rollback === value;
|
|
1716
|
+
};
|
|
1717
|
+
},
|
|
1718
|
+
impactedServices: (value) => {
|
|
1719
|
+
const targets = value;
|
|
1720
|
+
return (_cluster, analysis) => {
|
|
1721
|
+
if (!analysis) return false;
|
|
1722
|
+
return targets.some((t) => analysis.impacted_services.includes(t));
|
|
1723
|
+
};
|
|
1724
|
+
}
|
|
1725
|
+
};
|
|
1726
|
+
function compileCondition(condition) {
|
|
1727
|
+
const predicates = [];
|
|
1728
|
+
for (const [field, value] of Object.entries(condition)) {
|
|
1729
|
+
if (value === void 0) continue;
|
|
1730
|
+
const factory = MATCHER_MAP[field];
|
|
1731
|
+
if (factory) {
|
|
1732
|
+
predicates.push(factory(value));
|
|
1733
|
+
}
|
|
1734
|
+
}
|
|
1735
|
+
if (predicates.length === 0) {
|
|
1736
|
+
return () => true;
|
|
1737
|
+
}
|
|
1738
|
+
return (cluster, analysis) => predicates.every((p) => p(cluster, analysis));
|
|
1739
|
+
}
|
|
1740
|
+
|
|
1741
|
+
// src/infrastructure/rules/action-dispatcher.ts
|
|
1742
|
+
var HANDLER_MAP = {
|
|
1743
|
+
["suppress" /* Suppress */]: (_action, result) => {
|
|
1744
|
+
result.suppressed = true;
|
|
1745
|
+
},
|
|
1746
|
+
["route" /* Route */]: (action, result) => {
|
|
1747
|
+
result.actions.push(action);
|
|
1748
|
+
},
|
|
1749
|
+
["escalate" /* Escalate */]: (action, result) => {
|
|
1750
|
+
result.actions.push(action);
|
|
1751
|
+
},
|
|
1752
|
+
["tag" /* Tag */]: (action, result) => {
|
|
1753
|
+
const tagAction = action;
|
|
1754
|
+
result.tags[tagAction.key] = tagAction.value;
|
|
1755
|
+
}
|
|
1756
|
+
};
|
|
1757
|
+
function dispatchActions(actions) {
|
|
1758
|
+
const result = {
|
|
1759
|
+
suppressed: false,
|
|
1760
|
+
actions: [],
|
|
1761
|
+
tags: {}
|
|
1762
|
+
};
|
|
1763
|
+
for (const action of actions) {
|
|
1764
|
+
const handler = HANDLER_MAP[action.type];
|
|
1765
|
+
if (handler) {
|
|
1766
|
+
handler(action, result);
|
|
1767
|
+
}
|
|
1768
|
+
}
|
|
1769
|
+
return result;
|
|
1770
|
+
}
|
|
1771
|
+
|
|
1772
|
+
// src/infrastructure/rules/rule-engine.ts
|
|
1773
|
+
var RuleEngine = class {
|
|
1774
|
+
preLlmRules;
|
|
1775
|
+
postLlmRules;
|
|
1776
|
+
constructor(config) {
|
|
1777
|
+
this.preLlmRules = this.compileSection(config["pre-llm" /* PreLlm */].rules);
|
|
1778
|
+
this.postLlmRules = this.compileSection(config["post-llm" /* PostLlm */].rules);
|
|
1779
|
+
}
|
|
1780
|
+
/**
|
|
1781
|
+
* Evaluate PRE-LLM rules against a cluster.
|
|
1782
|
+
* First-match-wins — returns result of first matching rule.
|
|
1783
|
+
* If no rule matches, returns pass-through (suppressed=false, no actions).
|
|
1784
|
+
*/
|
|
1785
|
+
evaluatePreLlm(cluster) {
|
|
1786
|
+
return this.evaluateRules(this.preLlmRules, cluster);
|
|
1787
|
+
}
|
|
1788
|
+
/**
|
|
1789
|
+
* Evaluate POST-LLM rules against a cluster and LLM analysis.
|
|
1790
|
+
* First-match-wins — returns result of first matching rule.
|
|
1791
|
+
* If no rule matches, returns pass-through.
|
|
1792
|
+
*/
|
|
1793
|
+
evaluatePostLlm(cluster, analysis) {
|
|
1794
|
+
return this.evaluateRules(this.postLlmRules, cluster, analysis);
|
|
1795
|
+
}
|
|
1796
|
+
// ── Private helpers ──────────────────────────────────────────────────────
|
|
1797
|
+
compileSection(rules) {
|
|
1798
|
+
return rules.map((rule) => ({
|
|
1799
|
+
id: rule.id,
|
|
1800
|
+
predicate: compileCondition(rule.condition),
|
|
1801
|
+
result: dispatchActions(rule.actions)
|
|
1802
|
+
}));
|
|
1803
|
+
}
|
|
1804
|
+
evaluateRules(rules, cluster, analysis) {
|
|
1805
|
+
for (const rule of rules) {
|
|
1806
|
+
if (rule.predicate(cluster, analysis)) {
|
|
1807
|
+
return {
|
|
1808
|
+
...rule.result,
|
|
1809
|
+
matchedRuleId: rule.id
|
|
1810
|
+
};
|
|
1811
|
+
}
|
|
1812
|
+
}
|
|
1813
|
+
return {
|
|
1814
|
+
suppressed: false,
|
|
1815
|
+
actions: [],
|
|
1816
|
+
tags: {}
|
|
1817
|
+
};
|
|
1818
|
+
}
|
|
1819
|
+
};
|
|
1820
|
+
|
|
1382
1821
|
// src/infrastructure/notifier/factory.ts
|
|
1383
1822
|
function buildNotifierRegistry(config) {
|
|
1384
1823
|
const registry2 = new FactoryRegistry();
|
|
@@ -1399,7 +1838,16 @@ function buildNotifierRegistry(config) {
|
|
|
1399
1838
|
}
|
|
1400
1839
|
function createNotifier(config) {
|
|
1401
1840
|
const registry2 = buildNotifierRegistry(config);
|
|
1402
|
-
|
|
1841
|
+
const defaultNotifier = registry2.resolve(config.notifierType);
|
|
1842
|
+
if (!config.rulesConfigPath) {
|
|
1843
|
+
console.info("[createNotifier] RULES_CONFIG_PATH not set \u2014 rule engine disabled, using default notifier");
|
|
1844
|
+
return defaultNotifier;
|
|
1845
|
+
}
|
|
1846
|
+
const yamlContent = readFileSync(config.rulesConfigPath, "utf-8");
|
|
1847
|
+
parseRuleConfig(yamlContent);
|
|
1848
|
+
const channelRegistry = new ChannelRegistry();
|
|
1849
|
+
channelRegistry.setDefault(defaultNotifier);
|
|
1850
|
+
return new RoutingNotifier(channelRegistry, defaultNotifier);
|
|
1403
1851
|
}
|
|
1404
1852
|
|
|
1405
1853
|
// ../../node_modules/.pnpm/@smithy+smithy-client@4.12.13/node_modules/@smithy/smithy-client/dist-es/NoOpLogger.js
|
|
@@ -9799,7 +10247,7 @@ var GetParametersCommand = class extends Command.classBuilder().ep(commonParams2
|
|
|
9799
10247
|
};
|
|
9800
10248
|
|
|
9801
10249
|
// src/shared/config/index.ts
|
|
9802
|
-
import { z as
|
|
10250
|
+
import { z as z6 } from "zod";
|
|
9803
10251
|
async function loadSecretsFromSSM() {
|
|
9804
10252
|
const prefix = process.env.SSM_PREFIX;
|
|
9805
10253
|
if (!prefix) {
|
|
@@ -9835,51 +10283,53 @@ async function loadSecretsFromSSM() {
|
|
|
9835
10283
|
createLogger().error({ err }, "Failed to load SSM parameters");
|
|
9836
10284
|
}
|
|
9837
10285
|
}
|
|
9838
|
-
var ConfigSchema =
|
|
9839
|
-
llmProvider:
|
|
9840
|
-
llmApiKey:
|
|
9841
|
-
llmModel:
|
|
10286
|
+
var ConfigSchema = z6.object({
|
|
10287
|
+
llmProvider: z6.enum(["gemini", "claude", "openrouter", "qwen"]),
|
|
10288
|
+
llmApiKey: z6.string().min(1),
|
|
10289
|
+
llmModel: z6.string().optional().transform((v) => v === "" ? void 0 : v),
|
|
9842
10290
|
// Notifier selector — defaults to 'slack' for backward compatibility
|
|
9843
|
-
notifierType:
|
|
10291
|
+
notifierType: z6.enum(["slack", "teams"]).default("slack"),
|
|
9844
10292
|
// Slack fields — optional at schema level; superRefine enforces them conditionally
|
|
9845
|
-
slackBotToken:
|
|
9846
|
-
slackSigningSecret:
|
|
9847
|
-
slackChannel:
|
|
10293
|
+
slackBotToken: z6.string().startsWith("xoxb-").optional(),
|
|
10294
|
+
slackSigningSecret: z6.string().min(1).optional(),
|
|
10295
|
+
slackChannel: z6.string().startsWith("#").optional(),
|
|
9848
10296
|
// Teams field
|
|
9849
|
-
teamsWebhookUrl:
|
|
9850
|
-
lokiUrl:
|
|
10297
|
+
teamsWebhookUrl: z6.string().url().optional(),
|
|
10298
|
+
lokiUrl: z6.string().optional().transform((v) => v === "" ? void 0 : v),
|
|
9851
10299
|
// URL with embedded credentials — skip .url() which rejects user:pass@ format. Optional: containers may run without Loki; logger falls back to stdout. Empty string is coerced to undefined (env var unset vs empty are equivalent).
|
|
9852
|
-
redisUrl:
|
|
9853
|
-
sqsQueueUrl:
|
|
9854
|
-
dedupTtlSeconds:
|
|
9855
|
-
clusterWindowMs:
|
|
9856
|
-
logLevel:
|
|
9857
|
-
nodeEnv:
|
|
9858
|
-
llmFallbackModels:
|
|
10300
|
+
redisUrl: z6.string().url(),
|
|
10301
|
+
sqsQueueUrl: z6.string().url().optional().or(z6.literal("")),
|
|
10302
|
+
dedupTtlSeconds: z6.coerce.number().int().positive().default(300),
|
|
10303
|
+
clusterWindowMs: z6.coerce.number().int().positive().default(12e4),
|
|
10304
|
+
logLevel: z6.enum(["trace", "debug", "info", "warn", "error"]).default("info"),
|
|
10305
|
+
nodeEnv: z6.enum(["development", "test", "production"]).default("development"),
|
|
10306
|
+
llmFallbackModels: z6.string().optional().transform((v) => {
|
|
9859
10307
|
if (v === void 0) return LLM_FALLBACK_DEFAULTS.Models;
|
|
9860
10308
|
if (!v) return [];
|
|
9861
10309
|
return v.split(",").map((s) => s.trim()).filter(Boolean);
|
|
9862
10310
|
}),
|
|
9863
|
-
llmFallbackTimeoutMs:
|
|
10311
|
+
llmFallbackTimeoutMs: z6.coerce.number().int().positive().default(LLM_FALLBACK_DEFAULTS.TimeoutMs),
|
|
10312
|
+
// Optional path to rules.yaml for business rules engine. When not set, rule engine is disabled.
|
|
10313
|
+
rulesConfigPath: z6.string().optional().transform((v) => v === "" ? void 0 : v)
|
|
9864
10314
|
}).superRefine((data, ctx) => {
|
|
9865
10315
|
if (data.notifierType === "slack") {
|
|
9866
10316
|
if (!data.slackBotToken) {
|
|
9867
10317
|
ctx.addIssue({
|
|
9868
|
-
code:
|
|
10318
|
+
code: z6.ZodIssueCode.custom,
|
|
9869
10319
|
path: ["slackBotToken"],
|
|
9870
10320
|
message: "[notifierType: slack] SLACK_BOT_TOKEN is required and must start with xoxb-"
|
|
9871
10321
|
});
|
|
9872
10322
|
}
|
|
9873
10323
|
if (!data.slackChannel) {
|
|
9874
10324
|
ctx.addIssue({
|
|
9875
|
-
code:
|
|
10325
|
+
code: z6.ZodIssueCode.custom,
|
|
9876
10326
|
path: ["slackChannel"],
|
|
9877
10327
|
message: "[notifierType: slack] SLACK_CHANNEL is required and must start with #"
|
|
9878
10328
|
});
|
|
9879
10329
|
}
|
|
9880
10330
|
if (!data.slackSigningSecret) {
|
|
9881
10331
|
ctx.addIssue({
|
|
9882
|
-
code:
|
|
10332
|
+
code: z6.ZodIssueCode.custom,
|
|
9883
10333
|
path: ["slackSigningSecret"],
|
|
9884
10334
|
message: "[notifierType: slack] SLACK_SIGNING_SECRET is required (used to verify Slack interactivity callbacks)"
|
|
9885
10335
|
});
|
|
@@ -9888,7 +10338,7 @@ var ConfigSchema = z5.object({
|
|
|
9888
10338
|
if (data.notifierType === "teams") {
|
|
9889
10339
|
if (!data.teamsWebhookUrl) {
|
|
9890
10340
|
ctx.addIssue({
|
|
9891
|
-
code:
|
|
10341
|
+
code: z6.ZodIssueCode.custom,
|
|
9892
10342
|
path: ["teamsWebhookUrl"],
|
|
9893
10343
|
message: "[notifierType: teams] TEAMS_WEBHOOK_URL is required"
|
|
9894
10344
|
});
|
|
@@ -9898,14 +10348,14 @@ var ConfigSchema = z5.object({
|
|
|
9898
10348
|
parsed = new URL(data.teamsWebhookUrl);
|
|
9899
10349
|
} catch {
|
|
9900
10350
|
ctx.addIssue({
|
|
9901
|
-
code:
|
|
10351
|
+
code: z6.ZodIssueCode.custom,
|
|
9902
10352
|
path: ["teamsWebhookUrl"],
|
|
9903
10353
|
message: "[notifierType: teams] TEAMS_WEBHOOK_URL must be a valid URL"
|
|
9904
10354
|
});
|
|
9905
10355
|
}
|
|
9906
10356
|
if (parsed && !parsed.searchParams.has("api-version")) {
|
|
9907
10357
|
ctx.addIssue({
|
|
9908
|
-
code:
|
|
10358
|
+
code: z6.ZodIssueCode.custom,
|
|
9909
10359
|
path: ["teamsWebhookUrl"],
|
|
9910
10360
|
message: "[notifierType: teams] TEAMS_WEBHOOK_URL must include api-version= as a query parameter"
|
|
9911
10361
|
});
|
|
@@ -9932,7 +10382,8 @@ async function loadConfig2() {
|
|
|
9932
10382
|
logLevel: process.env["LOG_LEVEL"],
|
|
9933
10383
|
nodeEnv: process.env["NODE_ENV"],
|
|
9934
10384
|
llmFallbackModels: process.env["LLM_FALLBACK_MODELS"],
|
|
9935
|
-
llmFallbackTimeoutMs: process.env["LLM_FALLBACK_TIMEOUT_MS"]
|
|
10385
|
+
llmFallbackTimeoutMs: process.env["LLM_FALLBACK_TIMEOUT_MS"],
|
|
10386
|
+
rulesConfigPath: process.env["RULES_CONFIG_PATH"]
|
|
9936
10387
|
});
|
|
9937
10388
|
if (!result.success) {
|
|
9938
10389
|
const errorMessages = result.error.issues.map(
|
|
@@ -9950,6 +10401,7 @@ export {
|
|
|
9950
10401
|
AlertType,
|
|
9951
10402
|
AlertmanagerPayloadSchema,
|
|
9952
10403
|
CIRCUIT_BREAKER,
|
|
10404
|
+
ChannelRegistry,
|
|
9953
10405
|
ClaudeProvider,
|
|
9954
10406
|
ClusteringService,
|
|
9955
10407
|
ConsoleNotifier,
|
|
@@ -9979,8 +10431,18 @@ export {
|
|
|
9979
10431
|
RATE_LIMITER,
|
|
9980
10432
|
REDIS_KEY_PREFIX,
|
|
9981
10433
|
RedisDeduplicationStore,
|
|
10434
|
+
RoutingNotifier,
|
|
10435
|
+
RuleActionSchema,
|
|
10436
|
+
RuleActionType,
|
|
10437
|
+
RuleConditionSchema,
|
|
10438
|
+
RuleConfigurationSchema,
|
|
10439
|
+
RuleEngine,
|
|
10440
|
+
RuleEvaluationPhase,
|
|
10441
|
+
RuleSchema,
|
|
10442
|
+
RuleSectionSchema,
|
|
9982
10443
|
SLACK_API_URL,
|
|
9983
10444
|
SQSAlertQueue,
|
|
10445
|
+
SeverityLevel,
|
|
9984
10446
|
SlackNotifier,
|
|
9985
10447
|
TEAMS_WEBHOOK_TIMEOUT_MS,
|
|
9986
10448
|
TeamsNotifier,
|
|
@@ -9988,13 +10450,16 @@ export {
|
|
|
9988
10450
|
URGENCY_EMOJI,
|
|
9989
10451
|
UrgencyLevelSchema,
|
|
9990
10452
|
WEBHOOK_DEFAULTS,
|
|
10453
|
+
compileCondition,
|
|
9991
10454
|
createLLMProvider,
|
|
9992
10455
|
createLogger,
|
|
9993
10456
|
createNotifier,
|
|
10457
|
+
dispatchActions,
|
|
9994
10458
|
flushLoki,
|
|
9995
10459
|
loadConfig2 as loadConfig,
|
|
9996
10460
|
metrics_exports as metrics,
|
|
9997
10461
|
normalizePayload,
|
|
10462
|
+
parseRuleConfig,
|
|
9998
10463
|
reinitLogger,
|
|
9999
10464
|
startSqsLagPoller
|
|
10000
10465
|
};
|