@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 +52 -2
- package/dist/cli/index.js +270 -54
- package/package.json +1 -1
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
|
-
- [
|
|
110
|
-
- [ ] v0.3.0 —
|
|
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
|
|
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
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
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
|
-
|
|
152
|
-
|
|
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(
|
|
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 =
|
|
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.
|
|
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
|
|
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
|
-
|
|
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(
|
|
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
|
-
|
|
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(
|
|
696
|
+
console.error(chalk4.red(` \u274C Error: ${message}`));
|
|
533
697
|
console.log(
|
|
534
|
-
|
|
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
|
-
|
|
705
|
+
chalk4.bold.cyan("\u26A1 klag") + chalk4.gray(` v${VERSION}`) + " \u2502 " + chalk4.yellow("watch mode")
|
|
542
706
|
);
|
|
543
707
|
console.log("");
|
|
544
|
-
console.error(
|
|
708
|
+
console.error(chalk4.red(` \u274C Error: ${message}`));
|
|
545
709
|
console.error(
|
|
546
|
-
|
|
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
|
-
|
|
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${
|
|
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(
|
|
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(
|
|
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").
|
|
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
|
|
659
|
-
groupId
|
|
660
|
-
intervalMs
|
|
661
|
-
timeoutMs
|
|
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(
|
|
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
|
-
|
|
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(
|
|
900
|
+
console.error(chalk5.red(`
|
|
704
901
|
\u274C Cannot connect to broker
|
|
705
902
|
`));
|
|
706
|
-
console.error(
|
|
707
|
-
console.error(
|
|
708
|
-
console.error(
|
|
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
|
-
|
|
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(
|
|
934
|
+
console.error(chalk5.red(`
|
|
719
935
|
\u274C Consumer group not found
|
|
720
936
|
`));
|
|
721
|
-
console.error(
|
|
722
|
-
console.error(
|
|
723
|
-
console.error(
|
|
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
|
-
|
|
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(
|
|
948
|
+
console.error(chalk5.red(`
|
|
733
949
|
\u274C Error: ${message}
|
|
734
950
|
`));
|
|
735
951
|
process.exit(1);
|