@junando/core 0.7.4 → 0.8.2

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 CHANGED
@@ -668,6 +668,8 @@ declare class InMemoryAlertQueue implements IAlertQueue {
668
668
  publish(alert: NormalizedAlert): Promise<void>;
669
669
  }
670
670
 
671
+ declare function startSqsLagPoller(queueUrl: string, intervalMs: number, region?: string): () => void;
672
+
671
673
  declare class LokiTraceRepository implements ITraceRepository {
672
674
  private readonly lokiUrl;
673
675
  private readonly apiKey?;
@@ -693,28 +695,40 @@ declare function flushLoki(): Promise<void>;
693
695
  declare const registry: Registry<"text/plain; version=0.0.4; charset=utf-8">;
694
696
  declare const alertsReceived: Counter<"status">;
695
697
  declare const webhookRequestsTotal: Counter<"status" | "endpoint">;
696
- declare const alertsProcessed: Counter<string>;
698
+ declare const alertsProcessed: Counter<"result">;
697
699
  declare const alertClusters: Gauge<string>;
698
- declare const latency: Histogram<string>;
700
+ declare const latency: Histogram<"status">;
699
701
  declare const llmInferenceDuration: Histogram<"model">;
700
702
  declare const llmInferenceTotal: Counter<"status">;
701
703
  /** Tracks inline pipeline failures in local dev mode. */
702
704
  declare const pipelineInlineFailuresTotal: Counter<"reason">;
703
705
  /** Tracks Redis dedup fallback activations. */
704
706
  declare const dedupRedisFailoverTotal: Counter<string>;
707
+ /** Tracks new alert clusters (dedup: first-time fingerprint). */
708
+ declare const dedupNew: Counter<"source">;
709
+ /** Tracks duplicate alert clusters (dedup: fingerprint already seen). */
710
+ declare const dedupDuplicate: Counter<"source">;
711
+ /** Tracks outbound notification attempts by channel and outcome. */
712
+ declare const notificationsTotal: Counter<"channel" | "outcome">;
713
+ /** Tracks approximate number of unprocessed SQS messages (queue lag). */
714
+ declare const sqsQueueLag: Gauge<"queue_name">;
705
715
 
706
716
  declare const index_alertClusters: typeof alertClusters;
707
717
  declare const index_alertsProcessed: typeof alertsProcessed;
708
718
  declare const index_alertsReceived: typeof alertsReceived;
719
+ declare const index_dedupDuplicate: typeof dedupDuplicate;
720
+ declare const index_dedupNew: typeof dedupNew;
709
721
  declare const index_dedupRedisFailoverTotal: typeof dedupRedisFailoverTotal;
710
722
  declare const index_latency: typeof latency;
711
723
  declare const index_llmInferenceDuration: typeof llmInferenceDuration;
712
724
  declare const index_llmInferenceTotal: typeof llmInferenceTotal;
725
+ declare const index_notificationsTotal: typeof notificationsTotal;
713
726
  declare const index_pipelineInlineFailuresTotal: typeof pipelineInlineFailuresTotal;
714
727
  declare const index_registry: typeof registry;
728
+ declare const index_sqsQueueLag: typeof sqsQueueLag;
715
729
  declare const index_webhookRequestsTotal: typeof webhookRequestsTotal;
716
730
  declare namespace index {
717
- export { index_alertClusters as alertClusters, index_alertsProcessed as alertsProcessed, index_alertsReceived as alertsReceived, index_dedupRedisFailoverTotal as dedupRedisFailoverTotal, index_latency as latency, index_llmInferenceDuration as llmInferenceDuration, index_llmInferenceTotal as llmInferenceTotal, index_pipelineInlineFailuresTotal as pipelineInlineFailuresTotal, index_registry as registry, index_webhookRequestsTotal as webhookRequestsTotal };
731
+ export { index_alertClusters as alertClusters, index_alertsProcessed as alertsProcessed, index_alertsReceived as alertsReceived, index_dedupDuplicate as dedupDuplicate, index_dedupNew as dedupNew, index_dedupRedisFailoverTotal as dedupRedisFailoverTotal, index_latency as latency, index_llmInferenceDuration as llmInferenceDuration, index_llmInferenceTotal as llmInferenceTotal, index_notificationsTotal as notificationsTotal, index_pipelineInlineFailuresTotal as pipelineInlineFailuresTotal, index_registry as registry, index_sqsQueueLag as sqsQueueLag, index_webhookRequestsTotal as webhookRequestsTotal };
718
732
  }
719
733
 
720
- export { ALERT_TYPE_LABELS, type AlertCluster, AlertClusterSchema, type AlertErrorType, type AlertStatus, AlertStatusSchema, AlertType, type AlertmanagerPayload, AlertmanagerPayloadSchema, CIRCUIT_BREAKER, ClaudeProvider, ClusteringService, type Config, ConsoleNotifier, DEDUP_TTL_MS_MULTIPLIER, DEV_SERVER_PORT, Fingerprint, GeminiProvider, HOUR_MS, HTTP_TIMEOUT_MS, type IAlertQueue, type IDeduplicationStore, type IIndexer, type ILLMProvider, type INotifier, type ITraceRepository, InMemoryAlertQueue, InMemoryDeduplicationStore, InMemoryIndexer, type Incident, IncidentSchema, type LLMAnalysis, LLMAnalysisSchema, LLMProviderType, LLM_FALLBACK_DEFAULTS, LLM_MAX_TOKENS, LLM_MODELS, type Logger, type LoggerOptions, LokiTraceRepository, MockLLMProvider, MockTraceRepository, type NormalizedAlert, NormalizedAlertSchema, type OpenSearchHttpFetcher, type OpenSearchHttpResponse, OpenSearchIndexer, type OpenSearchIndexerDeps, PAYLOAD_DEFAULTS, ProcessIncidentUseCase, RATE_LIMITER, REDIS_KEY_PREFIX, RedisDeduplicationStore, SLACK_API_URL, SQSAlertQueue, type SignedHttpRequest, SlackNotifier, TEAMS_WEBHOOK_TIMEOUT_MS, TeamsNotifier, TeamsNotifierError, type TraceabilityDocument, URGENCY_EMOJI, type UrgencyLevel, UrgencyLevelSchema, WEBHOOK_DEFAULTS, createLLMProvider, createLogger, createNotifier, flushLoki, loadConfig, index as metrics, normalizePayload, reinitLogger };
734
+ export { ALERT_TYPE_LABELS, type AlertCluster, AlertClusterSchema, type AlertErrorType, type AlertStatus, AlertStatusSchema, AlertType, type AlertmanagerPayload, AlertmanagerPayloadSchema, CIRCUIT_BREAKER, ClaudeProvider, ClusteringService, type Config, ConsoleNotifier, DEDUP_TTL_MS_MULTIPLIER, DEV_SERVER_PORT, Fingerprint, GeminiProvider, HOUR_MS, HTTP_TIMEOUT_MS, type IAlertQueue, type IDeduplicationStore, type IIndexer, type ILLMProvider, type INotifier, type ITraceRepository, InMemoryAlertQueue, InMemoryDeduplicationStore, InMemoryIndexer, type Incident, IncidentSchema, type LLMAnalysis, LLMAnalysisSchema, LLMProviderType, LLM_FALLBACK_DEFAULTS, LLM_MAX_TOKENS, LLM_MODELS, type Logger, type LoggerOptions, LokiTraceRepository, MockLLMProvider, MockTraceRepository, type NormalizedAlert, NormalizedAlertSchema, type OpenSearchHttpFetcher, type OpenSearchHttpResponse, OpenSearchIndexer, type OpenSearchIndexerDeps, PAYLOAD_DEFAULTS, ProcessIncidentUseCase, RATE_LIMITER, REDIS_KEY_PREFIX, RedisDeduplicationStore, SLACK_API_URL, SQSAlertQueue, type SignedHttpRequest, SlackNotifier, TEAMS_WEBHOOK_TIMEOUT_MS, TeamsNotifier, TeamsNotifierError, type TraceabilityDocument, URGENCY_EMOJI, type UrgencyLevel, UrgencyLevelSchema, WEBHOOK_DEFAULTS, createLLMProvider, createLogger, createNotifier, flushLoki, loadConfig, index as metrics, normalizePayload, reinitLogger, startSqsLagPoller };
package/dist/index.js CHANGED
@@ -367,68 +367,22 @@ function normalizePayload(payload) {
367
367
  );
368
368
  }
369
369
 
370
- // src/application/use-cases/process-incident.use-case.ts
371
- var ProcessIncidentUseCase = class {
372
- constructor(deps) {
373
- this.deps = deps;
374
- this.clustering = deps.clustering ?? new ClusteringService();
375
- }
376
- deps;
377
- clustering;
378
- async execute(alerts, correlationId) {
379
- const { dedup, traces, llm, notifier, logger: logger5, dedupTtlSeconds } = this.deps;
380
- const log = logger5.child({ correlationId, useCase: "ProcessIncident" });
381
- log.info({ alertCount: alerts.length }, "Processing alert batch");
382
- const clusters = this.clustering.buildClusters(alerts);
383
- log.info({ clusterCount: clusters.length }, "Clusters built");
384
- this.deps.onClustersBuilt?.(clusters.length);
385
- for (const cluster of clusters) {
386
- const log2 = log.child({ fingerprint: cluster.fingerprint, service: cluster.serviceName });
387
- const isNew = await dedup.isNew(cluster.fingerprint, dedupTtlSeconds);
388
- if (!isNew) {
389
- log2.debug("Duplicate cluster \u2014 skipping");
390
- continue;
391
- }
392
- const spanLists = await Promise.all(
393
- cluster.representativeTraceIds.map(
394
- (id) => traces.findByTraceId(id).catch((err) => {
395
- log2.warn({ err, traceId: id }, "Trace fetch failed \u2014 continuing without it");
396
- return [];
397
- })
398
- )
399
- );
400
- const allSpans = spanLists.flat();
401
- log2.info({ spanCount: allSpans.length }, "Traces extracted");
402
- let analysis = null;
403
- try {
404
- analysis = await llm.analyze(cluster, allSpans);
405
- log2.info({ urgency: analysis.urgency_level }, "LLM analysis complete");
406
- } catch (err) {
407
- log2.warn({ err }, "LLM inference failed \u2014 notifying without diagnosis");
408
- }
409
- try {
410
- await notifier.send(cluster, analysis);
411
- log2.info("Notification sent");
412
- } catch (err) {
413
- log2.error({ err }, "Notification failed");
414
- throw err;
415
- }
416
- }
417
- }
418
- };
419
-
420
370
  // src/shared/metrics/index.ts
421
371
  var metrics_exports = {};
422
372
  __export(metrics_exports, {
423
373
  alertClusters: () => alertClusters,
424
374
  alertsProcessed: () => alertsProcessed,
425
375
  alertsReceived: () => alertsReceived,
376
+ dedupDuplicate: () => dedupDuplicate,
377
+ dedupNew: () => dedupNew,
426
378
  dedupRedisFailoverTotal: () => dedupRedisFailoverTotal,
427
379
  latency: () => latency,
428
380
  llmInferenceDuration: () => llmInferenceDuration,
429
381
  llmInferenceTotal: () => llmInferenceTotal,
382
+ notificationsTotal: () => notificationsTotal,
430
383
  pipelineInlineFailuresTotal: () => pipelineInlineFailuresTotal,
431
384
  registry: () => registry,
385
+ sqsQueueLag: () => sqsQueueLag,
432
386
  webhookRequestsTotal: () => webhookRequestsTotal
433
387
  });
434
388
  import { Registry, Counter, Gauge, Histogram } from "prom-client";
@@ -448,6 +402,7 @@ var webhookRequestsTotal = new Counter({
448
402
  var alertsProcessed = new Counter({
449
403
  name: "junando_alerts_processed_total",
450
404
  help: "Total number of alerts processed successfully",
405
+ labelNames: ["result"],
451
406
  registers: [registry]
452
407
  });
453
408
  var alertClusters = new Gauge({
@@ -458,7 +413,8 @@ var alertClusters = new Gauge({
458
413
  var latency = new Histogram({
459
414
  name: "junando_webhook_duration_seconds",
460
415
  help: "Webhook request duration in seconds",
461
- buckets: [0.01, 0.05, 0.1, 0.25, 0.5, 1],
416
+ labelNames: ["status"],
417
+ buckets: [5e-3, 0.01, 0.025, 0.05, 0.1, 0.25, 0.5],
462
418
  registers: [registry]
463
419
  });
464
420
  var llmInferenceDuration = new Histogram({
@@ -485,6 +441,82 @@ var dedupRedisFailoverTotal = new Counter({
485
441
  help: "Total number of Redis dedup fallback activations",
486
442
  registers: [registry]
487
443
  });
444
+ var dedupNew = new Counter({
445
+ name: "junando_dedup_new_total",
446
+ help: "Total number of new alert clusters (not yet seen)",
447
+ labelNames: ["source"],
448
+ registers: [registry]
449
+ });
450
+ var dedupDuplicate = new Counter({
451
+ name: "junando_dedup_duplicate_total",
452
+ help: "Total number of duplicate alert clusters skipped by dedup",
453
+ labelNames: ["source"],
454
+ registers: [registry]
455
+ });
456
+ var notificationsTotal = new Counter({
457
+ name: "junando_notifications_total",
458
+ help: "Total number of notification attempts",
459
+ labelNames: ["channel", "outcome"],
460
+ registers: [registry]
461
+ });
462
+ var sqsQueueLag = new Gauge({
463
+ name: "junando_sqs_queue_lag",
464
+ help: "Approximate number of unprocessed messages in the SQS queue",
465
+ labelNames: ["queue_name"],
466
+ registers: [registry]
467
+ });
468
+
469
+ // src/application/use-cases/process-incident.use-case.ts
470
+ var ProcessIncidentUseCase = class {
471
+ constructor(deps) {
472
+ this.deps = deps;
473
+ this.clustering = deps.clustering ?? new ClusteringService();
474
+ }
475
+ deps;
476
+ clustering;
477
+ async execute(alerts, correlationId) {
478
+ const { dedup, traces, llm, notifier, logger: logger5, dedupTtlSeconds } = this.deps;
479
+ const log = logger5.child({ correlationId, useCase: "ProcessIncident" });
480
+ log.info({ alertCount: alerts.length }, "Processing alert batch");
481
+ const clusters = this.clustering.buildClusters(alerts);
482
+ log.info({ clusterCount: clusters.length }, "Clusters built");
483
+ this.deps.onClustersBuilt?.(clusters.length);
484
+ for (const cluster of clusters) {
485
+ const log2 = log.child({ fingerprint: cluster.fingerprint, service: cluster.serviceName });
486
+ const isNew = await dedup.isNew(cluster.fingerprint, dedupTtlSeconds);
487
+ if (!isNew) {
488
+ log2.debug("Duplicate cluster \u2014 skipping");
489
+ dedupDuplicate.inc({ source: "alertmanager" });
490
+ continue;
491
+ }
492
+ dedupNew.inc({ source: "alertmanager" });
493
+ const spanLists = await Promise.all(
494
+ cluster.representativeTraceIds.map(
495
+ (id) => traces.findByTraceId(id).catch((err) => {
496
+ log2.warn({ err, traceId: id }, "Trace fetch failed \u2014 continuing without it");
497
+ return [];
498
+ })
499
+ )
500
+ );
501
+ const allSpans = spanLists.flat();
502
+ log2.info({ spanCount: allSpans.length }, "Traces extracted");
503
+ let analysis = null;
504
+ try {
505
+ analysis = await llm.analyze(cluster, allSpans);
506
+ log2.info({ urgency: analysis.urgency_level }, "LLM analysis complete");
507
+ } catch (err) {
508
+ log2.warn({ err }, "LLM inference failed \u2014 notifying without diagnosis");
509
+ }
510
+ try {
511
+ await notifier.send(cluster, analysis);
512
+ log2.info("Notification sent");
513
+ } catch (err) {
514
+ log2.error({ err }, "Notification failed");
515
+ throw err;
516
+ }
517
+ }
518
+ }
519
+ };
488
520
 
489
521
  // src/shared/logger/index.ts
490
522
  import pino from "pino";
@@ -988,18 +1020,24 @@ var SlackNotifier = class {
988
1020
  channel;
989
1021
  async send(cluster, analysis) {
990
1022
  const payload = analysis ? this.buildAnalysisMessage(cluster, analysis) : this.buildFallbackMessage(cluster);
991
- const res = await fetch(SLACK_API_URL, {
992
- method: "POST",
993
- headers: {
994
- "Content-Type": "application/json",
995
- Authorization: `Bearer ${this.botToken}`
996
- },
997
- body: JSON.stringify({ channel: this.channel, ...payload }),
998
- signal: AbortSignal.timeout(HTTP_TIMEOUT_MS.Default)
999
- });
1000
- if (!res.ok) throw new Error(`Slack API error: ${res.status}`);
1001
- const body = await res.json();
1002
- if (!body.ok) throw new Error(`Slack error: ${body.error}`);
1023
+ try {
1024
+ const res = await fetch(SLACK_API_URL, {
1025
+ method: "POST",
1026
+ headers: {
1027
+ "Content-Type": "application/json",
1028
+ Authorization: `Bearer ${this.botToken}`
1029
+ },
1030
+ body: JSON.stringify({ channel: this.channel, ...payload }),
1031
+ signal: AbortSignal.timeout(HTTP_TIMEOUT_MS.Default)
1032
+ });
1033
+ if (!res.ok) throw new Error(`Slack API error: ${res.status}`);
1034
+ const body = await res.json();
1035
+ if (!body.ok) throw new Error(`Slack error: ${body.error}`);
1036
+ notificationsTotal.inc({ channel: "slack", outcome: "success" });
1037
+ } catch (err) {
1038
+ notificationsTotal.inc({ channel: "slack", outcome: "failure" });
1039
+ throw err;
1040
+ }
1003
1041
  }
1004
1042
  buildAnalysisMessage(cluster, analysis) {
1005
1043
  const emoji = URGENCY_EMOJI[analysis.urgency_level] ?? "\u26AA";
@@ -1103,17 +1141,23 @@ LLM analysis unavailable \u2014 manual investigation required.`
1103
1141
  var ConsoleNotifier = class {
1104
1142
  sent = [];
1105
1143
  async send(cluster, analysis) {
1106
- this.sent.push({ cluster, analysis });
1107
- logger3.info(
1108
- {
1109
- cluster: {
1110
- serviceName: cluster.serviceName,
1111
- alertType: cluster.alertType
1144
+ try {
1145
+ this.sent.push({ cluster, analysis });
1146
+ logger3.info(
1147
+ {
1148
+ cluster: {
1149
+ serviceName: cluster.serviceName,
1150
+ alertType: cluster.alertType
1151
+ },
1152
+ analysis: analysis ?? "unavailable"
1112
1153
  },
1113
- analysis: analysis ?? "unavailable"
1114
- },
1115
- "--- Junando Notification ---"
1116
- );
1154
+ "--- Junando Notification ---"
1155
+ );
1156
+ notificationsTotal.inc({ channel: "unknown", outcome: "success" });
1157
+ } catch (err) {
1158
+ notificationsTotal.inc({ channel: "unknown", outcome: "failure" });
1159
+ throw err;
1160
+ }
1117
1161
  }
1118
1162
  };
1119
1163
 
@@ -1270,15 +1314,20 @@ var TeamsNotifier = class {
1270
1314
  `Teams webhook error ${res.status} (host: ${this.hostForErrors})`
1271
1315
  );
1272
1316
  }
1317
+ notificationsTotal.inc({ channel: "teams", outcome: "success" });
1273
1318
  } catch (err) {
1274
1319
  if (err instanceof TeamsNotifierError) {
1320
+ notificationsTotal.inc({ channel: "teams", outcome: "failure" });
1275
1321
  throw err;
1276
1322
  }
1277
1323
  if (err instanceof Error && err.name === "AbortError") {
1278
- throw new TeamsNotifierError(
1324
+ const timeoutErr = new TeamsNotifierError(
1279
1325
  `Teams webhook timed out after ${this.timeoutMs}ms (host: ${this.hostForErrors})`
1280
1326
  );
1327
+ notificationsTotal.inc({ channel: "teams", outcome: "failure" });
1328
+ throw timeoutErr;
1281
1329
  }
1330
+ notificationsTotal.inc({ channel: "teams", outcome: "failure" });
1282
1331
  throw err;
1283
1332
  } finally {
1284
1333
  clearTimeout(timer);
@@ -2305,6 +2354,7 @@ var _E = "Entries";
2305
2354
  var _EBR = "EmptyBatchRequest";
2306
2355
  var _F = "Failed";
2307
2356
  var _FR = "FailureReason";
2357
+ var _GQA = "GetQueueAttributes";
2308
2358
  var _GQAR = "GetQueueAttributesRequest";
2309
2359
  var _GQARe = "GetQueueAttributesResult";
2310
2360
  var _I = "Id";
@@ -3125,6 +3175,14 @@ var TagMap = [
3125
3175
  { [_xN]: _V }
3126
3176
  ]
3127
3177
  ];
3178
+ var GetQueueAttributes$ = [
3179
+ 9,
3180
+ n0,
3181
+ _GQA,
3182
+ 0,
3183
+ () => GetQueueAttributesRequest$,
3184
+ () => GetQueueAttributesResult$
3185
+ ];
3128
3186
  var SendMessage$ = [
3129
3187
  9,
3130
3188
  n0,
@@ -3287,6 +3345,12 @@ var SQSClient = class extends Client {
3287
3345
  }
3288
3346
  };
3289
3347
 
3348
+ // ../../node_modules/.pnpm/@aws-sdk+client-sqs@3.1045.0/node_modules/@aws-sdk/client-sqs/dist-es/commands/GetQueueAttributesCommand.js
3349
+ var GetQueueAttributesCommand = class extends Command.classBuilder().ep(commonParams).m(function(Command2, cs, config, o) {
3350
+ return [getEndpointPlugin(config, Command2.getEndpointParameterInstructions())];
3351
+ }).s("AmazonSQS", "GetQueueAttributes", {}).n("SQSClient", "GetQueueAttributesCommand").sc(GetQueueAttributes$).build() {
3352
+ };
3353
+
3290
3354
  // ../../node_modules/.pnpm/@aws-sdk+client-sqs@3.1045.0/node_modules/@aws-sdk/client-sqs/dist-es/commands/SendMessageCommand.js
3291
3355
  var SendMessageCommand = class extends Command.classBuilder().ep(commonParams).m(function(Command2, cs, config, o) {
3292
3356
  return [
@@ -3347,6 +3411,30 @@ var InMemoryAlertQueue = class {
3347
3411
  }
3348
3412
  };
3349
3413
 
3414
+ // src/infrastructure/queue/sqs-lag-poller.ts
3415
+ function startSqsLagPoller(queueUrl, intervalMs, region) {
3416
+ const client = new SQSClient(region ? { region } : {});
3417
+ const poll = async () => {
3418
+ try {
3419
+ const result = await client.send(
3420
+ new GetQueueAttributesCommand({
3421
+ QueueUrl: queueUrl,
3422
+ AttributeNames: ["ApproximateNumberOfMessages"]
3423
+ })
3424
+ );
3425
+ const raw = result.Attributes?.["ApproximateNumberOfMessages"];
3426
+ if (raw !== void 0) {
3427
+ sqsQueueLag.set({ queue_name: "alerts" }, parseInt(raw, 10));
3428
+ }
3429
+ } catch {
3430
+ }
3431
+ };
3432
+ const timer = setInterval(() => {
3433
+ void poll();
3434
+ }, intervalMs);
3435
+ return () => clearInterval(timer);
3436
+ }
3437
+
3350
3438
  // src/infrastructure/traces/loki-trace.adapter.ts
3351
3439
  var LokiTraceRepository = class {
3352
3440
  constructor(lokiUrl, apiKey) {
@@ -9850,5 +9938,6 @@ export {
9850
9938
  loadConfig2 as loadConfig,
9851
9939
  metrics_exports as metrics,
9852
9940
  normalizePayload,
9853
- reinitLogger
9941
+ reinitLogger,
9942
+ startSqsLagPoller
9854
9943
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@junando/core",
3
- "version": "0.7.4",
3
+ "version": "0.8.2",
4
4
  "description": "Core domain types, interfaces, and shared utilities for the Junando alerting platform",
5
5
  "license": "Apache-2.0",
6
6
  "type": "module",