limitforge 0.0.0-stage → 8.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.
@@ -0,0 +1,131 @@
1
+ import {
2
+ METRIC_TYPE,
3
+ METRIC_NAMES,
4
+ METRIC_METADATA
5
+ } from './telemetryEvents.js';
6
+ import { defaultMetricsCollector } from './metricsCollector.js';
7
+
8
+ export function escapeLabelValue(value) {
9
+ return String(value)
10
+ .replace(/\\/g, '\\\\')
11
+ .replace(/"/g, '\\"')
12
+ .replace(/\n/g, '\\n');
13
+ }
14
+
15
+ export function escapeHelpString(help) {
16
+ return String(help)
17
+ .replace(/\\/g, '\\\\')
18
+ .replace(/\n/g, '\\n');
19
+ }
20
+
21
+ export function formatLabels(labels = {}, additionalLabels = {}) {
22
+ const merged = { ...labels, ...additionalLabels };
23
+ const entries = Object.entries(merged).filter(
24
+ ([, v]) => v !== undefined && v !== null
25
+ );
26
+
27
+ if (entries.length === 0) {
28
+ return '';
29
+ }
30
+
31
+ entries.sort(([a], [b]) => a.localeCompare(b));
32
+
33
+ const formatted = entries
34
+ .map(([k, v]) => `${k}="${escapeLabelValue(v)}"`)
35
+ .join(',');
36
+
37
+ return `{${formatted}}`;
38
+ }
39
+
40
+ export function formatPrometheusMetrics(snapshot = {}) {
41
+ const lines = [];
42
+ const { counters = {}, gauges = {}, histograms = {} } = snapshot;
43
+
44
+ // format counters
45
+ for (const [name, records] of Object.entries(counters)) {
46
+ if (!Array.isArray(records) || records.length === 0) continue;
47
+
48
+ const meta = METRIC_METADATA[name] || {
49
+ type: METRIC_TYPE.COUNTER,
50
+ help: `Counter metric ${name}`
51
+ };
52
+
53
+ lines.push(`# HELP ${name} ${escapeHelpString(meta.help)}`);
54
+ lines.push(`# TYPE ${name} ${meta.type}`);
55
+
56
+ for (const record of records) {
57
+ const labelStr = formatLabels(record.labels);
58
+ lines.push(`${name}${labelStr} ${record.value}`);
59
+ }
60
+ }
61
+
62
+ // format gauges
63
+ for (const [name, records] of Object.entries(gauges)) {
64
+ if (!Array.isArray(records) || records.length === 0) continue;
65
+
66
+ const meta = METRIC_METADATA[name] || {
67
+ type: METRIC_TYPE.GAUGE,
68
+ help: `Gauge metric ${name}`
69
+ };
70
+
71
+ lines.push(`# HELP ${name} ${escapeHelpString(meta.help)}`);
72
+ lines.push(`# TYPE ${name} ${meta.type}`);
73
+
74
+ for (const record of records) {
75
+ const labelStr = formatLabels(record.labels);
76
+ lines.push(`${name}${labelStr} ${record.value}`);
77
+ }
78
+ }
79
+
80
+ // format histograms
81
+ for (const [name, records] of Object.entries(histograms)) {
82
+ if (!Array.isArray(records) || records.length === 0) continue;
83
+
84
+ const meta = METRIC_METADATA[name] || {
85
+ type: METRIC_TYPE.HISTOGRAM,
86
+ help: `Histogram metric ${name}`
87
+ };
88
+
89
+ lines.push(`# HELP ${name} ${escapeHelpString(meta.help)}`);
90
+ lines.push(`# TYPE ${name} ${meta.type}`);
91
+
92
+ for (const record of records) {
93
+ const baseLabels = record.labels || {};
94
+
95
+ // buckets
96
+ if (Array.isArray(record.buckets)) {
97
+ for (const bucket of record.buckets) {
98
+ const bucketLabels = formatLabels(baseLabels, { le: bucket.le });
99
+ lines.push(`${name}_bucket${bucketLabels} ${bucket.count}`);
100
+ }
101
+ }
102
+
103
+ // inf bucket matches total count
104
+ const infLabels = formatLabels(baseLabels, { le: '+Inf' });
105
+ lines.push(`${name}_bucket${infLabels} ${record.count}`);
106
+
107
+ // sum and count
108
+ const sumLabels = formatLabels(baseLabels);
109
+ lines.push(`${name}_sum${sumLabels} ${record.sum}`);
110
+ lines.push(`${name}_count${sumLabels} ${record.count}`);
111
+ }
112
+ }
113
+
114
+ return lines.length > 0 ? `${lines.join('\n')}\n` : '';
115
+ }
116
+
117
+ export function createPrometheusExporter(options = {}) {
118
+ const collector = options.collector || options.metricsCollector || defaultMetricsCollector;
119
+
120
+ return function prometheusExporterMiddleware(req, res, next) {
121
+ try {
122
+ const snapshot = collector.getSnapshot();
123
+ const output = formatPrometheusMetrics(snapshot);
124
+
125
+ res.setHeader('Content-Type', 'text/plain; version=0.0.4; charset=utf-8');
126
+ res.status(200).end(output);
127
+ } catch (err) {
128
+ next(err);
129
+ }
130
+ };
131
+ }
@@ -0,0 +1,58 @@
1
+
2
+
3
+ export const METRIC_TYPE = Object.freeze({
4
+ COUNTER: 'counter',
5
+ GAUGE: 'gauge',
6
+ HISTOGRAM: 'histogram'
7
+ });
8
+
9
+ export const METRIC_NAMES = Object.freeze({
10
+ REQUESTS_TOTAL: 'smartrate_requests_total',
11
+ STORE_ERRORS_TOTAL: 'smartrate_store_errors_total',
12
+ DEGRADED_REQUESTS_TOTAL: 'smartrate_degraded_requests_total',
13
+ CIRCUIT_BREAKER_STATE: 'smartrate_circuit_breaker_state',
14
+ STORE_DURATION_SECONDS: 'smartrate_store_duration_seconds'
15
+ });
16
+
17
+ export const CIRCUIT_STATE_GAUGE_VALUES = Object.freeze({
18
+ CLOSED: 0,
19
+ HALF_OPEN: 1,
20
+ OPEN: 2
21
+ });
22
+
23
+ export const DEFAULT_STORE_DURATION_BUCKETS = Object.freeze([
24
+ 0.0005,
25
+ 0.001,
26
+ 0.0025,
27
+ 0.005,
28
+ 0.01,
29
+ 0.025,
30
+ 0.05,
31
+ 0.1,
32
+ 0.25,
33
+ 0.5,
34
+ 1.0
35
+ ]);
36
+
37
+ export const METRIC_METADATA = Object.freeze({
38
+ [METRIC_NAMES.REQUESTS_TOTAL]: {
39
+ type: METRIC_TYPE.COUNTER,
40
+ help: 'Total number of rate limiter requests evaluated.'
41
+ },
42
+ [METRIC_NAMES.STORE_ERRORS_TOTAL]: {
43
+ type: METRIC_TYPE.COUNTER,
44
+ help: 'Total number of store or timeout errors encountered.'
45
+ },
46
+ [METRIC_NAMES.DEGRADED_REQUESTS_TOTAL]: {
47
+ type: METRIC_TYPE.COUNTER,
48
+ help: 'Total number of requests handled under degraded or fallback conditions.'
49
+ },
50
+ [METRIC_NAMES.CIRCUIT_BREAKER_STATE]: {
51
+ type: METRIC_TYPE.GAUGE,
52
+ help: 'Current state of the rate limiter circuit breaker (0=CLOSED, 1=HALF_OPEN, 2=OPEN).'
53
+ },
54
+ [METRIC_NAMES.STORE_DURATION_SECONDS]: {
55
+ type: METRIC_TYPE.HISTOGRAM,
56
+ help: 'Duration of rate limiter store operations in seconds.'
57
+ }
58
+ });
@@ -0,0 +1,41 @@
1
+ import crypto from 'node:crypto';
2
+
3
+ export const MAX_IDENTIFIER_LENGTH = 256;
4
+
5
+ export function buildRateLimitKey({
6
+ prefix = 'smartrate',
7
+ algorithm = 'fixed-window',
8
+ method = 'GET',
9
+ route = '/',
10
+ clientIdentifier = '127.0.0.1',
11
+ hashClientIdentifier = false
12
+ } = {}) {
13
+ const cleanMethod = (method || 'GET').toUpperCase();
14
+ const cleanRoute = (route || '/').split('?')[0] || '/';
15
+ const rawId = clientIdentifier && String(clientIdentifier).trim()
16
+ ? String(clientIdentifier).trim()
17
+ : '127.0.0.1';
18
+
19
+ let cleanId;
20
+ if (hashClientIdentifier === true) {
21
+ cleanId = crypto.createHash('sha256').update(rawId).digest('hex');
22
+ } else if (typeof hashClientIdentifier === 'function') {
23
+ cleanId = String(hashClientIdentifier(rawId) || rawId);
24
+ if (cleanId.length > MAX_IDENTIFIER_LENGTH) {
25
+ const digest = crypto.createHash('sha256').update(rawId).digest('hex');
26
+ cleanId = `${cleanId.slice(0, MAX_IDENTIFIER_LENGTH - digest.length - 1)}:${digest}`;
27
+ }
28
+ } else if (rawId.length > MAX_IDENTIFIER_LENGTH) {
29
+ cleanId = crypto.createHash('sha256').update(rawId).digest('hex');
30
+ } else {
31
+ cleanId = rawId;
32
+ }
33
+
34
+ if (algorithm && algorithm !== 'fixed-window') {
35
+ return `${prefix}:${algorithm}:${cleanMethod}:${cleanRoute}:${cleanId}`;
36
+ }
37
+
38
+ return `${prefix}:${cleanMethod}:${cleanRoute}:${cleanId}`;
39
+ }
40
+
41
+ export default buildRateLimitKey;
@@ -0,0 +1,49 @@
1
+ const UUID_REGEX = /[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}/gi;
2
+ const NUMERIC_SEGMENT_REGEX = /\/\d+(?=\/|$)/g;
3
+ const LONG_HEX_HASH_REGEX = /\/[0-9a-fA-F]{16,}(?=\/|$)/g;
4
+ const ULID_REGEX = /\/[0-7][0-9A-HJKMNP-TV-Z]{25}(?=\/|$)/gi;
5
+ const CUID_REGEX = /\/c[0-9a-z]{24}(?=\/|$)/gi;
6
+
7
+ export function sanitizePath(rawPath) {
8
+ if (!rawPath || typeof rawPath !== 'string') {
9
+ return '/';
10
+ }
11
+
12
+ const cleanPath = rawPath.split('?')[0];
13
+ const sanitized = cleanPath
14
+ .replace(UUID_REGEX, ':id')
15
+ .replace(ULID_REGEX, '/:id')
16
+ .replace(CUID_REGEX, '/:id')
17
+ .replace(NUMERIC_SEGMENT_REGEX, '/:id')
18
+ .replace(LONG_HEX_HASH_REGEX, '/:id')
19
+ .replace(/\/+/g, '/');
20
+
21
+ return sanitized || '/';
22
+ }
23
+
24
+ export function normalizeRoute(req, customNormalizer) {
25
+ if (typeof customNormalizer === 'function') {
26
+ try {
27
+ const custom = customNormalizer(req);
28
+ if (typeof custom === 'string' && custom.length > 0) {
29
+ return sanitizePath(custom);
30
+ }
31
+ } catch {
32
+ // ignore normalizer error and fallback
33
+ }
34
+ }
35
+
36
+ if (!req || typeof req !== 'object') {
37
+ return '/';
38
+ }
39
+
40
+ if (req.route && typeof req.route.path === 'string') {
41
+ const base = req.baseUrl || '';
42
+ const route = req.route.path;
43
+ const combined = `${base}${route}`.replace(/\/+/g, '/');
44
+ return combined || '/';
45
+ }
46
+
47
+ const fallbackPath = `${req.baseUrl || ''}${req.path || req.originalUrl || '/'}`;
48
+ return sanitizePath(fallbackPath);
49
+ }