@junando/webhook 0.7.4 → 0.8.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.
Files changed (2) hide show
  1. package/dist/handler.cjs +169 -82
  2. package/package.json +2 -2
package/dist/handler.cjs CHANGED
@@ -4468,62 +4468,6 @@ var init_normalize_payload = __esm({
4468
4468
  }
4469
4469
  });
4470
4470
 
4471
- // ../core/src/application/use-cases/process-incident.use-case.ts
4472
- var ProcessIncidentUseCase;
4473
- var init_process_incident_use_case = __esm({
4474
- "../core/src/application/use-cases/process-incident.use-case.ts"() {
4475
- init_clustering_service();
4476
- ProcessIncidentUseCase = class {
4477
- constructor(deps) {
4478
- this.deps = deps;
4479
- this.clustering = deps.clustering ?? new ClusteringService();
4480
- }
4481
- deps;
4482
- clustering;
4483
- async execute(alerts, correlationId) {
4484
- const { dedup, traces, llm, notifier, logger: logger8, dedupTtlSeconds } = this.deps;
4485
- const log = logger8.child({ correlationId, useCase: "ProcessIncident" });
4486
- log.info({ alertCount: alerts.length }, "Processing alert batch");
4487
- const clusters = this.clustering.buildClusters(alerts);
4488
- log.info({ clusterCount: clusters.length }, "Clusters built");
4489
- this.deps.onClustersBuilt?.(clusters.length);
4490
- for (const cluster of clusters) {
4491
- const log2 = log.child({ fingerprint: cluster.fingerprint, service: cluster.serviceName });
4492
- const isNew = await dedup.isNew(cluster.fingerprint, dedupTtlSeconds);
4493
- if (!isNew) {
4494
- log2.debug("Duplicate cluster \u2014 skipping");
4495
- continue;
4496
- }
4497
- const spanLists = await Promise.all(
4498
- cluster.representativeTraceIds.map(
4499
- (id) => traces.findByTraceId(id).catch((err) => {
4500
- log2.warn({ err, traceId: id }, "Trace fetch failed \u2014 continuing without it");
4501
- return [];
4502
- })
4503
- )
4504
- );
4505
- const allSpans = spanLists.flat();
4506
- log2.info({ spanCount: allSpans.length }, "Traces extracted");
4507
- let analysis = null;
4508
- try {
4509
- analysis = await llm.analyze(cluster, allSpans);
4510
- log2.info({ urgency: analysis.urgency_level }, "LLM analysis complete");
4511
- } catch (err) {
4512
- log2.warn({ err }, "LLM inference failed \u2014 notifying without diagnosis");
4513
- }
4514
- try {
4515
- await notifier.send(cluster, analysis);
4516
- log2.info("Notification sent");
4517
- } catch (err) {
4518
- log2.error({ err }, "Notification failed");
4519
- throw err;
4520
- }
4521
- }
4522
- }
4523
- };
4524
- }
4525
- });
4526
-
4527
4471
  // ../../node_modules/.pnpm/prom-client@15.1.3/node_modules/prom-client/lib/util.js
4528
4472
  var require_util = __commonJS({
4529
4473
  "../../node_modules/.pnpm/prom-client@15.1.3/node_modules/prom-client/lib/util.js"(exports2) {
@@ -9237,15 +9181,19 @@ __export(metrics_exports, {
9237
9181
  alertClusters: () => alertClusters,
9238
9182
  alertsProcessed: () => alertsProcessed,
9239
9183
  alertsReceived: () => alertsReceived,
9184
+ dedupDuplicate: () => dedupDuplicate,
9185
+ dedupNew: () => dedupNew,
9240
9186
  dedupRedisFailoverTotal: () => dedupRedisFailoverTotal,
9241
9187
  latency: () => latency,
9242
9188
  llmInferenceDuration: () => llmInferenceDuration,
9243
9189
  llmInferenceTotal: () => llmInferenceTotal,
9190
+ notificationsTotal: () => notificationsTotal,
9244
9191
  pipelineInlineFailuresTotal: () => pipelineInlineFailuresTotal,
9245
9192
  registry: () => registry,
9193
+ sqsQueueLag: () => sqsQueueLag,
9246
9194
  webhookRequestsTotal: () => webhookRequestsTotal
9247
9195
  });
9248
- var import_prom_client, registry, alertsReceived, webhookRequestsTotal, alertsProcessed, alertClusters, latency, llmInferenceDuration, llmInferenceTotal, pipelineInlineFailuresTotal, dedupRedisFailoverTotal;
9196
+ var import_prom_client, registry, alertsReceived, webhookRequestsTotal, alertsProcessed, alertClusters, latency, llmInferenceDuration, llmInferenceTotal, pipelineInlineFailuresTotal, dedupRedisFailoverTotal, dedupNew, dedupDuplicate, notificationsTotal, sqsQueueLag;
9249
9197
  var init_metrics2 = __esm({
9250
9198
  "../core/src/shared/metrics/index.ts"() {
9251
9199
  import_prom_client = __toESM(require_prom_client());
@@ -9265,6 +9213,7 @@ var init_metrics2 = __esm({
9265
9213
  alertsProcessed = new import_prom_client.Counter({
9266
9214
  name: "junando_alerts_processed_total",
9267
9215
  help: "Total number of alerts processed successfully",
9216
+ labelNames: ["result"],
9268
9217
  registers: [registry]
9269
9218
  });
9270
9219
  alertClusters = new import_prom_client.Gauge({
@@ -9275,7 +9224,8 @@ var init_metrics2 = __esm({
9275
9224
  latency = new import_prom_client.Histogram({
9276
9225
  name: "junando_webhook_duration_seconds",
9277
9226
  help: "Webhook request duration in seconds",
9278
- buckets: [0.01, 0.05, 0.1, 0.25, 0.5, 1],
9227
+ labelNames: ["status"],
9228
+ buckets: [5e-3, 0.01, 0.025, 0.05, 0.1, 0.25, 0.5],
9279
9229
  registers: [registry]
9280
9230
  });
9281
9231
  llmInferenceDuration = new import_prom_client.Histogram({
@@ -9302,6 +9252,89 @@ var init_metrics2 = __esm({
9302
9252
  help: "Total number of Redis dedup fallback activations",
9303
9253
  registers: [registry]
9304
9254
  });
9255
+ dedupNew = new import_prom_client.Counter({
9256
+ name: "junando_dedup_new_total",
9257
+ help: "Total number of new alert clusters (not yet seen)",
9258
+ labelNames: ["source"],
9259
+ registers: [registry]
9260
+ });
9261
+ dedupDuplicate = new import_prom_client.Counter({
9262
+ name: "junando_dedup_duplicate_total",
9263
+ help: "Total number of duplicate alert clusters skipped by dedup",
9264
+ labelNames: ["source"],
9265
+ registers: [registry]
9266
+ });
9267
+ notificationsTotal = new import_prom_client.Counter({
9268
+ name: "junando_notifications_total",
9269
+ help: "Total number of notification attempts",
9270
+ labelNames: ["channel", "outcome"],
9271
+ registers: [registry]
9272
+ });
9273
+ sqsQueueLag = new import_prom_client.Gauge({
9274
+ name: "junando_sqs_queue_lag",
9275
+ help: "Approximate number of unprocessed messages in the SQS queue",
9276
+ labelNames: ["queue_name"],
9277
+ registers: [registry]
9278
+ });
9279
+ }
9280
+ });
9281
+
9282
+ // ../core/src/application/use-cases/process-incident.use-case.ts
9283
+ var ProcessIncidentUseCase;
9284
+ var init_process_incident_use_case = __esm({
9285
+ "../core/src/application/use-cases/process-incident.use-case.ts"() {
9286
+ init_clustering_service();
9287
+ init_metrics2();
9288
+ ProcessIncidentUseCase = class {
9289
+ constructor(deps) {
9290
+ this.deps = deps;
9291
+ this.clustering = deps.clustering ?? new ClusteringService();
9292
+ }
9293
+ deps;
9294
+ clustering;
9295
+ async execute(alerts, correlationId) {
9296
+ const { dedup, traces, llm, notifier, logger: logger8, dedupTtlSeconds } = this.deps;
9297
+ const log = logger8.child({ correlationId, useCase: "ProcessIncident" });
9298
+ log.info({ alertCount: alerts.length }, "Processing alert batch");
9299
+ const clusters = this.clustering.buildClusters(alerts);
9300
+ log.info({ clusterCount: clusters.length }, "Clusters built");
9301
+ this.deps.onClustersBuilt?.(clusters.length);
9302
+ for (const cluster of clusters) {
9303
+ const log2 = log.child({ fingerprint: cluster.fingerprint, service: cluster.serviceName });
9304
+ const isNew = await dedup.isNew(cluster.fingerprint, dedupTtlSeconds);
9305
+ if (!isNew) {
9306
+ log2.debug("Duplicate cluster \u2014 skipping");
9307
+ dedupDuplicate.inc({ source: "alertmanager" });
9308
+ continue;
9309
+ }
9310
+ dedupNew.inc({ source: "alertmanager" });
9311
+ const spanLists = await Promise.all(
9312
+ cluster.representativeTraceIds.map(
9313
+ (id) => traces.findByTraceId(id).catch((err) => {
9314
+ log2.warn({ err, traceId: id }, "Trace fetch failed \u2014 continuing without it");
9315
+ return [];
9316
+ })
9317
+ )
9318
+ );
9319
+ const allSpans = spanLists.flat();
9320
+ log2.info({ spanCount: allSpans.length }, "Traces extracted");
9321
+ let analysis = null;
9322
+ try {
9323
+ analysis = await llm.analyze(cluster, allSpans);
9324
+ log2.info({ urgency: analysis.urgency_level }, "LLM analysis complete");
9325
+ } catch (err) {
9326
+ log2.warn({ err }, "LLM inference failed \u2014 notifying without diagnosis");
9327
+ }
9328
+ try {
9329
+ await notifier.send(cluster, analysis);
9330
+ log2.info("Notification sent");
9331
+ } catch (err) {
9332
+ log2.error({ err }, "Notification failed");
9333
+ throw err;
9334
+ }
9335
+ }
9336
+ }
9337
+ };
9305
9338
  }
9306
9339
  });
9307
9340
 
@@ -25820,6 +25853,7 @@ var init_slack_adapter = __esm({
25820
25853
  "../core/src/infrastructure/notifier/slack.adapter.ts"() {
25821
25854
  init_constants();
25822
25855
  init_logger();
25856
+ init_metrics2();
25823
25857
  logger3 = createLogger();
25824
25858
  SlackNotifier = class {
25825
25859
  constructor(botToken, channel) {
@@ -25830,18 +25864,24 @@ var init_slack_adapter = __esm({
25830
25864
  channel;
25831
25865
  async send(cluster, analysis) {
25832
25866
  const payload = analysis ? this.buildAnalysisMessage(cluster, analysis) : this.buildFallbackMessage(cluster);
25833
- const res = await fetch(SLACK_API_URL, {
25834
- method: "POST",
25835
- headers: {
25836
- "Content-Type": "application/json",
25837
- Authorization: `Bearer ${this.botToken}`
25838
- },
25839
- body: JSON.stringify({ channel: this.channel, ...payload }),
25840
- signal: AbortSignal.timeout(HTTP_TIMEOUT_MS.Default)
25841
- });
25842
- if (!res.ok) throw new Error(`Slack API error: ${res.status}`);
25843
- const body = await res.json();
25844
- if (!body.ok) throw new Error(`Slack error: ${body.error}`);
25867
+ try {
25868
+ const res = await fetch(SLACK_API_URL, {
25869
+ method: "POST",
25870
+ headers: {
25871
+ "Content-Type": "application/json",
25872
+ Authorization: `Bearer ${this.botToken}`
25873
+ },
25874
+ body: JSON.stringify({ channel: this.channel, ...payload }),
25875
+ signal: AbortSignal.timeout(HTTP_TIMEOUT_MS.Default)
25876
+ });
25877
+ if (!res.ok) throw new Error(`Slack API error: ${res.status}`);
25878
+ const body = await res.json();
25879
+ if (!body.ok) throw new Error(`Slack error: ${body.error}`);
25880
+ notificationsTotal.inc({ channel: "slack", outcome: "success" });
25881
+ } catch (err) {
25882
+ notificationsTotal.inc({ channel: "slack", outcome: "failure" });
25883
+ throw err;
25884
+ }
25845
25885
  }
25846
25886
  buildAnalysisMessage(cluster, analysis) {
25847
25887
  const emoji = URGENCY_EMOJI[analysis.urgency_level] ?? "\u26AA";
@@ -25945,17 +25985,23 @@ LLM analysis unavailable \u2014 manual investigation required.`
25945
25985
  ConsoleNotifier = class {
25946
25986
  sent = [];
25947
25987
  async send(cluster, analysis) {
25948
- this.sent.push({ cluster, analysis });
25949
- logger3.info(
25950
- {
25951
- cluster: {
25952
- serviceName: cluster.serviceName,
25953
- alertType: cluster.alertType
25988
+ try {
25989
+ this.sent.push({ cluster, analysis });
25990
+ logger3.info(
25991
+ {
25992
+ cluster: {
25993
+ serviceName: cluster.serviceName,
25994
+ alertType: cluster.alertType
25995
+ },
25996
+ analysis: analysis ?? "unavailable"
25954
25997
  },
25955
- analysis: analysis ?? "unavailable"
25956
- },
25957
- "--- Junando Notification ---"
25958
- );
25998
+ "--- Junando Notification ---"
25999
+ );
26000
+ notificationsTotal.inc({ channel: "unknown", outcome: "success" });
26001
+ } catch (err) {
26002
+ notificationsTotal.inc({ channel: "unknown", outcome: "failure" });
26003
+ throw err;
26004
+ }
25959
26005
  }
25960
26006
  };
25961
26007
  }
@@ -26078,6 +26124,7 @@ var TeamsNotifierError, TeamsNotifier;
26078
26124
  var init_teams_adapter = __esm({
26079
26125
  "../core/src/infrastructure/notifier/teams.adapter.ts"() {
26080
26126
  init_constants();
26127
+ init_metrics2();
26081
26128
  TeamsNotifierError = class extends Error {
26082
26129
  constructor(message) {
26083
26130
  super(message);
@@ -26118,15 +26165,20 @@ var init_teams_adapter = __esm({
26118
26165
  `Teams webhook error ${res.status} (host: ${this.hostForErrors})`
26119
26166
  );
26120
26167
  }
26168
+ notificationsTotal.inc({ channel: "teams", outcome: "success" });
26121
26169
  } catch (err) {
26122
26170
  if (err instanceof TeamsNotifierError) {
26171
+ notificationsTotal.inc({ channel: "teams", outcome: "failure" });
26123
26172
  throw err;
26124
26173
  }
26125
26174
  if (err instanceof Error && err.name === "AbortError") {
26126
- throw new TeamsNotifierError(
26175
+ const timeoutErr = new TeamsNotifierError(
26127
26176
  `Teams webhook timed out after ${this.timeoutMs}ms (host: ${this.hostForErrors})`
26128
26177
  );
26178
+ notificationsTotal.inc({ channel: "teams", outcome: "failure" });
26179
+ throw timeoutErr;
26129
26180
  }
26181
+ notificationsTotal.inc({ channel: "teams", outcome: "failure" });
26130
26182
  throw err;
26131
26183
  } finally {
26132
26184
  clearTimeout(timer);
@@ -53647,6 +53699,36 @@ var init_sqs_adapter = __esm({
53647
53699
  }
53648
53700
  });
53649
53701
 
53702
+ // ../core/src/infrastructure/queue/sqs-lag-poller.ts
53703
+ function startSqsLagPoller(queueUrl, intervalMs, region) {
53704
+ const client = new SQSClient(region ? { region } : {});
53705
+ const poll = async () => {
53706
+ try {
53707
+ const result = await client.send(
53708
+ new GetQueueAttributesCommand({
53709
+ QueueUrl: queueUrl,
53710
+ AttributeNames: ["ApproximateNumberOfMessages"]
53711
+ })
53712
+ );
53713
+ const raw = result.Attributes?.["ApproximateNumberOfMessages"];
53714
+ if (raw !== void 0) {
53715
+ sqsQueueLag.set({ queue_name: "alerts" }, parseInt(raw, 10));
53716
+ }
53717
+ } catch {
53718
+ }
53719
+ };
53720
+ const timer = setInterval(() => {
53721
+ void poll();
53722
+ }, intervalMs);
53723
+ return () => clearInterval(timer);
53724
+ }
53725
+ var init_sqs_lag_poller = __esm({
53726
+ "../core/src/infrastructure/queue/sqs-lag-poller.ts"() {
53727
+ init_dist_es61();
53728
+ init_metrics2();
53729
+ }
53730
+ });
53731
+
53650
53732
  // ../core/src/infrastructure/traces/loki-trace.adapter.ts
53651
53733
  var LokiTraceRepository, MockTraceRepository;
53652
53734
  var init_loki_trace_adapter = __esm({
@@ -68746,7 +68828,8 @@ __export(src_exports, {
68746
68828
  loadConfig: () => loadConfig3,
68747
68829
  metrics: () => metrics_exports,
68748
68830
  normalizePayload: () => normalizePayload,
68749
- reinitLogger: () => reinitLogger
68831
+ reinitLogger: () => reinitLogger,
68832
+ startSqsLagPoller: () => startSqsLagPoller
68750
68833
  });
68751
68834
  var init_src3 = __esm({
68752
68835
  "../core/src/index.ts"() {
@@ -68764,6 +68847,7 @@ var init_src3 = __esm({
68764
68847
  init_slack_adapter();
68765
68848
  init_teams_adapter();
68766
68849
  init_sqs_adapter();
68850
+ init_sqs_lag_poller();
68767
68851
  init_loki_trace_adapter();
68768
68852
  init_config7();
68769
68853
  init_constants();
@@ -68826,6 +68910,7 @@ var handler = async (event) => {
68826
68910
  };
68827
68911
  async function _handler(event) {
68828
68912
  const QUEUE_URL = process.env["SQS_QUEUE_URL"] ?? "";
68913
+ const start = Date.now();
68829
68914
  const correlationId = crypto2.randomUUID();
68830
68915
  if (event.rawPath === "/health") {
68831
68916
  metrics_exports.webhookRequestsTotal.inc({ endpoint: "/health", status: "200" });
@@ -68901,6 +68986,7 @@ async function _handler(event) {
68901
68986
  const parsed = AlertmanagerPayloadSchema.safeParse(raw);
68902
68987
  if (!parsed.success) {
68903
68988
  metrics_exports.webhookRequestsTotal.inc({ endpoint: "/webhook/alert", status: "422" });
68989
+ metrics_exports.latency.observe({ status: "error" }, (Date.now() - start) / 1e3);
68904
68990
  return {
68905
68991
  statusCode: 422,
68906
68992
  body: JSON.stringify({
@@ -68981,6 +69067,7 @@ async function _handler(event) {
68981
69067
  logger7.error({ err, correlationId }, "Failed to initialize inline pipeline");
68982
69068
  }
68983
69069
  }
69070
+ metrics_exports.latency.observe({ status: "success" }, (Date.now() - start) / 1e3);
68984
69071
  return {
68985
69072
  statusCode: 200,
68986
69073
  body: JSON.stringify({ accepted: alerts.length, correlationId })
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@junando/webhook",
3
- "version": "0.7.4",
3
+ "version": "0.8.0",
4
4
  "description": "AWS Lambda webhook handler — receives and enqueues external alert payloads for Junando",
5
5
  "license": "Apache-2.0",
6
6
  "type": "module",
@@ -20,7 +20,7 @@
20
20
  "dependencies": {
21
21
  "@aws-sdk/client-sqs": "^3.600.0",
22
22
  "zod": "^3.23.0",
23
- "@junando/core": "0.7.4"
23
+ "@junando/core": "0.8.0"
24
24
  },
25
25
  "devDependencies": {
26
26
  "@types/aws-lambda": "^8.10.145",