@closeup1202/klag 0.1.0 → 0.2.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.
package/README.md CHANGED
@@ -66,6 +66,48 @@ klag --broker localhost:9092 --group my-service --watch --interval 3000
66
66
 
67
67
  # JSON output (CI/pipeline integration)
68
68
  klag --broker localhost:9092 --group my-service --json
69
+
70
+ # SSL (system CA trust)
71
+ klag --broker kafka.prod:9092 --group my-service --ssl
72
+
73
+ # SSL with custom certificates
74
+ klag --broker kafka.prod:9092 --group my-service \
75
+ --ssl --ssl-ca /etc/kafka/ca.pem \
76
+ --ssl-cert /etc/kafka/client.crt --ssl-key /etc/kafka/client.key
77
+
78
+ # SASL authentication (password via environment variable — recommended)
79
+ KLAG_SASL_PASSWORD=secret klag --broker kafka.prod:9092 --group my-service \
80
+ --sasl-mechanism scram-sha-256 --sasl-username kafka-user
81
+
82
+ # SSL + SASL combined
83
+ KLAG_SASL_PASSWORD=secret klag --broker kafka.prod:9092 --group my-service \
84
+ --ssl --sasl-mechanism scram-sha-256 --sasl-username kafka-user
85
+ ```
86
+
87
+ ## Config file (.klagrc)
88
+
89
+ Create `.klagrc` in the current directory or `~/.klagrc` to store default options.
90
+ CLI arguments always take precedence over the config file.
91
+
92
+ ```json
93
+ {
94
+ "broker": "kafka.prod.internal:9092",
95
+ "group": "my-service",
96
+ "interval": 3000,
97
+ "ssl": {
98
+ "enabled": true,
99
+ "caPath": "/etc/kafka/ca.pem"
100
+ },
101
+ "sasl": {
102
+ "mechanism": "scram-sha-256",
103
+ "username": "kafka-user"
104
+ }
105
+ }
106
+ ```
107
+
108
+ With this file in place, you only need:
109
+ ```bash
110
+ KLAG_SASL_PASSWORD=secret klag
69
111
  ```
70
112
 
71
113
  ## Options
@@ -79,6 +121,13 @@ klag --broker localhost:9092 --group my-service --json
79
121
  | `-w, --watch` | Watch mode | `false` |
80
122
  | `--no-rate` | Skip rate sampling | `false` |
81
123
  | `--json` | JSON output | `false` |
124
+ | `--ssl` | Enable SSL/TLS | `false` |
125
+ | `--ssl-ca <path>` | CA certificate PEM file | - |
126
+ | `--ssl-cert <path>` | Client certificate PEM file | - |
127
+ | `--ssl-key <path>` | Client key PEM file | - |
128
+ | `--sasl-mechanism <type>` | `plain`, `scram-sha-256`, `scram-sha-512` | - |
129
+ | `--sasl-username <user>` | SASL username | - |
130
+ | `--sasl-password <pass>` | SASL password (prefer `KLAG_SASL_PASSWORD` env var) | - |
82
131
 
83
132
  ## Detectable root causes
84
133
 
@@ -106,8 +155,9 @@ All consumption pauses during rebalancing, which can cause a temporary lag spike
106
155
  ## Roadmap
107
156
 
108
157
  - [x] v0.1.0 — lag collection, hot partition, producer burst, slow consumer, rebalancing detection, watch mode with lag trend (▲▼)
109
- - [ ] v0.2.0 — multi-group monitoring
110
- - [ ] v0.3.0 — Slack alerts, Prometheus export
158
+ - [x] v0.2.0 — SSL/SASL authentication, `.klagrc` config file support
159
+ - [ ] v0.3.0 — multi-group monitoring
160
+ - [ ] v0.4.0 — Slack alerts, Prometheus export
111
161
 
112
162
  ## License
113
163
 
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 chalk3 from "chalk";
4
+ import chalk5 from "chalk";
5
5
  import { Command } from "commander";
6
6
 
7
7
  // src/analyzer/burstDetector.ts
@@ -138,20 +138,49 @@ function analyze(snapshot, rateSnapshot) {
138
138
  }
139
139
 
140
140
  // src/collector/lagCollector.ts
141
- import { AssignerProtocol, Kafka, logLevel } from "kafkajs";
142
- async function collectLag(options) {
143
- const kafka = new Kafka({
144
- clientId: "klag",
141
+ import { AssignerProtocol } from "kafkajs";
142
+
143
+ // src/collector/kafkaFactory.ts
144
+ import { readFileSync } from "fs";
145
+ import { Kafka, logLevel } from "kafkajs";
146
+ function createKafkaClient(clientId, options) {
147
+ return new Kafka({
148
+ clientId,
145
149
  brokers: [options.broker],
146
150
  logLevel: logLevel.NOTHING,
147
- // Hide kafkajs internal logs in CLI
148
151
  requestTimeout: options.timeoutMs ?? 5e3,
149
152
  connectionTimeout: options.timeoutMs ?? 3e3,
150
- retry: {
151
- retries: 1
152
- // Added — only 1 retry (default is 5)
153
+ retry: { retries: 1 },
154
+ ...options.ssl && { ssl: buildSslConfig(options.ssl) },
155
+ ...options.sasl?.password && {
156
+ sasl: buildSaslConfig(
157
+ options.sasl
158
+ )
153
159
  }
154
160
  });
161
+ }
162
+ function buildSaslConfig(sasl) {
163
+ const { mechanism, username, password } = sasl;
164
+ if (mechanism === "plain") return { mechanism: "plain", username, password };
165
+ if (mechanism === "scram-sha-256")
166
+ return { mechanism: "scram-sha-256", username, password };
167
+ return { mechanism: "scram-sha-512", username, password };
168
+ }
169
+ function buildSslConfig(ssl) {
170
+ if (!ssl) return {};
171
+ if (!ssl.caPath && !ssl.certPath && !ssl.keyPath) {
172
+ return true;
173
+ }
174
+ return {
175
+ ...ssl.caPath && { ca: [readFileSync(ssl.caPath)] },
176
+ ...ssl.certPath && { cert: readFileSync(ssl.certPath) },
177
+ ...ssl.keyPath && { key: readFileSync(ssl.keyPath) }
178
+ };
179
+ }
180
+
181
+ // src/collector/lagCollector.ts
182
+ async function collectLag(options) {
183
+ const kafka = createKafkaClient("klag", options);
155
184
  const admin = kafka.admin();
156
185
  try {
157
186
  await admin.connect();
@@ -170,7 +199,9 @@ async function collectLag(options) {
170
199
  const decoded = AssignerProtocol.MemberAssignment.decode(
171
200
  member.memberAssignment
172
201
  );
173
- for (const [topic, partitions2] of Object.entries(decoded?.assignment)) {
202
+ for (const [topic, partitions2] of Object.entries(
203
+ decoded?.assignment ?? {}
204
+ )) {
174
205
  if (!topicPartitionMap.has(topic)) {
175
206
  topicPartitionMap.set(topic, /* @__PURE__ */ new Set());
176
207
  }
@@ -250,21 +281,10 @@ async function collectLag(options) {
250
281
  }
251
282
 
252
283
  // src/collector/rateCollector.ts
253
- import { Kafka as Kafka2, logLevel as logLevel2 } from "kafkajs";
254
284
  async function collectRate(options, knownTopics) {
255
285
  const intervalMs = options.intervalMs ?? 5e3;
256
286
  const intervalSec = intervalMs / 1e3;
257
- const kafka = new Kafka2({
258
- clientId: "klag-rate",
259
- brokers: [options.broker],
260
- logLevel: logLevel2.NOTHING,
261
- requestTimeout: options.timeoutMs ?? 5e3,
262
- connectionTimeout: options.timeoutMs ?? 3e3,
263
- retry: {
264
- retries: 1
265
- // Added — only 1 retry (default is 5)
266
- }
267
- });
287
+ const kafka = createKafkaClient("klag-rate", options);
268
288
  const admin = kafka.admin();
269
289
  try {
270
290
  await admin.connect();
@@ -343,7 +363,7 @@ import chalk from "chalk";
343
363
  import Table from "cli-table3";
344
364
 
345
365
  // src/types/index.ts
346
- var VERSION = "0.1.0";
366
+ var VERSION = "0.2.0";
347
367
  function classifyLag(lag) {
348
368
  if (lag < 100n) return "OK";
349
369
  if (lag < 1000n) return "WARN";
@@ -470,8 +490,138 @@ function printLagTable(snapshot, rcaResults = [], rateSnapshot, watchMode = fals
470
490
  }
471
491
  }
472
492
 
493
+ // src/cli/authBuilder.ts
494
+ import chalk2 from "chalk";
495
+ function buildAuthOptions(raw) {
496
+ const result = {};
497
+ if (raw.ssl || raw.sslCa || raw.sslCert || raw.sslKey) {
498
+ result.ssl = {
499
+ enabled: true,
500
+ ...raw.sslCa && { caPath: raw.sslCa },
501
+ ...raw.sslCert && { certPath: raw.sslCert },
502
+ ...raw.sslKey && { keyPath: raw.sslKey }
503
+ };
504
+ }
505
+ if (raw.saslMechanism) {
506
+ if (!raw.saslUsername) {
507
+ throw new Error(
508
+ "--sasl-username is required when --sasl-mechanism is specified."
509
+ );
510
+ }
511
+ const password = resolvePassword(raw.saslPassword);
512
+ result.sasl = {
513
+ mechanism: raw.saslMechanism,
514
+ username: raw.saslUsername,
515
+ password
516
+ };
517
+ }
518
+ return result;
519
+ }
520
+ function resolvePassword(cliPassword) {
521
+ const envPassword = process.env.KLAG_SASL_PASSWORD;
522
+ if (envPassword) {
523
+ return envPassword;
524
+ }
525
+ if (cliPassword) {
526
+ console.error(
527
+ chalk2.yellow(
528
+ "\n\u26A0 Warning: --sasl-password passed via CLI argument.\n This may be visible in process listings (ps aux).\n Consider using the KLAG_SASL_PASSWORD environment variable instead.\n"
529
+ )
530
+ );
531
+ return cliPassword;
532
+ }
533
+ throw new Error(
534
+ "SASL password is required.\n Set the KLAG_SASL_PASSWORD environment variable or use --sasl-password."
535
+ );
536
+ }
537
+
538
+ // src/cli/configLoader.ts
539
+ import { existsSync, readFileSync as readFileSync2 } from "fs";
540
+ import { homedir } from "os";
541
+ import { join } from "path";
542
+ import chalk3 from "chalk";
543
+ var RC_FILENAME = ".klagrc";
544
+ var KNOWN_KEYS = [
545
+ "broker",
546
+ "group",
547
+ "interval",
548
+ "timeout",
549
+ "ssl",
550
+ "sasl"
551
+ ];
552
+ function loadConfig() {
553
+ const candidates = [
554
+ join(process.cwd(), RC_FILENAME),
555
+ join(homedir(), RC_FILENAME)
556
+ ];
557
+ for (const filePath of candidates) {
558
+ if (existsSync(filePath)) {
559
+ return { config: parseConfig(filePath), loadedFrom: filePath };
560
+ }
561
+ }
562
+ return null;
563
+ }
564
+ function parseConfig(filePath) {
565
+ let raw;
566
+ try {
567
+ raw = JSON.parse(readFileSync2(filePath, "utf-8"));
568
+ } catch {
569
+ throw new Error(
570
+ `Failed to parse ${filePath}
571
+ Make sure it contains valid JSON.`
572
+ );
573
+ }
574
+ if (typeof raw !== "object" || raw === null || Array.isArray(raw)) {
575
+ throw new Error(`${filePath} must be a JSON object.`);
576
+ }
577
+ const obj = raw;
578
+ const unknownKeys = Object.keys(obj).filter(
579
+ (k) => !KNOWN_KEYS.includes(k)
580
+ );
581
+ if (unknownKeys.length > 0) {
582
+ console.error(
583
+ chalk3.yellow(
584
+ `
585
+ \u26A0 Unknown key(s) in ${filePath}: ${unknownKeys.join(", ")}
586
+ `
587
+ )
588
+ );
589
+ }
590
+ if (obj.broker !== void 0 && typeof obj.broker !== "string") {
591
+ throw new Error(`${filePath}: "broker" must be a string.`);
592
+ }
593
+ if (obj.group !== void 0 && typeof obj.group !== "string") {
594
+ throw new Error(`${filePath}: "group" must be a string.`);
595
+ }
596
+ if (obj.interval !== void 0 && typeof obj.interval !== "number") {
597
+ throw new Error(`${filePath}: "interval" must be a number.`);
598
+ }
599
+ if (obj.timeout !== void 0 && typeof obj.timeout !== "number") {
600
+ throw new Error(`${filePath}: "timeout" must be a number.`);
601
+ }
602
+ const sasl = obj.sasl;
603
+ if (sasl?.password) {
604
+ console.error(
605
+ chalk3.yellow(
606
+ `
607
+ \u26A0 Warning: SASL password found in ${filePath}.
608
+ Storing passwords in config files is not recommended.
609
+ Consider using the KLAG_SASL_PASSWORD environment variable instead.
610
+ `
611
+ )
612
+ );
613
+ }
614
+ return obj;
615
+ }
616
+
473
617
  // src/cli/validators.ts
618
+ import { existsSync as existsSync2 } from "fs";
474
619
  import { InvalidArgumentError } from "commander";
620
+ var VALID_SASL_MECHANISMS = [
621
+ "plain",
622
+ "scram-sha-256",
623
+ "scram-sha-512"
624
+ ];
475
625
  function parseInterval(value) {
476
626
  const parsed = parseInt(value, 10);
477
627
  if (Number.isNaN(parsed) || parsed < 1e3) {
@@ -501,9 +651,23 @@ function parseTimeout(value) {
501
651
  }
502
652
  return parsed;
503
653
  }
654
+ function parseSaslMechanism(value) {
655
+ if (!VALID_SASL_MECHANISMS.includes(value)) {
656
+ throw new InvalidArgumentError(
657
+ `--sasl-mechanism must be one of: ${VALID_SASL_MECHANISMS.join(", ")}.`
658
+ );
659
+ }
660
+ return value;
661
+ }
662
+ function parseCertPath(value) {
663
+ if (!existsSync2(value)) {
664
+ throw new InvalidArgumentError(`Certificate file not found: ${value}`);
665
+ }
666
+ return value;
667
+ }
504
668
 
505
669
  // src/cli/watcher.ts
506
- import chalk2 from "chalk";
670
+ import chalk4 from "chalk";
507
671
  var MAX_RETRIES = 3;
508
672
  function clearScreen() {
509
673
  process.stdout.write("\x1Bc");
@@ -519,31 +683,31 @@ function printWatchHeader(intervalMs, updatedAt) {
519
683
  hour12: false
520
684
  });
521
685
  console.log(
522
- chalk2.bold.cyan("\u26A1 klag") + chalk2.gray(` v${VERSION}`) + " \u2502 " + chalk2.yellow("watch mode") + " \u2502 " + chalk2.gray(`${intervalSec}s refresh`) + " \u2502 " + chalk2.gray("Ctrl+C to exit")
686
+ 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")
523
687
  );
524
- console.log(chalk2.gray(` Last updated: ${timeStr} (${tz})`));
688
+ console.log(chalk4.gray(` Last updated: ${timeStr} (${tz})`));
525
689
  }
526
690
  function printWatchError(message, retryCount, retryIn) {
527
691
  clearScreen();
528
692
  console.log(
529
- chalk2.bold.cyan("\u26A1 klag") + chalk2.gray(` v${VERSION}`) + " \u2502 " + chalk2.yellow("watch mode") + " \u2502 " + chalk2.gray("Ctrl+C to exit")
693
+ chalk4.bold.cyan("\u26A1 klag") + chalk4.gray(` v${VERSION}`) + " \u2502 " + chalk4.yellow("watch mode") + " \u2502 " + chalk4.gray("Ctrl+C to exit")
530
694
  );
531
695
  console.log("");
532
- console.error(chalk2.red(` \u274C Error: ${message}`));
696
+ console.error(chalk4.red(` \u274C Error: ${message}`));
533
697
  console.log(
534
- chalk2.yellow(` Retrying ${retryCount}/${MAX_RETRIES}... in ${retryIn}s`)
698
+ chalk4.yellow(` Retrying ${retryCount}/${MAX_RETRIES}... in ${retryIn}s`)
535
699
  );
536
700
  console.log("");
537
701
  }
538
702
  function printWatchFatal(message) {
539
703
  clearScreen();
540
704
  console.log(
541
- chalk2.bold.cyan("\u26A1 klag") + chalk2.gray(` v${VERSION}`) + " \u2502 " + chalk2.yellow("watch mode")
705
+ chalk4.bold.cyan("\u26A1 klag") + chalk4.gray(` v${VERSION}`) + " \u2502 " + chalk4.yellow("watch mode")
542
706
  );
543
707
  console.log("");
544
- console.error(chalk2.red(` \u274C Error: ${message}`));
708
+ console.error(chalk4.red(` \u274C Error: ${message}`));
545
709
  console.error(
546
- chalk2.red(` All ${MAX_RETRIES} retries failed \u2014 exiting watch mode`)
710
+ chalk4.red(` All ${MAX_RETRIES} retries failed \u2014 exiting watch mode`)
547
711
  );
548
712
  console.log("");
549
713
  }
@@ -566,7 +730,7 @@ async function runOnce(options, noRate, previous) {
566
730
  const topics = [...new Set(snapshot.partitions.map((p) => p.topic))];
567
731
  const waitSec = (options.intervalMs ?? 5e3) / 1e3;
568
732
  process.stdout.write(
569
- chalk2.gray(` Sampling rates... (waiting ${waitSec}s) `)
733
+ chalk4.gray(` Sampling rates... (waiting ${waitSec}s) `)
570
734
  );
571
735
  rateSnapshot = await collectRate(options, topics);
572
736
  process.stdout.write(`\r${" ".repeat(50)}\r`);
@@ -583,7 +747,7 @@ function printCountdown(seconds) {
583
747
  let remaining = seconds;
584
748
  const tick = () => {
585
749
  process.stdout.write(
586
- `\r${chalk2.gray(` [\u25CF] Next refresh in ${remaining}s...`)} `
750
+ `\r${chalk4.gray(` [\u25CF] Next refresh in ${remaining}s...`)} `
587
751
  );
588
752
  if (remaining === 0) {
589
753
  process.stdout.write(`\r${" ".repeat(40)}\r`);
@@ -608,12 +772,12 @@ function getFriendlyMessage(err, broker) {
608
772
  }
609
773
  async function startWatch(options, noRate) {
610
774
  process.on("SIGINT", () => {
611
- console.log(chalk2.gray("\n\n Watch mode exited\n"));
775
+ console.log(chalk4.gray("\n\n Watch mode exited\n"));
612
776
  process.exit(0);
613
777
  });
614
778
  const intervalMs = options.intervalMs ?? 5e3;
615
779
  const waitSec = Math.ceil(intervalMs / 1e3);
616
- process.stdout.write(chalk2.gray(" Connecting to broker..."));
780
+ process.stdout.write(chalk4.gray(" Connecting to broker..."));
617
781
  let errorCount = 0;
618
782
  let previousSnapshot;
619
783
  while (true) {
@@ -652,19 +816,52 @@ program.name("klag").description("Kafka consumer lag root cause analyzer").versi
652
816
  ).option("-w, --watch", "Watch mode \u2014 refresh every interval").option("-t, --timeout <ms>", "Connection timeout in ms", parseTimeout, 5e3).option(
653
817
  "--no-rate",
654
818
  "Skip rate sampling (faster, no PRODUCER_BURST detection)"
655
- ).option("--json", "Output raw JSON instead of table").action(async (options) => {
819
+ ).option("--json", "Output raw JSON instead of table").option("--ssl", "Enable SSL/TLS (uses system CA trust)").option("--ssl-ca <path>", "Path to CA certificate PEM file", parseCertPath).option(
820
+ "--ssl-cert <path>",
821
+ "Path to client certificate PEM file",
822
+ parseCertPath
823
+ ).option("--ssl-key <path>", "Path to client key PEM file", parseCertPath).option(
824
+ "--sasl-mechanism <mechanism>",
825
+ "SASL mechanism: plain, scram-sha-256, scram-sha-512",
826
+ parseSaslMechanism
827
+ ).option("--sasl-username <username>", "SASL username").option(
828
+ "--sasl-password <password>",
829
+ "SASL password (prefer KLAG_SASL_PASSWORD env var)"
830
+ ).action(async (options) => {
656
831
  try {
832
+ const loaded = loadConfig();
833
+ const rc = loaded?.config ?? {};
834
+ if (loaded) {
835
+ process.stderr.write(
836
+ chalk5.gray(` Using config: ${loaded.loadedFrom}
837
+ `)
838
+ );
839
+ }
840
+ const broker = options.broker !== "localhost:9092" ? options.broker : rc.broker ?? options.broker;
841
+ const groupId = options.group ?? rc.group;
842
+ const intervalMs = options.interval !== 5e3 ? options.interval : rc.interval ?? options.interval;
843
+ const timeoutMs = options.timeout !== 5e3 ? options.timeout : rc.timeout ?? options.timeout;
844
+ const auth = buildAuthOptions({
845
+ ssl: options.ssl || rc.ssl?.enabled,
846
+ sslCa: options.sslCa ?? rc.ssl?.caPath,
847
+ sslCert: options.sslCert ?? rc.ssl?.certPath,
848
+ sslKey: options.sslKey ?? rc.ssl?.keyPath,
849
+ saslMechanism: options.saslMechanism ?? rc.sasl?.mechanism,
850
+ saslUsername: options.saslUsername ?? rc.sasl?.username,
851
+ saslPassword: options.saslPassword ?? rc.sasl?.password
852
+ });
657
853
  const kafkaOptions = {
658
- broker: options.broker,
659
- groupId: options.group,
660
- intervalMs: options.interval,
661
- timeoutMs: options.timeout
854
+ broker,
855
+ groupId,
856
+ intervalMs,
857
+ timeoutMs,
858
+ ...auth
662
859
  };
663
860
  if (options.watch) {
664
861
  await startWatch(kafkaOptions, options.rate === false);
665
862
  return;
666
863
  }
667
- process.stdout.write(chalk3.gray(" Connecting to broker..."));
864
+ process.stdout.write(chalk5.gray(" Connecting to broker..."));
668
865
  const snapshot = await collectLag(kafkaOptions);
669
866
  process.stdout.write(`\r${" ".repeat(50)}\r`);
670
867
  let rateSnapshot;
@@ -672,7 +869,7 @@ program.name("klag").description("Kafka consumer lag root cause analyzer").versi
672
869
  const topics = [...new Set(snapshot.partitions.map((p) => p.topic))];
673
870
  const waitSec = (kafkaOptions.intervalMs ?? 5e3) / 1e3;
674
871
  process.stdout.write(
675
- chalk3.gray(` Sampling rates... (waiting ${waitSec}s) `)
872
+ chalk5.gray(` Sampling rates... (waiting ${waitSec}s) `)
676
873
  );
677
874
  rateSnapshot = await collectRate(kafkaOptions, topics);
678
875
  process.stdout.write(`\r${" ".repeat(50)}\r`);
@@ -700,36 +897,55 @@ program.name("klag").description("Kafka consumer lag root cause analyzer").versi
700
897
  process.stdout.write(`\r${" ".repeat(50)}\r`);
701
898
  const message = err instanceof Error ? err.message : String(err);
702
899
  if (message.includes("ECONNREFUSED") || message.includes("ETIMEDOUT") || message.includes("Connection error") || message.includes("connect ECONNREFUSED")) {
703
- console.error(chalk3.red(`
900
+ console.error(chalk5.red(`
704
901
  \u274C Cannot connect to broker
705
902
  `));
706
- console.error(chalk3.yellow(" Check the following:"));
707
- console.error(chalk3.gray(` \u2022 Is Kafka running: docker ps`));
708
- console.error(chalk3.gray(` \u2022 Broker address: ${options.broker}`));
903
+ console.error(chalk5.yellow(" Check the following:"));
904
+ console.error(chalk5.gray(` \u2022 Is Kafka running: docker ps`));
905
+ console.error(chalk5.gray(` \u2022 Broker address: ${options.broker}`));
709
906
  console.error(
710
- chalk3.gray(
907
+ chalk5.gray(
711
908
  ` \u2022 Port accessibility: nc -zv ${options.broker.split(":")[0]} ${options.broker.split(":")[1]}`
712
909
  )
713
910
  );
714
911
  console.error("");
715
912
  process.exit(1);
716
913
  }
914
+ if (message.includes("SASLAuthenticationFailed") || message.includes("Authentication failed") || message.includes("SASL")) {
915
+ console.error(chalk5.red(`
916
+ \u274C SASL authentication failed
917
+ `));
918
+ console.error(chalk5.yellow(" Check the following:"));
919
+ console.error(
920
+ chalk5.gray(` \u2022 Mechanism: ${options.saslMechanism ?? "(none)"}`)
921
+ );
922
+ console.error(
923
+ chalk5.gray(` \u2022 Username: ${options.saslUsername ?? "(none)"}`)
924
+ );
925
+ console.error(
926
+ chalk5.gray(
927
+ ` \u2022 Password: set via KLAG_SASL_PASSWORD or --sasl-password`
928
+ )
929
+ );
930
+ console.error("");
931
+ process.exit(1);
932
+ }
717
933
  if (message.includes("not found") || message.includes("Dead state")) {
718
- console.error(chalk3.red(`
934
+ console.error(chalk5.red(`
719
935
  \u274C Consumer group not found
720
936
  `));
721
- console.error(chalk3.yellow(" Check the following:"));
722
- console.error(chalk3.gray(` \u2022 Group ID: ${options.group}`));
723
- console.error(chalk3.gray(` \u2022 List existing groups:`));
937
+ console.error(chalk5.yellow(" Check the following:"));
938
+ console.error(chalk5.gray(` \u2022 Group ID: ${options.group}`));
939
+ console.error(chalk5.gray(` \u2022 List existing groups:`));
724
940
  console.error(
725
- chalk3.gray(
941
+ chalk5.gray(
726
942
  ` kafka-consumer-groups.sh --bootstrap-server ${options.broker} --list`
727
943
  )
728
944
  );
729
945
  console.error("");
730
946
  process.exit(1);
731
947
  }
732
- console.error(chalk3.red(`
948
+ console.error(chalk5.red(`
733
949
  \u274C Error: ${message}
734
950
  `));
735
951
  process.exit(1);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@closeup1202/klag",
3
- "version": "0.1.0",
3
+ "version": "0.2.0",
4
4
  "description": "Kafka consumer lag root cause analyzer",
5
5
  "type": "module",
6
6
  "bin": {