mastercontroller 1.3.10 → 1.3.13

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.
@@ -0,0 +1,416 @@
1
+ /**
2
+ * PrometheusExporter - Production-grade metrics for Prometheus monitoring
3
+ * Version: 1.0.0
4
+ *
5
+ * Provides a /_metrics endpoint compatible with Prometheus scraping format.
6
+ * Tracks HTTP request metrics, system metrics, and custom application metrics.
7
+ *
8
+ * Usage in MasterControl pipeline:
9
+ *
10
+ * const { prometheusExporter } = require('./monitoring/PrometheusExporter');
11
+ *
12
+ * // Register the middleware
13
+ * master.pipeline.use(prometheusExporter.middleware());
14
+ *
15
+ * Metrics endpoint: GET /_metrics
16
+ * Returns: Prometheus text format metrics
17
+ *
18
+ * Optional: Install prom-client for advanced features
19
+ * npm install prom-client --save-optional
20
+ */
21
+
22
+ const os = require('os');
23
+ const { logger } = require('../error/MasterErrorLogger');
24
+
25
+ class PrometheusExporter {
26
+ constructor(options = {}) {
27
+ this.options = {
28
+ endpoint: options.endpoint || '/_metrics',
29
+ prefix: options.prefix || 'mastercontroller_',
30
+ collectDefaultMetrics: options.collectDefaultMetrics !== false,
31
+ ...options
32
+ };
33
+
34
+ this.startTime = Date.now();
35
+
36
+ // HTTP request metrics
37
+ this.httpRequestsTotal = {}; // Counter by method, path, status
38
+ this.httpRequestDuration = {}; // Histogram by method, path
39
+ this.httpRequestsInFlight = 0; // Current active requests
40
+ this.httpRequestSizeBytes = {}; // Histogram of request sizes
41
+ this.httpResponseSizeBytes = {}; // Histogram of response sizes
42
+
43
+ // Custom metrics storage
44
+ this.customMetrics = new Map();
45
+
46
+ // Try to load prom-client if available (optional peer dependency)
47
+ try {
48
+ this.promClient = require('prom-client');
49
+ this._setupPromClient();
50
+ } catch (e) {
51
+ // prom-client not installed, use simple implementation
52
+ this.promClient = null;
53
+ logger.info({
54
+ code: 'MC_PROMETHEUS_SIMPLE_MODE',
55
+ message: 'Running Prometheus exporter in simple mode (install prom-client for advanced features)'
56
+ });
57
+ }
58
+ }
59
+
60
+ /**
61
+ * Setup prom-client if available
62
+ */
63
+ _setupPromClient() {
64
+ const { Registry, Counter, Histogram, Gauge } = this.promClient;
65
+ this.register = new Registry();
66
+
67
+ // HTTP request counter
68
+ this.httpRequestCounter = new Counter({
69
+ name: `${this.options.prefix}http_requests_total`,
70
+ help: 'Total number of HTTP requests',
71
+ labelNames: ['method', 'path', 'status'],
72
+ registers: [this.register]
73
+ });
74
+
75
+ // HTTP request duration histogram
76
+ this.httpDurationHistogram = new Histogram({
77
+ name: `${this.options.prefix}http_request_duration_seconds`,
78
+ help: 'HTTP request duration in seconds',
79
+ labelNames: ['method', 'path', 'status'],
80
+ buckets: [0.001, 0.005, 0.01, 0.05, 0.1, 0.5, 1, 5, 10],
81
+ registers: [this.register]
82
+ });
83
+
84
+ // Active requests gauge
85
+ this.activeRequestsGauge = new Gauge({
86
+ name: `${this.options.prefix}http_requests_in_flight`,
87
+ help: 'Number of HTTP requests currently being processed',
88
+ registers: [this.register]
89
+ });
90
+
91
+ // Request size histogram
92
+ this.requestSizeHistogram = new Histogram({
93
+ name: `${this.options.prefix}http_request_size_bytes`,
94
+ help: 'HTTP request size in bytes',
95
+ labelNames: ['method', 'path'],
96
+ buckets: [100, 1000, 10000, 100000, 1000000, 10000000],
97
+ registers: [this.register]
98
+ });
99
+
100
+ // Response size histogram
101
+ this.responseSizeHistogram = new Histogram({
102
+ name: `${this.options.prefix}http_response_size_bytes`,
103
+ help: 'HTTP response size in bytes',
104
+ labelNames: ['method', 'path', 'status'],
105
+ buckets: [100, 1000, 10000, 100000, 1000000, 10000000],
106
+ registers: [this.register]
107
+ });
108
+
109
+ // Collect default system metrics if enabled
110
+ if (this.options.collectDefaultMetrics) {
111
+ this.promClient.collectDefaultMetrics({
112
+ register: this.register,
113
+ prefix: this.options.prefix
114
+ });
115
+ }
116
+ }
117
+
118
+ /**
119
+ * Middleware for MasterPipeline - tracks all HTTP requests
120
+ */
121
+ middleware() {
122
+ const self = this;
123
+
124
+ return async (ctx, next) => {
125
+ const requestPath = ctx.request.url.split('?')[0];
126
+
127
+ // Handle metrics endpoint
128
+ if (requestPath === self.options.endpoint) {
129
+ return await self._handleMetricsEndpoint(ctx);
130
+ }
131
+
132
+ // Track request metrics
133
+ const startTime = Date.now();
134
+ self.httpRequestsInFlight++;
135
+
136
+ if (self.activeRequestsGauge) {
137
+ self.activeRequestsGauge.inc();
138
+ }
139
+
140
+ // Get request size
141
+ const requestSize = parseInt(ctx.request.headers['content-length'] || 0);
142
+
143
+ try {
144
+ // Continue pipeline
145
+ await next();
146
+
147
+ // Record metrics on success
148
+ const duration = (Date.now() - startTime) / 1000; // Convert to seconds
149
+ const method = ctx.request.method;
150
+ const status = ctx.response.statusCode || 200;
151
+ const responseSize = parseInt(ctx.response.getHeader('content-length') || 0);
152
+
153
+ self._recordRequest(method, requestPath, status, duration, requestSize, responseSize);
154
+
155
+ } catch (error) {
156
+ // Record error metrics
157
+ const duration = (Date.now() - startTime) / 1000;
158
+ const method = ctx.request.method;
159
+ const status = 500;
160
+
161
+ self._recordRequest(method, requestPath, status, duration, requestSize, 0);
162
+
163
+ throw error; // Re-throw for error handling middleware
164
+
165
+ } finally {
166
+ self.httpRequestsInFlight--;
167
+
168
+ if (self.activeRequestsGauge) {
169
+ self.activeRequestsGauge.dec();
170
+ }
171
+ }
172
+ };
173
+ }
174
+
175
+ /**
176
+ * Record HTTP request metrics
177
+ */
178
+ _recordRequest(method, path, status, duration, requestSize, responseSize) {
179
+ // Normalize path (remove IDs, etc.) for better grouping
180
+ const normalizedPath = this._normalizePath(path);
181
+
182
+ if (this.promClient) {
183
+ // Use prom-client
184
+ this.httpRequestCounter.inc({ method, path: normalizedPath, status });
185
+ this.httpDurationHistogram.observe({ method, path: normalizedPath, status }, duration);
186
+
187
+ if (requestSize > 0) {
188
+ this.requestSizeHistogram.observe({ method, path: normalizedPath }, requestSize);
189
+ }
190
+
191
+ if (responseSize > 0) {
192
+ this.responseSizeHistogram.observe({ method, path: normalizedPath, status }, responseSize);
193
+ }
194
+
195
+ } else {
196
+ // Simple implementation
197
+ const key = `${method}:${normalizedPath}:${status}`;
198
+
199
+ if (!this.httpRequestsTotal[key]) {
200
+ this.httpRequestsTotal[key] = { count: 0, totalDuration: 0 };
201
+ }
202
+
203
+ this.httpRequestsTotal[key].count++;
204
+ this.httpRequestsTotal[key].totalDuration += duration;
205
+
206
+ if (!this.httpRequestDuration[key]) {
207
+ this.httpRequestDuration[key] = [];
208
+ }
209
+ this.httpRequestDuration[key].push(duration);
210
+
211
+ // Keep only last 1000 durations per endpoint to prevent memory leak
212
+ if (this.httpRequestDuration[key].length > 1000) {
213
+ this.httpRequestDuration[key].shift();
214
+ }
215
+ }
216
+ }
217
+
218
+ /**
219
+ * Normalize path for metrics (remove IDs, hashes, etc.)
220
+ */
221
+ _normalizePath(path) {
222
+ return path
223
+ .replace(/\/[0-9a-f]{24}$/i, '/:id') // MongoDB ObjectId
224
+ .replace(/\/[0-9]+$/, '/:id') // Numeric IDs
225
+ .replace(/\/[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i, '/:uuid') // UUIDs
226
+ .replace(/\?.*$/, ''); // Remove query params
227
+ }
228
+
229
+ /**
230
+ * Handle /_metrics endpoint
231
+ */
232
+ async _handleMetricsEndpoint(ctx) {
233
+ try {
234
+ logger.debug({
235
+ code: 'MC_METRICS_SCRAPE',
236
+ message: 'Metrics scrape requested',
237
+ ip: ctx.request.connection.remoteAddress
238
+ });
239
+
240
+ let metricsText;
241
+
242
+ if (this.promClient) {
243
+ // Use prom-client to generate metrics
244
+ metricsText = await this.register.metrics();
245
+ } else {
246
+ // Simple implementation
247
+ metricsText = this._generateSimpleMetrics();
248
+ }
249
+
250
+ // Set response headers
251
+ ctx.response.setHeader('Content-Type', 'text/plain; version=0.0.4');
252
+ ctx.response.setHeader('Cache-Control', 'no-cache, no-store, must-revalidate');
253
+ ctx.response.statusCode = 200;
254
+ ctx.response.end(metricsText);
255
+
256
+ } catch (error) {
257
+ logger.error({
258
+ code: 'MC_METRICS_ERROR',
259
+ message: 'Metrics generation failed',
260
+ error: error.message,
261
+ stack: error.stack
262
+ });
263
+
264
+ ctx.response.statusCode = 500;
265
+ ctx.response.setHeader('Content-Type', 'text/plain');
266
+ ctx.response.end('# Error generating metrics\n');
267
+ }
268
+ }
269
+
270
+ /**
271
+ * Generate simple metrics text (when prom-client not available)
272
+ */
273
+ _generateSimpleMetrics() {
274
+ const lines = [];
275
+ const now = Date.now();
276
+
277
+ // Add header
278
+ lines.push('# MasterController Metrics (Simple Mode)');
279
+ lines.push('# Install prom-client for advanced metrics');
280
+ lines.push('');
281
+
282
+ // Process uptime
283
+ const uptime = Math.floor((now - this.startTime) / 1000);
284
+ lines.push('# HELP process_uptime_seconds Process uptime in seconds');
285
+ lines.push('# TYPE process_uptime_seconds gauge');
286
+ lines.push(`process_uptime_seconds ${uptime}`);
287
+ lines.push('');
288
+
289
+ // HTTP requests total
290
+ lines.push('# HELP mastercontroller_http_requests_total Total number of HTTP requests');
291
+ lines.push('# TYPE mastercontroller_http_requests_total counter');
292
+ for (const [key, data] of Object.entries(this.httpRequestsTotal)) {
293
+ const [method, path, status] = key.split(':');
294
+ lines.push(`mastercontroller_http_requests_total{method="${method}",path="${path}",status="${status}"} ${data.count}`);
295
+ }
296
+ lines.push('');
297
+
298
+ // HTTP request duration (avg)
299
+ lines.push('# HELP mastercontroller_http_request_duration_seconds_avg Average HTTP request duration');
300
+ lines.push('# TYPE mastercontroller_http_request_duration_seconds_avg gauge');
301
+ for (const [key, data] of Object.entries(this.httpRequestsTotal)) {
302
+ const [method, path, status] = key.split(':');
303
+ const avgDuration = data.totalDuration / data.count;
304
+ lines.push(`mastercontroller_http_request_duration_seconds_avg{method="${method}",path="${path}",status="${status}"} ${avgDuration.toFixed(6)}`);
305
+ }
306
+ lines.push('');
307
+
308
+ // Active requests
309
+ lines.push('# HELP mastercontroller_http_requests_in_flight Number of HTTP requests in flight');
310
+ lines.push('# TYPE mastercontroller_http_requests_in_flight gauge');
311
+ lines.push(`mastercontroller_http_requests_in_flight ${this.httpRequestsInFlight}`);
312
+ lines.push('');
313
+
314
+ // Memory metrics
315
+ const memory = process.memoryUsage();
316
+ lines.push('# HELP process_memory_heap_used_bytes Heap memory used in bytes');
317
+ lines.push('# TYPE process_memory_heap_used_bytes gauge');
318
+ lines.push(`process_memory_heap_used_bytes ${memory.heapUsed}`);
319
+ lines.push('');
320
+
321
+ lines.push('# HELP process_memory_heap_total_bytes Total heap memory in bytes');
322
+ lines.push('# TYPE process_memory_heap_total_bytes gauge');
323
+ lines.push(`process_memory_heap_total_bytes ${memory.heapTotal}`);
324
+ lines.push('');
325
+
326
+ lines.push('# HELP process_memory_rss_bytes Resident set size in bytes');
327
+ lines.push('# TYPE process_memory_rss_bytes gauge');
328
+ lines.push(`process_memory_rss_bytes ${memory.rss}`);
329
+ lines.push('');
330
+
331
+ // CPU metrics
332
+ const cpuUsage = process.cpuUsage();
333
+ lines.push('# HELP process_cpu_user_microseconds User CPU time in microseconds');
334
+ lines.push('# TYPE process_cpu_user_microseconds counter');
335
+ lines.push(`process_cpu_user_microseconds ${cpuUsage.user}`);
336
+ lines.push('');
337
+
338
+ lines.push('# HELP process_cpu_system_microseconds System CPU time in microseconds');
339
+ lines.push('# TYPE process_cpu_system_microseconds counter');
340
+ lines.push(`process_cpu_system_microseconds ${cpuUsage.system}`);
341
+ lines.push('');
342
+
343
+ // Custom metrics
344
+ for (const [name, metric] of this.customMetrics) {
345
+ lines.push(`# HELP ${this.options.prefix}${name} ${metric.help || 'Custom metric'}`);
346
+ lines.push(`# TYPE ${this.options.prefix}${name} ${metric.type || 'gauge'}`);
347
+
348
+ if (metric.labels) {
349
+ for (const [labelKey, value] of Object.entries(metric.labels)) {
350
+ const labelStr = Object.entries(labelKey)
351
+ .map(([k, v]) => `${k}="${v}"`)
352
+ .join(',');
353
+ lines.push(`${this.options.prefix}${name}{${labelStr}} ${value}`);
354
+ }
355
+ } else {
356
+ lines.push(`${this.options.prefix}${name} ${metric.value}`);
357
+ }
358
+ lines.push('');
359
+ }
360
+
361
+ return lines.join('\n');
362
+ }
363
+
364
+ /**
365
+ * Register custom metric
366
+ */
367
+ registerMetric(name, type, help, value, labels = null) {
368
+ this.customMetrics.set(name, { type, help, value, labels });
369
+ }
370
+
371
+ /**
372
+ * Update custom metric value
373
+ */
374
+ updateMetric(name, value, labels = null) {
375
+ const metric = this.customMetrics.get(name);
376
+ if (metric) {
377
+ if (labels) {
378
+ if (!metric.labels) {
379
+ metric.labels = {};
380
+ }
381
+ metric.labels[JSON.stringify(labels)] = value;
382
+ } else {
383
+ metric.value = value;
384
+ }
385
+ }
386
+ }
387
+
388
+ /**
389
+ * Increment counter metric
390
+ */
391
+ incrementCounter(name, labels = null) {
392
+ const metric = this.customMetrics.get(name);
393
+ if (metric && metric.type === 'counter') {
394
+ if (labels) {
395
+ const key = JSON.stringify(labels);
396
+ if (!metric.labels) {
397
+ metric.labels = {};
398
+ }
399
+ metric.labels[key] = (metric.labels[key] || 0) + 1;
400
+ } else {
401
+ metric.value = (metric.value || 0) + 1;
402
+ }
403
+ }
404
+ }
405
+ }
406
+
407
+ // Singleton instance
408
+ const prometheusExporter = new PrometheusExporter({
409
+ endpoint: '/_metrics',
410
+ collectDefaultMetrics: true
411
+ });
412
+
413
+ module.exports = {
414
+ PrometheusExporter,
415
+ prometheusExporter
416
+ };
package/package.json CHANGED
@@ -1,4 +1,50 @@
1
1
  {
2
+ "name": "mastercontroller",
3
+ "version": "1.3.13",
4
+ "description": "Fortune 500 ready Node.js MVC framework with enterprise security, monitoring, and horizontal scaling",
5
+ "main": "MasterControl.js",
6
+ "license": "MIT",
7
+ "homepage": "https://github.com/Tailor/MasterController#readme",
8
+ "repository": {
9
+ "type": "git",
10
+ "url": "git+https://github.com/Tailor/MasterController.git"
11
+ },
12
+ "bugs": {
13
+ "url": "https://github.com/Tailor/MasterController/issues"
14
+ },
15
+ "keywords": [
16
+ "mvc",
17
+ "framework",
18
+ "web",
19
+ "api",
20
+ "rest",
21
+ "enterprise",
22
+ "fortune-500",
23
+ "security",
24
+ "csrf",
25
+ "rate-limiting",
26
+ "monitoring",
27
+ "prometheus",
28
+ "redis",
29
+ "session",
30
+ "horizontal-scaling",
31
+ "load-balancer",
32
+ "production-ready"
33
+ ],
34
+ "engines": {
35
+ "node": ">=18.0.0"
36
+ },
37
+ "scripts": {
38
+ "test": "echo \"Error: no test specified\" && exit 1",
39
+ "lint": "eslint *.js **/*.js --fix",
40
+ "lint:check": "eslint *.js **/*.js",
41
+ "format": "prettier --write \"**/*.js\"",
42
+ "format:check": "prettier --check \"**/*.js\"",
43
+ "security-audit": "npm audit && npm audit signatures",
44
+ "security-scan": "snyk test --severity-threshold=high",
45
+ "prepare": "npm run lint:check || true",
46
+ "prepublishOnly": "npm run security-audit || true"
47
+ },
2
48
  "dependencies": {
3
49
  "content-type": "^1.0.5",
4
50
  "cookie": "^1.1.1",
@@ -7,17 +53,24 @@
7
53
  "qs": "^6.14.1",
8
54
  "winston": "^3.19.0"
9
55
  },
10
- "description": "A class library that makes using the Master Framework a breeze",
11
- "homepage": "https://github.com/Tailor/MasterController#readme",
12
- "license": "MIT",
13
- "main": "MasterControl.js",
14
- "name": "mastercontroller",
15
- "repository": {
16
- "type": "git",
17
- "url": "git+https://github.com/Tailor/MasterController.git"
56
+ "optionalDependencies": {
57
+ "ioredis": "^5.3.2",
58
+ "prom-client": "^15.1.0"
18
59
  },
19
- "scripts": {
20
- "test": "echo \"Error: no test specified\" && exit 1"
60
+ "peerDependencies": {
61
+ "ioredis": "^5.0.0",
62
+ "prom-client": "^14.0.0 || ^15.0.0"
63
+ },
64
+ "peerDependenciesMeta": {
65
+ "ioredis": {
66
+ "optional": true
67
+ },
68
+ "prom-client": {
69
+ "optional": true
70
+ }
21
71
  },
22
- "version": "1.3.10"
72
+ "devDependencies": {
73
+ "eslint": "^8.56.0",
74
+ "prettier": "^3.2.4"
75
+ }
23
76
  }