replica-failover-mongodb-ts 2.2.4 → 3.0.3

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
@@ -5,14 +5,52 @@
5
5
  [![Node.js](https://img.shields.io/badge/Node.js-20.x-green)](https://nodejs.org/)
6
6
  [![TypeScript](https://img.shields.io/badge/TypeScript-5.x-blue)](https://www.typescriptlang.org/)
7
7
 
8
- ## 🚀 Quick Start (Dashboard CLI)
8
+ ## 🚀 QuickStart (Plug & Play)
9
9
 
10
- Se você quer apenas rodar o painel de controle visualmente para testar qualquer API:
10
+ Escolha como você quer usar o projeto:
11
11
 
12
- ```bash
13
- npm install -g replica-failover-mongodb-ts
14
- node-balancer-dashboard
15
- ```
12
+ ### Opção A: Via NPM (Apenas Dashboard)
13
+ Ideal se você já tem um cluster MongoDB e quer apenas visualizar/controlar.
14
+
15
+ 1. **Instale a ferramenta globalmente:**
16
+ ```bash
17
+ npm install -g replica-failover-mongodb-ts
18
+ ```
19
+
20
+ 2. **Rode o dashboard:**
21
+ ```bash
22
+ node-balancer-dashboard
23
+ ```
24
+
25
+ 3. **Responda às perguntas de configuração:**
26
+ O sistema pedirá os dados do seu ambiente. Exemplo de preenchimento:
27
+
28
+ - **API URL**: `http://localhost:3000/api/users`
29
+ - **MongoDB Nodes**: `mongodb://localhost:27017,mongodb://localhost:27018`
30
+ - **Enable Docker Control?**: `Yes` (Se quiser parar/iniciar containers pelo painel)
31
+ - **Docker Container Names**: `mongo1,mongo2,mongo3`
32
+
33
+ ### Opção B: Via Git (Ambiente Completo)
34
+ Ideal para ver a mágica acontecer do zero (cria API + Banco + Réplicas).
35
+
36
+ 1. **Clone e Instale:**
37
+ ```bash
38
+ git clone https://github.com/JoaoIto/node-balancer.git
39
+ cd node-balancer
40
+ npm install
41
+ ```
42
+
43
+ 2. **Suba o Ambiente (Docker):**
44
+ ```bash
45
+ docker-compose up -d --build
46
+ ```
47
+ *Aguarde ~30s para o cluster configurar.*
48
+
49
+ 3. **Rode o Dashboard:**
50
+ ```bash
51
+ npm run dashboard
52
+ ```
53
+ *Pronto! Selecione "RUN CHAOS DEMO" e divirta-se.*
16
54
 
17
55
  ---
18
56
 
@@ -32,8 +70,9 @@ O Node Balancer é uma API escalável construída utilizando Node.js, MongoDB co
32
70
  4. [Visual Dashboard (Painel de Controle)](#visual-dashboard-painel-de-controle)
33
71
  5. [Uso Avançado do Dashboard (CLI)](#uso-avançado-do-dashboard-cli)
34
72
  6. [Testes e Automação (Chaos Testing)](#testes-e-automação-chaos-testing)
35
- 7. [Documentação Detalhada](#documentação-detalhada)
36
- 8. [Configuração Manual (Referência)](#configuração-manual-referência)
73
+ 7. [Observabilidade (v3.0)](#observabilidade-v30)
74
+ 8. [Documentação Detalhada](#documentação-detalhada)
75
+ 9. [Configuração Manual (Referência)](#configuração-manual-referência)
37
76
 
38
77
  ---
39
78
 
@@ -201,6 +240,29 @@ npm run ops:demo
201
240
 
202
241
  ---
203
242
 
243
+ ## Observabilidade (v3.0)
244
+
245
+ A versão 3.0 introduz recursos avançados de monitoramento para produção:
246
+
247
+ ### 🔔 Webhooks (Rápido)
248
+ Receba alertas no seu Slack ou Discord. Basta passar a URL ao iniciar:
249
+
250
+ ```typescript
251
+ const db = new ConnectionManager({
252
+ nodes: [...],
253
+ webhookUrl: 'https://discord.com/api/webhooks/...' // Sua URL aqui
254
+ });
255
+ ```
256
+ *O sistema fará um POST automático com JSON sempre que houver um failover.*
257
+
258
+ ### 📊 Métricas e Real-time
259
+ - **Prometheus**: Acesse `http://localhost:3000/metrics` para ver dados de latência e conexão.
260
+ - **WebSocket**: Conecte via Socket.io para receber logs em tempo real.
261
+
262
+ 👉 **[Leia o guia completo de Observabilidade (Português)](docs/observability.md)**
263
+
264
+ ---
265
+
204
266
  ## Documentação Detalhada
205
267
 
206
268
  Para mais detalhes, consulte os guias na pasta `docs/`:
@@ -208,6 +270,7 @@ Para mais detalhes, consulte os guias na pasta `docs/`:
208
270
  - 🖥️ **[Guia do Dashboard (Visual Runner)](docs/dashboard-runner.md)**: Manual completo do painel interativo.
209
271
  - 📄 **[Guia de Testes e Execução (Demo Runner)](docs/demo-runner.md)**: Passo a passo detalhado de como rodar os testes manuais e automatizados.
210
272
  - 🛠️ **[Documentação dos Scripts](docs/scripts.md)**: Explicação técnica de como os scripts de automação funcionam.
273
+ - 📡 **[Observabilidade e Alertas](docs/observability.md)**: Guia de configuração de Webhooks, Métricas e WebSocket.
211
274
 
212
275
  ---
213
276
 
@@ -8,10 +8,16 @@ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, ge
8
8
  step((generator = generator.apply(thisArg, _arguments || [])).next());
9
9
  });
10
10
  };
11
+ var __importDefault = (this && this.__importDefault) || function (mod) {
12
+ return (mod && mod.__esModule) ? mod : { "default": mod };
13
+ };
11
14
  Object.defineProperty(exports, "__esModule", { value: true });
12
15
  exports.ConnectionManager = void 0;
13
16
  const mongodb_1 = require("mongodb");
17
+ const axios_1 = __importDefault(require("axios"));
14
18
  const logger_1 = require("../middlewares/logger");
19
+ const websocket_1 = require("./websocket");
20
+ const metrics_1 = require("./metrics");
15
21
  class ConnectionManager {
16
22
  constructor(opts) {
17
23
  var _a;
@@ -22,6 +28,7 @@ class ConnectionManager {
22
28
  this.nodes = (opts.nodes || []).map((u, i) => ({ uri: u, name: `node${i + 1}` }));
23
29
  this.dbName = opts.dbName || 'node-balancer';
24
30
  this.healthCheckIntervalMs = (_a = opts.healthCheckIntervalMs) !== null && _a !== void 0 ? _a : 5000;
31
+ this.webhookUrl = opts.webhookUrl;
25
32
  }
26
33
  init() {
27
34
  return __awaiter(this, void 0, void 0, function* () {
@@ -77,12 +84,16 @@ class ConnectionManager {
77
84
  this.primaryDb = client.db(this.dbName);
78
85
  // register monitoring events on the client
79
86
  client.on('topologyDescriptionChanged', (td) => {
87
+ var _a;
80
88
  logger_1.logger.info(`topologyDescriptionChanged: ${JSON.stringify(this.summarizeTopology(td))}`);
81
- this.recordEvent('topologyDescriptionChanged', { td: this.summarizeTopology(td) }).catch(() => { });
89
+ const summary = this.summarizeTopology(td);
90
+ this.recordEvent('topologyDescriptionChanged', { td: summary }).catch(() => { });
91
+ (_a = (0, websocket_1.getIO)()) === null || _a === void 0 ? void 0 : _a.emit('topology-change', summary);
82
92
  });
83
93
  client.on('serverHeartbeatFailed', (event) => {
84
94
  logger_1.logger.warn(`serverHeartbeatFailed: ${JSON.stringify(event)}`);
85
95
  this.recordEvent('serverHeartbeatFailed', { event }).catch(() => { });
96
+ this.sendAlert('serverHeartbeatFailed', event).catch(() => { });
86
97
  });
87
98
  client.on('serverHeartbeatSucceeded', (event) => {
88
99
  logger_1.logger.debug(`serverHeartbeatSucceeded: ${JSON.stringify(event)}`);
@@ -92,6 +103,7 @@ class ConnectionManager {
92
103
  this.recordEvent('clientClose', {}).catch(() => { });
93
104
  });
94
105
  logger_1.logger.info('Primary client attached.');
106
+ metrics_1.connectionStatus.set(1);
95
107
  }
96
108
  summarizeTopology(td) {
97
109
  // gentle summary - driver topologyDescription shape may vary
@@ -130,21 +142,38 @@ class ConnectionManager {
130
142
  }
131
143
  recordEvent(type, payload) {
132
144
  return __awaiter(this, void 0, void 0, function* () {
145
+ var _a;
133
146
  try {
134
- if (!this.primaryDb)
135
- return;
136
- yield this.primaryDb.collection('logs').insertOne({
147
+ const event = {
137
148
  ts: new Date(),
138
149
  level: 'event',
139
150
  type,
140
151
  payload,
141
- });
152
+ };
153
+ (_a = (0, websocket_1.getIO)()) === null || _a === void 0 ? void 0 : _a.emit('log', event);
154
+ if (!this.primaryDb)
155
+ return;
156
+ yield this.primaryDb.collection('logs').insertOne(event);
142
157
  }
143
158
  catch (err) {
144
159
  logger_1.logger.warn('recordEvent failed: ' + err.message);
145
160
  }
146
161
  });
147
162
  }
163
+ sendAlert(event, details) {
164
+ return __awaiter(this, void 0, void 0, function* () {
165
+ if (!this.webhookUrl)
166
+ return;
167
+ try {
168
+ yield axios_1.default.post(this.webhookUrl, {
169
+ text: `⚠️ **NodeBalancer Alert**\n**Event**: ${event}\n**Details**: \`\`\`${JSON.stringify(details, null, 2)}\`\`\``
170
+ });
171
+ }
172
+ catch (err) {
173
+ logger_1.logger.warn(`Failed to send webhook alert: ${err.message}`);
174
+ }
175
+ });
176
+ }
148
177
  startHealthChecks() {
149
178
  if (this.healthInterval)
150
179
  clearInterval(this.healthInterval);
@@ -165,6 +194,7 @@ class ConnectionManager {
165
194
  catch (_a) { }
166
195
  this.primaryClient = null;
167
196
  this.primaryDb = null;
197
+ metrics_1.connectionStatus.set(0);
168
198
  }
169
199
  // attempt to find a writable node among nodes (directConnection)
170
200
  for (const n of this.nodes) {
@@ -176,6 +206,8 @@ class ConnectionManager {
176
206
  logger_1.logger.info(`Health-check: promoted ${n.uri} to primary connection`);
177
207
  this.attachClient(c);
178
208
  yield this.recordEvent('promote', { node: n.uri });
209
+ yield this.sendAlert('promote', { node: n.uri, message: 'Promoted new primary connection' });
210
+ metrics_1.failoverCount.inc();
179
211
  return;
180
212
  }
181
213
  else {
@@ -189,6 +221,7 @@ class ConnectionManager {
189
221
  // no writable found
190
222
  logger_1.logger.error('Health-check: no writable nodes found.');
191
223
  yield this.recordEvent('no-writable', {});
224
+ yield this.sendAlert('no-writable', { message: 'CRITICAL: No writable nodes found in cluster!' });
192
225
  });
193
226
  }
194
227
  getDb() {
@@ -212,6 +245,7 @@ class ConnectionManager {
212
245
  meta,
213
246
  durationMs: took,
214
247
  });
248
+ metrics_1.operationDuration.observe({ operation: 'read', collection: collectionName, success: 'true' }, took / 1000);
215
249
  return res;
216
250
  }
217
251
  catch (err) {
@@ -225,6 +259,7 @@ class ConnectionManager {
225
259
  meta,
226
260
  durationMs: took,
227
261
  });
262
+ metrics_1.operationDuration.observe({ operation: 'read', collection: collectionName, success: 'false' }, took / 1000);
228
263
  throw err;
229
264
  }
230
265
  });
@@ -246,6 +281,7 @@ class ConnectionManager {
246
281
  meta,
247
282
  durationMs: took,
248
283
  });
284
+ metrics_1.operationDuration.observe({ operation: 'write', collection: collectionName, success: 'true' }, took / 1000);
249
285
  return res;
250
286
  }
251
287
  catch (err) {
@@ -259,13 +295,16 @@ class ConnectionManager {
259
295
  meta,
260
296
  durationMs: took,
261
297
  });
298
+ metrics_1.operationDuration.observe({ operation: 'write', collection: collectionName, success: 'false' }, took / 1000);
262
299
  throw err;
263
300
  }
264
301
  });
265
302
  }
266
303
  safeLog(doc) {
267
304
  return __awaiter(this, void 0, void 0, function* () {
305
+ var _a;
268
306
  try {
307
+ (_a = (0, websocket_1.getIO)()) === null || _a === void 0 ? void 0 : _a.emit('log', doc);
269
308
  if (!this.primaryDb) {
270
309
  logger_1.logger.warn('safeLog: no primaryDb, skipping db log. Logging to console instead.');
271
310
  logger_1.logger.info(JSON.stringify(doc));
@@ -0,0 +1,29 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.operationDuration = exports.failoverCount = exports.connectionStatus = exports.register = void 0;
7
+ const prom_client_1 = __importDefault(require("prom-client"));
8
+ // Create a Registry
9
+ exports.register = new prom_client_1.default.Registry();
10
+ // Add default metrics (cpu, memory, etc.)
11
+ prom_client_1.default.collectDefaultMetrics({ register: exports.register });
12
+ // Define custom metrics
13
+ exports.connectionStatus = new prom_client_1.default.Gauge({
14
+ name: 'node_balancer_connection_status',
15
+ help: 'Status of the MongoDB connection (1 = connected, 0 = disconnected)',
16
+ registers: [exports.register]
17
+ });
18
+ exports.failoverCount = new prom_client_1.default.Counter({
19
+ name: 'node_balancer_failover_count',
20
+ help: 'Total number of failover events triggered',
21
+ registers: [exports.register]
22
+ });
23
+ exports.operationDuration = new prom_client_1.default.Histogram({
24
+ name: 'node_balancer_operation_duration_seconds',
25
+ help: 'Duration of database operations in seconds',
26
+ labelNames: ['operation', 'collection', 'success'],
27
+ buckets: [0.01, 0.05, 0.1, 0.5, 1, 2, 5],
28
+ registers: [exports.register]
29
+ });
@@ -0,0 +1,30 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.getIO = exports.initWebSocket = void 0;
4
+ const socket_io_1 = require("socket.io");
5
+ const logger_1 = require("../middlewares/logger");
6
+ let io = null;
7
+ const initWebSocket = (httpServer) => {
8
+ io = new socket_io_1.Server(httpServer, {
9
+ cors: {
10
+ origin: "*",
11
+ methods: ["GET", "POST"]
12
+ }
13
+ });
14
+ io.on('connection', (socket) => {
15
+ logger_1.logger.info(`New WebSocket connection: ${socket.id}`);
16
+ socket.on('disconnect', () => {
17
+ logger_1.logger.info(`WebSocket disconnected: ${socket.id}`);
18
+ });
19
+ });
20
+ logger_1.logger.info('WebSocket Server initialized');
21
+ return io;
22
+ };
23
+ exports.initWebSocket = initWebSocket;
24
+ const getIO = () => {
25
+ if (!io) {
26
+ logger_1.logger.warn('getIO called before initialization');
27
+ }
28
+ return io;
29
+ };
30
+ exports.getIO = getIO;
@@ -18,70 +18,26 @@ const blessed_contrib_1 = __importDefault(require("blessed-contrib"));
18
18
  const mongodb_1 = require("mongodb");
19
19
  const http_1 = __importDefault(require("http"));
20
20
  const child_process_1 = require("child_process");
21
+ const inquirer_1 = __importDefault(require("inquirer"));
22
+ const socket_io_client_1 = require("socket.io-client");
21
23
  // Parse Args
22
24
  const args = process.argv.slice(2);
23
- function getArg(flag, def) {
25
+ function getArg(flag, def = null) {
24
26
  const idx = args.indexOf(flag);
25
27
  return idx !== -1 && args[idx + 1] ? args[idx + 1] : def;
26
28
  }
27
- const API_URL = getArg('--api-url', 'http://localhost:3000/api/users');
28
- const NODES_ARG = getArg('--nodes', 'mongodb://localhost:27017/node-balancer,mongodb://localhost:27018/node-balancer,mongodb://localhost:27019/node-balancer');
29
- const DOCKER_CONTAINERS_ARG = getArg('--docker-containers', 'mongo1,mongo2,mongo3');
30
- const NO_DOCKER = args.includes('--no-docker');
31
- const NODES = NODES_ARG.split(',').map((uri, i) => ({
32
- name: `node${i + 1}`,
33
- uri: uri.trim()
34
- }));
35
- const DOCKER_NODES = DOCKER_CONTAINERS_ARG.split(',').map(n => n.trim());
36
- // Screen Setup
37
- const screen = blessed_1.default.screen({
38
- smartCSR: true,
39
- title: 'NodeBalancer Control Center'
40
- });
41
- const grid = new blessed_contrib_1.default.grid({ rows: 12, cols: 12, screen: screen });
42
- // Components
43
- const topologyTable = grid.set(0, 0, 6, 4, blessed_contrib_1.default.table, {
44
- keys: true,
45
- fg: 'white',
46
- selectedFg: 'white',
47
- selectedBg: 'blue',
48
- interactive: false,
49
- label: 'Cluster Topology',
50
- border: { type: "line", fg: "cyan" },
51
- columnSpacing: 3,
52
- columnWidth: [10, 15, 10]
53
- });
54
- const latencyLine = grid.set(0, 4, 6, 8, blessed_contrib_1.default.line, {
55
- style: { line: "yellow", text: "green", baseline: "black" },
56
- xLabelPadding: 3,
57
- xPadding: 5,
58
- showLegend: true,
59
- legend: { width: 20 },
60
- label: 'API Response Time (ms)'
61
- });
62
- const logBox = grid.set(6, 0, 6, 8, blessed_contrib_1.default.log, {
63
- fg: "green",
64
- selectedFg: "green",
65
- label: 'Execution Logs'
66
- });
67
- const controls = grid.set(6, 8, 6, 4, blessed_1.default.list, {
68
- label: 'Actions (Enter to Execute)',
69
- keys: true,
70
- vi: true,
71
- mouse: true,
72
- style: { selected: { bg: 'blue' }, item: { fg: 'white' } },
73
- items: [
74
- 'RUN CHAOS DEMO (Auto)',
75
- 'SEND BATCH (2 POST + 1 GET)',
76
- ...(NO_DOCKER ? [] : [
77
- 'STOP PRIMARY',
78
- ...DOCKER_NODES.map(n => `START ${n.toUpperCase()}`),
79
- 'START STACK'
80
- ]),
81
- 'EXIT'
82
- ]
83
- });
84
- // State
29
+ // Global Config (Mutable)
30
+ let API_URL = '';
31
+ let NODES = [];
32
+ let DOCKER_NODES = [];
33
+ let NO_DOCKER = false;
34
+ // TUI State (Lazy Init)
35
+ let screen;
36
+ let grid;
37
+ let topologyTable;
38
+ let latencyLine;
39
+ let logBox;
40
+ let controls;
85
41
  let latencyData = {
86
42
  title: 'Latency',
87
43
  x: Array(20).fill('.'),
@@ -89,6 +45,10 @@ let latencyData = {
89
45
  };
90
46
  // Utils
91
47
  function log(msg) {
48
+ if (!logBox) {
49
+ console.log(msg);
50
+ return;
51
+ }
92
52
  const time = new Date().toISOString().split('T')[1].split('.')[0];
93
53
  logBox.log(`[${time}] ${msg}`);
94
54
  screen.render();
@@ -133,6 +93,8 @@ function request(method) {
133
93
  });
134
94
  }
135
95
  function updateLatencyGraph(ms) {
96
+ if (!latencyLine)
97
+ return;
136
98
  latencyData.y.shift();
137
99
  latencyData.y.push(ms);
138
100
  latencyLine.setData([latencyData]);
@@ -157,6 +119,8 @@ function getPrimary() {
157
119
  }
158
120
  function updateTopology() {
159
121
  return __awaiter(this, void 0, void 0, function* () {
122
+ if (!topologyTable)
123
+ return;
160
124
  const rows = [];
161
125
  for (const node of NODES) {
162
126
  let status = 'DOWN';
@@ -164,7 +128,7 @@ function updateTopology() {
164
128
  try {
165
129
  const client = new mongodb_1.MongoClient(node.uri, { serverSelectionTimeoutMS: 500, directConnection: true });
166
130
  yield client.connect();
167
- const db = client.db('node-balancer'); // Assuming DB name is consistent or part of URI, but here hardcoded for now or could be arg
131
+ const db = client.db('node-balancer');
168
132
  const hello = yield db.command({ hello: 1 });
169
133
  count = (yield db.collection('users').countDocuments()).toString();
170
134
  yield client.close();
@@ -200,9 +164,7 @@ function runChaosDemo() {
200
164
  }
201
165
  else {
202
166
  // Chaos
203
- const primaryName = yield getPrimary(); // returns node1, node2...
204
- // We need to map node name to container name if they differ, but here we assume index matching or we need smarter logic
205
- // For simplicity in this generic version, let's try to find the container name based on index
167
+ const primaryName = yield getPrimary();
206
168
  const primaryIndex = NODES.findIndex(n => n.name === primaryName);
207
169
  const containerName = primaryIndex !== -1 ? DOCKER_NODES[primaryIndex] : null;
208
170
  if (containerName) {
@@ -241,75 +203,199 @@ function runChaosDemo() {
241
203
  log('✅ DEMO COMPLETED');
242
204
  });
243
205
  }
244
- // Controls Event Handler
245
- controls.on('select', (item, index) => __awaiter(void 0, void 0, void 0, function* () {
246
- const cmd = item.getText();
247
- if (cmd.includes('RUN CHAOS DEMO')) {
248
- runChaosDemo();
249
- }
250
- else if (cmd.includes('SEND BATCH')) {
251
- runBatch();
252
- }
253
- else if (cmd.includes('STOP PRIMARY')) {
254
- if (NO_DOCKER)
255
- return log('Docker disabled.');
256
- const pName = yield getPrimary();
257
- const pIdx = NODES.findIndex(n => n.name === pName);
258
- const container = pIdx !== -1 ? DOCKER_NODES[pIdx] : null;
259
- if (container) {
260
- log(`Stopping ${container}...`);
206
+ function initTui() {
207
+ screen = blessed_1.default.screen({
208
+ smartCSR: true,
209
+ title: 'NodeBalancer Control Center'
210
+ });
211
+ grid = new blessed_contrib_1.default.grid({ rows: 12, cols: 12, screen: screen });
212
+ topologyTable = grid.set(0, 0, 6, 4, blessed_contrib_1.default.table, {
213
+ keys: true,
214
+ fg: 'white',
215
+ selectedFg: 'white',
216
+ selectedBg: 'blue',
217
+ interactive: false,
218
+ label: 'Cluster Topology',
219
+ border: { type: "line", fg: "cyan" },
220
+ columnSpacing: 3,
221
+ columnWidth: [10, 15, 10]
222
+ });
223
+ latencyLine = grid.set(0, 4, 6, 8, blessed_contrib_1.default.line, {
224
+ style: { line: "yellow", text: "green", baseline: "black" },
225
+ xLabelPadding: 3,
226
+ xPadding: 5,
227
+ showLegend: true,
228
+ legend: { width: 20 },
229
+ label: 'API Response Time (ms)'
230
+ });
231
+ logBox = grid.set(6, 0, 6, 8, blessed_contrib_1.default.log, {
232
+ fg: "green",
233
+ selectedFg: "green",
234
+ label: 'Execution Logs'
235
+ });
236
+ controls = grid.set(6, 8, 6, 4, blessed_1.default.list, {
237
+ label: 'Actions (Enter to Execute)',
238
+ keys: true,
239
+ vi: true,
240
+ mouse: true,
241
+ style: { selected: { bg: 'blue' }, item: { fg: 'white' } },
242
+ items: [
243
+ 'RUN CHAOS DEMO (Auto)',
244
+ 'SEND BATCH (2 POST + 1 GET)',
245
+ ...(NO_DOCKER ? [] : [
246
+ 'STOP PRIMARY',
247
+ ...DOCKER_NODES.map(n => `START ${n.toUpperCase()}`),
248
+ 'START STACK'
249
+ ]),
250
+ 'EXIT'
251
+ ]
252
+ });
253
+ controls.on('select', (item, index) => __awaiter(this, void 0, void 0, function* () {
254
+ const cmd = item.getText();
255
+ if (cmd.includes('RUN CHAOS DEMO')) {
256
+ runChaosDemo();
257
+ }
258
+ else if (cmd.includes('SEND BATCH')) {
259
+ runBatch();
260
+ }
261
+ else if (cmd.includes('STOP PRIMARY')) {
262
+ if (NO_DOCKER)
263
+ return log('Docker disabled.');
264
+ const pName = yield getPrimary();
265
+ const pIdx = NODES.findIndex(n => n.name === pName);
266
+ const container = pIdx !== -1 ? DOCKER_NODES[pIdx] : null;
267
+ if (container) {
268
+ log(`Stopping ${container}...`);
269
+ try {
270
+ (0, child_process_1.execSync)(`docker stop ${container}`);
271
+ log('Stopped.');
272
+ }
273
+ catch (e) {
274
+ log('Error.');
275
+ }
276
+ }
277
+ else {
278
+ log('No Primary found.');
279
+ }
280
+ }
281
+ else if (cmd.includes('START MONGO') || cmd.includes('START NODE')) {
282
+ if (NO_DOCKER)
283
+ return log('Docker disabled.');
284
+ const parts = cmd.split(' ');
285
+ const container = parts[1].toLowerCase();
286
+ log(`Starting ${container}...`);
261
287
  try {
262
- (0, child_process_1.execSync)(`docker stop ${container}`);
263
- log('Stopped.');
288
+ (0, child_process_1.execSync)(`docker start ${container}`);
289
+ log('Started.');
264
290
  }
265
291
  catch (e) {
266
292
  log('Error.');
267
293
  }
268
294
  }
269
- else {
270
- log('No Primary found.');
271
- }
272
- }
273
- else if (cmd.includes('START MONGO') || cmd.includes('START NODE')) { // Generic match
274
- if (NO_DOCKER)
275
- return log('Docker disabled.');
276
- // Extract container name from string "START MONGO1"
277
- const parts = cmd.split(' ');
278
- const container = parts[1].toLowerCase(); // mongo1
279
- log(`Starting ${container}...`);
280
- try {
281
- (0, child_process_1.execSync)(`docker start ${container}`);
282
- log('Started.');
295
+ else if (cmd.includes('START STACK')) {
296
+ if (NO_DOCKER)
297
+ return log('Docker disabled.');
298
+ log('Starting stack...');
299
+ try {
300
+ (0, child_process_1.execSync)('docker-compose up -d');
301
+ log('Stack up.');
302
+ }
303
+ catch (e) {
304
+ log(`Error: ${e.message.split('\n')[0]}`);
305
+ }
283
306
  }
284
- catch (e) {
285
- log('Error.');
307
+ else if (cmd.includes('EXIT')) {
308
+ process.exit(0);
286
309
  }
287
- }
288
- else if (cmd.includes('START STACK')) {
289
- if (NO_DOCKER)
290
- return log('Docker disabled.');
291
- log('Starting stack...');
292
- try {
293
- (0, child_process_1.execSync)('docker-compose up -d');
294
- log('Stack up.');
310
+ }));
311
+ screen.key(['escape', 'q', 'C-c'], () => process.exit(0));
312
+ controls.focus();
313
+ screen.render();
314
+ }
315
+ // Main Execution
316
+ function main() {
317
+ return __awaiter(this, void 0, void 0, function* () {
318
+ // Check if flags are provided, otherwise prompt
319
+ const argApiUrl = getArg('--api-url');
320
+ const argNodes = getArg('--nodes');
321
+ const argDocker = getArg('--docker-containers');
322
+ const argNoDocker = args.includes('--no-docker');
323
+ if (argApiUrl && argNodes) {
324
+ // Non-interactive mode (Flags provided)
325
+ API_URL = argApiUrl;
326
+ NODES = argNodes.split(',').map((uri, i) => ({ name: `node${i + 1}`, uri: uri.trim() }));
327
+ DOCKER_NODES = (argDocker || 'mongo1,mongo2,mongo3').split(',').map(n => n.trim());
328
+ NO_DOCKER = argNoDocker;
295
329
  }
296
- catch (e) {
297
- log(`Error: ${e.message.split('\n')[0]}`);
330
+ else {
331
+ // Interactive mode
332
+ console.clear();
333
+ console.log('🤖 NodeBalancer Dashboard Setup\n');
334
+ const answers = yield inquirer_1.default.prompt([
335
+ {
336
+ type: 'input',
337
+ name: 'apiUrl',
338
+ message: 'API URL:',
339
+ default: 'http://localhost:3000/api/users'
340
+ },
341
+ {
342
+ type: 'input',
343
+ name: 'nodes',
344
+ message: 'MongoDB Nodes (comma separated):',
345
+ default: 'mongodb://localhost:27017/node-balancer,mongodb://localhost:27018/node-balancer,mongodb://localhost:27019/node-balancer'
346
+ },
347
+ {
348
+ type: 'confirm',
349
+ name: 'enableDocker',
350
+ message: 'Enable Docker Control (Stop/Start containers)?',
351
+ default: true
352
+ },
353
+ {
354
+ type: 'input',
355
+ name: 'dockerContainers',
356
+ message: 'Docker Container Names (comma separated):',
357
+ default: 'mongo1,mongo2,mongo3',
358
+ when: (answers) => answers.enableDocker
359
+ }
360
+ ]);
361
+ API_URL = answers.apiUrl;
362
+ NODES = answers.nodes.split(',').map((uri, i) => ({ name: `node${i + 1}`, uri: uri.trim() }));
363
+ NO_DOCKER = !answers.enableDocker;
364
+ DOCKER_NODES = (answers.dockerContainers || '').split(',').map((n) => n.trim());
298
365
  }
299
- }
300
- else if (cmd.includes('EXIT')) {
301
- process.exit(0);
302
- }
303
- }));
304
- // Loops
305
- setInterval(updateTopology, 2000);
306
- // Init
307
- controls.focus();
308
- log('Control Center Ready.');
309
- if (NO_DOCKER)
310
- log('Docker Control: DISABLED');
311
- log(`API: ${API_URL}`);
312
- updateTopology();
313
- screen.render();
314
- // Exit
315
- screen.key(['escape', 'q', 'C-c'], () => process.exit(0));
366
+ // Init TUI AFTER prompts
367
+ initTui();
368
+ log('Control Center Ready.');
369
+ if (NO_DOCKER)
370
+ log('Docker Control: DISABLED');
371
+ log(`API: ${API_URL}`);
372
+ // Init WebSocket
373
+ const socket = (0, socket_io_client_1.io)(API_URL.replace('/api/users', '').replace('/api', ''));
374
+ socket.on('connect', () => log('✅ WebSocket Connected'));
375
+ socket.on('disconnect', () => log(' WebSocket Disconnected'));
376
+ socket.on('log', (data) => {
377
+ let msg = '';
378
+ if (data.type === 'promote')
379
+ msg = `👑 NEW PRIMARY: ${data.payload.node}`;
380
+ else if (data.type === 'no-writable')
381
+ msg = `🚨 CRITICAL: NO WRITABLE NODES`;
382
+ else if (data.op)
383
+ msg = `${data.op.toUpperCase()} ${data.collection} (${data.durationMs}ms) ${data.success ? '✅' : '❌'}`;
384
+ else
385
+ msg = JSON.stringify(data);
386
+ log(`[WS] ${msg}`);
387
+ });
388
+ socket.on('topology-change', () => {
389
+ log(`[WS] Topology Change`);
390
+ updateTopology();
391
+ });
392
+ // Loops
393
+ setInterval(updateTopology, 2000);
394
+ updateTopology();
395
+ });
396
+ }
397
+ // Start
398
+ main().catch(err => {
399
+ console.error(err);
400
+ process.exit(1);
401
+ });
package/dist/server.js CHANGED
@@ -1,11 +1,32 @@
1
1
  "use strict";
2
+ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
3
+ function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
4
+ return new (P || (P = Promise))(function (resolve, reject) {
5
+ function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
6
+ function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
7
+ function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
8
+ step((generator = generator.apply(thisArg, _arguments || [])).next());
9
+ });
10
+ };
2
11
  var __importDefault = (this && this.__importDefault) || function (mod) {
3
12
  return (mod && mod.__esModule) ? mod : { "default": mod };
4
13
  };
5
14
  Object.defineProperty(exports, "__esModule", { value: true });
6
15
  const app_1 = __importDefault(require("./app"));
7
16
  const logger_1 = require("./middlewares/logger");
17
+ const websocket_1 = require("./config/websocket");
18
+ const metrics_1 = require("./config/metrics");
8
19
  const PORT = process.env.PORT || 3000;
9
- app_1.default.listen(PORT, () => {
20
+ app_1.default.get('/metrics', (req, res) => __awaiter(void 0, void 0, void 0, function* () {
21
+ try {
22
+ res.set('Content-Type', metrics_1.register.contentType);
23
+ res.end(yield metrics_1.register.metrics());
24
+ }
25
+ catch (ex) {
26
+ res.status(500).end(ex);
27
+ }
28
+ }));
29
+ const server = app_1.default.listen(PORT, () => {
10
30
  logger_1.logger.info(`Servidor rodando na porta ${PORT}`);
11
31
  });
32
+ (0, websocket_1.initWebSocket)(server);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "replica-failover-mongodb-ts",
3
- "version": "2.2.4",
3
+ "version": "3.0.3",
4
4
  "description": "O sistema replica um dos conceitos básicos da arquitetura de sistemas distribuído, na qual ele tem uma API em express.js com TypeScript, e usando o mongoDB com replicaSET e failover, consegue automáticamente garantir disponibilidade e redundância de dados das requisições.",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",
@@ -31,12 +31,18 @@
31
31
  "license": "ISC",
32
32
  "dependencies": {
33
33
  "@types/blessed": "^0.1.26",
34
+ "@types/inquirer": "^9.0.9",
35
+ "axios": "^1.13.2",
34
36
  "blessed": "^0.1.81",
35
37
  "blessed-contrib": "^4.11.0",
36
38
  "dotenv": "^17.2.3",
37
39
  "express": "^5.1.0",
40
+ "inquirer": "^8.0.0",
38
41
  "mongoose": "^8.19.1",
39
42
  "morgan": "^1.10.1",
43
+ "prom-client": "^15.1.3",
44
+ "socket.io": "^4.8.1",
45
+ "socket.io-client": "^4.8.1",
40
46
  "winston": "^3.18.3"
41
47
  },
42
48
  "devDependencies": {