@junando/worker 0.9.0 → 0.10.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/handler.cjs +235 -13
- package/package.json +2 -2
package/dist/handler.cjs
CHANGED
|
@@ -59921,6 +59921,50 @@ function createLLMProvider(provider, apiKey, model, options) {
|
|
|
59921
59921
|
return factory(apiKey, model, options);
|
|
59922
59922
|
}
|
|
59923
59923
|
|
|
59924
|
+
// ../core/src/shared/factory-registry.ts
|
|
59925
|
+
var FactoryRegistry = class {
|
|
59926
|
+
_factories = /* @__PURE__ */ new Map();
|
|
59927
|
+
_default = () => {
|
|
59928
|
+
throw new Error(`No factory registered and no default available`);
|
|
59929
|
+
};
|
|
59930
|
+
/**
|
|
59931
|
+
* Register a factory for a given key.
|
|
59932
|
+
* Overwrites any existing registration for that key.
|
|
59933
|
+
*/
|
|
59934
|
+
register(key, factory) {
|
|
59935
|
+
this._factories.set(key, factory);
|
|
59936
|
+
}
|
|
59937
|
+
/**
|
|
59938
|
+
* Set the default factory to use when no key matches.
|
|
59939
|
+
*/
|
|
59940
|
+
registerDefault(factory) {
|
|
59941
|
+
this._default = factory;
|
|
59942
|
+
}
|
|
59943
|
+
/**
|
|
59944
|
+
* Resolve the factory for a given key.
|
|
59945
|
+
* Returns the default if no specific factory is registered for that key.
|
|
59946
|
+
*/
|
|
59947
|
+
resolve(key) {
|
|
59948
|
+
const factory = this._factories.get(key);
|
|
59949
|
+
if (factory) {
|
|
59950
|
+
return factory();
|
|
59951
|
+
}
|
|
59952
|
+
return this._default();
|
|
59953
|
+
}
|
|
59954
|
+
/**
|
|
59955
|
+
* Check if a factory is registered for a given key.
|
|
59956
|
+
*/
|
|
59957
|
+
has(key) {
|
|
59958
|
+
return this._factories.has(key);
|
|
59959
|
+
}
|
|
59960
|
+
/**
|
|
59961
|
+
* Return all registered keys.
|
|
59962
|
+
*/
|
|
59963
|
+
keys() {
|
|
59964
|
+
return Array.from(this._factories.keys());
|
|
59965
|
+
}
|
|
59966
|
+
};
|
|
59967
|
+
|
|
59924
59968
|
// ../core/src/infrastructure/notifier/slack.adapter.ts
|
|
59925
59969
|
createLogger();
|
|
59926
59970
|
function sanitizeEndpointPath(endpointPath) {
|
|
@@ -60230,14 +60274,26 @@ var TeamsNotifier = class {
|
|
|
60230
60274
|
};
|
|
60231
60275
|
|
|
60232
60276
|
// ../core/src/infrastructure/notifier/factory.ts
|
|
60277
|
+
function buildNotifierRegistry(config) {
|
|
60278
|
+
const registry2 = new FactoryRegistry();
|
|
60279
|
+
registry2.register("teams", () => {
|
|
60280
|
+
if (!config.teamsWebhookUrl) {
|
|
60281
|
+
throw new Error("NOTIFIER_TYPE=teams requires TEAMS_WEBHOOK_URL to be set");
|
|
60282
|
+
}
|
|
60283
|
+
return new TeamsNotifier(config.teamsWebhookUrl);
|
|
60284
|
+
});
|
|
60285
|
+
registry2.register("slack", () => {
|
|
60286
|
+
if (!config.slackBotToken || !config.slackChannel) {
|
|
60287
|
+
throw new Error("NOTIFIER_TYPE=slack requires SLACK_BOT_TOKEN and SLACK_CHANNEL to be set");
|
|
60288
|
+
}
|
|
60289
|
+
return new SlackNotifier(config.slackBotToken, config.slackChannel);
|
|
60290
|
+
});
|
|
60291
|
+
registry2.registerDefault(() => new SlackNotifier("dummy-token", "#alerts"));
|
|
60292
|
+
return registry2;
|
|
60293
|
+
}
|
|
60233
60294
|
function createNotifier(config) {
|
|
60234
|
-
|
|
60235
|
-
|
|
60236
|
-
return new TeamsNotifier(config.teamsWebhookUrl);
|
|
60237
|
-
case "slack":
|
|
60238
|
-
default:
|
|
60239
|
-
return new SlackNotifier(config.slackBotToken, config.slackChannel);
|
|
60240
|
-
}
|
|
60295
|
+
const registry2 = buildNotifierRegistry(config);
|
|
60296
|
+
return registry2.resolve(config.notifierType);
|
|
60241
60297
|
}
|
|
60242
60298
|
|
|
60243
60299
|
// ../../node_modules/.pnpm/@aws-sdk+client-sqs@3.1045.0/node_modules/@aws-sdk/client-sqs/dist-es/SQSClient.js
|
|
@@ -65488,6 +65544,144 @@ async function loadConfig3() {
|
|
|
65488
65544
|
|
|
65489
65545
|
// src/handler.ts
|
|
65490
65546
|
var import_ioredis = __toESM(require_built3());
|
|
65547
|
+
|
|
65548
|
+
// src/adapters/csv-input.adapter.ts
|
|
65549
|
+
var DEFAULT_CSV_COLUMN_MAPPING = {
|
|
65550
|
+
serviceCol: 0,
|
|
65551
|
+
messageCol: 1,
|
|
65552
|
+
severityCol: 2,
|
|
65553
|
+
timestampCol: 3
|
|
65554
|
+
};
|
|
65555
|
+
var SEVERITY_TO_ALERT_TYPE = {
|
|
65556
|
+
error: "http_500" /* Error */,
|
|
65557
|
+
critical: "http_500" /* Error */,
|
|
65558
|
+
high: "http_500" /* Error */,
|
|
65559
|
+
warning: "latency_spike" /* Warning */,
|
|
65560
|
+
warn: "latency_spike" /* Warning */,
|
|
65561
|
+
latency: "latency_spike" /* Warning */,
|
|
65562
|
+
success: "recovery" /* Success */,
|
|
65563
|
+
recovery: "recovery" /* Success */,
|
|
65564
|
+
resolved: "recovery" /* Success */,
|
|
65565
|
+
info: "recovery" /* Success */
|
|
65566
|
+
};
|
|
65567
|
+
external_exports.object({
|
|
65568
|
+
correlationId: external_exports.string().uuid(),
|
|
65569
|
+
alerts: external_exports.array(NormalizedAlertSchema)
|
|
65570
|
+
});
|
|
65571
|
+
function isCsvBody(body) {
|
|
65572
|
+
const trimmed = body.trim();
|
|
65573
|
+
if (!trimmed) return false;
|
|
65574
|
+
if (trimmed.startsWith("{") || trimmed.startsWith("[")) return false;
|
|
65575
|
+
return trimmed.includes(",") && (trimmed.includes("\n") || trimmed.split(",").length >= 3);
|
|
65576
|
+
}
|
|
65577
|
+
function parseCsvBody(body, mapping = DEFAULT_CSV_COLUMN_MAPPING) {
|
|
65578
|
+
const lines = body.trim().split("\n");
|
|
65579
|
+
if (lines.length < 2) return null;
|
|
65580
|
+
const headerLine = lines[0];
|
|
65581
|
+
const headers = parseRow(headerLine);
|
|
65582
|
+
const alerts = [];
|
|
65583
|
+
for (let i8 = 1; i8 < lines.length; i8++) {
|
|
65584
|
+
const row = lines[i8];
|
|
65585
|
+
const values = parseRow(row);
|
|
65586
|
+
if (values.length === 0) continue;
|
|
65587
|
+
try {
|
|
65588
|
+
const service = getColumnValue(headers, values, mapping.serviceCol);
|
|
65589
|
+
const message = getColumnValue(headers, values, mapping.messageCol);
|
|
65590
|
+
const severity = getColumnValue(headers, values, mapping.severityCol);
|
|
65591
|
+
const timestamp = getColumnValue(headers, values, mapping.timestampCol);
|
|
65592
|
+
const fingerprint = mapping.fingerprintCol != null ? getColumnValue(headers, values, mapping.fingerprintCol) || generateFingerprint(service, message) : generateFingerprint(service, message);
|
|
65593
|
+
const endpoint = mapping.endpointCol != null ? getColumnValue(headers, values, mapping.endpointCol) || "/" : "/";
|
|
65594
|
+
if (!service || !message || !severity || !timestamp) continue;
|
|
65595
|
+
const alertType = mapSeverityToAlertType(severity);
|
|
65596
|
+
if (!alertType) continue;
|
|
65597
|
+
const alert = {
|
|
65598
|
+
fingerprint,
|
|
65599
|
+
alertName: message.slice(0, 200),
|
|
65600
|
+
// truncate long names
|
|
65601
|
+
status: "firing",
|
|
65602
|
+
serviceName: service.trim(),
|
|
65603
|
+
alertType,
|
|
65604
|
+
endpointPath: endpoint.trim() || "/",
|
|
65605
|
+
startsAt: normalizeTimestamp(timestamp),
|
|
65606
|
+
labels: parseExtraLabels(mapping.extraLabels),
|
|
65607
|
+
annotations: { source: "csv-adapter" }
|
|
65608
|
+
};
|
|
65609
|
+
const result = NormalizedAlertSchema.safeParse(alert);
|
|
65610
|
+
if (result.success) {
|
|
65611
|
+
alerts.push(result.data);
|
|
65612
|
+
}
|
|
65613
|
+
} catch {
|
|
65614
|
+
continue;
|
|
65615
|
+
}
|
|
65616
|
+
}
|
|
65617
|
+
if (alerts.length === 0) return null;
|
|
65618
|
+
return {
|
|
65619
|
+
correlationId: crypto.randomUUID(),
|
|
65620
|
+
alerts
|
|
65621
|
+
};
|
|
65622
|
+
}
|
|
65623
|
+
function parseRow(line) {
|
|
65624
|
+
const result = [];
|
|
65625
|
+
let current = "";
|
|
65626
|
+
let inQuotes = false;
|
|
65627
|
+
for (let i8 = 0; i8 < line.length; i8++) {
|
|
65628
|
+
const char = line[i8];
|
|
65629
|
+
if (char === '"') {
|
|
65630
|
+
inQuotes = !inQuotes;
|
|
65631
|
+
} else if (char === "," && !inQuotes) {
|
|
65632
|
+
result.push(current.trim());
|
|
65633
|
+
current = "";
|
|
65634
|
+
} else {
|
|
65635
|
+
current += char;
|
|
65636
|
+
}
|
|
65637
|
+
}
|
|
65638
|
+
result.push(current.trim());
|
|
65639
|
+
return result;
|
|
65640
|
+
}
|
|
65641
|
+
function getColumnValue(headers, values, col) {
|
|
65642
|
+
if (typeof col === "number") {
|
|
65643
|
+
return values[col] ?? "";
|
|
65644
|
+
}
|
|
65645
|
+
const idx = headers.indexOf(col);
|
|
65646
|
+
return idx >= 0 ? values[idx] ?? "" : "";
|
|
65647
|
+
}
|
|
65648
|
+
function mapSeverityToAlertType(severity) {
|
|
65649
|
+
const lower = severity.toLowerCase().trim();
|
|
65650
|
+
return SEVERITY_TO_ALERT_TYPE[lower] ?? null;
|
|
65651
|
+
}
|
|
65652
|
+
function normalizeTimestamp(value) {
|
|
65653
|
+
const trimmed = value.trim();
|
|
65654
|
+
if (/\d{4}-\d{2}-\d{2}T/.test(trimmed)) return trimmed;
|
|
65655
|
+
if (/^\d{10}$/.test(trimmed)) {
|
|
65656
|
+
return new Date(Number(trimmed) * 1e3).toISOString();
|
|
65657
|
+
}
|
|
65658
|
+
if (/^\d{13}$/.test(trimmed)) {
|
|
65659
|
+
return new Date(Number(trimmed)).toISOString();
|
|
65660
|
+
}
|
|
65661
|
+
const parsed = Date.parse(trimmed);
|
|
65662
|
+
if (!isNaN(parsed)) return new Date(parsed).toISOString();
|
|
65663
|
+
return (/* @__PURE__ */ new Date()).toISOString();
|
|
65664
|
+
}
|
|
65665
|
+
function parseExtraLabels(extra) {
|
|
65666
|
+
if (!extra) return {};
|
|
65667
|
+
const labels = {};
|
|
65668
|
+
for (const pair of extra.split(",")) {
|
|
65669
|
+
const [k8, v2] = pair.split("=").map((s2) => s2.trim());
|
|
65670
|
+
if (k8) labels[k8] = v2 ?? "";
|
|
65671
|
+
}
|
|
65672
|
+
return labels;
|
|
65673
|
+
}
|
|
65674
|
+
function generateFingerprint(service, message) {
|
|
65675
|
+
const raw = `${service}:${message}`;
|
|
65676
|
+
let hash = 0;
|
|
65677
|
+
for (let i8 = 0; i8 < raw.length; i8++) {
|
|
65678
|
+
const char = raw.charCodeAt(i8);
|
|
65679
|
+
hash = char * 31 + hash >>> 0;
|
|
65680
|
+
}
|
|
65681
|
+
return `csv-${hash.toString(16).padStart(8, "0")}`;
|
|
65682
|
+
}
|
|
65683
|
+
|
|
65684
|
+
// src/handler.ts
|
|
65491
65685
|
var SQSMessageSchema = external_exports.object({
|
|
65492
65686
|
correlationId: external_exports.string().uuid(),
|
|
65493
65687
|
alerts: external_exports.array(NormalizedAlertSchema)
|
|
@@ -65529,6 +65723,12 @@ var handler = async (event) => {
|
|
|
65529
65723
|
await flushLoki();
|
|
65530
65724
|
}
|
|
65531
65725
|
};
|
|
65726
|
+
function parseIntEnv(key) {
|
|
65727
|
+
const val = process.env[key];
|
|
65728
|
+
if (!val) return void 0;
|
|
65729
|
+
const parsed = Number(val);
|
|
65730
|
+
return isNaN(parsed) ? void 0 : parsed;
|
|
65731
|
+
}
|
|
65532
65732
|
async function _handler(event) {
|
|
65533
65733
|
let useCaseInstance;
|
|
65534
65734
|
try {
|
|
@@ -65540,13 +65740,35 @@ async function _handler(event) {
|
|
|
65540
65740
|
for (const record of event.Records) {
|
|
65541
65741
|
let parsed;
|
|
65542
65742
|
try {
|
|
65543
|
-
|
|
65544
|
-
|
|
65545
|
-
|
|
65546
|
-
|
|
65547
|
-
|
|
65743
|
+
if (isCsvBody(record.body)) {
|
|
65744
|
+
const fpCol = parseIntEnv("CSV_FINGERPRINT_COL");
|
|
65745
|
+
const epCol = parseIntEnv("CSV_ENDPOINT_COL");
|
|
65746
|
+
const extraLabels = process.env["CSV_EXTRA_LABELS"];
|
|
65747
|
+
const mapping = {
|
|
65748
|
+
serviceCol: Number(process.env["CSV_SERVICE_COL"] ?? 0),
|
|
65749
|
+
messageCol: Number(process.env["CSV_MESSAGE_COL"] ?? 1),
|
|
65750
|
+
severityCol: Number(process.env["CSV_SEVERITY_COL"] ?? 2),
|
|
65751
|
+
timestampCol: Number(process.env["CSV_TIMESTAMP_COL"] ?? 3),
|
|
65752
|
+
...fpCol !== void 0 && { fingerprintCol: fpCol },
|
|
65753
|
+
...epCol !== void 0 && { endpointCol: epCol },
|
|
65754
|
+
...extraLabels !== void 0 && { extraLabels }
|
|
65755
|
+
};
|
|
65756
|
+
const csvResult = parseCsvBody(record.body, mapping);
|
|
65757
|
+
if (!csvResult) {
|
|
65758
|
+
logger7.error({ record: record.messageId }, "CSV parse returned no valid alerts");
|
|
65759
|
+
continue;
|
|
65760
|
+
}
|
|
65761
|
+
parsed = csvResult;
|
|
65762
|
+
logger7.info({ alertCount: parsed.alerts.length }, "Parsed CSV SQS message");
|
|
65763
|
+
} else {
|
|
65764
|
+
const raw = JSON.parse(record.body);
|
|
65765
|
+
const result = SQSMessageSchema.safeParse(raw);
|
|
65766
|
+
if (!result.success) {
|
|
65767
|
+
logger7.error({ err: result.error.issues }, "Invalid SQS message body");
|
|
65768
|
+
continue;
|
|
65769
|
+
}
|
|
65770
|
+
parsed = result.data;
|
|
65548
65771
|
}
|
|
65549
|
-
parsed = result.data;
|
|
65550
65772
|
} catch (err) {
|
|
65551
65773
|
logger7.error({ err }, "Failed to parse SQS message body");
|
|
65552
65774
|
continue;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@junando/worker",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.10.1",
|
|
4
4
|
"description": "AWS Lambda SQS worker — processes alert events and dispatches notifications for Junando",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"type": "module",
|
|
@@ -22,7 +22,7 @@
|
|
|
22
22
|
"@aws-sdk/client-ssm": "^3.600.0",
|
|
23
23
|
"ioredis": "^5.4.1",
|
|
24
24
|
"zod": "^3.23.0",
|
|
25
|
-
"@junando/core": "0.
|
|
25
|
+
"@junando/core": "0.10.1"
|
|
26
26
|
},
|
|
27
27
|
"devDependencies": {
|
|
28
28
|
"@types/aws-lambda": "^8.10.145",
|