@closeup1202/klag 0.2.0 → 0.3.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/README.md +14 -13
- package/dist/cli/index.js +184 -56
- package/package.json +4 -2
package/README.md
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
> Know **why** your Kafka consumer lag is growing — in 5 seconds from the terminal
|
|
4
4
|
|
|
5
|
-
[](https://www.npmjs.com/package/@closeup1202/klag)
|
|
6
6
|
[](https://opensource.org/licenses/MIT)
|
|
7
7
|
|
|
8
8
|
## Compared to existing tools
|
|
@@ -21,21 +21,21 @@ npx @closeup1202/klag --broker localhost:9092 --group my-service
|
|
|
21
21
|
|
|
22
22
|
## Output example
|
|
23
23
|
```
|
|
24
|
-
⚡ klag
|
|
24
|
+
⚡ klag 0.3.0
|
|
25
25
|
|
|
26
26
|
🔍 Consumer Group: my-service
|
|
27
27
|
Broker: localhost:9092
|
|
28
|
-
Collected At: 2026-03-
|
|
28
|
+
Collected At: 2026-03-28 17:27:27 (Asia/Seoul)
|
|
29
29
|
|
|
30
|
-
Group Status :
|
|
30
|
+
Group Status : 🚨 CRITICAL Total Lag : 1,234 Drain : ∞
|
|
31
31
|
|
|
32
|
-
|
|
33
|
-
│ Topic │ Partition │ Committed Offset │ Log-End Offset │ Lag │ Status │ Produce Rate │ Consume Rate │
|
|
34
|
-
|
|
35
|
-
│ orders │ 0 │ 8,796 │ 10,000 │ 1,204 │ 🔴 HIGH │ 40.0 msg/s │ 0.0 msg/s │
|
|
36
|
-
│ orders │ 1 │ 9,988 │ 10,000 │ 12 │ 🟢 OK │ 0.0 msg/s │ 0.0 msg/s │
|
|
37
|
-
│ orders │ 2 │ 9,982 │ 10,000 │ 18 │ 🟢 OK │ 0.0 msg/s │ 0.0 msg/s │
|
|
38
|
-
|
|
32
|
+
┌────────┬───────────┬──────────────────┬────────────────┬───────┬─────────┬──────┬──────────────┬──────────────┐
|
|
33
|
+
│ Topic │ Partition │ Committed Offset │ Log-End Offset │ Lag │ Status │ Drain│ Produce Rate │ Consume Rate │
|
|
34
|
+
├────────┼───────────┼──────────────────┼────────────────┼───────┼─────────┼──────┼──────────────┼──────────────┤
|
|
35
|
+
│ orders │ 0 │ 8,796 │ 10,000 │ 1,204 │ 🔴 HIGH │ ∞ │ 40.0 msg/s │ 0.0 msg/s │
|
|
36
|
+
│ orders │ 1 │ 9,988 │ 10,000 │ 12 │ 🟢 OK │ — │ 0.0 msg/s │ 0.0 msg/s │
|
|
37
|
+
│ orders │ 2 │ 9,982 │ 10,000 │ 18 │ 🟢 OK │ — │ 0.0 msg/s │ 0.0 msg/s │
|
|
38
|
+
└────────┴───────────┴──────────────────┴────────────────┴───────┴─────────┴──────┴──────────────┴──────────────┘
|
|
39
39
|
|
|
40
40
|
🔎 Root Cause Analysis
|
|
41
41
|
[PRODUCER_BURST] orders
|
|
@@ -156,8 +156,9 @@ All consumption pauses during rebalancing, which can cause a temporary lag spike
|
|
|
156
156
|
|
|
157
157
|
- [x] v0.1.0 — lag collection, hot partition, producer burst, slow consumer, rebalancing detection, watch mode with lag trend (▲▼)
|
|
158
158
|
- [x] v0.2.0 — SSL/SASL authentication, `.klagrc` config file support
|
|
159
|
-
- [
|
|
160
|
-
- [ ] v0.4.0 —
|
|
159
|
+
- [x] v0.3.0 — time-to-drain severity classification, Drain column per partition
|
|
160
|
+
- [ ] v0.4.0 — multi-group monitoring
|
|
161
|
+
- [ ] v0.5.0 — Slack alerts, Prometheus export
|
|
161
162
|
|
|
162
163
|
## License
|
|
163
164
|
|
package/dist/cli/index.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
3
|
// src/cli/index.ts
|
|
4
|
-
import
|
|
4
|
+
import chalk6 from "chalk";
|
|
5
5
|
import { Command } from "commander";
|
|
6
6
|
|
|
7
7
|
// src/analyzer/burstDetector.ts
|
|
@@ -363,12 +363,30 @@ import chalk from "chalk";
|
|
|
363
363
|
import Table from "cli-table3";
|
|
364
364
|
|
|
365
365
|
// src/types/index.ts
|
|
366
|
-
var VERSION = "0.
|
|
367
|
-
function classifyLag(lag) {
|
|
368
|
-
if (lag
|
|
369
|
-
if (
|
|
366
|
+
var VERSION = "0.3.1";
|
|
367
|
+
function classifyLag(lag, consumeRate) {
|
|
368
|
+
if (lag === 0n) return "OK";
|
|
369
|
+
if (consumeRate !== void 0) {
|
|
370
|
+
if (consumeRate === 0) return "HIGH";
|
|
371
|
+
const drainSec = Number(lag) / consumeRate;
|
|
372
|
+
if (drainSec < 60) return "OK";
|
|
373
|
+
if (drainSec < 300) return "WARN";
|
|
374
|
+
return "HIGH";
|
|
375
|
+
}
|
|
376
|
+
if (lag < 10000n) return "OK";
|
|
377
|
+
if (lag < 100000n) return "WARN";
|
|
370
378
|
return "HIGH";
|
|
371
379
|
}
|
|
380
|
+
function formatDrainTime(lag, consumeRate) {
|
|
381
|
+
if (lag === 0n) return "\u2014";
|
|
382
|
+
if (consumeRate === 0) return "\u221E";
|
|
383
|
+
const sec = Math.ceil(Number(lag) / consumeRate);
|
|
384
|
+
if (sec < 60) return `${sec}s`;
|
|
385
|
+
const m = Math.floor(sec / 60);
|
|
386
|
+
const s = sec % 60;
|
|
387
|
+
if (sec < 3600) return s > 0 ? `${m}m${s}s` : `${m}m`;
|
|
388
|
+
return `>${Math.floor(sec / 3600)}h`;
|
|
389
|
+
}
|
|
372
390
|
|
|
373
391
|
// src/reporter/tableReporter.ts
|
|
374
392
|
var LEVEL_ICON = {
|
|
@@ -388,8 +406,8 @@ function formatTrend(lagDiff) {
|
|
|
388
406
|
if (lagDiff > 0n) return chalk.red(`\u25B2 +${lagDiff.toLocaleString()}`);
|
|
389
407
|
return chalk.green(`\u25BC ${lagDiff.toLocaleString()}`);
|
|
390
408
|
}
|
|
391
|
-
function groupStatus(totalLag) {
|
|
392
|
-
const level = classifyLag(totalLag);
|
|
409
|
+
function groupStatus(totalLag, totalConsumeRate) {
|
|
410
|
+
const level = classifyLag(totalLag, totalConsumeRate);
|
|
393
411
|
if (level === "OK") return chalk.green("\u2705 OK");
|
|
394
412
|
if (level === "WARN") return chalk.yellow("\u26A0\uFE0F WARNING");
|
|
395
413
|
return chalk.red("\u{1F6A8} CRITICAL");
|
|
@@ -418,18 +436,25 @@ function printLagTable(snapshot, rcaResults = [], rateSnapshot, watchMode = fals
|
|
|
418
436
|
chalk.bold(" Collected At: ") + chalk.gray(`${localTime} (${tz})`)
|
|
419
437
|
);
|
|
420
438
|
console.log("");
|
|
421
|
-
const status = groupStatus(totalLag);
|
|
422
|
-
const totalStr = chalk.bold(formatLag(totalLag));
|
|
423
|
-
console.log(` Group Status : ${status} Total Lag : ${totalStr}`);
|
|
424
|
-
console.log("");
|
|
425
439
|
const hasRate = !!rateSnapshot && rateSnapshot.partitions.length > 0;
|
|
426
440
|
const hasTrend = watchMode;
|
|
427
441
|
const rateMap = /* @__PURE__ */ new Map();
|
|
442
|
+
let totalConsumeRate;
|
|
428
443
|
if (hasRate && rateSnapshot) {
|
|
444
|
+
let sum = 0;
|
|
429
445
|
for (const r of rateSnapshot.partitions) {
|
|
430
446
|
rateMap.set(`${r.topic}-${r.partition}`, r);
|
|
447
|
+
sum += r.consumeRate;
|
|
431
448
|
}
|
|
449
|
+
totalConsumeRate = sum;
|
|
432
450
|
}
|
|
451
|
+
const status = groupStatus(totalLag, totalConsumeRate);
|
|
452
|
+
const totalStr = chalk.bold(formatLag(totalLag));
|
|
453
|
+
const drainStr = totalConsumeRate !== void 0 ? ` Drain : ${chalk.cyan(formatDrainTime(totalLag, totalConsumeRate))}` : "";
|
|
454
|
+
console.log(
|
|
455
|
+
` Group Status : ${status} Total Lag : ${totalStr}${drainStr}`
|
|
456
|
+
);
|
|
457
|
+
console.log("");
|
|
433
458
|
const head = [
|
|
434
459
|
chalk.bold("Topic"),
|
|
435
460
|
chalk.bold("Partition"),
|
|
@@ -438,7 +463,11 @@ function printLagTable(snapshot, rcaResults = [], rateSnapshot, watchMode = fals
|
|
|
438
463
|
chalk.bold("Lag"),
|
|
439
464
|
...hasTrend ? [chalk.bold("Trend")] : [],
|
|
440
465
|
chalk.bold("Status"),
|
|
441
|
-
...hasRate ? [
|
|
466
|
+
...hasRate ? [
|
|
467
|
+
chalk.bold("Drain"),
|
|
468
|
+
chalk.bold("Produce Rate"),
|
|
469
|
+
chalk.bold("Consume Rate")
|
|
470
|
+
] : []
|
|
442
471
|
];
|
|
443
472
|
const table = new Table({
|
|
444
473
|
head,
|
|
@@ -450,16 +479,19 @@ function printLagTable(snapshot, rcaResults = [], rateSnapshot, watchMode = fals
|
|
|
450
479
|
"right",
|
|
451
480
|
...hasTrend ? ["right"] : [],
|
|
452
481
|
"center",
|
|
453
|
-
...hasRate ? ["right", "right"] : []
|
|
482
|
+
...hasRate ? ["right", "right", "right"] : []
|
|
454
483
|
],
|
|
455
484
|
style: { head: [], border: ["grey"] }
|
|
456
485
|
});
|
|
457
486
|
let lastTopic = "";
|
|
458
487
|
for (const p of partitions) {
|
|
459
|
-
const level = classifyLag(p.lag);
|
|
460
|
-
const lagStr = level === "HIGH" ? chalk.red(formatLag(p.lag)) : level === "WARN" ? chalk.yellow(formatLag(p.lag)) : chalk.green(formatLag(p.lag));
|
|
461
488
|
const rateEntry = rateMap.get(`${p.topic}-${p.partition}`);
|
|
489
|
+
const level = classifyLag(p.lag, rateEntry?.consumeRate);
|
|
490
|
+
const lagStr = level === "HIGH" ? chalk.red(formatLag(p.lag)) : level === "WARN" ? chalk.yellow(formatLag(p.lag)) : chalk.green(formatLag(p.lag));
|
|
462
491
|
const rateColumns = hasRate ? [
|
|
492
|
+
chalk.cyan(
|
|
493
|
+
rateEntry !== void 0 ? formatDrainTime(p.lag, rateEntry.consumeRate) : "\u2014"
|
|
494
|
+
),
|
|
463
495
|
chalk.yellow(formatRate(rateEntry?.produceRate ?? 0)),
|
|
464
496
|
chalk.cyan(formatRate(rateEntry?.consumeRate ?? 0))
|
|
465
497
|
] : [];
|
|
@@ -614,6 +646,104 @@ function parseConfig(filePath) {
|
|
|
614
646
|
return obj;
|
|
615
647
|
}
|
|
616
648
|
|
|
649
|
+
// src/cli/groupPicker.ts
|
|
650
|
+
import chalk4 from "chalk";
|
|
651
|
+
import prompts from "prompts";
|
|
652
|
+
var GROUP_STATE_ORDER = {
|
|
653
|
+
Stable: 0,
|
|
654
|
+
Empty: 1,
|
|
655
|
+
PreparingRebalance: 2,
|
|
656
|
+
CompletingRebalance: 3,
|
|
657
|
+
Dead: 4
|
|
658
|
+
};
|
|
659
|
+
async function pickGroup(options) {
|
|
660
|
+
const kafka = createKafkaClient("klag-picker", { ...options, groupId: "" });
|
|
661
|
+
const admin = kafka.admin();
|
|
662
|
+
process.stdout.write(chalk4.gray(" Fetching consumer groups..."));
|
|
663
|
+
let groups = [];
|
|
664
|
+
try {
|
|
665
|
+
await admin.connect();
|
|
666
|
+
const result = await admin.listGroups();
|
|
667
|
+
groups = result.groups;
|
|
668
|
+
} finally {
|
|
669
|
+
await admin.disconnect();
|
|
670
|
+
}
|
|
671
|
+
process.stdout.write(`\r${" ".repeat(40)}\r`);
|
|
672
|
+
if (groups.length === 0) {
|
|
673
|
+
console.error(chalk4.red("\n\u274C No consumer groups found on this broker\n"));
|
|
674
|
+
process.exit(1);
|
|
675
|
+
}
|
|
676
|
+
const kafka2 = createKafkaClient("klag-picker-desc", {
|
|
677
|
+
...options,
|
|
678
|
+
groupId: ""
|
|
679
|
+
});
|
|
680
|
+
const admin2 = kafka2.admin();
|
|
681
|
+
process.stdout.write(chalk4.gray(" Loading group states... "));
|
|
682
|
+
const stateMap = /* @__PURE__ */ new Map();
|
|
683
|
+
try {
|
|
684
|
+
await admin2.connect();
|
|
685
|
+
const groupIds = groups.map((g) => g.groupId);
|
|
686
|
+
const described = await admin2.describeGroups(groupIds);
|
|
687
|
+
for (const g of described.groups) {
|
|
688
|
+
stateMap.set(g.groupId, g.state);
|
|
689
|
+
}
|
|
690
|
+
} catch {
|
|
691
|
+
} finally {
|
|
692
|
+
await admin2.disconnect();
|
|
693
|
+
}
|
|
694
|
+
process.stdout.write(`\r${" ".repeat(40)}\r`);
|
|
695
|
+
const sorted = [...groups].sort((a, b) => {
|
|
696
|
+
const stateA = GROUP_STATE_ORDER[stateMap.get(a.groupId) ?? ""] ?? 99;
|
|
697
|
+
const stateB = GROUP_STATE_ORDER[stateMap.get(b.groupId) ?? ""] ?? 99;
|
|
698
|
+
return stateA !== stateB ? stateA - stateB : a.groupId.localeCompare(b.groupId);
|
|
699
|
+
});
|
|
700
|
+
const choices = sorted.map((g) => {
|
|
701
|
+
const state = stateMap.get(g.groupId);
|
|
702
|
+
const stateLabel = state ? stateColor(state) : chalk4.gray("unknown");
|
|
703
|
+
return {
|
|
704
|
+
title: `${g.groupId} ${stateLabel}`,
|
|
705
|
+
value: g.groupId
|
|
706
|
+
};
|
|
707
|
+
});
|
|
708
|
+
console.log("");
|
|
709
|
+
const response = await prompts(
|
|
710
|
+
{
|
|
711
|
+
type: "autocomplete",
|
|
712
|
+
name: "groupId",
|
|
713
|
+
message: "Select a consumer group",
|
|
714
|
+
choices,
|
|
715
|
+
suggest: (input, choices2) => Promise.resolve(
|
|
716
|
+
choices2.filter(
|
|
717
|
+
(c) => c.value.toLowerCase().includes(input.toLowerCase())
|
|
718
|
+
)
|
|
719
|
+
)
|
|
720
|
+
},
|
|
721
|
+
{
|
|
722
|
+
onCancel: () => {
|
|
723
|
+
console.log(chalk4.gray("\n Cancelled\n"));
|
|
724
|
+
process.exit(0);
|
|
725
|
+
}
|
|
726
|
+
}
|
|
727
|
+
);
|
|
728
|
+
console.log("");
|
|
729
|
+
return response.groupId;
|
|
730
|
+
}
|
|
731
|
+
function stateColor(state) {
|
|
732
|
+
switch (state) {
|
|
733
|
+
case "Stable":
|
|
734
|
+
return chalk4.green(`(${state})`);
|
|
735
|
+
case "Empty":
|
|
736
|
+
return chalk4.gray(`(${state})`);
|
|
737
|
+
case "PreparingRebalance":
|
|
738
|
+
case "CompletingRebalance":
|
|
739
|
+
return chalk4.yellow(`(${state})`);
|
|
740
|
+
case "Dead":
|
|
741
|
+
return chalk4.red(`(${state})`);
|
|
742
|
+
default:
|
|
743
|
+
return chalk4.gray(`(${state})`);
|
|
744
|
+
}
|
|
745
|
+
}
|
|
746
|
+
|
|
617
747
|
// src/cli/validators.ts
|
|
618
748
|
import { existsSync as existsSync2 } from "fs";
|
|
619
749
|
import { InvalidArgumentError } from "commander";
|
|
@@ -667,7 +797,7 @@ function parseCertPath(value) {
|
|
|
667
797
|
}
|
|
668
798
|
|
|
669
799
|
// src/cli/watcher.ts
|
|
670
|
-
import
|
|
800
|
+
import chalk5 from "chalk";
|
|
671
801
|
var MAX_RETRIES = 3;
|
|
672
802
|
function clearScreen() {
|
|
673
803
|
process.stdout.write("\x1Bc");
|
|
@@ -683,31 +813,31 @@ function printWatchHeader(intervalMs, updatedAt) {
|
|
|
683
813
|
hour12: false
|
|
684
814
|
});
|
|
685
815
|
console.log(
|
|
686
|
-
|
|
816
|
+
chalk5.bold.cyan("\u26A1 klag") + chalk5.gray(` v${VERSION}`) + " \u2502 " + chalk5.yellow("watch mode") + " \u2502 " + chalk5.gray(`${intervalSec}s refresh`) + " \u2502 " + chalk5.gray("Ctrl+C to exit")
|
|
687
817
|
);
|
|
688
|
-
console.log(
|
|
818
|
+
console.log(chalk5.gray(` Last updated: ${timeStr} (${tz})`));
|
|
689
819
|
}
|
|
690
820
|
function printWatchError(message, retryCount, retryIn) {
|
|
691
821
|
clearScreen();
|
|
692
822
|
console.log(
|
|
693
|
-
|
|
823
|
+
chalk5.bold.cyan("\u26A1 klag") + chalk5.gray(` v${VERSION}`) + " \u2502 " + chalk5.yellow("watch mode") + " \u2502 " + chalk5.gray("Ctrl+C to exit")
|
|
694
824
|
);
|
|
695
825
|
console.log("");
|
|
696
|
-
console.error(
|
|
826
|
+
console.error(chalk5.red(` \u274C Error: ${message}`));
|
|
697
827
|
console.log(
|
|
698
|
-
|
|
828
|
+
chalk5.yellow(` Retrying ${retryCount}/${MAX_RETRIES}... in ${retryIn}s`)
|
|
699
829
|
);
|
|
700
830
|
console.log("");
|
|
701
831
|
}
|
|
702
832
|
function printWatchFatal(message) {
|
|
703
833
|
clearScreen();
|
|
704
834
|
console.log(
|
|
705
|
-
|
|
835
|
+
chalk5.bold.cyan("\u26A1 klag") + chalk5.gray(` v${VERSION}`) + " \u2502 " + chalk5.yellow("watch mode")
|
|
706
836
|
);
|
|
707
837
|
console.log("");
|
|
708
|
-
console.error(
|
|
838
|
+
console.error(chalk5.red(` \u274C Error: ${message}`));
|
|
709
839
|
console.error(
|
|
710
|
-
|
|
840
|
+
chalk5.red(` All ${MAX_RETRIES} retries failed \u2014 exiting watch mode`)
|
|
711
841
|
);
|
|
712
842
|
console.log("");
|
|
713
843
|
}
|
|
@@ -730,7 +860,7 @@ async function runOnce(options, noRate, previous) {
|
|
|
730
860
|
const topics = [...new Set(snapshot.partitions.map((p) => p.topic))];
|
|
731
861
|
const waitSec = (options.intervalMs ?? 5e3) / 1e3;
|
|
732
862
|
process.stdout.write(
|
|
733
|
-
|
|
863
|
+
chalk5.gray(` Sampling rates... (waiting ${waitSec}s) `)
|
|
734
864
|
);
|
|
735
865
|
rateSnapshot = await collectRate(options, topics);
|
|
736
866
|
process.stdout.write(`\r${" ".repeat(50)}\r`);
|
|
@@ -747,7 +877,7 @@ function printCountdown(seconds) {
|
|
|
747
877
|
let remaining = seconds;
|
|
748
878
|
const tick = () => {
|
|
749
879
|
process.stdout.write(
|
|
750
|
-
`\r${
|
|
880
|
+
`\r${chalk5.gray(` [\u25CF] Next refresh in ${remaining}s...`)} `
|
|
751
881
|
);
|
|
752
882
|
if (remaining === 0) {
|
|
753
883
|
process.stdout.write(`\r${" ".repeat(40)}\r`);
|
|
@@ -772,12 +902,12 @@ function getFriendlyMessage(err, broker) {
|
|
|
772
902
|
}
|
|
773
903
|
async function startWatch(options, noRate) {
|
|
774
904
|
process.on("SIGINT", () => {
|
|
775
|
-
console.log(
|
|
905
|
+
console.log(chalk5.gray("\n\n Watch mode exited\n"));
|
|
776
906
|
process.exit(0);
|
|
777
907
|
});
|
|
778
908
|
const intervalMs = options.intervalMs ?? 5e3;
|
|
779
909
|
const waitSec = Math.ceil(intervalMs / 1e3);
|
|
780
|
-
process.stdout.write(
|
|
910
|
+
process.stdout.write(chalk5.gray(" Connecting to broker..."));
|
|
781
911
|
let errorCount = 0;
|
|
782
912
|
let previousSnapshot;
|
|
783
913
|
while (true) {
|
|
@@ -808,7 +938,10 @@ program.name("klag").description("Kafka consumer lag root cause analyzer").versi
|
|
|
808
938
|
"Kafka broker address",
|
|
809
939
|
parseBroker,
|
|
810
940
|
"localhost:9092"
|
|
811
|
-
).
|
|
941
|
+
).option(
|
|
942
|
+
"-g, --group <groupId>",
|
|
943
|
+
"Consumer group ID (omit to pick interactively)"
|
|
944
|
+
).option(
|
|
812
945
|
"-i, --interval <ms>",
|
|
813
946
|
"Rate sampling interval in ms",
|
|
814
947
|
parseInterval,
|
|
@@ -833,12 +966,11 @@ program.name("klag").description("Kafka consumer lag root cause analyzer").versi
|
|
|
833
966
|
const rc = loaded?.config ?? {};
|
|
834
967
|
if (loaded) {
|
|
835
968
|
process.stderr.write(
|
|
836
|
-
|
|
969
|
+
chalk6.gray(` Using config: ${loaded.loadedFrom}
|
|
837
970
|
`)
|
|
838
971
|
);
|
|
839
972
|
}
|
|
840
973
|
const broker = options.broker !== "localhost:9092" ? options.broker : rc.broker ?? options.broker;
|
|
841
|
-
const groupId = options.group ?? rc.group;
|
|
842
974
|
const intervalMs = options.interval !== 5e3 ? options.interval : rc.interval ?? options.interval;
|
|
843
975
|
const timeoutMs = options.timeout !== 5e3 ? options.timeout : rc.timeout ?? options.timeout;
|
|
844
976
|
const auth = buildAuthOptions({
|
|
@@ -850,18 +982,14 @@ program.name("klag").description("Kafka consumer lag root cause analyzer").versi
|
|
|
850
982
|
saslUsername: options.saslUsername ?? rc.sasl?.username,
|
|
851
983
|
saslPassword: options.saslPassword ?? rc.sasl?.password
|
|
852
984
|
});
|
|
853
|
-
const
|
|
854
|
-
|
|
855
|
-
|
|
856
|
-
intervalMs,
|
|
857
|
-
timeoutMs,
|
|
858
|
-
...auth
|
|
859
|
-
};
|
|
985
|
+
const baseOptions = { broker, intervalMs, timeoutMs, ...auth };
|
|
986
|
+
const groupId = options.group ?? rc.group ?? await pickGroup(baseOptions);
|
|
987
|
+
const kafkaOptions = { ...baseOptions, groupId };
|
|
860
988
|
if (options.watch) {
|
|
861
989
|
await startWatch(kafkaOptions, options.rate === false);
|
|
862
990
|
return;
|
|
863
991
|
}
|
|
864
|
-
process.stdout.write(
|
|
992
|
+
process.stdout.write(chalk6.gray(" Connecting to broker..."));
|
|
865
993
|
const snapshot = await collectLag(kafkaOptions);
|
|
866
994
|
process.stdout.write(`\r${" ".repeat(50)}\r`);
|
|
867
995
|
let rateSnapshot;
|
|
@@ -869,7 +997,7 @@ program.name("klag").description("Kafka consumer lag root cause analyzer").versi
|
|
|
869
997
|
const topics = [...new Set(snapshot.partitions.map((p) => p.topic))];
|
|
870
998
|
const waitSec = (kafkaOptions.intervalMs ?? 5e3) / 1e3;
|
|
871
999
|
process.stdout.write(
|
|
872
|
-
|
|
1000
|
+
chalk6.gray(` Sampling rates... (waiting ${waitSec}s) `)
|
|
873
1001
|
);
|
|
874
1002
|
rateSnapshot = await collectRate(kafkaOptions, topics);
|
|
875
1003
|
process.stdout.write(`\r${" ".repeat(50)}\r`);
|
|
@@ -897,14 +1025,14 @@ program.name("klag").description("Kafka consumer lag root cause analyzer").versi
|
|
|
897
1025
|
process.stdout.write(`\r${" ".repeat(50)}\r`);
|
|
898
1026
|
const message = err instanceof Error ? err.message : String(err);
|
|
899
1027
|
if (message.includes("ECONNREFUSED") || message.includes("ETIMEDOUT") || message.includes("Connection error") || message.includes("connect ECONNREFUSED")) {
|
|
900
|
-
console.error(
|
|
1028
|
+
console.error(chalk6.red(`
|
|
901
1029
|
\u274C Cannot connect to broker
|
|
902
1030
|
`));
|
|
903
|
-
console.error(
|
|
904
|
-
console.error(
|
|
905
|
-
console.error(
|
|
1031
|
+
console.error(chalk6.yellow(" Check the following:"));
|
|
1032
|
+
console.error(chalk6.gray(` \u2022 Is Kafka running: docker ps`));
|
|
1033
|
+
console.error(chalk6.gray(` \u2022 Broker address: ${options.broker}`));
|
|
906
1034
|
console.error(
|
|
907
|
-
|
|
1035
|
+
chalk6.gray(
|
|
908
1036
|
` \u2022 Port accessibility: nc -zv ${options.broker.split(":")[0]} ${options.broker.split(":")[1]}`
|
|
909
1037
|
)
|
|
910
1038
|
);
|
|
@@ -912,18 +1040,18 @@ program.name("klag").description("Kafka consumer lag root cause analyzer").versi
|
|
|
912
1040
|
process.exit(1);
|
|
913
1041
|
}
|
|
914
1042
|
if (message.includes("SASLAuthenticationFailed") || message.includes("Authentication failed") || message.includes("SASL")) {
|
|
915
|
-
console.error(
|
|
1043
|
+
console.error(chalk6.red(`
|
|
916
1044
|
\u274C SASL authentication failed
|
|
917
1045
|
`));
|
|
918
|
-
console.error(
|
|
1046
|
+
console.error(chalk6.yellow(" Check the following:"));
|
|
919
1047
|
console.error(
|
|
920
|
-
|
|
1048
|
+
chalk6.gray(` \u2022 Mechanism: ${options.saslMechanism ?? "(none)"}`)
|
|
921
1049
|
);
|
|
922
1050
|
console.error(
|
|
923
|
-
|
|
1051
|
+
chalk6.gray(` \u2022 Username: ${options.saslUsername ?? "(none)"}`)
|
|
924
1052
|
);
|
|
925
1053
|
console.error(
|
|
926
|
-
|
|
1054
|
+
chalk6.gray(
|
|
927
1055
|
` \u2022 Password: set via KLAG_SASL_PASSWORD or --sasl-password`
|
|
928
1056
|
)
|
|
929
1057
|
);
|
|
@@ -931,21 +1059,21 @@ program.name("klag").description("Kafka consumer lag root cause analyzer").versi
|
|
|
931
1059
|
process.exit(1);
|
|
932
1060
|
}
|
|
933
1061
|
if (message.includes("not found") || message.includes("Dead state")) {
|
|
934
|
-
console.error(
|
|
1062
|
+
console.error(chalk6.red(`
|
|
935
1063
|
\u274C Consumer group not found
|
|
936
1064
|
`));
|
|
937
|
-
console.error(
|
|
938
|
-
console.error(
|
|
939
|
-
console.error(
|
|
1065
|
+
console.error(chalk6.yellow(" Check the following:"));
|
|
1066
|
+
console.error(chalk6.gray(` \u2022 Group ID: ${options.group}`));
|
|
1067
|
+
console.error(chalk6.gray(` \u2022 List existing groups:`));
|
|
940
1068
|
console.error(
|
|
941
|
-
|
|
1069
|
+
chalk6.gray(
|
|
942
1070
|
` kafka-consumer-groups.sh --bootstrap-server ${options.broker} --list`
|
|
943
1071
|
)
|
|
944
1072
|
);
|
|
945
1073
|
console.error("");
|
|
946
1074
|
process.exit(1);
|
|
947
1075
|
}
|
|
948
|
-
console.error(
|
|
1076
|
+
console.error(chalk6.red(`
|
|
949
1077
|
\u274C Error: ${message}
|
|
950
1078
|
`));
|
|
951
1079
|
process.exit(1);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@closeup1202/klag",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.3.1",
|
|
4
4
|
"description": "Kafka consumer lag root cause analyzer",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -39,11 +39,13 @@
|
|
|
39
39
|
"chalk": "^5.6.2",
|
|
40
40
|
"cli-table3": "^0.6.5",
|
|
41
41
|
"commander": "^14.0.3",
|
|
42
|
-
"kafkajs": "^2.2.4"
|
|
42
|
+
"kafkajs": "^2.2.4",
|
|
43
|
+
"prompts": "^2.4.2"
|
|
43
44
|
},
|
|
44
45
|
"devDependencies": {
|
|
45
46
|
"@biomejs/biome": "^2.4.8",
|
|
46
47
|
"@types/node": "^25.5.0",
|
|
48
|
+
"@types/prompts": "^2.4.9",
|
|
47
49
|
"tsup": "^8.5.1",
|
|
48
50
|
"tsx": "^4.21.0",
|
|
49
51
|
"typescript": "^6.0.2",
|