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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "nextlimiter",
3
- "version": "1.0.5",
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 };
@@ -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