@junando/core 0.10.0 → 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 +1483 -5
- package/dist/index.js +575 -53
- 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,53 @@ 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
|
+
|
|
1133
|
+
// src/shared/factory-registry.ts
|
|
1134
|
+
var FactoryRegistry = class {
|
|
1135
|
+
_factories = /* @__PURE__ */ new Map();
|
|
1136
|
+
_default = () => {
|
|
1137
|
+
throw new Error(`No factory registered and no default available`);
|
|
1138
|
+
};
|
|
1139
|
+
/**
|
|
1140
|
+
* Register a factory for a given key.
|
|
1141
|
+
* Overwrites any existing registration for that key.
|
|
1142
|
+
*/
|
|
1143
|
+
register(key, factory) {
|
|
1144
|
+
this._factories.set(key, factory);
|
|
1145
|
+
}
|
|
1146
|
+
/**
|
|
1147
|
+
* Set the default factory to use when no key matches.
|
|
1148
|
+
*/
|
|
1149
|
+
registerDefault(factory) {
|
|
1150
|
+
this._default = factory;
|
|
1151
|
+
}
|
|
1152
|
+
/**
|
|
1153
|
+
* Resolve the factory for a given key.
|
|
1154
|
+
* Returns the default if no specific factory is registered for that key.
|
|
1155
|
+
*/
|
|
1156
|
+
resolve(key) {
|
|
1157
|
+
const factory = this._factories.get(key);
|
|
1158
|
+
if (factory) {
|
|
1159
|
+
return factory();
|
|
1160
|
+
}
|
|
1161
|
+
return this._default();
|
|
1162
|
+
}
|
|
1163
|
+
/**
|
|
1164
|
+
* Check if a factory is registered for a given key.
|
|
1165
|
+
*/
|
|
1166
|
+
has(key) {
|
|
1167
|
+
return this._factories.has(key);
|
|
1168
|
+
}
|
|
1169
|
+
/**
|
|
1170
|
+
* Return all registered keys.
|
|
1171
|
+
*/
|
|
1172
|
+
keys() {
|
|
1173
|
+
return Array.from(this._factories.keys());
|
|
1174
|
+
}
|
|
1175
|
+
};
|
|
1176
|
+
|
|
1008
1177
|
// src/infrastructure/notifier/slack.adapter.ts
|
|
1009
1178
|
var logger3 = createLogger();
|
|
1010
1179
|
function sanitizeEndpointPath(endpointPath) {
|
|
@@ -1018,7 +1187,7 @@ var SlackNotifier = class {
|
|
|
1018
1187
|
}
|
|
1019
1188
|
botToken;
|
|
1020
1189
|
channel;
|
|
1021
|
-
async send(cluster, analysis) {
|
|
1190
|
+
async send(cluster, analysis, _channel) {
|
|
1022
1191
|
const payload = analysis ? this.buildAnalysisMessage(cluster, analysis) : this.buildFallbackMessage(cluster);
|
|
1023
1192
|
try {
|
|
1024
1193
|
const res = await fetch(SLACK_API_URL, {
|
|
@@ -1140,7 +1309,7 @@ LLM analysis unavailable \u2014 manual investigation required.`
|
|
|
1140
1309
|
};
|
|
1141
1310
|
var ConsoleNotifier = class {
|
|
1142
1311
|
sent = [];
|
|
1143
|
-
async send(cluster, analysis) {
|
|
1312
|
+
async send(cluster, analysis, _channel) {
|
|
1144
1313
|
try {
|
|
1145
1314
|
this.sent.push({ cluster, analysis });
|
|
1146
1315
|
logger3.info(
|
|
@@ -1297,7 +1466,7 @@ var TeamsNotifier = class {
|
|
|
1297
1466
|
// If parsing happens inside the catch block and throws, the original error
|
|
1298
1467
|
// context (timeout, network failure, etc.) would be lost.
|
|
1299
1468
|
hostForErrors;
|
|
1300
|
-
async send(cluster, analysis) {
|
|
1469
|
+
async send(cluster, analysis, _channel) {
|
|
1301
1470
|
const card = analysis ? buildAnalysisCard(cluster, analysis) : buildFallbackCard(cluster);
|
|
1302
1471
|
const payload = buildAdaptiveCardPayload(card);
|
|
1303
1472
|
const controller = new AbortController();
|
|
@@ -1335,15 +1504,350 @@ var TeamsNotifier = class {
|
|
|
1335
1504
|
}
|
|
1336
1505
|
};
|
|
1337
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
|
+
|
|
1338
1821
|
// src/infrastructure/notifier/factory.ts
|
|
1822
|
+
function buildNotifierRegistry(config) {
|
|
1823
|
+
const registry2 = new FactoryRegistry();
|
|
1824
|
+
registry2.register("teams", () => {
|
|
1825
|
+
if (!config.teamsWebhookUrl) {
|
|
1826
|
+
throw new Error("NOTIFIER_TYPE=teams requires TEAMS_WEBHOOK_URL to be set");
|
|
1827
|
+
}
|
|
1828
|
+
return new TeamsNotifier(config.teamsWebhookUrl);
|
|
1829
|
+
});
|
|
1830
|
+
registry2.register("slack", () => {
|
|
1831
|
+
if (!config.slackBotToken || !config.slackChannel) {
|
|
1832
|
+
throw new Error("NOTIFIER_TYPE=slack requires SLACK_BOT_TOKEN and SLACK_CHANNEL to be set");
|
|
1833
|
+
}
|
|
1834
|
+
return new SlackNotifier(config.slackBotToken, config.slackChannel);
|
|
1835
|
+
});
|
|
1836
|
+
registry2.registerDefault(() => new SlackNotifier("dummy-token", "#alerts"));
|
|
1837
|
+
return registry2;
|
|
1838
|
+
}
|
|
1339
1839
|
function createNotifier(config) {
|
|
1340
|
-
|
|
1341
|
-
|
|
1342
|
-
|
|
1343
|
-
|
|
1344
|
-
|
|
1345
|
-
|
|
1346
|
-
|
|
1840
|
+
const registry2 = buildNotifierRegistry(config);
|
|
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);
|
|
1347
1851
|
}
|
|
1348
1852
|
|
|
1349
1853
|
// ../../node_modules/.pnpm/@smithy+smithy-client@4.12.13/node_modules/@smithy/smithy-client/dist-es/NoOpLogger.js
|
|
@@ -9743,7 +10247,7 @@ var GetParametersCommand = class extends Command.classBuilder().ep(commonParams2
|
|
|
9743
10247
|
};
|
|
9744
10248
|
|
|
9745
10249
|
// src/shared/config/index.ts
|
|
9746
|
-
import { z as
|
|
10250
|
+
import { z as z6 } from "zod";
|
|
9747
10251
|
async function loadSecretsFromSSM() {
|
|
9748
10252
|
const prefix = process.env.SSM_PREFIX;
|
|
9749
10253
|
if (!prefix) {
|
|
@@ -9779,51 +10283,53 @@ async function loadSecretsFromSSM() {
|
|
|
9779
10283
|
createLogger().error({ err }, "Failed to load SSM parameters");
|
|
9780
10284
|
}
|
|
9781
10285
|
}
|
|
9782
|
-
var ConfigSchema =
|
|
9783
|
-
llmProvider:
|
|
9784
|
-
llmApiKey:
|
|
9785
|
-
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),
|
|
9786
10290
|
// Notifier selector — defaults to 'slack' for backward compatibility
|
|
9787
|
-
notifierType:
|
|
10291
|
+
notifierType: z6.enum(["slack", "teams"]).default("slack"),
|
|
9788
10292
|
// Slack fields — optional at schema level; superRefine enforces them conditionally
|
|
9789
|
-
slackBotToken:
|
|
9790
|
-
slackSigningSecret:
|
|
9791
|
-
slackChannel:
|
|
10293
|
+
slackBotToken: z6.string().startsWith("xoxb-").optional(),
|
|
10294
|
+
slackSigningSecret: z6.string().min(1).optional(),
|
|
10295
|
+
slackChannel: z6.string().startsWith("#").optional(),
|
|
9792
10296
|
// Teams field
|
|
9793
|
-
teamsWebhookUrl:
|
|
9794
|
-
lokiUrl:
|
|
10297
|
+
teamsWebhookUrl: z6.string().url().optional(),
|
|
10298
|
+
lokiUrl: z6.string().optional().transform((v) => v === "" ? void 0 : v),
|
|
9795
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).
|
|
9796
|
-
redisUrl:
|
|
9797
|
-
sqsQueueUrl:
|
|
9798
|
-
dedupTtlSeconds:
|
|
9799
|
-
clusterWindowMs:
|
|
9800
|
-
logLevel:
|
|
9801
|
-
nodeEnv:
|
|
9802
|
-
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) => {
|
|
9803
10307
|
if (v === void 0) return LLM_FALLBACK_DEFAULTS.Models;
|
|
9804
10308
|
if (!v) return [];
|
|
9805
10309
|
return v.split(",").map((s) => s.trim()).filter(Boolean);
|
|
9806
10310
|
}),
|
|
9807
|
-
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)
|
|
9808
10314
|
}).superRefine((data, ctx) => {
|
|
9809
10315
|
if (data.notifierType === "slack") {
|
|
9810
10316
|
if (!data.slackBotToken) {
|
|
9811
10317
|
ctx.addIssue({
|
|
9812
|
-
code:
|
|
10318
|
+
code: z6.ZodIssueCode.custom,
|
|
9813
10319
|
path: ["slackBotToken"],
|
|
9814
10320
|
message: "[notifierType: slack] SLACK_BOT_TOKEN is required and must start with xoxb-"
|
|
9815
10321
|
});
|
|
9816
10322
|
}
|
|
9817
10323
|
if (!data.slackChannel) {
|
|
9818
10324
|
ctx.addIssue({
|
|
9819
|
-
code:
|
|
10325
|
+
code: z6.ZodIssueCode.custom,
|
|
9820
10326
|
path: ["slackChannel"],
|
|
9821
10327
|
message: "[notifierType: slack] SLACK_CHANNEL is required and must start with #"
|
|
9822
10328
|
});
|
|
9823
10329
|
}
|
|
9824
10330
|
if (!data.slackSigningSecret) {
|
|
9825
10331
|
ctx.addIssue({
|
|
9826
|
-
code:
|
|
10332
|
+
code: z6.ZodIssueCode.custom,
|
|
9827
10333
|
path: ["slackSigningSecret"],
|
|
9828
10334
|
message: "[notifierType: slack] SLACK_SIGNING_SECRET is required (used to verify Slack interactivity callbacks)"
|
|
9829
10335
|
});
|
|
@@ -9832,7 +10338,7 @@ var ConfigSchema = z5.object({
|
|
|
9832
10338
|
if (data.notifierType === "teams") {
|
|
9833
10339
|
if (!data.teamsWebhookUrl) {
|
|
9834
10340
|
ctx.addIssue({
|
|
9835
|
-
code:
|
|
10341
|
+
code: z6.ZodIssueCode.custom,
|
|
9836
10342
|
path: ["teamsWebhookUrl"],
|
|
9837
10343
|
message: "[notifierType: teams] TEAMS_WEBHOOK_URL is required"
|
|
9838
10344
|
});
|
|
@@ -9842,14 +10348,14 @@ var ConfigSchema = z5.object({
|
|
|
9842
10348
|
parsed = new URL(data.teamsWebhookUrl);
|
|
9843
10349
|
} catch {
|
|
9844
10350
|
ctx.addIssue({
|
|
9845
|
-
code:
|
|
10351
|
+
code: z6.ZodIssueCode.custom,
|
|
9846
10352
|
path: ["teamsWebhookUrl"],
|
|
9847
10353
|
message: "[notifierType: teams] TEAMS_WEBHOOK_URL must be a valid URL"
|
|
9848
10354
|
});
|
|
9849
10355
|
}
|
|
9850
10356
|
if (parsed && !parsed.searchParams.has("api-version")) {
|
|
9851
10357
|
ctx.addIssue({
|
|
9852
|
-
code:
|
|
10358
|
+
code: z6.ZodIssueCode.custom,
|
|
9853
10359
|
path: ["teamsWebhookUrl"],
|
|
9854
10360
|
message: "[notifierType: teams] TEAMS_WEBHOOK_URL must include api-version= as a query parameter"
|
|
9855
10361
|
});
|
|
@@ -9876,7 +10382,8 @@ async function loadConfig2() {
|
|
|
9876
10382
|
logLevel: process.env["LOG_LEVEL"],
|
|
9877
10383
|
nodeEnv: process.env["NODE_ENV"],
|
|
9878
10384
|
llmFallbackModels: process.env["LLM_FALLBACK_MODELS"],
|
|
9879
|
-
llmFallbackTimeoutMs: process.env["LLM_FALLBACK_TIMEOUT_MS"]
|
|
10385
|
+
llmFallbackTimeoutMs: process.env["LLM_FALLBACK_TIMEOUT_MS"],
|
|
10386
|
+
rulesConfigPath: process.env["RULES_CONFIG_PATH"]
|
|
9880
10387
|
});
|
|
9881
10388
|
if (!result.success) {
|
|
9882
10389
|
const errorMessages = result.error.issues.map(
|
|
@@ -9894,11 +10401,13 @@ export {
|
|
|
9894
10401
|
AlertType,
|
|
9895
10402
|
AlertmanagerPayloadSchema,
|
|
9896
10403
|
CIRCUIT_BREAKER,
|
|
10404
|
+
ChannelRegistry,
|
|
9897
10405
|
ClaudeProvider,
|
|
9898
10406
|
ClusteringService,
|
|
9899
10407
|
ConsoleNotifier,
|
|
9900
10408
|
DEDUP_TTL_MS_MULTIPLIER,
|
|
9901
10409
|
DEV_SERVER_PORT,
|
|
10410
|
+
FactoryRegistry,
|
|
9902
10411
|
Fingerprint,
|
|
9903
10412
|
GeminiProvider,
|
|
9904
10413
|
HOUR_MS,
|
|
@@ -9922,8 +10431,18 @@ export {
|
|
|
9922
10431
|
RATE_LIMITER,
|
|
9923
10432
|
REDIS_KEY_PREFIX,
|
|
9924
10433
|
RedisDeduplicationStore,
|
|
10434
|
+
RoutingNotifier,
|
|
10435
|
+
RuleActionSchema,
|
|
10436
|
+
RuleActionType,
|
|
10437
|
+
RuleConditionSchema,
|
|
10438
|
+
RuleConfigurationSchema,
|
|
10439
|
+
RuleEngine,
|
|
10440
|
+
RuleEvaluationPhase,
|
|
10441
|
+
RuleSchema,
|
|
10442
|
+
RuleSectionSchema,
|
|
9925
10443
|
SLACK_API_URL,
|
|
9926
10444
|
SQSAlertQueue,
|
|
10445
|
+
SeverityLevel,
|
|
9927
10446
|
SlackNotifier,
|
|
9928
10447
|
TEAMS_WEBHOOK_TIMEOUT_MS,
|
|
9929
10448
|
TeamsNotifier,
|
|
@@ -9931,13 +10450,16 @@ export {
|
|
|
9931
10450
|
URGENCY_EMOJI,
|
|
9932
10451
|
UrgencyLevelSchema,
|
|
9933
10452
|
WEBHOOK_DEFAULTS,
|
|
10453
|
+
compileCondition,
|
|
9934
10454
|
createLLMProvider,
|
|
9935
10455
|
createLogger,
|
|
9936
10456
|
createNotifier,
|
|
10457
|
+
dispatchActions,
|
|
9937
10458
|
flushLoki,
|
|
9938
10459
|
loadConfig2 as loadConfig,
|
|
9939
10460
|
metrics_exports as metrics,
|
|
9940
10461
|
normalizePayload,
|
|
10462
|
+
parseRuleConfig,
|
|
9941
10463
|
reinitLogger,
|
|
9942
10464
|
startSqsLagPoller
|
|
9943
10465
|
};
|