@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.
- package/dist/cli/index.js +139 -43
- 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
|
|
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.
|
|
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
|
|
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
|
-
|
|
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(
|
|
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
|
-
|
|
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(
|
|
826
|
+
console.error(chalk5.red(` \u274C Error: ${message}`));
|
|
729
827
|
console.log(
|
|
730
|
-
|
|
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
|
-
|
|
835
|
+
chalk5.bold.cyan("\u26A1 klag") + chalk5.gray(` v${VERSION}`) + " \u2502 " + chalk5.yellow("watch mode")
|
|
738
836
|
);
|
|
739
837
|
console.log("");
|
|
740
|
-
console.error(
|
|
838
|
+
console.error(chalk5.red(` \u274C Error: ${message}`));
|
|
741
839
|
console.error(
|
|
742
|
-
|
|
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
|
-
|
|
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${
|
|
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(
|
|
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(
|
|
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
|
-
).
|
|
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
|
-
|
|
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
|
|
886
|
-
|
|
887
|
-
|
|
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(
|
|
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
|
-
|
|
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(
|
|
1028
|
+
console.error(chalk6.red(`
|
|
933
1029
|
\u274C Cannot connect to broker
|
|
934
1030
|
`));
|
|
935
|
-
console.error(
|
|
936
|
-
console.error(
|
|
937
|
-
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}`));
|
|
938
1034
|
console.error(
|
|
939
|
-
|
|
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(
|
|
1043
|
+
console.error(chalk6.red(`
|
|
948
1044
|
\u274C SASL authentication failed
|
|
949
1045
|
`));
|
|
950
|
-
console.error(
|
|
1046
|
+
console.error(chalk6.yellow(" Check the following:"));
|
|
951
1047
|
console.error(
|
|
952
|
-
|
|
1048
|
+
chalk6.gray(` \u2022 Mechanism: ${options.saslMechanism ?? "(none)"}`)
|
|
953
1049
|
);
|
|
954
1050
|
console.error(
|
|
955
|
-
|
|
1051
|
+
chalk6.gray(` \u2022 Username: ${options.saslUsername ?? "(none)"}`)
|
|
956
1052
|
);
|
|
957
1053
|
console.error(
|
|
958
|
-
|
|
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(
|
|
1062
|
+
console.error(chalk6.red(`
|
|
967
1063
|
\u274C Consumer group not found
|
|
968
1064
|
`));
|
|
969
|
-
console.error(
|
|
970
|
-
console.error(
|
|
971
|
-
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:`));
|
|
972
1068
|
console.error(
|
|
973
|
-
|
|
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(
|
|
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.
|
|
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",
|