@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
package/CHANGELOG.md
ADDED
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
All notable changes to this project will be documented in this file.
|
|
4
|
+
|
|
5
|
+
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
|
|
6
|
+
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
7
|
+
|
|
8
|
+
## [Unreleased]
|
|
9
|
+
|
|
10
|
+
## [1.0.0] - 2026-06-14
|
|
11
|
+
|
|
12
|
+
### Added
|
|
13
|
+
|
|
14
|
+
- Redis analyzer CLI with memory, hit-rate, persistence, replication, keyspace, connection, and slow-command commands
|
|
15
|
+
- URI, host/port, TLS, password, and database-number connection support
|
|
16
|
+
- `.analyzerrc.json` profiles, compare mode, HTML reports, and watch mode
|
|
17
|
+
- Interactive CLI with report generation and runtime settings
|
|
18
|
+
- Markdown, JSON, and HTML reports under `./reports/`
|
|
19
|
+
- Diff reporting between JSON snapshots
|
|
20
|
+
- Automated tests for option parsing, Redis INFO parsing, health scoring, and report generation
|
|
21
|
+
- GitHub Actions CI and publish workflows
|
|
22
|
+
- Dependabot configuration for npm and GitHub Actions
|
|
23
|
+
- MIT license and OSS metadata
|
|
24
|
+
|
|
25
|
+
### Changed
|
|
26
|
+
|
|
27
|
+
- Full analysis generates Markdown, JSON, and optional HTML reports in one run
|
|
28
|
+
|
|
29
|
+
### Fixed
|
|
30
|
+
|
|
31
|
+
- Explicit `--profile` selection overrides sourced environment defaults for the active run
|
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Denis Cuciuc
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,244 @@
|
|
|
1
|
+
# Redis Analyzer
|
|
2
|
+
|
|
3
|
+
[](https://nodejs.org/)
|
|
4
|
+
[](https://www.npmjs.com/package/@deniscuciuc/redis-analyzer)
|
|
5
|
+
[](https://www.npmjs.com/package/@deniscuciuc/redis-analyzer)
|
|
6
|
+
[](LICENSE)
|
|
7
|
+
[](https://www.typescriptlang.org/)
|
|
8
|
+
[](https://github.com/deniscuciuc/db-analyzer-redis/actions/workflows/ci.yml)
|
|
9
|
+
|
|
10
|
+
A CLI tool that analyzes Redis health with a focus on memory pressure, cache hit rate, persistence safety, replication lag, connection pressure, and slow commands. It emits structured JSON for automation or writes Markdown, JSON, and optional HTML reports to `./reports/`.
|
|
11
|
+
|
|
12
|
+
## Quick start
|
|
13
|
+
|
|
14
|
+
No installation required:
|
|
15
|
+
|
|
16
|
+
```bash
|
|
17
|
+
npx @deniscuciuc/redis-analyzer -h localhost -p 6379 -c health
|
|
18
|
+
npx @deniscuciuc/redis-analyzer --uri redis://localhost:6379/0 -c full --json > report.json
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
Or install globally:
|
|
22
|
+
|
|
23
|
+
```bash
|
|
24
|
+
npm install -g @deniscuciuc/redis-analyzer
|
|
25
|
+
redis-analyzer -h your-host -p 6379 -c health
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
> **Working with an AI agent?** See [.github/copilot-instructions.md](.github/copilot-instructions.md) for the integrated GitHub Copilot workflow and JSON contracts.
|
|
29
|
+
|
|
30
|
+
---
|
|
31
|
+
|
|
32
|
+
## Table of Contents
|
|
33
|
+
|
|
34
|
+
- [Features](#features)
|
|
35
|
+
- [Requirements](#requirements)
|
|
36
|
+
- [Development / local setup](#development--local-setup)
|
|
37
|
+
- [Configuration](#configuration)
|
|
38
|
+
- [Usage](#usage)
|
|
39
|
+
- [Commands](#commands)
|
|
40
|
+
- [CLI options](#cli-options)
|
|
41
|
+
- [Output formats](#output-formats)
|
|
42
|
+
- [Health score](#health-score)
|
|
43
|
+
- [Architecture](#architecture)
|
|
44
|
+
- [Contributing](#contributing)
|
|
45
|
+
|
|
46
|
+
---
|
|
47
|
+
|
|
48
|
+
## Features
|
|
49
|
+
|
|
50
|
+
- Memory usage analysis with fragmentation and RSS overhead checks
|
|
51
|
+
- Cache hit-rate analysis with eviction and expiry context
|
|
52
|
+
- Persistence safety checks for RDB and AOF
|
|
53
|
+
- Replication health checks for standalone, primary, and replica nodes
|
|
54
|
+
- Slow command analysis from `SLOWLOG`
|
|
55
|
+
- Keyspace summaries by logical database
|
|
56
|
+
- Interactive mode, compare mode, HTML reports, and watch mode for safe commands
|
|
57
|
+
|
|
58
|
+
## Requirements
|
|
59
|
+
|
|
60
|
+
- Node.js >= 20
|
|
61
|
+
- pnpm >= 10
|
|
62
|
+
- Redis 6+
|
|
63
|
+
|
|
64
|
+
## Development / local setup
|
|
65
|
+
|
|
66
|
+
```bash
|
|
67
|
+
pnpm install
|
|
68
|
+
cp .env.example .env
|
|
69
|
+
# edit .env with your Redis connection details
|
|
70
|
+
pnpm build
|
|
71
|
+
node dist/index.js --help
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
## Configuration
|
|
75
|
+
|
|
76
|
+
### Environment variables (`.env`)
|
|
77
|
+
|
|
78
|
+
```bash
|
|
79
|
+
export REDIS_HOST=localhost
|
|
80
|
+
export REDIS_PORT=6379
|
|
81
|
+
export REDIS_DB=0
|
|
82
|
+
export REDIS_PASSWORD=secret
|
|
83
|
+
export REDIS_TLS=false
|
|
84
|
+
|
|
85
|
+
# Alternative: URI format
|
|
86
|
+
# export REDIS_URI=redis://:secret@redis.internal:6379/0
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
### Config file (`.analyzerrc.json`)
|
|
90
|
+
|
|
91
|
+
Place `.analyzerrc.json` in your project root (or `~/.config/db-analyzer/config.json` for global settings). Copy `analyzerrc.example.json` to get started:
|
|
92
|
+
|
|
93
|
+
```bash
|
|
94
|
+
cp analyzerrc.example.json .analyzerrc.json
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
Profiles let you switch between Redis instances:
|
|
98
|
+
|
|
99
|
+
```bash
|
|
100
|
+
. ./.env && npx ts-node index.ts -c health --profile prod
|
|
101
|
+
pnpm analyze:health -- --profile local
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
CLI flags always win. When you explicitly select `--profile`, that profile overrides the sourced environment defaults for that run.
|
|
105
|
+
|
|
106
|
+
## Usage
|
|
107
|
+
|
|
108
|
+
### Interactive mode
|
|
109
|
+
|
|
110
|
+
```bash
|
|
111
|
+
pnpm start
|
|
112
|
+
```
|
|
113
|
+
|
|
114
|
+
### npm scripts
|
|
115
|
+
|
|
116
|
+
```bash
|
|
117
|
+
pnpm analyze
|
|
118
|
+
pnpm analyze:help
|
|
119
|
+
pnpm analyze:health
|
|
120
|
+
pnpm analyze:memory
|
|
121
|
+
pnpm analyze:hit-rate
|
|
122
|
+
pnpm analyze:slow
|
|
123
|
+
pnpm analyze:keys
|
|
124
|
+
pnpm analyze:connections
|
|
125
|
+
pnpm analyze:persistence
|
|
126
|
+
pnpm analyze:replication
|
|
127
|
+
pnpm analyze:config
|
|
128
|
+
pnpm analyze:html
|
|
129
|
+
pnpm analyze:watch
|
|
130
|
+
pnpm server:info
|
|
131
|
+
pnpm build
|
|
132
|
+
pnpm lint
|
|
133
|
+
pnpm test
|
|
134
|
+
```
|
|
135
|
+
|
|
136
|
+
### Direct CLI
|
|
137
|
+
|
|
138
|
+
```bash
|
|
139
|
+
. ./.env && npx ts-node index.ts -j -c health
|
|
140
|
+
. ./.env && npx ts-node index.ts -j -c slow-commands --slow-threshold 5000
|
|
141
|
+
npx ts-node index.ts --uri redis://localhost:6379/0 -c full --html
|
|
142
|
+
```
|
|
143
|
+
|
|
144
|
+
---
|
|
145
|
+
|
|
146
|
+
## Commands
|
|
147
|
+
|
|
148
|
+
| Command | Description |
|
|
149
|
+
| ------- | ----------- |
|
|
150
|
+
| `full` | Complete analysis (default) |
|
|
151
|
+
| `health` | Health score and key metrics |
|
|
152
|
+
| `server-info` | Redis version, mode, uptime, config file |
|
|
153
|
+
| `memory` | Memory usage, fragmentation, and overhead |
|
|
154
|
+
| `hit-rate` | Cache hit ratio, expirations, and evictions |
|
|
155
|
+
| `slow-commands` | `SLOWLOG` analysis with top command types |
|
|
156
|
+
| `keys` | Keyspace counts, expiries, and average TTL |
|
|
157
|
+
| `connections` | Connected, blocked, rejected, and buffer metrics |
|
|
158
|
+
| `persistence` | RDB and AOF health |
|
|
159
|
+
| `replication` | Primary / replica status and lag |
|
|
160
|
+
| `config` | Important Redis configuration values |
|
|
161
|
+
|
|
162
|
+
## CLI options
|
|
163
|
+
|
|
164
|
+
| Option | Short | Description | Default |
|
|
165
|
+
| ------ | ----- | ----------- | ------- |
|
|
166
|
+
| `--host` | `-h` | Redis host | `localhost` |
|
|
167
|
+
| `--port` | `-p` | Redis port | `6379` |
|
|
168
|
+
| `--password` | `-a` | Redis password | - |
|
|
169
|
+
| `--db` | `-n` | Database number | `0` |
|
|
170
|
+
| `--tls` | | Enable TLS | `false` |
|
|
171
|
+
| `--uri` | | Redis URI (`redis://...`) | - |
|
|
172
|
+
| `--profile` | | Use named profile from `.analyzerrc.json` | - |
|
|
173
|
+
| `--config` | | Use a custom config file path | auto-search |
|
|
174
|
+
| `--slow-threshold` | | Slow command threshold in microseconds | `10000` |
|
|
175
|
+
| `--max-slow-commands` | | Max `SLOWLOG` rows to fetch | `25` |
|
|
176
|
+
| `--compare` | | Compare against a previous JSON report | - |
|
|
177
|
+
| `--watch` | | Poll interval in seconds | - |
|
|
178
|
+
| `--command` | `-c` | Run a specific analysis command | `full` |
|
|
179
|
+
| `--json` | `-j` | Output JSON | `false` |
|
|
180
|
+
| `--quiet` | `-q` | Suppress non-essential output | `false` |
|
|
181
|
+
| `--output` | `-o` | Reports directory | `./reports` |
|
|
182
|
+
| `--html` | | Also generate an HTML report | `false` |
|
|
183
|
+
| `--interactive` | `-i` | Interactive menu | `false` |
|
|
184
|
+
|
|
185
|
+
---
|
|
186
|
+
|
|
187
|
+
## Output formats
|
|
188
|
+
|
|
189
|
+
### JSON
|
|
190
|
+
|
|
191
|
+
Use `-j` to emit machine-readable JSON to stdout.
|
|
192
|
+
|
|
193
|
+
### Markdown / HTML reports
|
|
194
|
+
|
|
195
|
+
`full` analysis writes:
|
|
196
|
+
|
|
197
|
+
- Markdown report
|
|
198
|
+
- JSON report
|
|
199
|
+
- Optional HTML report when `--html` is set
|
|
200
|
+
|
|
201
|
+
### Compare mode
|
|
202
|
+
|
|
203
|
+
Compare two snapshots:
|
|
204
|
+
|
|
205
|
+
```bash
|
|
206
|
+
. ./.env && npx ts-node index.ts -c full --compare ./reports/redis-analysis-prev.json
|
|
207
|
+
```
|
|
208
|
+
|
|
209
|
+
## Health score
|
|
210
|
+
|
|
211
|
+
The analyzer starts at **100** and deducts points for:
|
|
212
|
+
|
|
213
|
+
- high memory usage or fragmentation
|
|
214
|
+
- low cache hit rate
|
|
215
|
+
- failed or stale persistence
|
|
216
|
+
- replication lag or broken links
|
|
217
|
+
- rejected client connections
|
|
218
|
+
|
|
219
|
+
Score guide:
|
|
220
|
+
|
|
221
|
+
| Score | Status |
|
|
222
|
+
| ----- | ------ |
|
|
223
|
+
| 90-100 | Excellent |
|
|
224
|
+
| 70-89 | Good |
|
|
225
|
+
| 50-69 | Warning |
|
|
226
|
+
| 0-49 | Critical |
|
|
227
|
+
|
|
228
|
+
## Architecture
|
|
229
|
+
|
|
230
|
+
```text
|
|
231
|
+
index.ts # CLI bootstrap and Redis connection setup
|
|
232
|
+
src/cli/{options,runner}.ts # CLI parsing and command execution
|
|
233
|
+
src/config/loader.ts # Config loading and profile resolution
|
|
234
|
+
src/collectors/stats-collector.ts # INFO / SLOWLOG / CONFIG collection
|
|
235
|
+
src/analyzers/*.ts # Memory, performance, persistence, replication analysis
|
|
236
|
+
src/reporters/*.ts # Markdown, HTML, and diff rendering
|
|
237
|
+
src/interactive/{index,display,menus}.ts
|
|
238
|
+
src/watch/runner.ts # Watch mode loop
|
|
239
|
+
tests/*.test.ts # Automated tests
|
|
240
|
+
```
|
|
241
|
+
|
|
242
|
+
## Contributing
|
|
243
|
+
|
|
244
|
+
See [CONTRIBUTING.md](CONTRIBUTING.md).
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
{
|
|
2
|
+
"profiles": {
|
|
3
|
+
"local": {
|
|
4
|
+
"host": "localhost",
|
|
5
|
+
"port": 6379,
|
|
6
|
+
"db": 0
|
|
7
|
+
},
|
|
8
|
+
"prod": {
|
|
9
|
+
"uri": "redis://:password@prod-redis.internal:6379/0",
|
|
10
|
+
"tls": true
|
|
11
|
+
}
|
|
12
|
+
},
|
|
13
|
+
"defaultProfile": "local",
|
|
14
|
+
"slowCommandThreshold": 10000,
|
|
15
|
+
"maxSlowCommands": 25,
|
|
16
|
+
"output": "./reports"
|
|
17
|
+
}
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../index.ts"],"names":[],"mappings":""}
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
"use strict";
|
|
3
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
4
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
5
|
+
};
|
|
6
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
7
|
+
const ioredis_1 = __importDefault(require("ioredis"));
|
|
8
|
+
const options_1 = require("./src/cli/options");
|
|
9
|
+
const runner_1 = require("./src/cli/runner");
|
|
10
|
+
const loader_1 = require("./src/config/loader");
|
|
11
|
+
const constants_1 = require("./src/constants");
|
|
12
|
+
const interactive_1 = require("./src/interactive");
|
|
13
|
+
const runner_2 = require("./src/watch/runner");
|
|
14
|
+
function resolveValue(cliValue, envValue, profileValue, fallbackValue, preferProfile) {
|
|
15
|
+
if (cliValue !== undefined && cliValue !== fallbackValue) {
|
|
16
|
+
return cliValue;
|
|
17
|
+
}
|
|
18
|
+
if (preferProfile) {
|
|
19
|
+
return profileValue ?? envValue ?? cliValue ?? fallbackValue;
|
|
20
|
+
}
|
|
21
|
+
return envValue ?? profileValue ?? cliValue ?? fallbackValue;
|
|
22
|
+
}
|
|
23
|
+
function resolveOptionalValue(cliValue, envValue, profileValue, preferProfile) {
|
|
24
|
+
if (cliValue !== undefined) {
|
|
25
|
+
return cliValue;
|
|
26
|
+
}
|
|
27
|
+
if (preferProfile) {
|
|
28
|
+
return profileValue ?? envValue;
|
|
29
|
+
}
|
|
30
|
+
return envValue ?? profileValue;
|
|
31
|
+
}
|
|
32
|
+
function parseNumber(value) {
|
|
33
|
+
if (!value) {
|
|
34
|
+
return undefined;
|
|
35
|
+
}
|
|
36
|
+
const parsed = Number.parseInt(value, 10);
|
|
37
|
+
return Number.isFinite(parsed) ? parsed : undefined;
|
|
38
|
+
}
|
|
39
|
+
function parseBoolean(value) {
|
|
40
|
+
if (!value) {
|
|
41
|
+
return undefined;
|
|
42
|
+
}
|
|
43
|
+
return value === "true" || value === "1";
|
|
44
|
+
}
|
|
45
|
+
function resolveConnectionDetails(connection) {
|
|
46
|
+
if (!connection.uri) {
|
|
47
|
+
return connection;
|
|
48
|
+
}
|
|
49
|
+
try {
|
|
50
|
+
const parsed = new URL(connection.uri);
|
|
51
|
+
return {
|
|
52
|
+
...connection,
|
|
53
|
+
host: parsed.hostname || connection.host,
|
|
54
|
+
port: parsed.port ? Number.parseInt(parsed.port, 10) : connection.port,
|
|
55
|
+
db: parsed.pathname
|
|
56
|
+
? Number.parseInt(parsed.pathname.replace("/", ""), 10) || connection.db
|
|
57
|
+
: connection.db,
|
|
58
|
+
};
|
|
59
|
+
}
|
|
60
|
+
catch {
|
|
61
|
+
return connection;
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
async function closeClient(client) {
|
|
65
|
+
try {
|
|
66
|
+
await client.quit();
|
|
67
|
+
}
|
|
68
|
+
catch {
|
|
69
|
+
client.disconnect();
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
async function main() {
|
|
73
|
+
const options = (0, options_1.parseOptions)();
|
|
74
|
+
const config = (0, loader_1.loadConfig)(options.config);
|
|
75
|
+
const profile = (0, loader_1.resolveProfile)(config, options.profile);
|
|
76
|
+
const preferProfile = Boolean(options.profile);
|
|
77
|
+
if (options.watch !== undefined && options.json) {
|
|
78
|
+
throw new Error("--watch cannot be combined with --json.");
|
|
79
|
+
}
|
|
80
|
+
const connection = resolveConnectionDetails({
|
|
81
|
+
host: resolveValue(options.host, process.env.REDIS_HOST, profile.host, constants_1.DEFAULTS.host, preferProfile),
|
|
82
|
+
port: resolveValue(options.port, parseNumber(process.env.REDIS_PORT), profile.port, constants_1.DEFAULTS.port, preferProfile),
|
|
83
|
+
password: resolveOptionalValue(options.password, process.env.REDIS_PASSWORD, profile.password, preferProfile),
|
|
84
|
+
db: resolveValue(options.db, parseNumber(process.env.REDIS_DB), profile.db, constants_1.DEFAULTS.db, preferProfile),
|
|
85
|
+
tls: resolveValue(options.tls, parseBoolean(process.env.REDIS_TLS), profile.tls, false, preferProfile),
|
|
86
|
+
uri: resolveOptionalValue(options.uri, process.env.REDIS_URI, profile.uri, preferProfile),
|
|
87
|
+
});
|
|
88
|
+
const runtimeOptions = {
|
|
89
|
+
...options,
|
|
90
|
+
outputDir: resolveValue(options.outputDir, undefined, config.output, constants_1.DEFAULTS.output, false),
|
|
91
|
+
slowCommandThreshold: resolveValue(options.slowCommandThreshold, undefined, config.slowCommandThreshold, constants_1.DEFAULTS.slowCommandThreshold, false),
|
|
92
|
+
maxSlowCommands: resolveValue(options.maxSlowCommands, undefined, config.maxSlowCommands, constants_1.DEFAULTS.maxSlowCommands, false),
|
|
93
|
+
};
|
|
94
|
+
const client = connection.uri
|
|
95
|
+
? new ioredis_1.default(connection.uri, {
|
|
96
|
+
lazyConnect: true,
|
|
97
|
+
tls: connection.tls ? {} : undefined,
|
|
98
|
+
})
|
|
99
|
+
: new ioredis_1.default({
|
|
100
|
+
host: connection.host,
|
|
101
|
+
port: connection.port,
|
|
102
|
+
password: connection.password,
|
|
103
|
+
db: connection.db,
|
|
104
|
+
tls: connection.tls ? {} : undefined,
|
|
105
|
+
lazyConnect: true,
|
|
106
|
+
});
|
|
107
|
+
try {
|
|
108
|
+
await client.connect();
|
|
109
|
+
await client.ping();
|
|
110
|
+
if (runtimeOptions.interactive) {
|
|
111
|
+
const interactive = new interactive_1.InteractiveCLI(client, connection, runtimeOptions);
|
|
112
|
+
await interactive.start();
|
|
113
|
+
return;
|
|
114
|
+
}
|
|
115
|
+
if (runtimeOptions.watch !== undefined) {
|
|
116
|
+
await (0, runner_2.runWatchLoop)({
|
|
117
|
+
intervalSeconds: runtimeOptions.watch,
|
|
118
|
+
command: runtimeOptions.command,
|
|
119
|
+
runCommand: () => (0, runner_1.executeCommand)(client, runtimeOptions, connection),
|
|
120
|
+
});
|
|
121
|
+
return;
|
|
122
|
+
}
|
|
123
|
+
await (0, runner_1.executeCommand)(client, runtimeOptions, connection);
|
|
124
|
+
}
|
|
125
|
+
catch (error) {
|
|
126
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
127
|
+
throw new Error(`Cannot connect to Redis: ${message}`);
|
|
128
|
+
}
|
|
129
|
+
finally {
|
|
130
|
+
await closeClient(client);
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
main().catch((error) => {
|
|
134
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
135
|
+
console.error("Error during analysis:", message);
|
|
136
|
+
process.exit(1);
|
|
137
|
+
});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"memory-analyzer.d.ts","sourceRoot":"","sources":["../../../src/analyzers/memory-analyzer.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,cAAc,EAAE,SAAS,EAAE,MAAM,UAAU,CAAC;AAG1D,qBAAa,cAAc;IAC1B,OAAO,CAAC,IAAI,EAAE,SAAS,GAAG,cAAc;CA+DxC"}
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.MemoryAnalyzer = void 0;
|
|
4
|
+
const constants_1 = require("../constants");
|
|
5
|
+
const format_1 = require("../utils/format");
|
|
6
|
+
class MemoryAnalyzer {
|
|
7
|
+
analyze(info) {
|
|
8
|
+
const hasLimit = info.maxmemory > 0;
|
|
9
|
+
const usagePercent = hasLimit
|
|
10
|
+
? (info.usedMemory / info.maxmemory) * 100
|
|
11
|
+
: 0;
|
|
12
|
+
const fragRatio = info.memFragmentationRatio;
|
|
13
|
+
const fragSeverity = fragRatio >= constants_1.THRESHOLDS.memory.fragCritical
|
|
14
|
+
? "critical"
|
|
15
|
+
: fragRatio >= constants_1.THRESHOLDS.memory.fragWarning
|
|
16
|
+
? "warning"
|
|
17
|
+
: "ok";
|
|
18
|
+
const recommendations = [];
|
|
19
|
+
if (hasLimit && usagePercent >= constants_1.THRESHOLDS.memory.usageCritical) {
|
|
20
|
+
recommendations.push(`Memory usage at ${usagePercent.toFixed(1)}% — increase maxmemory or reduce data size immediately.`);
|
|
21
|
+
}
|
|
22
|
+
else if (hasLimit && usagePercent >= constants_1.THRESHOLDS.memory.usageWarning) {
|
|
23
|
+
recommendations.push(`Memory at ${usagePercent.toFixed(1)}% of maxmemory — plan capacity increase.`);
|
|
24
|
+
}
|
|
25
|
+
if (!hasLimit) {
|
|
26
|
+
recommendations.push("maxmemory is not set — Redis will grow until an OOM kill unless the host limits it.");
|
|
27
|
+
}
|
|
28
|
+
if (fragSeverity === "critical") {
|
|
29
|
+
recommendations.push(`High fragmentation ratio ${fragRatio.toFixed(2)} (${(0, format_1.formatBytes)(info.memFragmentationBytes)} wasted). Run MEMORY PURGE or restart Redis during a low-traffic window.`);
|
|
30
|
+
}
|
|
31
|
+
else if (fragSeverity === "warning") {
|
|
32
|
+
recommendations.push(`Moderate fragmentation ratio ${fragRatio.toFixed(2)}. Monitor allocator pressure and peak RSS.`);
|
|
33
|
+
}
|
|
34
|
+
if (info.usedMemory > 0 && info.usedMemoryRss > info.usedMemory * 2) {
|
|
35
|
+
recommendations.push(`RSS (${(0, format_1.formatBytes)(info.usedMemoryRss)}) is more than 2x allocated memory (${info.usedMemoryHuman}). Consider MEMORY PURGE, jemalloc tuning, or a controlled restart.`);
|
|
36
|
+
}
|
|
37
|
+
return {
|
|
38
|
+
usedMemory: info.usedMemory,
|
|
39
|
+
usedMemoryHuman: info.usedMemoryHuman,
|
|
40
|
+
maxMemory: info.maxmemory,
|
|
41
|
+
maxMemoryHuman: info.maxmemoryHuman,
|
|
42
|
+
usagePercent,
|
|
43
|
+
fragRatio,
|
|
44
|
+
fragBytes: info.memFragmentationBytes,
|
|
45
|
+
fragSeverity,
|
|
46
|
+
rssOverhead: info.usedMemoryRss - info.usedMemory,
|
|
47
|
+
peakMemory: info.usedMemoryPeak,
|
|
48
|
+
dataMemory: info.usedMemoryDataset,
|
|
49
|
+
overheadMemory: info.usedMemoryOverhead,
|
|
50
|
+
recommendations,
|
|
51
|
+
};
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
exports.MemoryAnalyzer = MemoryAnalyzer;
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import type { HitRateAnalysis, RedisInfo, SlowCommand, SlowCommandAnalysis } from "../types";
|
|
2
|
+
export declare class PerformanceAnalyzer {
|
|
3
|
+
analyzeHitRate(info: RedisInfo): HitRateAnalysis;
|
|
4
|
+
analyzeSlowCommands(commands: SlowCommand[], totalLogged: number, threshold: number): SlowCommandAnalysis;
|
|
5
|
+
}
|
|
6
|
+
//# sourceMappingURL=performance-analyzer.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"performance-analyzer.d.ts","sourceRoot":"","sources":["../../../src/analyzers/performance-analyzer.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EACX,eAAe,EACf,SAAS,EACT,WAAW,EACX,mBAAmB,EAEnB,MAAM,UAAU,CAAC;AAElB,qBAAa,mBAAmB;IAC/B,cAAc,CAAC,IAAI,EAAE,SAAS,GAAG,eAAe;IAwChD,mBAAmB,CAClB,QAAQ,EAAE,WAAW,EAAE,EACvB,WAAW,EAAE,MAAM,EACnB,SAAS,EAAE,MAAM,GACf,mBAAmB;CAmDtB"}
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.PerformanceAnalyzer = void 0;
|
|
4
|
+
const constants_1 = require("../constants");
|
|
5
|
+
class PerformanceAnalyzer {
|
|
6
|
+
analyzeHitRate(info) {
|
|
7
|
+
const total = info.keyspaceHits + info.keyspaceMisses;
|
|
8
|
+
const hitRate = total === 0 ? 100 : (info.keyspaceHits / total) * 100;
|
|
9
|
+
const evictionSeverity = info.evictedKeys > 0 && info.maxmemoryPolicy === "noeviction"
|
|
10
|
+
? "critical"
|
|
11
|
+
: info.evictedKeys > 1000
|
|
12
|
+
? "warning"
|
|
13
|
+
: "ok";
|
|
14
|
+
const recommendations = [];
|
|
15
|
+
if (hitRate < constants_1.THRESHOLDS.hitRate.critical) {
|
|
16
|
+
recommendations.push(`Cache hit rate is critically low at ${hitRate.toFixed(1)}%. Review TTL strategy, memory sizing, and whether Redis is serving the right workload.`);
|
|
17
|
+
}
|
|
18
|
+
else if (hitRate < constants_1.THRESHOLDS.hitRate.warning) {
|
|
19
|
+
recommendations.push(`Cache hit rate ${hitRate.toFixed(1)}% is below the 90% target. Review hot keys, eviction pressure, and expiry policy.`);
|
|
20
|
+
}
|
|
21
|
+
if (info.evictedKeys > 0) {
|
|
22
|
+
recommendations.push(`${info.evictedKeys.toLocaleString()} keys have been evicted (policy: ${info.maxmemoryPolicy}). Increase headroom or adjust TTL and eviction policy.`);
|
|
23
|
+
}
|
|
24
|
+
return {
|
|
25
|
+
hits: info.keyspaceHits,
|
|
26
|
+
misses: info.keyspaceMisses,
|
|
27
|
+
hitRate,
|
|
28
|
+
evictedKeys: info.evictedKeys,
|
|
29
|
+
expiredKeys: info.expiredKeys,
|
|
30
|
+
evictionPolicy: info.maxmemoryPolicy,
|
|
31
|
+
evictionSeverity,
|
|
32
|
+
recommendations,
|
|
33
|
+
};
|
|
34
|
+
}
|
|
35
|
+
analyzeSlowCommands(commands, totalLogged, threshold) {
|
|
36
|
+
const byCommand = new Map();
|
|
37
|
+
for (const command of commands) {
|
|
38
|
+
const name = command.command[0]?.toUpperCase() ?? "UNKNOWN";
|
|
39
|
+
const current = byCommand.get(name) ?? { count: 0, totalMs: 0 };
|
|
40
|
+
byCommand.set(name, {
|
|
41
|
+
count: current.count + 1,
|
|
42
|
+
totalMs: current.totalMs + command.durationMs,
|
|
43
|
+
});
|
|
44
|
+
}
|
|
45
|
+
const topCommandTypes = [...byCommand.entries()]
|
|
46
|
+
.sort((left, right) => right[1].count - left[1].count)
|
|
47
|
+
.slice(0, 5)
|
|
48
|
+
.map(([command, summary]) => ({
|
|
49
|
+
command,
|
|
50
|
+
count: summary.count,
|
|
51
|
+
avgMs: summary.totalMs / summary.count,
|
|
52
|
+
}));
|
|
53
|
+
const recommendations = [];
|
|
54
|
+
if (totalLogged > 100) {
|
|
55
|
+
recommendations.push(`${totalLogged} slow commands were logged. Check for O(N) operations such as KEYS, large set scans, or long list traversals.`);
|
|
56
|
+
}
|
|
57
|
+
const dangerousCommands = new Set([
|
|
58
|
+
"KEYS",
|
|
59
|
+
"FLUSHDB",
|
|
60
|
+
"FLUSHALL",
|
|
61
|
+
"DEBUG",
|
|
62
|
+
"CONFIG",
|
|
63
|
+
]);
|
|
64
|
+
for (const command of topCommandTypes) {
|
|
65
|
+
if (dangerousCommands.has(command.command)) {
|
|
66
|
+
recommendations.push(`${command.command} appears in the slow log. Replace KEYS with SCAN and avoid administrative commands on the hot path.`);
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
return {
|
|
70
|
+
totalLogged,
|
|
71
|
+
commands,
|
|
72
|
+
topCommandTypes,
|
|
73
|
+
threshold,
|
|
74
|
+
recommendations,
|
|
75
|
+
};
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
exports.PerformanceAnalyzer = PerformanceAnalyzer;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"persistence-analyzer.d.ts","sourceRoot":"","sources":["../../../src/analyzers/persistence-analyzer.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,mBAAmB,EAAE,SAAS,EAAE,MAAM,UAAU,CAAC;AAE/D,qBAAa,mBAAmB;IAC/B,OAAO,CAAC,IAAI,EAAE,SAAS,GAAG,mBAAmB;CAsE7C"}
|