@closeup1202/klag 0.3.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.
Files changed (2) hide show
  1. package/dist/cli/index.js +139 -43
  2. package/package.json +4 -2
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 chalk5 from "chalk";
4
+ import chalk6 from "chalk";
5
5
  import { Command } from "commander";
6
6
 
7
7
  // src/analyzer/burstDetector.ts
@@ -363,7 +363,7 @@ import chalk from "chalk";
363
363
  import Table from "cli-table3";
364
364
 
365
365
  // src/types/index.ts
366
- var VERSION = "0.3.0";
366
+ var VERSION = "0.3.1";
367
367
  function classifyLag(lag, consumeRate) {
368
368
  if (lag === 0n) return "OK";
369
369
  if (consumeRate !== void 0) {
@@ -646,6 +646,104 @@ function parseConfig(filePath) {
646
646
  return obj;
647
647
  }
648
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
+
649
747
  // src/cli/validators.ts
650
748
  import { existsSync as existsSync2 } from "fs";
651
749
  import { InvalidArgumentError } from "commander";
@@ -699,7 +797,7 @@ function parseCertPath(value) {
699
797
  }
700
798
 
701
799
  // src/cli/watcher.ts
702
- import chalk4 from "chalk";
800
+ import chalk5 from "chalk";
703
801
  var MAX_RETRIES = 3;
704
802
  function clearScreen() {
705
803
  process.stdout.write("\x1Bc");
@@ -715,31 +813,31 @@ function printWatchHeader(intervalMs, updatedAt) {
715
813
  hour12: false
716
814
  });
717
815
  console.log(
718
- chalk4.bold.cyan("\u26A1 klag") + chalk4.gray(` v${VERSION}`) + " \u2502 " + chalk4.yellow("watch mode") + " \u2502 " + chalk4.gray(`${intervalSec}s refresh`) + " \u2502 " + chalk4.gray("Ctrl+C to exit")
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")
719
817
  );
720
- console.log(chalk4.gray(` Last updated: ${timeStr} (${tz})`));
818
+ console.log(chalk5.gray(` Last updated: ${timeStr} (${tz})`));
721
819
  }
722
820
  function printWatchError(message, retryCount, retryIn) {
723
821
  clearScreen();
724
822
  console.log(
725
- chalk4.bold.cyan("\u26A1 klag") + chalk4.gray(` v${VERSION}`) + " \u2502 " + chalk4.yellow("watch mode") + " \u2502 " + chalk4.gray("Ctrl+C to exit")
823
+ chalk5.bold.cyan("\u26A1 klag") + chalk5.gray(` v${VERSION}`) + " \u2502 " + chalk5.yellow("watch mode") + " \u2502 " + chalk5.gray("Ctrl+C to exit")
726
824
  );
727
825
  console.log("");
728
- console.error(chalk4.red(` \u274C Error: ${message}`));
826
+ console.error(chalk5.red(` \u274C Error: ${message}`));
729
827
  console.log(
730
- chalk4.yellow(` Retrying ${retryCount}/${MAX_RETRIES}... in ${retryIn}s`)
828
+ chalk5.yellow(` Retrying ${retryCount}/${MAX_RETRIES}... in ${retryIn}s`)
731
829
  );
732
830
  console.log("");
733
831
  }
734
832
  function printWatchFatal(message) {
735
833
  clearScreen();
736
834
  console.log(
737
- chalk4.bold.cyan("\u26A1 klag") + chalk4.gray(` v${VERSION}`) + " \u2502 " + chalk4.yellow("watch mode")
835
+ chalk5.bold.cyan("\u26A1 klag") + chalk5.gray(` v${VERSION}`) + " \u2502 " + chalk5.yellow("watch mode")
738
836
  );
739
837
  console.log("");
740
- console.error(chalk4.red(` \u274C Error: ${message}`));
838
+ console.error(chalk5.red(` \u274C Error: ${message}`));
741
839
  console.error(
742
- chalk4.red(` All ${MAX_RETRIES} retries failed \u2014 exiting watch mode`)
840
+ chalk5.red(` All ${MAX_RETRIES} retries failed \u2014 exiting watch mode`)
743
841
  );
744
842
  console.log("");
745
843
  }
@@ -762,7 +860,7 @@ async function runOnce(options, noRate, previous) {
762
860
  const topics = [...new Set(snapshot.partitions.map((p) => p.topic))];
763
861
  const waitSec = (options.intervalMs ?? 5e3) / 1e3;
764
862
  process.stdout.write(
765
- chalk4.gray(` Sampling rates... (waiting ${waitSec}s) `)
863
+ chalk5.gray(` Sampling rates... (waiting ${waitSec}s) `)
766
864
  );
767
865
  rateSnapshot = await collectRate(options, topics);
768
866
  process.stdout.write(`\r${" ".repeat(50)}\r`);
@@ -779,7 +877,7 @@ function printCountdown(seconds) {
779
877
  let remaining = seconds;
780
878
  const tick = () => {
781
879
  process.stdout.write(
782
- `\r${chalk4.gray(` [\u25CF] Next refresh in ${remaining}s...`)} `
880
+ `\r${chalk5.gray(` [\u25CF] Next refresh in ${remaining}s...`)} `
783
881
  );
784
882
  if (remaining === 0) {
785
883
  process.stdout.write(`\r${" ".repeat(40)}\r`);
@@ -804,12 +902,12 @@ function getFriendlyMessage(err, broker) {
804
902
  }
805
903
  async function startWatch(options, noRate) {
806
904
  process.on("SIGINT", () => {
807
- console.log(chalk4.gray("\n\n Watch mode exited\n"));
905
+ console.log(chalk5.gray("\n\n Watch mode exited\n"));
808
906
  process.exit(0);
809
907
  });
810
908
  const intervalMs = options.intervalMs ?? 5e3;
811
909
  const waitSec = Math.ceil(intervalMs / 1e3);
812
- process.stdout.write(chalk4.gray(" Connecting to broker..."));
910
+ process.stdout.write(chalk5.gray(" Connecting to broker..."));
813
911
  let errorCount = 0;
814
912
  let previousSnapshot;
815
913
  while (true) {
@@ -840,7 +938,10 @@ program.name("klag").description("Kafka consumer lag root cause analyzer").versi
840
938
  "Kafka broker address",
841
939
  parseBroker,
842
940
  "localhost:9092"
843
- ).requiredOption("-g, --group <groupId>", "Consumer group ID").option(
941
+ ).option(
942
+ "-g, --group <groupId>",
943
+ "Consumer group ID (omit to pick interactively)"
944
+ ).option(
844
945
  "-i, --interval <ms>",
845
946
  "Rate sampling interval in ms",
846
947
  parseInterval,
@@ -865,12 +966,11 @@ program.name("klag").description("Kafka consumer lag root cause analyzer").versi
865
966
  const rc = loaded?.config ?? {};
866
967
  if (loaded) {
867
968
  process.stderr.write(
868
- chalk5.gray(` Using config: ${loaded.loadedFrom}
969
+ chalk6.gray(` Using config: ${loaded.loadedFrom}
869
970
  `)
870
971
  );
871
972
  }
872
973
  const broker = options.broker !== "localhost:9092" ? options.broker : rc.broker ?? options.broker;
873
- const groupId = options.group ?? rc.group;
874
974
  const intervalMs = options.interval !== 5e3 ? options.interval : rc.interval ?? options.interval;
875
975
  const timeoutMs = options.timeout !== 5e3 ? options.timeout : rc.timeout ?? options.timeout;
876
976
  const auth = buildAuthOptions({
@@ -882,18 +982,14 @@ program.name("klag").description("Kafka consumer lag root cause analyzer").versi
882
982
  saslUsername: options.saslUsername ?? rc.sasl?.username,
883
983
  saslPassword: options.saslPassword ?? rc.sasl?.password
884
984
  });
885
- const kafkaOptions = {
886
- broker,
887
- groupId,
888
- intervalMs,
889
- timeoutMs,
890
- ...auth
891
- };
985
+ const baseOptions = { broker, intervalMs, timeoutMs, ...auth };
986
+ const groupId = options.group ?? rc.group ?? await pickGroup(baseOptions);
987
+ const kafkaOptions = { ...baseOptions, groupId };
892
988
  if (options.watch) {
893
989
  await startWatch(kafkaOptions, options.rate === false);
894
990
  return;
895
991
  }
896
- process.stdout.write(chalk5.gray(" Connecting to broker..."));
992
+ process.stdout.write(chalk6.gray(" Connecting to broker..."));
897
993
  const snapshot = await collectLag(kafkaOptions);
898
994
  process.stdout.write(`\r${" ".repeat(50)}\r`);
899
995
  let rateSnapshot;
@@ -901,7 +997,7 @@ program.name("klag").description("Kafka consumer lag root cause analyzer").versi
901
997
  const topics = [...new Set(snapshot.partitions.map((p) => p.topic))];
902
998
  const waitSec = (kafkaOptions.intervalMs ?? 5e3) / 1e3;
903
999
  process.stdout.write(
904
- chalk5.gray(` Sampling rates... (waiting ${waitSec}s) `)
1000
+ chalk6.gray(` Sampling rates... (waiting ${waitSec}s) `)
905
1001
  );
906
1002
  rateSnapshot = await collectRate(kafkaOptions, topics);
907
1003
  process.stdout.write(`\r${" ".repeat(50)}\r`);
@@ -929,14 +1025,14 @@ program.name("klag").description("Kafka consumer lag root cause analyzer").versi
929
1025
  process.stdout.write(`\r${" ".repeat(50)}\r`);
930
1026
  const message = err instanceof Error ? err.message : String(err);
931
1027
  if (message.includes("ECONNREFUSED") || message.includes("ETIMEDOUT") || message.includes("Connection error") || message.includes("connect ECONNREFUSED")) {
932
- console.error(chalk5.red(`
1028
+ console.error(chalk6.red(`
933
1029
  \u274C Cannot connect to broker
934
1030
  `));
935
- console.error(chalk5.yellow(" Check the following:"));
936
- console.error(chalk5.gray(` \u2022 Is Kafka running: docker ps`));
937
- console.error(chalk5.gray(` \u2022 Broker address: ${options.broker}`));
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}`));
938
1034
  console.error(
939
- chalk5.gray(
1035
+ chalk6.gray(
940
1036
  ` \u2022 Port accessibility: nc -zv ${options.broker.split(":")[0]} ${options.broker.split(":")[1]}`
941
1037
  )
942
1038
  );
@@ -944,18 +1040,18 @@ program.name("klag").description("Kafka consumer lag root cause analyzer").versi
944
1040
  process.exit(1);
945
1041
  }
946
1042
  if (message.includes("SASLAuthenticationFailed") || message.includes("Authentication failed") || message.includes("SASL")) {
947
- console.error(chalk5.red(`
1043
+ console.error(chalk6.red(`
948
1044
  \u274C SASL authentication failed
949
1045
  `));
950
- console.error(chalk5.yellow(" Check the following:"));
1046
+ console.error(chalk6.yellow(" Check the following:"));
951
1047
  console.error(
952
- chalk5.gray(` \u2022 Mechanism: ${options.saslMechanism ?? "(none)"}`)
1048
+ chalk6.gray(` \u2022 Mechanism: ${options.saslMechanism ?? "(none)"}`)
953
1049
  );
954
1050
  console.error(
955
- chalk5.gray(` \u2022 Username: ${options.saslUsername ?? "(none)"}`)
1051
+ chalk6.gray(` \u2022 Username: ${options.saslUsername ?? "(none)"}`)
956
1052
  );
957
1053
  console.error(
958
- chalk5.gray(
1054
+ chalk6.gray(
959
1055
  ` \u2022 Password: set via KLAG_SASL_PASSWORD or --sasl-password`
960
1056
  )
961
1057
  );
@@ -963,21 +1059,21 @@ program.name("klag").description("Kafka consumer lag root cause analyzer").versi
963
1059
  process.exit(1);
964
1060
  }
965
1061
  if (message.includes("not found") || message.includes("Dead state")) {
966
- console.error(chalk5.red(`
1062
+ console.error(chalk6.red(`
967
1063
  \u274C Consumer group not found
968
1064
  `));
969
- console.error(chalk5.yellow(" Check the following:"));
970
- console.error(chalk5.gray(` \u2022 Group ID: ${options.group}`));
971
- console.error(chalk5.gray(` \u2022 List existing groups:`));
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:`));
972
1068
  console.error(
973
- chalk5.gray(
1069
+ chalk6.gray(
974
1070
  ` kafka-consumer-groups.sh --bootstrap-server ${options.broker} --list`
975
1071
  )
976
1072
  );
977
1073
  console.error("");
978
1074
  process.exit(1);
979
1075
  }
980
- console.error(chalk5.red(`
1076
+ console.error(chalk6.red(`
981
1077
  \u274C Error: ${message}
982
1078
  `));
983
1079
  process.exit(1);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@closeup1202/klag",
3
- "version": "0.3.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",