@push.rocks/smartproxy 19.6.6 → 19.6.8

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.
Files changed (48) hide show
  1. package/dist_ts/core/models/wrapped-socket.d.ts +4 -0
  2. package/dist_ts/core/models/wrapped-socket.js +18 -1
  3. package/dist_ts/core/routing/matchers/path.js +3 -2
  4. package/dist_ts/proxies/smart-proxy/connection-manager.d.ts +4 -7
  5. package/dist_ts/proxies/smart-proxy/connection-manager.js +22 -22
  6. package/dist_ts/proxies/smart-proxy/http-proxy-bridge.d.ts +4 -3
  7. package/dist_ts/proxies/smart-proxy/http-proxy-bridge.js +9 -9
  8. package/dist_ts/proxies/smart-proxy/metrics-collector.d.ts +68 -56
  9. package/dist_ts/proxies/smart-proxy/metrics-collector.js +252 -176
  10. package/dist_ts/proxies/smart-proxy/models/interfaces.d.ts +6 -1
  11. package/dist_ts/proxies/smart-proxy/models/metrics-types.d.ts +99 -47
  12. package/dist_ts/proxies/smart-proxy/nftables-manager.d.ts +4 -4
  13. package/dist_ts/proxies/smart-proxy/nftables-manager.js +6 -6
  14. package/dist_ts/proxies/smart-proxy/port-manager.d.ts +4 -7
  15. package/dist_ts/proxies/smart-proxy/port-manager.js +6 -9
  16. package/dist_ts/proxies/smart-proxy/route-connection-handler.d.ts +4 -15
  17. package/dist_ts/proxies/smart-proxy/route-connection-handler.js +133 -130
  18. package/dist_ts/proxies/smart-proxy/security-manager.d.ts +3 -3
  19. package/dist_ts/proxies/smart-proxy/security-manager.js +9 -9
  20. package/dist_ts/proxies/smart-proxy/smart-proxy.d.ts +20 -13
  21. package/dist_ts/proxies/smart-proxy/smart-proxy.js +16 -13
  22. package/dist_ts/proxies/smart-proxy/throughput-tracker.d.ts +36 -0
  23. package/dist_ts/proxies/smart-proxy/throughput-tracker.js +117 -0
  24. package/dist_ts/proxies/smart-proxy/timeout-manager.d.ts +5 -4
  25. package/dist_ts/proxies/smart-proxy/timeout-manager.js +20 -16
  26. package/dist_ts/proxies/smart-proxy/tls-manager.d.ts +3 -3
  27. package/dist_ts/proxies/smart-proxy/tls-manager.js +12 -12
  28. package/dist_ts/routing/router/http-router.js +2 -2
  29. package/package.json +1 -1
  30. package/readme.hints.md +0 -0
  31. package/readme.md +239 -73
  32. package/readme.plan.md +364 -0
  33. package/ts/core/models/wrapped-socket.ts +18 -0
  34. package/ts/core/routing/matchers/path.ts +2 -1
  35. package/ts/proxies/smart-proxy/connection-manager.ts +23 -21
  36. package/ts/proxies/smart-proxy/http-proxy-bridge.ts +9 -8
  37. package/ts/proxies/smart-proxy/metrics-collector.ts +305 -188
  38. package/ts/proxies/smart-proxy/models/interfaces.ts +8 -1
  39. package/ts/proxies/smart-proxy/models/metrics-types.ts +99 -41
  40. package/ts/proxies/smart-proxy/nftables-manager.ts +5 -5
  41. package/ts/proxies/smart-proxy/port-manager.ts +6 -14
  42. package/ts/proxies/smart-proxy/route-connection-handler.ts +141 -138
  43. package/ts/proxies/smart-proxy/security-manager.ts +8 -8
  44. package/ts/proxies/smart-proxy/smart-proxy.ts +26 -35
  45. package/ts/proxies/smart-proxy/throughput-tracker.ts +144 -0
  46. package/ts/proxies/smart-proxy/timeout-manager.ts +22 -16
  47. package/ts/proxies/smart-proxy/tls-manager.ts +11 -11
  48. package/ts/routing/router/http-router.ts +1 -1
@@ -1,258 +1,370 @@
1
1
  import * as plugins from '../../plugins.js';
2
2
  import type { SmartProxy } from './smart-proxy.js';
3
- import type { IProxyStats, IProxyStatsExtended } from './models/metrics-types.js';
3
+ import type {
4
+ IMetrics,
5
+ IThroughputData,
6
+ IThroughputHistoryPoint,
7
+ IByteTracker
8
+ } from './models/metrics-types.js';
9
+ import { ThroughputTracker } from './throughput-tracker.js';
4
10
  import { logger } from '../../core/utils/logger.js';
5
11
 
6
12
  /**
7
- * Collects and computes metrics for SmartProxy on-demand
13
+ * Collects and provides metrics for SmartProxy with clean API
8
14
  */
9
- export class MetricsCollector implements IProxyStatsExtended {
10
- // RPS tracking (the only state we need to maintain)
11
- private requestTimestamps: number[] = [];
12
- private readonly RPS_WINDOW_SIZE = 60000; // 1 minute window
13
- private readonly MAX_TIMESTAMPS = 5000; // Maximum timestamps to keep
15
+ export class MetricsCollector implements IMetrics {
16
+ // Throughput tracking
17
+ private throughputTracker: ThroughputTracker;
14
18
 
15
- // Optional caching for performance
16
- private cachedMetrics: {
17
- timestamp: number;
18
- connectionsByRoute?: Map<string, number>;
19
- connectionsByIP?: Map<string, number>;
20
- } = { timestamp: 0 };
19
+ // Request tracking
20
+ private requestTimestamps: number[] = [];
21
+ private totalRequests: number = 0;
21
22
 
22
- private readonly CACHE_TTL = 1000; // 1 second cache
23
+ // Connection byte tracking for per-route/IP metrics
24
+ private connectionByteTrackers = new Map<string, IByteTracker>();
23
25
 
24
- // RxJS subscription for connection events
26
+ // Subscriptions
27
+ private samplingInterval?: NodeJS.Timeout;
25
28
  private connectionSubscription?: plugins.smartrx.rxjs.Subscription;
26
29
 
30
+ // Configuration
31
+ private readonly sampleIntervalMs: number;
32
+ private readonly retentionSeconds: number;
33
+
27
34
  constructor(
28
- private smartProxy: SmartProxy
35
+ private smartProxy: SmartProxy,
36
+ config?: {
37
+ sampleIntervalMs?: number;
38
+ retentionSeconds?: number;
39
+ }
29
40
  ) {
30
- // Subscription will be set up in start() method
41
+ this.sampleIntervalMs = config?.sampleIntervalMs || 1000;
42
+ this.retentionSeconds = config?.retentionSeconds || 3600;
43
+ this.throughputTracker = new ThroughputTracker(this.retentionSeconds);
31
44
  }
32
45
 
33
- /**
34
- * Get the current number of active connections
35
- */
36
- public getActiveConnections(): number {
37
- return this.smartProxy.connectionManager.getConnectionCount();
38
- }
39
-
40
- /**
41
- * Get connection counts grouped by route name
42
- */
43
- public getConnectionsByRoute(): Map<string, number> {
44
- const now = Date.now();
45
-
46
- // Return cached value if fresh
47
- if (this.cachedMetrics.connectionsByRoute &&
48
- now - this.cachedMetrics.timestamp < this.CACHE_TTL) {
49
- return new Map(this.cachedMetrics.connectionsByRoute);
50
- }
46
+ // Connection metrics implementation
47
+ public connections = {
48
+ active: (): number => {
49
+ return this.smartProxy.connectionManager.getConnectionCount();
50
+ },
51
51
 
52
- // Compute fresh value
53
- const routeCounts = new Map<string, number>();
54
- const connections = this.smartProxy.connectionManager.getConnections();
52
+ total: (): number => {
53
+ const stats = this.smartProxy.connectionManager.getTerminationStats();
54
+ let total = this.smartProxy.connectionManager.getConnectionCount();
55
+
56
+ for (const reason in stats.incoming) {
57
+ total += stats.incoming[reason];
58
+ }
59
+
60
+ return total;
61
+ },
55
62
 
56
- if (this.smartProxy.settings?.enableDetailedLogging) {
57
- logger.log('debug', `MetricsCollector: Computing route connections`, {
58
- totalConnections: connections.size,
59
- component: 'metrics'
60
- });
61
- }
63
+ byRoute: (): Map<string, number> => {
64
+ const routeCounts = new Map<string, number>();
65
+ const connections = this.smartProxy.connectionManager.getConnections();
66
+
67
+ for (const [_, record] of connections) {
68
+ const routeName = (record as any).routeName ||
69
+ record.routeConfig?.name ||
70
+ 'unknown';
71
+
72
+ const current = routeCounts.get(routeName) || 0;
73
+ routeCounts.set(routeName, current + 1);
74
+ }
75
+
76
+ return routeCounts;
77
+ },
62
78
 
63
- for (const [_, record] of connections) {
64
- // Try different ways to get the route name
65
- const routeName = (record as any).routeName ||
66
- record.routeConfig?.name ||
67
- (record.routeConfig as any)?.routeName ||
68
- 'unknown';
79
+ byIP: (): Map<string, number> => {
80
+ const ipCounts = new Map<string, number>();
69
81
 
70
- if (this.smartProxy.settings?.enableDetailedLogging) {
71
- logger.log('debug', `MetricsCollector: Connection route info`, {
72
- connectionId: record.id,
73
- routeName,
74
- hasRouteConfig: !!record.routeConfig,
75
- routeConfigName: record.routeConfig?.name,
76
- routeConfigKeys: record.routeConfig ? Object.keys(record.routeConfig) : [],
77
- component: 'metrics'
78
- });
82
+ for (const [_, record] of this.smartProxy.connectionManager.getConnections()) {
83
+ const ip = record.remoteIP;
84
+ const current = ipCounts.get(ip) || 0;
85
+ ipCounts.set(ip, current + 1);
79
86
  }
80
87
 
81
- const current = routeCounts.get(routeName) || 0;
82
- routeCounts.set(routeName, current + 1);
83
- }
88
+ return ipCounts;
89
+ },
84
90
 
85
- // Cache and return
86
- this.cachedMetrics.connectionsByRoute = routeCounts;
87
- this.cachedMetrics.timestamp = now;
88
- return new Map(routeCounts);
89
- }
91
+ topIPs: (limit: number = 10): Array<{ ip: string; count: number }> => {
92
+ const ipCounts = this.connections.byIP();
93
+ return Array.from(ipCounts.entries())
94
+ .sort((a, b) => b[1] - a[1])
95
+ .slice(0, limit)
96
+ .map(([ip, count]) => ({ ip, count }));
97
+ }
98
+ };
90
99
 
91
- /**
92
- * Get connection counts grouped by IP address
93
- */
94
- public getConnectionsByIP(): Map<string, number> {
95
- const now = Date.now();
100
+ // Throughput metrics implementation
101
+ public throughput = {
102
+ instant: (): IThroughputData => {
103
+ return this.throughputTracker.getRate(1);
104
+ },
96
105
 
97
- // Return cached value if fresh
98
- if (this.cachedMetrics.connectionsByIP &&
99
- now - this.cachedMetrics.timestamp < this.CACHE_TTL) {
100
- return new Map(this.cachedMetrics.connectionsByIP);
101
- }
106
+ recent: (): IThroughputData => {
107
+ return this.throughputTracker.getRate(10);
108
+ },
102
109
 
103
- // Compute fresh value
104
- const ipCounts = new Map<string, number>();
105
- for (const [_, record] of this.smartProxy.connectionManager.getConnections()) {
106
- const ip = record.remoteIP;
107
- const current = ipCounts.get(ip) || 0;
108
- ipCounts.set(ip, current + 1);
109
- }
110
+ average: (): IThroughputData => {
111
+ return this.throughputTracker.getRate(60);
112
+ },
110
113
 
111
- // Cache and return
112
- this.cachedMetrics.connectionsByIP = ipCounts;
113
- this.cachedMetrics.timestamp = now;
114
- return new Map(ipCounts);
115
- }
116
-
117
- /**
118
- * Get the total number of connections since proxy start
119
- */
120
- public getTotalConnections(): number {
121
- // Get from termination stats
122
- const stats = this.smartProxy.connectionManager.getTerminationStats();
123
- let total = this.smartProxy.connectionManager.getConnectionCount(); // Add active connections
114
+ custom: (seconds: number): IThroughputData => {
115
+ return this.throughputTracker.getRate(seconds);
116
+ },
124
117
 
125
- // Add all terminated connections
126
- for (const reason in stats.incoming) {
127
- total += stats.incoming[reason];
128
- }
118
+ history: (seconds: number): Array<IThroughputHistoryPoint> => {
119
+ return this.throughputTracker.getHistory(seconds);
120
+ },
129
121
 
130
- return total;
131
- }
122
+ byRoute: (windowSeconds: number = 60): Map<string, IThroughputData> => {
123
+ const routeThroughput = new Map<string, IThroughputData>();
124
+ const now = Date.now();
125
+ const windowStart = now - (windowSeconds * 1000);
126
+
127
+ // Aggregate bytes by route with proper time calculation
128
+ const routeData = new Map<string, { bytesIn: number; bytesOut: number; totalDuration: number }>();
129
+
130
+ for (const [_, tracker] of this.connectionByteTrackers) {
131
+ // Only include connections that were active within the window
132
+ if (tracker.lastUpdate > windowStart || tracker.startTime > windowStart) {
133
+ // Calculate the actual duration this connection was active within the window
134
+ const connectionStart = Math.max(tracker.startTime, windowStart);
135
+ const connectionEnd = tracker.lastUpdate;
136
+ const durationInWindow = (connectionEnd - connectionStart) / 1000; // Convert to seconds
137
+
138
+ if (durationInWindow > 0) {
139
+ const current = routeData.get(tracker.routeName) || { bytesIn: 0, bytesOut: 0, totalDuration: 0 };
140
+ current.bytesIn += tracker.bytesIn;
141
+ current.bytesOut += tracker.bytesOut;
142
+ current.totalDuration += durationInWindow;
143
+ routeData.set(tracker.routeName, current);
144
+ }
145
+ }
146
+ }
147
+
148
+ // Convert to rates (bytes per second)
149
+ for (const [route, data] of routeData) {
150
+ if (data.totalDuration > 0) {
151
+ routeThroughput.set(route, {
152
+ in: Math.round(data.bytesIn / data.totalDuration),
153
+ out: Math.round(data.bytesOut / data.totalDuration)
154
+ });
155
+ }
156
+ }
157
+
158
+ return routeThroughput;
159
+ },
160
+
161
+ byIP: (windowSeconds: number = 60): Map<string, IThroughputData> => {
162
+ const ipThroughput = new Map<string, IThroughputData>();
163
+ const now = Date.now();
164
+ const windowStart = now - (windowSeconds * 1000);
165
+
166
+ // Aggregate bytes by IP with proper time calculation
167
+ const ipData = new Map<string, { bytesIn: number; bytesOut: number; totalDuration: number }>();
168
+
169
+ for (const [_, tracker] of this.connectionByteTrackers) {
170
+ // Only include connections that were active within the window
171
+ if (tracker.lastUpdate > windowStart || tracker.startTime > windowStart) {
172
+ // Calculate the actual duration this connection was active within the window
173
+ const connectionStart = Math.max(tracker.startTime, windowStart);
174
+ const connectionEnd = tracker.lastUpdate;
175
+ const durationInWindow = (connectionEnd - connectionStart) / 1000; // Convert to seconds
176
+
177
+ if (durationInWindow > 0) {
178
+ const current = ipData.get(tracker.remoteIP) || { bytesIn: 0, bytesOut: 0, totalDuration: 0 };
179
+ current.bytesIn += tracker.bytesIn;
180
+ current.bytesOut += tracker.bytesOut;
181
+ current.totalDuration += durationInWindow;
182
+ ipData.set(tracker.remoteIP, current);
183
+ }
184
+ }
185
+ }
186
+
187
+ // Convert to rates (bytes per second)
188
+ for (const [ip, data] of ipData) {
189
+ if (data.totalDuration > 0) {
190
+ ipThroughput.set(ip, {
191
+ in: Math.round(data.bytesIn / data.totalDuration),
192
+ out: Math.round(data.bytesOut / data.totalDuration)
193
+ });
194
+ }
195
+ }
196
+
197
+ return ipThroughput;
198
+ }
199
+ };
132
200
 
133
- /**
134
- * Get the current requests per second rate
135
- */
136
- public getRequestsPerSecond(): number {
137
- const now = Date.now();
138
- const windowStart = now - this.RPS_WINDOW_SIZE;
201
+ // Request metrics implementation
202
+ public requests = {
203
+ perSecond: (): number => {
204
+ const now = Date.now();
205
+ const oneSecondAgo = now - 1000;
206
+
207
+ // Clean old timestamps
208
+ this.requestTimestamps = this.requestTimestamps.filter(ts => ts > now - 60000);
209
+
210
+ // Count requests in last second
211
+ const recentRequests = this.requestTimestamps.filter(ts => ts > oneSecondAgo);
212
+ return recentRequests.length;
213
+ },
139
214
 
140
- // Clean old timestamps
141
- this.requestTimestamps = this.requestTimestamps.filter(ts => ts > windowStart);
215
+ perMinute: (): number => {
216
+ const now = Date.now();
217
+ const oneMinuteAgo = now - 60000;
218
+
219
+ // Count requests in last minute
220
+ const recentRequests = this.requestTimestamps.filter(ts => ts > oneMinuteAgo);
221
+ return recentRequests.length;
222
+ },
142
223
 
143
- // Calculate RPS based on window
144
- const requestsInWindow = this.requestTimestamps.length;
145
- return requestsInWindow / (this.RPS_WINDOW_SIZE / 1000);
146
- }
224
+ total: (): number => {
225
+ return this.totalRequests;
226
+ }
227
+ };
147
228
 
148
- /**
149
- * Record a new request for RPS tracking
150
- */
151
- public recordRequest(): void {
152
- const now = Date.now();
153
- this.requestTimestamps.push(now);
229
+ // Totals implementation
230
+ public totals = {
231
+ bytesIn: (): number => {
232
+ let total = 0;
233
+
234
+ // Sum from all active connections
235
+ for (const [_, record] of this.smartProxy.connectionManager.getConnections()) {
236
+ total += record.bytesReceived;
237
+ }
238
+
239
+ // TODO: Add historical data from terminated connections
240
+
241
+ return total;
242
+ },
154
243
 
155
- // Prevent unbounded growth - clean up more aggressively
156
- if (this.requestTimestamps.length > this.MAX_TIMESTAMPS) {
157
- // Keep only timestamps within the window
158
- const cutoff = now - this.RPS_WINDOW_SIZE;
159
- this.requestTimestamps = this.requestTimestamps.filter(ts => ts > cutoff);
244
+ bytesOut: (): number => {
245
+ let total = 0;
246
+
247
+ // Sum from all active connections
248
+ for (const [_, record] of this.smartProxy.connectionManager.getConnections()) {
249
+ total += record.bytesSent;
250
+ }
251
+
252
+ // TODO: Add historical data from terminated connections
253
+
254
+ return total;
255
+ },
256
+
257
+ connections: (): number => {
258
+ return this.connections.total();
160
259
  }
161
- }
260
+ };
162
261
 
163
- /**
164
- * Get total throughput (bytes transferred)
165
- */
166
- public getThroughput(): { bytesIn: number; bytesOut: number } {
167
- let bytesIn = 0;
168
- let bytesOut = 0;
262
+ // Percentiles implementation (placeholder for now)
263
+ public percentiles = {
264
+ connectionDuration: (): { p50: number; p95: number; p99: number } => {
265
+ // TODO: Implement percentile calculations
266
+ return { p50: 0, p95: 0, p99: 0 };
267
+ },
169
268
 
170
- // Sum bytes from all active connections
171
- for (const [_, record] of this.smartProxy.connectionManager.getConnections()) {
172
- bytesIn += record.bytesReceived;
173
- bytesOut += record.bytesSent;
269
+ bytesTransferred: (): {
270
+ in: { p50: number; p95: number; p99: number };
271
+ out: { p50: number; p95: number; p99: number };
272
+ } => {
273
+ // TODO: Implement percentile calculations
274
+ return {
275
+ in: { p50: 0, p95: 0, p99: 0 },
276
+ out: { p50: 0, p95: 0, p99: 0 }
277
+ };
174
278
  }
175
-
176
- return { bytesIn, bytesOut };
177
- }
279
+ };
178
280
 
179
281
  /**
180
- * Get throughput rate (bytes per second) for last minute
282
+ * Record a new request
181
283
  */
182
- public getThroughputRate(): { bytesInPerSec: number; bytesOutPerSec: number } {
284
+ public recordRequest(connectionId: string, routeName: string, remoteIP: string): void {
183
285
  const now = Date.now();
184
- let recentBytesIn = 0;
185
- let recentBytesOut = 0;
286
+ this.requestTimestamps.push(now);
287
+ this.totalRequests++;
288
+
289
+ // Initialize byte tracker for this connection
290
+ this.connectionByteTrackers.set(connectionId, {
291
+ connectionId,
292
+ routeName,
293
+ remoteIP,
294
+ bytesIn: 0,
295
+ bytesOut: 0,
296
+ startTime: now,
297
+ lastUpdate: now
298
+ });
186
299
 
187
- // Calculate bytes transferred in last minute from active connections
188
- for (const [_, record] of this.smartProxy.connectionManager.getConnections()) {
189
- const connectionAge = now - record.incomingStartTime;
190
- if (connectionAge < 60000) { // Connection started within last minute
191
- recentBytesIn += record.bytesReceived;
192
- recentBytesOut += record.bytesSent;
193
- } else {
194
- // For older connections, estimate rate based on average
195
- const rate = connectionAge / 60000;
196
- recentBytesIn += record.bytesReceived / rate;
197
- recentBytesOut += record.bytesSent / rate;
300
+ // Cleanup old request timestamps
301
+ if (this.requestTimestamps.length > 5000) {
302
+ // First try to clean up old timestamps (older than 1 minute)
303
+ const cutoff = now - 60000;
304
+ this.requestTimestamps = this.requestTimestamps.filter(ts => ts > cutoff);
305
+
306
+ // If still too many, enforce hard cap of 5000 most recent
307
+ if (this.requestTimestamps.length > 5000) {
308
+ this.requestTimestamps = this.requestTimestamps.slice(-5000);
198
309
  }
199
310
  }
200
-
201
- return {
202
- bytesInPerSec: Math.round(recentBytesIn / 60),
203
- bytesOutPerSec: Math.round(recentBytesOut / 60)
204
- };
205
311
  }
206
312
 
207
313
  /**
208
- * Get top IPs by connection count
314
+ * Record bytes transferred for a connection
209
315
  */
210
- public getTopIPs(limit: number = 10): Array<{ ip: string; connections: number }> {
211
- const ipCounts = this.getConnectionsByIP();
212
- const sorted = Array.from(ipCounts.entries())
213
- .sort((a, b) => b[1] - a[1])
214
- .slice(0, limit)
215
- .map(([ip, connections]) => ({ ip, connections }));
316
+ public recordBytes(connectionId: string, bytesIn: number, bytesOut: number): void {
317
+ // Update global throughput tracker
318
+ this.throughputTracker.recordBytes(bytesIn, bytesOut);
216
319
 
217
- return sorted;
218
- }
219
-
220
- /**
221
- * Check if an IP has reached the connection limit
222
- */
223
- public isIPBlocked(ip: string, maxConnectionsPerIP: number): boolean {
224
- const ipCounts = this.getConnectionsByIP();
225
- const currentConnections = ipCounts.get(ip) || 0;
226
- return currentConnections >= maxConnectionsPerIP;
320
+ // Update connection-specific tracker
321
+ const tracker = this.connectionByteTrackers.get(connectionId);
322
+ if (tracker) {
323
+ tracker.bytesIn += bytesIn;
324
+ tracker.bytesOut += bytesOut;
325
+ tracker.lastUpdate = Date.now();
326
+ }
227
327
  }
228
328
 
229
329
  /**
230
- * Clean up old request timestamps
330
+ * Clean up tracking for a closed connection
231
331
  */
232
- private cleanupOldRequests(): void {
233
- const cutoff = Date.now() - this.RPS_WINDOW_SIZE;
234
- this.requestTimestamps = this.requestTimestamps.filter(ts => ts > cutoff);
332
+ public removeConnection(connectionId: string): void {
333
+ this.connectionByteTrackers.delete(connectionId);
235
334
  }
236
335
 
237
336
  /**
238
- * Start the metrics collector and set up subscriptions
337
+ * Start the metrics collector
239
338
  */
240
339
  public start(): void {
241
340
  if (!this.smartProxy.routeConnectionHandler) {
242
341
  throw new Error('MetricsCollector: RouteConnectionHandler not available');
243
342
  }
244
343
 
245
- // Subscribe to the newConnectionSubject from RouteConnectionHandler
344
+ // Start periodic sampling
345
+ this.samplingInterval = setInterval(() => {
346
+ this.throughputTracker.takeSample();
347
+
348
+ // Clean up old connection trackers (connections closed more than 5 minutes ago)
349
+ const cutoff = Date.now() - 300000;
350
+ for (const [id, tracker] of this.connectionByteTrackers) {
351
+ if (tracker.lastUpdate < cutoff) {
352
+ this.connectionByteTrackers.delete(id);
353
+ }
354
+ }
355
+ }, this.sampleIntervalMs);
356
+
357
+ // Subscribe to new connections
246
358
  this.connectionSubscription = this.smartProxy.routeConnectionHandler.newConnectionSubject.subscribe({
247
359
  next: (record) => {
248
- this.recordRequest();
360
+ const routeName = record.routeConfig?.name || 'unknown';
361
+ this.recordRequest(record.id, routeName, record.remoteIP);
249
362
 
250
- // Optional: Log connection details
251
363
  if (this.smartProxy.settings?.enableDetailedLogging) {
252
364
  logger.log('debug', `MetricsCollector: New connection recorded`, {
253
365
  connectionId: record.id,
254
366
  remoteIP: record.remoteIP,
255
- routeName: record.routeConfig?.name || 'unknown',
367
+ routeName,
256
368
  component: 'metrics'
257
369
  });
258
370
  }
@@ -269,9 +381,14 @@ export class MetricsCollector implements IProxyStatsExtended {
269
381
  }
270
382
 
271
383
  /**
272
- * Stop the metrics collector and clean up resources
384
+ * Stop the metrics collector
273
385
  */
274
386
  public stop(): void {
387
+ if (this.samplingInterval) {
388
+ clearInterval(this.samplingInterval);
389
+ this.samplingInterval = undefined;
390
+ }
391
+
275
392
  if (this.connectionSubscription) {
276
393
  this.connectionSubscription.unsubscribe();
277
394
  this.connectionSubscription = undefined;
@@ -281,7 +398,7 @@ export class MetricsCollector implements IProxyStatsExtended {
281
398
  }
282
399
 
283
400
  /**
284
- * Alias for stop() for backward compatibility
401
+ * Alias for stop() for compatibility
285
402
  */
286
403
  public destroy(): void {
287
404
  this.stop();
@@ -105,6 +105,13 @@ export interface ISmartProxyOptions {
105
105
  useHttpProxy?: number[]; // Array of ports to forward to HttpProxy
106
106
  httpProxyPort?: number; // Port where HttpProxy is listening (default: 8443)
107
107
 
108
+ // Metrics configuration
109
+ metrics?: {
110
+ enabled?: boolean;
111
+ sampleIntervalMs?: number;
112
+ retentionSeconds?: number;
113
+ };
114
+
108
115
  /**
109
116
  * Global ACME configuration options for SmartProxy
110
117
  *
@@ -142,7 +149,7 @@ export interface IConnectionRecord {
142
149
  outgoingClosedTime?: number;
143
150
  lockedDomain?: string; // Used to lock this connection to the initial SNI
144
151
  connectionClosed: boolean; // Flag to prevent multiple cleanup attempts
145
- cleanupTimer?: NodeJS.Timeout; // Timer for max lifetime/inactivity
152
+ cleanupTimer?: NodeJS.Timeout | null; // Timer for max lifetime/inactivity
146
153
  alertFallbackTimeout?: NodeJS.Timeout; // Timer for fallback after alert
147
154
  lastActivity: number; // Last activity timestamp for inactivity detection
148
155
  pendingData: Buffer[]; // Buffer to hold data during connection setup