nextlimiter 1.0.5 → 1.0.6
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/package.json +1 -1
- package/src/analytics/prometheus.js +122 -0
- package/src/core/limiter.js +36 -0
- package/src/index.js +4 -0
- package/types/index.d.ts +18 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "nextlimiter",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.6",
|
|
4
4
|
"description": "Production-ready rate limiting for Node.js — sliding window, token bucket, SaaS plans, smart limiting, and built-in analytics.",
|
|
5
5
|
"main": "src/index.js",
|
|
6
6
|
"types": "types/index.d.ts",
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Pure Node.js Prometheus Exposition Formatter
|
|
5
|
+
* Zero dependencies. Converts limiter stats to valid Prometheus text format.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
class PrometheusFormatter {
|
|
9
|
+
constructor(limiter) {
|
|
10
|
+
this.limiter = limiter;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
contentType() {
|
|
14
|
+
return 'text/plain; version=0.0.4; charset=utf-8';
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Escape label values according to Prometheus rules:
|
|
19
|
+
* \ -> \\
|
|
20
|
+
* " -> \"
|
|
21
|
+
* \n -> \n
|
|
22
|
+
*/
|
|
23
|
+
_escapeLabel(str) {
|
|
24
|
+
return String(str)
|
|
25
|
+
.replace(/\\/g, '\\\\')
|
|
26
|
+
.replace(/"/g, '\\"')
|
|
27
|
+
.replace(/\n/g, '\\n');
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Format a single metric block (help, type, and samples).
|
|
32
|
+
*/
|
|
33
|
+
_metricBlock(name, help, type, samples) {
|
|
34
|
+
if (!samples || samples.length === 0) return '';
|
|
35
|
+
let block = `# HELP ${name} ${help}\n`;
|
|
36
|
+
block += `# TYPE ${name} ${type}\n`;
|
|
37
|
+
|
|
38
|
+
for (const sample of samples) {
|
|
39
|
+
const labelEntries = Object.entries(sample.labels || {});
|
|
40
|
+
if (labelEntries.length > 0) {
|
|
41
|
+
const labelStrs = labelEntries.map(([k, v]) => `${k}="${this._escapeLabel(v)}"`);
|
|
42
|
+
block += `${name}{${labelStrs.join(',')}} ${sample.value}\n`;
|
|
43
|
+
} else {
|
|
44
|
+
block += `${name} ${sample.value}\n`;
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
return block + '\n';
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
format() {
|
|
51
|
+
const stats = this.limiter.getStats();
|
|
52
|
+
let output = '';
|
|
53
|
+
|
|
54
|
+
// 1. Total requests (Counter)
|
|
55
|
+
output += this._metricBlock(
|
|
56
|
+
'nextlimiter_requests_total',
|
|
57
|
+
'Total requests processed by nextlimiter',
|
|
58
|
+
'counter',
|
|
59
|
+
[
|
|
60
|
+
{ labels: { status: 'allowed' }, value: stats.allowedRequests },
|
|
61
|
+
{ labels: { status: 'blocked' }, value: stats.blockedRequests }
|
|
62
|
+
]
|
|
63
|
+
);
|
|
64
|
+
|
|
65
|
+
// 2. Block rate (Gauge)
|
|
66
|
+
output += this._metricBlock(
|
|
67
|
+
'nextlimiter_block_rate',
|
|
68
|
+
'Ratio of blocked to total requests (0.0 to 1.0)',
|
|
69
|
+
'gauge',
|
|
70
|
+
[{ labels: {}, value: isNaN(stats.blockRate) ? 0 : stats.blockRate }]
|
|
71
|
+
);
|
|
72
|
+
|
|
73
|
+
// 3. Top blocked IPs (Gauge)
|
|
74
|
+
const topBlockedSamples = (stats.topBlocked || []).slice(0, 10).map((item) => {
|
|
75
|
+
// Clean up IP label
|
|
76
|
+
let ip = item.key.replace(/^nextlimiter:(ip:)?/, '');
|
|
77
|
+
return { labels: { ip }, value: item.count };
|
|
78
|
+
});
|
|
79
|
+
if (topBlockedSamples.length > 0) {
|
|
80
|
+
output += this._metricBlock(
|
|
81
|
+
'nextlimiter_top_blocked_ip',
|
|
82
|
+
'Block count per offending IP',
|
|
83
|
+
'gauge',
|
|
84
|
+
topBlockedSamples
|
|
85
|
+
);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
// 4. Top keys by volume (Gauge)
|
|
89
|
+
const topKeysSamples = (stats.topKeys || []).slice(0, 10).map((item) => {
|
|
90
|
+
return { labels: { key: item.key }, value: item.count };
|
|
91
|
+
});
|
|
92
|
+
if (topKeysSamples.length > 0) {
|
|
93
|
+
output += this._metricBlock(
|
|
94
|
+
'nextlimiter_top_keys_total',
|
|
95
|
+
'Total request count per key',
|
|
96
|
+
'gauge',
|
|
97
|
+
topKeysSamples
|
|
98
|
+
);
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
// 5. Uptime seconds (Gauge)
|
|
102
|
+
output += this._metricBlock(
|
|
103
|
+
'nextlimiter_uptime_seconds',
|
|
104
|
+
'Seconds since limiter was created',
|
|
105
|
+
'gauge',
|
|
106
|
+
[{ labels: {}, value: stats.uptimeMs / 1000 }]
|
|
107
|
+
);
|
|
108
|
+
|
|
109
|
+
// 6. Info (Gauge = 1)
|
|
110
|
+
const version = '1.0.5'; // Current pkg version
|
|
111
|
+
output += this._metricBlock(
|
|
112
|
+
'nextlimiter_info',
|
|
113
|
+
'Static info about the limiter instance',
|
|
114
|
+
'gauge',
|
|
115
|
+
[{ labels: { strategy: stats.config.strategy, version }, value: 1 }]
|
|
116
|
+
);
|
|
117
|
+
|
|
118
|
+
return output;
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
module.exports = { PrometheusFormatter };
|
package/src/core/limiter.js
CHANGED
|
@@ -12,6 +12,7 @@ const { AnalyticsTracker } = require('../analytics/tracker');
|
|
|
12
12
|
const { SmartDetector } = require('../smart/detector');
|
|
13
13
|
const { setHeaders } = require('../middleware/headers');
|
|
14
14
|
const { checkAccess } = require('./accessControl');
|
|
15
|
+
const { PrometheusFormatter } = require('../analytics/prometheus');
|
|
15
16
|
|
|
16
17
|
const STRATEGY_MAP = {
|
|
17
18
|
'fixed-window': fixedWindowCheck,
|
|
@@ -146,6 +147,41 @@ class Limiter {
|
|
|
146
147
|
};
|
|
147
148
|
}
|
|
148
149
|
|
|
150
|
+
/**
|
|
151
|
+
* Express route handler for serving Prometheus metrics.
|
|
152
|
+
* Exposes getStats() data in plain text format (version=0.0.4).
|
|
153
|
+
*
|
|
154
|
+
* @example
|
|
155
|
+
* app.get('/metrics', limiter.metricsHandler());
|
|
156
|
+
*/
|
|
157
|
+
metricsHandler() {
|
|
158
|
+
return (req, res) => {
|
|
159
|
+
try {
|
|
160
|
+
const formatter = new PrometheusFormatter(this);
|
|
161
|
+
res.set('Content-Type', formatter.contentType());
|
|
162
|
+
res.send(formatter.format());
|
|
163
|
+
} catch (err) {
|
|
164
|
+
res.status(500).type('text/plain').send(`Error generating metrics: ${err.message}`);
|
|
165
|
+
}
|
|
166
|
+
};
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
/**
|
|
170
|
+
* Global middleware that automatically intercepts GET /metrics requests
|
|
171
|
+
* and serves the Prometheus exposition format. Passes through all other routes.
|
|
172
|
+
*
|
|
173
|
+
* @example
|
|
174
|
+
* app.use(limiter.metricsMiddleware());
|
|
175
|
+
*/
|
|
176
|
+
metricsMiddleware() {
|
|
177
|
+
return (req, res, next) => {
|
|
178
|
+
if (req.method === 'GET' && req.path === '/metrics') {
|
|
179
|
+
return this.metricsHandler()(req, res);
|
|
180
|
+
}
|
|
181
|
+
next();
|
|
182
|
+
};
|
|
183
|
+
}
|
|
184
|
+
|
|
149
185
|
/**
|
|
150
186
|
* Programmatic rate limit check — use outside of HTTP middleware context.
|
|
151
187
|
*
|
package/src/index.js
CHANGED
|
@@ -5,6 +5,7 @@ const { PRESETS, DEFAULT_PLANS } = require('./core/config');
|
|
|
5
5
|
const { MemoryStore } = require('./store/memoryStore');
|
|
6
6
|
const { RedisStore } = require('./store/redisStore');
|
|
7
7
|
const { ipMatchesCidr, ipMatchesList } = require('./utils/cidr');
|
|
8
|
+
const { PrometheusFormatter } = require('./analytics/prometheus');
|
|
8
9
|
|
|
9
10
|
/**
|
|
10
11
|
* Create a fully configured rate limiter instance.
|
|
@@ -107,6 +108,9 @@ module.exports = {
|
|
|
107
108
|
ipMatchesCidr,
|
|
108
109
|
ipMatchesList,
|
|
109
110
|
|
|
111
|
+
// Analytics export
|
|
112
|
+
PrometheusFormatter,
|
|
113
|
+
|
|
110
114
|
// Constants
|
|
111
115
|
PRESETS,
|
|
112
116
|
DEFAULT_PLANS,
|
package/types/index.d.ts
CHANGED
|
@@ -139,6 +139,12 @@ export interface RateLimitResult {
|
|
|
139
139
|
|
|
140
140
|
// ── Analytics ────────────────────────────────────────────────────────────────
|
|
141
141
|
|
|
142
|
+
export declare class PrometheusFormatter {
|
|
143
|
+
constructor(limiter: Limiter);
|
|
144
|
+
format(): string;
|
|
145
|
+
contentType(): string;
|
|
146
|
+
}
|
|
147
|
+
|
|
142
148
|
export interface KeyCount {
|
|
143
149
|
key: string;
|
|
144
150
|
count: number;
|
|
@@ -181,6 +187,18 @@ export declare class Limiter {
|
|
|
181
187
|
/** Returns an Express-compatible middleware function */
|
|
182
188
|
middleware(): (req: Request, res: Response, next: NextFunction) => Promise<void>;
|
|
183
189
|
|
|
190
|
+
/**
|
|
191
|
+
* Express route handler for serving Prometheus metrics.
|
|
192
|
+
* Exposes getStats() data in plain text format (version=0.0.4).
|
|
193
|
+
*/
|
|
194
|
+
metricsHandler(): (req: any, res: any) => void;
|
|
195
|
+
|
|
196
|
+
/**
|
|
197
|
+
* Global middleware that automatically intercepts GET /metrics requests.
|
|
198
|
+
* Passes through all other routes.
|
|
199
|
+
*/
|
|
200
|
+
metricsMiddleware(): (req: any, res: any, next: any) => void;
|
|
201
|
+
|
|
184
202
|
/** Programmatic rate limit check by key string */
|
|
185
203
|
check(key: string): Promise<RateLimitResult>;
|
|
186
204
|
|