@deniscuciuc/redis-analyzer 1.0.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/CHANGELOG.md +31 -0
- package/LICENSE +21 -0
- package/README.md +244 -0
- package/analyzerrc.example.json +17 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +137 -0
- package/dist/src/analyzers/memory-analyzer.d.ts +5 -0
- package/dist/src/analyzers/memory-analyzer.d.ts.map +1 -0
- package/dist/src/analyzers/memory-analyzer.js +54 -0
- package/dist/src/analyzers/performance-analyzer.d.ts +6 -0
- package/dist/src/analyzers/performance-analyzer.d.ts.map +1 -0
- package/dist/src/analyzers/performance-analyzer.js +78 -0
- package/dist/src/analyzers/persistence-analyzer.d.ts +5 -0
- package/dist/src/analyzers/persistence-analyzer.d.ts.map +1 -0
- package/dist/src/analyzers/persistence-analyzer.js +59 -0
- package/dist/src/analyzers/replication-analyzer.d.ts +5 -0
- package/dist/src/analyzers/replication-analyzer.d.ts.map +1 -0
- package/dist/src/analyzers/replication-analyzer.js +52 -0
- package/dist/src/cli/options.d.ts +24 -0
- package/dist/src/cli/options.d.ts.map +1 -0
- package/dist/src/cli/options.js +155 -0
- package/dist/src/cli/runner.d.ts +13 -0
- package/dist/src/cli/runner.d.ts.map +1 -0
- package/dist/src/cli/runner.js +214 -0
- package/dist/src/collectors/stats-collector.d.ts +15 -0
- package/dist/src/collectors/stats-collector.d.ts.map +1 -0
- package/dist/src/collectors/stats-collector.js +151 -0
- package/dist/src/config/loader.d.ts +13 -0
- package/dist/src/config/loader.d.ts.map +1 -0
- package/dist/src/config/loader.js +63 -0
- package/dist/src/constants.d.ts +52 -0
- package/dist/src/constants.d.ts.map +1 -0
- package/dist/src/constants.js +93 -0
- package/dist/src/health.d.ts +10 -0
- package/dist/src/health.d.ts.map +1 -0
- package/dist/src/health.js +100 -0
- package/dist/src/interactive/display.d.ts +13 -0
- package/dist/src/interactive/display.d.ts.map +1 -0
- package/dist/src/interactive/display.js +130 -0
- package/dist/src/interactive/index.d.ts +23 -0
- package/dist/src/interactive/index.d.ts.map +1 -0
- package/dist/src/interactive/index.js +236 -0
- package/dist/src/interactive/menus.d.ts +25 -0
- package/dist/src/interactive/menus.d.ts.map +1 -0
- package/dist/src/interactive/menus.js +49 -0
- package/dist/src/reporters/diff-reporter.d.ts +21 -0
- package/dist/src/reporters/diff-reporter.d.ts.map +1 -0
- package/dist/src/reporters/diff-reporter.js +96 -0
- package/dist/src/reporters/html-reporter.d.ts +9 -0
- package/dist/src/reporters/html-reporter.d.ts.map +1 -0
- package/dist/src/reporters/html-reporter.js +140 -0
- package/dist/src/reporters/report-generator.d.ts +23 -0
- package/dist/src/reporters/report-generator.d.ts.map +1 -0
- package/dist/src/reporters/report-generator.js +239 -0
- package/dist/src/types.d.ts +184 -0
- package/dist/src/types.d.ts.map +1 -0
- package/dist/src/types.js +2 -0
- package/dist/src/utils/format.d.ts +6 -0
- package/dist/src/utils/format.d.ts.map +1 -0
- package/dist/src/utils/format.js +32 -0
- package/dist/src/utils/print.d.ts +8 -0
- package/dist/src/utils/print.d.ts.map +1 -0
- package/dist/src/utils/print.js +42 -0
- package/dist/src/watch/runner.d.ts +8 -0
- package/dist/src/watch/runner.d.ts.map +1 -0
- package/dist/src/watch/runner.js +49 -0
- package/dist/tests/analysis-and-reports.test.d.ts +2 -0
- package/dist/tests/analysis-and-reports.test.d.ts.map +1 -0
- package/dist/tests/analysis-and-reports.test.js +172 -0
- package/dist/tests/collector-and-options.test.d.ts +2 -0
- package/dist/tests/collector-and-options.test.d.ts.map +1 -0
- package/dist/tests/collector-and-options.test.js +110 -0
- package/package.json +82 -0
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/interactive/index.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,KAAK,EAAE,MAAM,SAAS,CAAC;AAKrC,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,gBAAgB,CAAC;AAUpD,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,UAAU,CAAC;AAYhD,qBAAa,cAAc;IAQzB,OAAO,CAAC,QAAQ,CAAC,MAAM;IACvB,OAAO,CAAC,QAAQ,CAAC,UAAU;IAC3B,OAAO,CAAC,QAAQ,CAAC,cAAc;IAThC,OAAO,CAAC,QAAQ,CAAC,SAAS,CAAiB;IAC3C,OAAO,CAAC,QAAQ,CAAC,MAAM,CAAwB;IAC/C,OAAO,CAAC,QAAQ,CAAC,WAAW,CAA6B;IACzD,OAAO,CAAC,QAAQ,CAAC,WAAW,CAA6B;IACzD,OAAO,CAAC,QAAQ,CAAC,WAAW,CAA6B;gBAGvC,MAAM,EAAE,KAAK,EACb,UAAU,EAAE,eAAe,EAC3B,cAAc,EAAE,aAAa;IAKzC,KAAK,IAAI,OAAO,CAAC,IAAI,CAAC;IAiC5B,OAAO,CAAC,QAAQ;YAQF,YAAY;YA+BZ,gBAAgB;YAQhB,SAAS;YAyDT,WAAW;YAqCX,SAAS;YAgBT,YAAY;CAoC1B"}
|
|
@@ -0,0 +1,236 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
14
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
15
|
+
}) : function(o, v) {
|
|
16
|
+
o["default"] = v;
|
|
17
|
+
});
|
|
18
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
19
|
+
var ownKeys = function(o) {
|
|
20
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
21
|
+
var ar = [];
|
|
22
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
23
|
+
return ar;
|
|
24
|
+
};
|
|
25
|
+
return ownKeys(o);
|
|
26
|
+
};
|
|
27
|
+
return function (mod) {
|
|
28
|
+
if (mod && mod.__esModule) return mod;
|
|
29
|
+
var result = {};
|
|
30
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
31
|
+
__setModuleDefault(result, mod);
|
|
32
|
+
return result;
|
|
33
|
+
};
|
|
34
|
+
})();
|
|
35
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
|
+
exports.InteractiveCLI = void 0;
|
|
37
|
+
const prompts_1 = require("@inquirer/prompts");
|
|
38
|
+
const memory_analyzer_1 = require("../analyzers/memory-analyzer");
|
|
39
|
+
const performance_analyzer_1 = require("../analyzers/performance-analyzer");
|
|
40
|
+
const persistence_analyzer_1 = require("../analyzers/persistence-analyzer");
|
|
41
|
+
const replication_analyzer_1 = require("../analyzers/replication-analyzer");
|
|
42
|
+
const runner_1 = require("../cli/runner");
|
|
43
|
+
const stats_collector_1 = require("../collectors/stats-collector");
|
|
44
|
+
const constants_1 = require("../constants");
|
|
45
|
+
const diff_reporter_1 = require("../reporters/diff-reporter");
|
|
46
|
+
const report_generator_1 = require("../reporters/report-generator");
|
|
47
|
+
const runner_2 = require("../watch/runner");
|
|
48
|
+
const display = __importStar(require("./display"));
|
|
49
|
+
const menus_1 = require("./menus");
|
|
50
|
+
class InteractiveCLI {
|
|
51
|
+
client;
|
|
52
|
+
connection;
|
|
53
|
+
runtimeOptions;
|
|
54
|
+
collector;
|
|
55
|
+
memory = new memory_analyzer_1.MemoryAnalyzer();
|
|
56
|
+
performance = new performance_analyzer_1.PerformanceAnalyzer();
|
|
57
|
+
persistence = new persistence_analyzer_1.PersistenceAnalyzer();
|
|
58
|
+
replication = new replication_analyzer_1.ReplicationAnalyzer();
|
|
59
|
+
constructor(client, connection, runtimeOptions) {
|
|
60
|
+
this.client = client;
|
|
61
|
+
this.connection = connection;
|
|
62
|
+
this.runtimeOptions = runtimeOptions;
|
|
63
|
+
this.collector = new stats_collector_1.StatsCollector(client);
|
|
64
|
+
}
|
|
65
|
+
async start() {
|
|
66
|
+
console.clear();
|
|
67
|
+
console.log(`\n Redis Analyzer — ${this.connection.host}:${this.connection.port}/${this.connection.db}\n`);
|
|
68
|
+
let running = true;
|
|
69
|
+
while (running) {
|
|
70
|
+
const action = await (0, prompts_1.select)({
|
|
71
|
+
message: "Main menu",
|
|
72
|
+
choices: menus_1.MAIN_MENU_CHOICES,
|
|
73
|
+
});
|
|
74
|
+
switch (action) {
|
|
75
|
+
case "analysis":
|
|
76
|
+
await this.analysisMenu();
|
|
77
|
+
break;
|
|
78
|
+
case "reports":
|
|
79
|
+
await this.reportsMenu();
|
|
80
|
+
break;
|
|
81
|
+
case "watch":
|
|
82
|
+
await this.watchMenu();
|
|
83
|
+
break;
|
|
84
|
+
case "settings":
|
|
85
|
+
await this.settingsMenu();
|
|
86
|
+
break;
|
|
87
|
+
case "exit":
|
|
88
|
+
running = false;
|
|
89
|
+
break;
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
reporter() {
|
|
94
|
+
return new report_generator_1.ReportGenerator(this.runtimeOptions.outputDir, {
|
|
95
|
+
outputDir: this.runtimeOptions.outputDir,
|
|
96
|
+
slowCommandThreshold: this.runtimeOptions.slowCommandThreshold,
|
|
97
|
+
maxSlowCommands: this.runtimeOptions.maxSlowCommands,
|
|
98
|
+
});
|
|
99
|
+
}
|
|
100
|
+
async analysisMenu() {
|
|
101
|
+
const choice = await (0, prompts_1.select)({
|
|
102
|
+
message: "Run analysis",
|
|
103
|
+
choices: menus_1.ANALYSIS_MENU_CHOICES,
|
|
104
|
+
});
|
|
105
|
+
switch (choice) {
|
|
106
|
+
case "full":
|
|
107
|
+
display.showFullReport(await (0, runner_1.buildFullReport)(this.client, this.runtimeOptions, this.connection));
|
|
108
|
+
break;
|
|
109
|
+
case "health":
|
|
110
|
+
display.showHealth(await (0, runner_1.buildFullReport)(this.client, this.runtimeOptions, this.connection));
|
|
111
|
+
break;
|
|
112
|
+
case "single":
|
|
113
|
+
await this.singleModuleMenu();
|
|
114
|
+
break;
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
async singleModuleMenu() {
|
|
118
|
+
const command = await (0, prompts_1.select)({
|
|
119
|
+
message: "Select command",
|
|
120
|
+
choices: menus_1.MODULE_CHOICES,
|
|
121
|
+
});
|
|
122
|
+
await this.runModule(command);
|
|
123
|
+
}
|
|
124
|
+
async runModule(command) {
|
|
125
|
+
const info = command === "slow-commands" ? undefined : await this.collector.getInfo();
|
|
126
|
+
switch (command) {
|
|
127
|
+
case "health":
|
|
128
|
+
display.showHealth(await (0, runner_1.buildFullReport)(this.client, this.runtimeOptions, this.connection));
|
|
129
|
+
break;
|
|
130
|
+
case "server-info":
|
|
131
|
+
display.showServerInfo(info);
|
|
132
|
+
break;
|
|
133
|
+
case "memory":
|
|
134
|
+
display.showMemory(this.memory.analyze(info));
|
|
135
|
+
break;
|
|
136
|
+
case "hit-rate":
|
|
137
|
+
display.showHitRate(this.performance.analyzeHitRate(info));
|
|
138
|
+
break;
|
|
139
|
+
case "slow-commands": {
|
|
140
|
+
const [commands, totalLogged] = await Promise.all([
|
|
141
|
+
this.collector.getSlowLog(this.runtimeOptions.maxSlowCommands),
|
|
142
|
+
this.collector.getSlowLogLength(),
|
|
143
|
+
]);
|
|
144
|
+
display.showSlowCommands(this.performance.analyzeSlowCommands(commands, totalLogged, this.runtimeOptions.slowCommandThreshold));
|
|
145
|
+
break;
|
|
146
|
+
}
|
|
147
|
+
case "keys":
|
|
148
|
+
display.showKeyspaces((0, runner_1.summarizeKeyspaces)(info).keyspaces);
|
|
149
|
+
break;
|
|
150
|
+
case "connections":
|
|
151
|
+
display.showConnections(info);
|
|
152
|
+
break;
|
|
153
|
+
case "persistence":
|
|
154
|
+
display.showPersistence(this.persistence.analyze(info));
|
|
155
|
+
break;
|
|
156
|
+
case "replication":
|
|
157
|
+
display.showReplication(this.replication.analyze(info));
|
|
158
|
+
break;
|
|
159
|
+
case "config":
|
|
160
|
+
display.showConfig(await this.collector.getConfigValues(constants_1.IMPORTANT_CONFIG_KEYS));
|
|
161
|
+
break;
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
async reportsMenu() {
|
|
165
|
+
const choice = await (0, prompts_1.select)({
|
|
166
|
+
message: "Generate report",
|
|
167
|
+
choices: menus_1.REPORTS_MENU_CHOICES,
|
|
168
|
+
});
|
|
169
|
+
if (choice === "back") {
|
|
170
|
+
return;
|
|
171
|
+
}
|
|
172
|
+
const report = await (0, runner_1.buildFullReport)(this.client, this.runtimeOptions, this.connection);
|
|
173
|
+
const reporter = this.reporter();
|
|
174
|
+
if (choice === "markdown" || choice === "html") {
|
|
175
|
+
const markdown = await reporter.generateFullReport(report);
|
|
176
|
+
const json = await reporter.generateJsonReport(report);
|
|
177
|
+
console.log(` ✅ Markdown: ${markdown}`);
|
|
178
|
+
console.log(` ✅ JSON: ${json}`);
|
|
179
|
+
if (choice === "html") {
|
|
180
|
+
const html = await reporter.generateHtmlReport(report);
|
|
181
|
+
console.log(` ✅ HTML: ${html}`);
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
if (choice === "diff") {
|
|
185
|
+
const previousPath = await (0, prompts_1.input)({
|
|
186
|
+
message: "Path to previous JSON report:",
|
|
187
|
+
});
|
|
188
|
+
diff_reporter_1.DiffReporter.print(diff_reporter_1.DiffReporter.diff(report, (0, runner_1.loadPreviousReport)(previousPath)));
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
async watchMenu() {
|
|
192
|
+
const command = await (0, prompts_1.select)({
|
|
193
|
+
message: "Select watch command",
|
|
194
|
+
choices: menus_1.WATCH_COMMAND_CHOICES,
|
|
195
|
+
});
|
|
196
|
+
const interval = await (0, prompts_1.input)({
|
|
197
|
+
message: "Watch interval in seconds",
|
|
198
|
+
default: String(this.runtimeOptions.watch ?? 30),
|
|
199
|
+
});
|
|
200
|
+
await (0, runner_2.runWatchLoop)({
|
|
201
|
+
intervalSeconds: Number.parseInt(interval, 10),
|
|
202
|
+
command,
|
|
203
|
+
runCommand: () => this.runModule(command),
|
|
204
|
+
});
|
|
205
|
+
}
|
|
206
|
+
async settingsMenu() {
|
|
207
|
+
const choice = await (0, prompts_1.select)({
|
|
208
|
+
message: "Settings",
|
|
209
|
+
choices: menus_1.SETTINGS_MENU_CHOICES,
|
|
210
|
+
});
|
|
211
|
+
switch (choice) {
|
|
212
|
+
case "show":
|
|
213
|
+
console.log(this.runtimeOptions);
|
|
214
|
+
break;
|
|
215
|
+
case "output":
|
|
216
|
+
this.runtimeOptions.outputDir = await (0, prompts_1.input)({
|
|
217
|
+
message: "Output directory",
|
|
218
|
+
default: this.runtimeOptions.outputDir,
|
|
219
|
+
});
|
|
220
|
+
break;
|
|
221
|
+
case "slow-threshold":
|
|
222
|
+
this.runtimeOptions.slowCommandThreshold = Number.parseInt(await (0, prompts_1.input)({
|
|
223
|
+
message: "Slow command threshold in microseconds",
|
|
224
|
+
default: String(this.runtimeOptions.slowCommandThreshold),
|
|
225
|
+
}), 10);
|
|
226
|
+
break;
|
|
227
|
+
case "max-slow":
|
|
228
|
+
this.runtimeOptions.maxSlowCommands = Number.parseInt(await (0, prompts_1.input)({
|
|
229
|
+
message: "Max slow commands to fetch",
|
|
230
|
+
default: String(this.runtimeOptions.maxSlowCommands),
|
|
231
|
+
}), 10);
|
|
232
|
+
break;
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
exports.InteractiveCLI = InteractiveCLI;
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
export declare const MAIN_MENU_CHOICES: {
|
|
2
|
+
name: string;
|
|
3
|
+
value: string;
|
|
4
|
+
}[];
|
|
5
|
+
export declare const ANALYSIS_MENU_CHOICES: {
|
|
6
|
+
name: string;
|
|
7
|
+
value: string;
|
|
8
|
+
}[];
|
|
9
|
+
export declare const MODULE_CHOICES: {
|
|
10
|
+
name: string;
|
|
11
|
+
value: string;
|
|
12
|
+
}[];
|
|
13
|
+
export declare const REPORTS_MENU_CHOICES: {
|
|
14
|
+
name: string;
|
|
15
|
+
value: string;
|
|
16
|
+
}[];
|
|
17
|
+
export declare const SETTINGS_MENU_CHOICES: {
|
|
18
|
+
name: string;
|
|
19
|
+
value: string;
|
|
20
|
+
}[];
|
|
21
|
+
export declare const WATCH_COMMAND_CHOICES: {
|
|
22
|
+
name: string;
|
|
23
|
+
value: string;
|
|
24
|
+
}[];
|
|
25
|
+
//# sourceMappingURL=menus.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"menus.d.ts","sourceRoot":"","sources":["../../../src/interactive/menus.ts"],"names":[],"mappings":"AAAA,eAAO,MAAM,iBAAiB;;;GAM7B,CAAC;AAEF,eAAO,MAAM,qBAAqB;;;GAKjC,CAAC;AAEF,eAAO,MAAM,cAAc;;;GAW1B,CAAC;AAEF,eAAO,MAAM,oBAAoB;;;GAKhC,CAAC;AAEF,eAAO,MAAM,qBAAqB;;;GAMjC,CAAC;AAEF,eAAO,MAAM,qBAAqB;;;GAOjC,CAAC"}
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.WATCH_COMMAND_CHOICES = exports.SETTINGS_MENU_CHOICES = exports.REPORTS_MENU_CHOICES = exports.MODULE_CHOICES = exports.ANALYSIS_MENU_CHOICES = exports.MAIN_MENU_CHOICES = void 0;
|
|
4
|
+
exports.MAIN_MENU_CHOICES = [
|
|
5
|
+
{ name: "Run analysis", value: "analysis" },
|
|
6
|
+
{ name: "Generate reports", value: "reports" },
|
|
7
|
+
{ name: "Watch mode", value: "watch" },
|
|
8
|
+
{ name: "Settings", value: "settings" },
|
|
9
|
+
{ name: "Exit", value: "exit" },
|
|
10
|
+
];
|
|
11
|
+
exports.ANALYSIS_MENU_CHOICES = [
|
|
12
|
+
{ name: "Full analysis", value: "full" },
|
|
13
|
+
{ name: "Health", value: "health" },
|
|
14
|
+
{ name: "Single command", value: "single" },
|
|
15
|
+
{ name: "Back", value: "back" },
|
|
16
|
+
];
|
|
17
|
+
exports.MODULE_CHOICES = [
|
|
18
|
+
{ name: "Health", value: "health" },
|
|
19
|
+
{ name: "Server info", value: "server-info" },
|
|
20
|
+
{ name: "Memory", value: "memory" },
|
|
21
|
+
{ name: "Hit rate", value: "hit-rate" },
|
|
22
|
+
{ name: "Slow commands", value: "slow-commands" },
|
|
23
|
+
{ name: "Keys", value: "keys" },
|
|
24
|
+
{ name: "Connections", value: "connections" },
|
|
25
|
+
{ name: "Persistence", value: "persistence" },
|
|
26
|
+
{ name: "Replication", value: "replication" },
|
|
27
|
+
{ name: "Config", value: "config" },
|
|
28
|
+
];
|
|
29
|
+
exports.REPORTS_MENU_CHOICES = [
|
|
30
|
+
{ name: "Markdown + JSON", value: "markdown" },
|
|
31
|
+
{ name: "Markdown + JSON + HTML", value: "html" },
|
|
32
|
+
{ name: "Diff against previous JSON report", value: "diff" },
|
|
33
|
+
{ name: "Back", value: "back" },
|
|
34
|
+
];
|
|
35
|
+
exports.SETTINGS_MENU_CHOICES = [
|
|
36
|
+
{ name: "Show current settings", value: "show" },
|
|
37
|
+
{ name: "Change output directory", value: "output" },
|
|
38
|
+
{ name: "Change slow command threshold", value: "slow-threshold" },
|
|
39
|
+
{ name: "Change max slow commands", value: "max-slow" },
|
|
40
|
+
{ name: "Back", value: "back" },
|
|
41
|
+
];
|
|
42
|
+
exports.WATCH_COMMAND_CHOICES = [
|
|
43
|
+
{ name: "Health", value: "health" },
|
|
44
|
+
{ name: "Connections", value: "connections" },
|
|
45
|
+
{ name: "Hit rate", value: "hit-rate" },
|
|
46
|
+
{ name: "Slow commands", value: "slow-commands" },
|
|
47
|
+
{ name: "Keys", value: "keys" },
|
|
48
|
+
{ name: "Replication", value: "replication" },
|
|
49
|
+
];
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import type { FullReport } from "../types";
|
|
2
|
+
export interface MetricDiff {
|
|
3
|
+
label: string;
|
|
4
|
+
before: number | string;
|
|
5
|
+
after: number | string;
|
|
6
|
+
delta?: number;
|
|
7
|
+
trend: "better" | "worse" | "neutral" | "unchanged";
|
|
8
|
+
}
|
|
9
|
+
export interface ReportDiff {
|
|
10
|
+
currentAt: string;
|
|
11
|
+
previousAt: string;
|
|
12
|
+
timeDelta: string;
|
|
13
|
+
metrics: MetricDiff[];
|
|
14
|
+
newIssues: string[];
|
|
15
|
+
resolvedIssues: string[];
|
|
16
|
+
}
|
|
17
|
+
export declare const DiffReporter: {
|
|
18
|
+
diff(current: FullReport, previous: FullReport): ReportDiff;
|
|
19
|
+
print(diff: ReportDiff, write?: (message?: unknown, ...optionalParams: unknown[]) => void): void;
|
|
20
|
+
};
|
|
21
|
+
//# sourceMappingURL=diff-reporter.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"diff-reporter.d.ts","sourceRoot":"","sources":["../../../src/reporters/diff-reporter.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,UAAU,CAAC;AAG3C,MAAM,WAAW,UAAU;IAC1B,KAAK,EAAE,MAAM,CAAC;IACd,MAAM,EAAE,MAAM,GAAG,MAAM,CAAC;IACxB,KAAK,EAAE,MAAM,GAAG,MAAM,CAAC;IACvB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,KAAK,EAAE,QAAQ,GAAG,OAAO,GAAG,SAAS,GAAG,WAAW,CAAC;CACpD;AAED,MAAM,WAAW,UAAU;IAC1B,SAAS,EAAE,MAAM,CAAC;IAClB,UAAU,EAAE,MAAM,CAAC;IACnB,SAAS,EAAE,MAAM,CAAC;IAClB,OAAO,EAAE,UAAU,EAAE,CAAC;IACtB,SAAS,EAAE,MAAM,EAAE,CAAC;IACpB,cAAc,EAAE,MAAM,EAAE,CAAC;CACzB;AAID,eAAO,MAAM,YAAY;kBACV,UAAU,YAAY,UAAU,GAAG,UAAU;gBA8DpD,UAAU,UACT,CACN,OAAO,CAAC,EAAE,OAAO,EACjB,GAAG,cAAc,EAAE,OAAO,EAAE,KACxB,IAAI,GACP,IAAI;CA2BP,CAAC"}
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.DiffReporter = void 0;
|
|
4
|
+
const format_1 = require("../utils/format");
|
|
5
|
+
exports.DiffReporter = {
|
|
6
|
+
diff(current, previous) {
|
|
7
|
+
const currentIssues = collectIssues(current);
|
|
8
|
+
const previousIssues = collectIssues(previous);
|
|
9
|
+
return {
|
|
10
|
+
currentAt: toIsoString(current.generatedAt),
|
|
11
|
+
previousAt: toIsoString(previous.generatedAt),
|
|
12
|
+
timeDelta: describeTimeDelta(previous.generatedAt, current.generatedAt),
|
|
13
|
+
metrics: [
|
|
14
|
+
createMetricDiff("Health score", previous.healthScore, current.healthScore, "higher"),
|
|
15
|
+
createMetricDiff("Hit rate", previous.hitRate.hitRate, current.hitRate.hitRate, "higher"),
|
|
16
|
+
createMetricDiff("Used memory", previous.memory.usedMemory, current.memory.usedMemory, "lower"),
|
|
17
|
+
createMetricDiff("Slow commands", previous.slowCommands.totalLogged, current.slowCommands.totalLogged, "lower"),
|
|
18
|
+
createMetricDiff("Ops / sec", previous.metrics.opsPerSec, current.metrics.opsPerSec, "neutral"),
|
|
19
|
+
createMetricDiff("Connected clients", previous.metrics.connectedClients, current.metrics.connectedClients, "neutral"),
|
|
20
|
+
createMetricDiff("Rejected connections", previous.metrics.rejectedConnections, current.metrics.rejectedConnections, "lower"),
|
|
21
|
+
],
|
|
22
|
+
newIssues: currentIssues.filter((issue) => !previousIssues.includes(issue)),
|
|
23
|
+
resolvedIssues: previousIssues.filter((issue) => !currentIssues.includes(issue)),
|
|
24
|
+
};
|
|
25
|
+
},
|
|
26
|
+
print(diff, write = console.log) {
|
|
27
|
+
write(`\nReport diff (${diff.previousAt} → ${diff.currentAt}, ${diff.timeDelta})`);
|
|
28
|
+
for (const metric of diff.metrics) {
|
|
29
|
+
const arrow = metric.trend === "better" ? "⬆️" : metric.trend === "worse" ? "⬇️" : "↔️";
|
|
30
|
+
const delta = metric.delta === undefined || metric.delta === 0
|
|
31
|
+
? ""
|
|
32
|
+
: ` (${metric.delta > 0 ? "+" : ""}${formatValue(metric.label, metric.delta)})`;
|
|
33
|
+
write(`${arrow} ${metric.label.padEnd(20)} ${formatValue(metric.label, metric.before)} → ${formatValue(metric.label, metric.after)}${delta}`);
|
|
34
|
+
}
|
|
35
|
+
if (diff.newIssues.length > 0) {
|
|
36
|
+
write(`⚠️ New issues (${diff.newIssues.length}): ${diff.newIssues.join(", ")}`);
|
|
37
|
+
}
|
|
38
|
+
if (diff.resolvedIssues.length > 0) {
|
|
39
|
+
write(`✓ Resolved (${diff.resolvedIssues.length}): ${diff.resolvedIssues.join(", ")}`);
|
|
40
|
+
}
|
|
41
|
+
},
|
|
42
|
+
};
|
|
43
|
+
function createMetricDiff(label, before, after, direction) {
|
|
44
|
+
const delta = Math.round((after - before) * 100) / 100;
|
|
45
|
+
if (delta === 0) {
|
|
46
|
+
return { label, before, after, delta: 0, trend: "unchanged" };
|
|
47
|
+
}
|
|
48
|
+
if (direction === "neutral") {
|
|
49
|
+
return { label, before, after, delta, trend: "neutral" };
|
|
50
|
+
}
|
|
51
|
+
const improved = (direction === "higher" && delta > 0) ||
|
|
52
|
+
(direction === "lower" && delta < 0);
|
|
53
|
+
return { label, before, after, delta, trend: improved ? "better" : "worse" };
|
|
54
|
+
}
|
|
55
|
+
function collectIssues(report) {
|
|
56
|
+
const issues = report.recommendations.map((item) => `${item.priority}:${item.category}:${item.message}`);
|
|
57
|
+
if (report.memory.fragSeverity !== "ok") {
|
|
58
|
+
issues.push(`fragmentation:${report.memory.fragSeverity}`);
|
|
59
|
+
}
|
|
60
|
+
if (report.persistence.severity !== "ok") {
|
|
61
|
+
issues.push(`persistence:${report.persistence.severity}`);
|
|
62
|
+
}
|
|
63
|
+
if (report.replication.severity !== "ok") {
|
|
64
|
+
issues.push(`replication:${report.replication.severity}`);
|
|
65
|
+
}
|
|
66
|
+
return issues;
|
|
67
|
+
}
|
|
68
|
+
function describeTimeDelta(previousAt, currentAt) {
|
|
69
|
+
const previous = new Date(previousAt);
|
|
70
|
+
const current = new Date(currentAt);
|
|
71
|
+
const minutes = Math.max(1, Math.round((current.getTime() - previous.getTime()) / 60000));
|
|
72
|
+
if (minutes < 60) {
|
|
73
|
+
return `${minutes} minute${minutes === 1 ? "" : "s"} apart`;
|
|
74
|
+
}
|
|
75
|
+
const hours = Math.round(minutes / 60);
|
|
76
|
+
if (hours < 48) {
|
|
77
|
+
return `${hours} hour${hours === 1 ? "" : "s"} apart`;
|
|
78
|
+
}
|
|
79
|
+
const days = Math.round(hours / 24);
|
|
80
|
+
return `${days} day${days === 1 ? "" : "s"} apart`;
|
|
81
|
+
}
|
|
82
|
+
function toIsoString(value) {
|
|
83
|
+
return new Date(value).toISOString();
|
|
84
|
+
}
|
|
85
|
+
function formatValue(label, value) {
|
|
86
|
+
if (typeof value === "string") {
|
|
87
|
+
return value;
|
|
88
|
+
}
|
|
89
|
+
if (label === "Used memory") {
|
|
90
|
+
return (0, format_1.formatBytes)(value);
|
|
91
|
+
}
|
|
92
|
+
if (label === "Hit rate") {
|
|
93
|
+
return (0, format_1.formatPercent)(value);
|
|
94
|
+
}
|
|
95
|
+
return Number.isInteger(value) ? value.toString() : value.toFixed(2);
|
|
96
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import type { FullRedisReport } from "../types";
|
|
2
|
+
export declare class HtmlReporter {
|
|
3
|
+
static generate(report: FullRedisReport): string;
|
|
4
|
+
private static summaryCard;
|
|
5
|
+
private static table;
|
|
6
|
+
private static healthClass;
|
|
7
|
+
private static escape;
|
|
8
|
+
}
|
|
9
|
+
//# sourceMappingURL=html-reporter.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"html-reporter.d.ts","sourceRoot":"","sources":["../../../src/reporters/html-reporter.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,UAAU,CAAC;AAShD,qBAAa,YAAY;IACxB,MAAM,CAAC,QAAQ,CAAC,MAAM,EAAE,eAAe,GAAG,MAAM;IAuHhD,OAAO,CAAC,MAAM,CAAC,WAAW;IAI1B,OAAO,CAAC,MAAM,CAAC,KAAK;IAQpB,OAAO,CAAC,MAAM,CAAC,WAAW;IAU1B,OAAO,CAAC,MAAM,CAAC,MAAM;CAQrB"}
|
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.HtmlReporter = void 0;
|
|
4
|
+
const format_1 = require("../utils/format");
|
|
5
|
+
// biome-ignore lint/complexity/noStaticOnlyClass: grouped HTML helpers keep the template readable.
|
|
6
|
+
class HtmlReporter {
|
|
7
|
+
static generate(report) {
|
|
8
|
+
return `<!DOCTYPE html>
|
|
9
|
+
<html lang="en">
|
|
10
|
+
<head>
|
|
11
|
+
<meta charset="utf-8" />
|
|
12
|
+
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
|
13
|
+
<title>Redis Analysis Report</title>
|
|
14
|
+
<style>
|
|
15
|
+
:root {
|
|
16
|
+
--bg: #0f172a;
|
|
17
|
+
--surface: #111827;
|
|
18
|
+
--surface-alt: #1f2937;
|
|
19
|
+
--text: #e5e7eb;
|
|
20
|
+
--muted: #94a3b8;
|
|
21
|
+
--border: #334155;
|
|
22
|
+
--good: #22c55e;
|
|
23
|
+
--warn: #f59e0b;
|
|
24
|
+
--bad: #ef4444;
|
|
25
|
+
}
|
|
26
|
+
body { margin: 0; font-family: Inter, system-ui, sans-serif; background: var(--bg); color: var(--text); }
|
|
27
|
+
main { width: min(1180px, calc(100% - 2rem)); margin: 0 auto; padding: 2rem 0 3rem; }
|
|
28
|
+
h1, h2, h3 { margin: 0 0 0.75rem; }
|
|
29
|
+
.card, section { background: var(--surface); border: 1px solid var(--border); border-radius: 1rem; padding: 1.25rem; }
|
|
30
|
+
.grid { display: grid; gap: 1rem; grid-template-columns: repeat(auto-fit, minmax(220px, 1fr)); margin: 1.25rem 0; }
|
|
31
|
+
.muted { color: var(--muted); }
|
|
32
|
+
table { width: 100%; border-collapse: collapse; margin-top: 0.75rem; }
|
|
33
|
+
th, td { border-bottom: 1px solid var(--border); text-align: left; padding: 0.65rem 0.5rem; vertical-align: top; }
|
|
34
|
+
th { color: var(--muted); font-weight: 600; }
|
|
35
|
+
code { background: var(--surface-alt); border-radius: 0.4rem; padding: 0.1rem 0.35rem; }
|
|
36
|
+
.pill { display: inline-block; border-radius: 999px; padding: 0.2rem 0.65rem; font-weight: 700; }
|
|
37
|
+
.good { background: rgba(34, 197, 94, 0.15); color: #86efac; }
|
|
38
|
+
.warn { background: rgba(245, 158, 11, 0.15); color: #fcd34d; }
|
|
39
|
+
.bad { background: rgba(239, 68, 68, 0.15); color: #fca5a5; }
|
|
40
|
+
section + section { margin-top: 1rem; }
|
|
41
|
+
ul { padding-left: 1.25rem; }
|
|
42
|
+
</style>
|
|
43
|
+
</head>
|
|
44
|
+
<body>
|
|
45
|
+
<main>
|
|
46
|
+
<div class="card">
|
|
47
|
+
<p class="muted">${HtmlReporter.escape(`${report.host}:${report.port}/${report.db}`)} · ${HtmlReporter.escape(report.generatedAt.toISOString())}</p>
|
|
48
|
+
<h1>Redis Analysis Report</h1>
|
|
49
|
+
<span class="pill ${HtmlReporter.healthClass(report.healthScore)}">Health score: ${report.healthScore}/100</span>
|
|
50
|
+
<div class="grid">
|
|
51
|
+
${HtmlReporter.summaryCard("Used Memory", report.memory.usedMemoryHuman)}
|
|
52
|
+
${HtmlReporter.summaryCard("Hit Rate", (0, format_1.formatPercent)(report.hitRate.hitRate))}
|
|
53
|
+
${HtmlReporter.summaryCard("Ops / sec", (0, format_1.formatNumber)(report.metrics.opsPerSec))}
|
|
54
|
+
${HtmlReporter.summaryCard("Total Keys", (0, format_1.formatNumber)(report.metrics.totalKeyCount))}
|
|
55
|
+
${HtmlReporter.summaryCard("Connected Clients", (0, format_1.formatNumber)(report.metrics.connectedClients))}
|
|
56
|
+
${HtmlReporter.summaryCard("Slow Commands", (0, format_1.formatNumber)(report.slowCommands.totalLogged))}
|
|
57
|
+
</div>
|
|
58
|
+
</div>
|
|
59
|
+
|
|
60
|
+
<section>
|
|
61
|
+
<h2>Recommendations</h2>
|
|
62
|
+
<ul>
|
|
63
|
+
${report.recommendations.map((item) => `<li><strong>${HtmlReporter.escape(item.priority)}</strong> <code>${HtmlReporter.escape(item.category)}</code> — ${HtmlReporter.escape(item.message)}</li>`).join("") || "<li>No recommendations</li>"}
|
|
64
|
+
</ul>
|
|
65
|
+
</section>
|
|
66
|
+
|
|
67
|
+
<section>
|
|
68
|
+
<h2>Memory</h2>
|
|
69
|
+
${HtmlReporter.table(["Metric", "Value"], [
|
|
70
|
+
["Used Memory", report.memory.usedMemoryHuman],
|
|
71
|
+
["Max Memory", report.memory.maxMemoryHuman || "unlimited"],
|
|
72
|
+
["Usage Percent", (0, format_1.formatPercent)(report.memory.usagePercent)],
|
|
73
|
+
["Fragmentation Ratio", report.memory.fragRatio.toFixed(2)],
|
|
74
|
+
["Fragmentation Waste", (0, format_1.formatBytes)(report.memory.fragBytes)],
|
|
75
|
+
["RSS Overhead", (0, format_1.formatBytes)(report.memory.rssOverhead)],
|
|
76
|
+
])}
|
|
77
|
+
</section>
|
|
78
|
+
|
|
79
|
+
<section>
|
|
80
|
+
<h2>Cache & Persistence</h2>
|
|
81
|
+
${HtmlReporter.table(["Metric", "Value"], [
|
|
82
|
+
["Hit Rate", (0, format_1.formatPercent)(report.hitRate.hitRate)],
|
|
83
|
+
["Evicted Keys", (0, format_1.formatNumber)(report.hitRate.evictedKeys)],
|
|
84
|
+
["Expired Keys", (0, format_1.formatNumber)(report.hitRate.expiredKeys)],
|
|
85
|
+
["RDB Status", report.persistence.rdb.lastStatus],
|
|
86
|
+
["AOF Enabled", report.persistence.aof.enabled ? "yes" : "no"],
|
|
87
|
+
["AOF Status", report.persistence.aof.lastStatus],
|
|
88
|
+
])}
|
|
89
|
+
</section>
|
|
90
|
+
|
|
91
|
+
<section>
|
|
92
|
+
<h2>Slow Commands</h2>
|
|
93
|
+
${HtmlReporter.table(["Command", "Count", "Avg Duration"], report.slowCommands.topCommandTypes.map((item) => [
|
|
94
|
+
item.command,
|
|
95
|
+
(0, format_1.formatNumber)(item.count),
|
|
96
|
+
(0, format_1.formatDuration)(item.avgMs),
|
|
97
|
+
]))}
|
|
98
|
+
</section>
|
|
99
|
+
|
|
100
|
+
<section>
|
|
101
|
+
<h2>Keyspaces</h2>
|
|
102
|
+
${HtmlReporter.table(["DB", "Keys", "Expiring Keys", "Avg TTL"], report.keyspaces.map((keyspace) => [
|
|
103
|
+
`db${keyspace.db}`,
|
|
104
|
+
(0, format_1.formatNumber)(keyspace.keys),
|
|
105
|
+
(0, format_1.formatNumber)(keyspace.expires),
|
|
106
|
+
(0, format_1.formatDuration)(keyspace.avgTtl),
|
|
107
|
+
]))}
|
|
108
|
+
</section>
|
|
109
|
+
</main>
|
|
110
|
+
</body>
|
|
111
|
+
</html>`;
|
|
112
|
+
}
|
|
113
|
+
static summaryCard(label, value) {
|
|
114
|
+
return `<div class="card"><h3>${HtmlReporter.escape(label)}</h3><p>${HtmlReporter.escape(value)}</p></div>`;
|
|
115
|
+
}
|
|
116
|
+
static table(headers, rows) {
|
|
117
|
+
if (rows.length === 0) {
|
|
118
|
+
return '<p class="muted">No data available.</p>';
|
|
119
|
+
}
|
|
120
|
+
return `<table><thead><tr>${headers.map((header) => `<th>${HtmlReporter.escape(header)}</th>`).join("")}</tr></thead><tbody>${rows.map((row) => `<tr>${row.map((cell) => `<td>${HtmlReporter.escape(cell)}</td>`).join("")}</tr>`).join("")}</tbody></table>`;
|
|
121
|
+
}
|
|
122
|
+
static healthClass(score) {
|
|
123
|
+
if (score >= 90) {
|
|
124
|
+
return "good";
|
|
125
|
+
}
|
|
126
|
+
if (score >= 70) {
|
|
127
|
+
return "warn";
|
|
128
|
+
}
|
|
129
|
+
return "bad";
|
|
130
|
+
}
|
|
131
|
+
static escape(value) {
|
|
132
|
+
return value
|
|
133
|
+
.replaceAll("&", "&")
|
|
134
|
+
.replaceAll("<", "<")
|
|
135
|
+
.replaceAll(">", ">")
|
|
136
|
+
.replaceAll('"', """)
|
|
137
|
+
.replaceAll("'", "'");
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
exports.HtmlReporter = HtmlReporter;
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import type { AnalyzerOptions, FullRedisReport } from "../types";
|
|
2
|
+
export declare class ReportGenerator {
|
|
3
|
+
private readonly outputDir;
|
|
4
|
+
constructor(outputDir?: string, _options?: AnalyzerOptions);
|
|
5
|
+
generateFullReport(report: FullRedisReport, timestamp?: string): Promise<string>;
|
|
6
|
+
generateJsonReport(report: FullRedisReport, timestamp?: string): Promise<string>;
|
|
7
|
+
generateHtmlReport(report: FullRedisReport, timestamp?: string): Promise<string>;
|
|
8
|
+
printSummary(report: FullRedisReport): void;
|
|
9
|
+
private ensureOutputDir;
|
|
10
|
+
private buildMarkdownReport;
|
|
11
|
+
private buildHeader;
|
|
12
|
+
private buildExecutiveSummary;
|
|
13
|
+
private buildMetricsSection;
|
|
14
|
+
private buildMemorySection;
|
|
15
|
+
private buildHitRateSection;
|
|
16
|
+
private buildPersistenceSection;
|
|
17
|
+
private buildReplicationSection;
|
|
18
|
+
private buildSlowCommandsSection;
|
|
19
|
+
private buildKeyspacesSection;
|
|
20
|
+
private buildConfigSection;
|
|
21
|
+
private buildRecommendationsSection;
|
|
22
|
+
}
|
|
23
|
+
//# sourceMappingURL=report-generator.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"report-generator.d.ts","sourceRoot":"","sources":["../../../src/reporters/report-generator.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EACX,eAAe,EACf,eAAe,EAGf,MAAM,UAAU,CAAC;AAgBlB,qBAAa,eAAe;IAE1B,OAAO,CAAC,QAAQ,CAAC,SAAS;gBAAT,SAAS,GAAE,MAAoB,EAChD,QAAQ,GAAE,eAAoB;IAKzB,kBAAkB,CACvB,MAAM,EAAE,eAAe,EACvB,SAAS,CAAC,EAAE,MAAM,GAChB,OAAO,CAAC,MAAM,CAAC;IAQZ,kBAAkB,CACvB,MAAM,EAAE,eAAe,EACvB,SAAS,CAAC,EAAE,MAAM,GAChB,OAAO,CAAC,MAAM,CAAC;IAQZ,kBAAkB,CACvB,MAAM,EAAE,eAAe,EACvB,SAAS,CAAC,EAAE,MAAM,GAChB,OAAO,CAAC,MAAM,CAAC;IAQlB,YAAY,CAAC,MAAM,EAAE,eAAe,GAAG,IAAI;YAiB7B,eAAe;IAM7B,OAAO,CAAC,mBAAmB;IAgB3B,OAAO,CAAC,WAAW;IAUnB,OAAO,CAAC,qBAAqB;IAqB7B,OAAO,CAAC,mBAAmB;IAgB3B,OAAO,CAAC,kBAAkB;IAgB1B,OAAO,CAAC,mBAAmB;IAa3B,OAAO,CAAC,uBAAuB;IAc/B,OAAO,CAAC,uBAAuB;IAY/B,OAAO,CAAC,wBAAwB;IAoBhC,OAAO,CAAC,qBAAqB;IAc7B,OAAO,CAAC,kBAAkB;IAW1B,OAAO,CAAC,2BAA2B;CAUnC"}
|