@push.rocks/smartproxy 19.5.25 → 19.6.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,285 @@
1
+ import * as plugins from '../../plugins.js';
2
+ import type { SmartProxy } from './smart-proxy.js';
3
+ import type { IProxyStats, IProxyStatsExtended } from './models/metrics-types.js';
4
+ import { logger } from '../../core/utils/logger.js';
5
+
6
+ /**
7
+ * Collects and computes metrics for SmartProxy on-demand
8
+ */
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
+
14
+ // Optional caching for performance
15
+ private cachedMetrics: {
16
+ timestamp: number;
17
+ connectionsByRoute?: Map<string, number>;
18
+ connectionsByIP?: Map<string, number>;
19
+ } = { timestamp: 0 };
20
+
21
+ private readonly CACHE_TTL = 1000; // 1 second cache
22
+
23
+ // RxJS subscription for connection events
24
+ private connectionSubscription?: plugins.smartrx.rxjs.Subscription;
25
+
26
+ constructor(
27
+ private smartProxy: SmartProxy
28
+ ) {
29
+ // Subscription will be set up in start() method
30
+ }
31
+
32
+ /**
33
+ * Get the current number of active connections
34
+ */
35
+ public getActiveConnections(): number {
36
+ return this.smartProxy.connectionManager.getConnectionCount();
37
+ }
38
+
39
+ /**
40
+ * Get connection counts grouped by route name
41
+ */
42
+ public getConnectionsByRoute(): Map<string, number> {
43
+ const now = Date.now();
44
+
45
+ // Return cached value if fresh
46
+ if (this.cachedMetrics.connectionsByRoute &&
47
+ now - this.cachedMetrics.timestamp < this.CACHE_TTL) {
48
+ return new Map(this.cachedMetrics.connectionsByRoute);
49
+ }
50
+
51
+ // Compute fresh value
52
+ const routeCounts = new Map<string, number>();
53
+ const connections = this.smartProxy.connectionManager.getConnections();
54
+
55
+ if (this.smartProxy.settings?.enableDetailedLogging) {
56
+ logger.log('debug', `MetricsCollector: Computing route connections`, {
57
+ totalConnections: connections.size,
58
+ component: 'metrics'
59
+ });
60
+ }
61
+
62
+ for (const [_, record] of connections) {
63
+ // Try different ways to get the route name
64
+ const routeName = (record as any).routeName ||
65
+ record.routeConfig?.name ||
66
+ (record.routeConfig as any)?.routeName ||
67
+ 'unknown';
68
+
69
+ if (this.smartProxy.settings?.enableDetailedLogging) {
70
+ logger.log('debug', `MetricsCollector: Connection route info`, {
71
+ connectionId: record.id,
72
+ routeName,
73
+ hasRouteConfig: !!record.routeConfig,
74
+ routeConfigName: record.routeConfig?.name,
75
+ routeConfigKeys: record.routeConfig ? Object.keys(record.routeConfig) : [],
76
+ component: 'metrics'
77
+ });
78
+ }
79
+
80
+ const current = routeCounts.get(routeName) || 0;
81
+ routeCounts.set(routeName, current + 1);
82
+ }
83
+
84
+ // Cache and return
85
+ this.cachedMetrics.connectionsByRoute = routeCounts;
86
+ this.cachedMetrics.timestamp = now;
87
+ return new Map(routeCounts);
88
+ }
89
+
90
+ /**
91
+ * Get connection counts grouped by IP address
92
+ */
93
+ public getConnectionsByIP(): Map<string, number> {
94
+ const now = Date.now();
95
+
96
+ // Return cached value if fresh
97
+ if (this.cachedMetrics.connectionsByIP &&
98
+ now - this.cachedMetrics.timestamp < this.CACHE_TTL) {
99
+ return new Map(this.cachedMetrics.connectionsByIP);
100
+ }
101
+
102
+ // Compute fresh value
103
+ const ipCounts = new Map<string, number>();
104
+ for (const [_, record] of this.smartProxy.connectionManager.getConnections()) {
105
+ const ip = record.remoteIP;
106
+ const current = ipCounts.get(ip) || 0;
107
+ ipCounts.set(ip, current + 1);
108
+ }
109
+
110
+ // Cache and return
111
+ this.cachedMetrics.connectionsByIP = ipCounts;
112
+ this.cachedMetrics.timestamp = now;
113
+ return new Map(ipCounts);
114
+ }
115
+
116
+ /**
117
+ * Get the total number of connections since proxy start
118
+ */
119
+ public getTotalConnections(): number {
120
+ // Get from termination stats
121
+ const stats = this.smartProxy.connectionManager.getTerminationStats();
122
+ let total = this.smartProxy.connectionManager.getConnectionCount(); // Add active connections
123
+
124
+ // Add all terminated connections
125
+ for (const reason in stats.incoming) {
126
+ total += stats.incoming[reason];
127
+ }
128
+
129
+ return total;
130
+ }
131
+
132
+ /**
133
+ * Get the current requests per second rate
134
+ */
135
+ public getRequestsPerSecond(): number {
136
+ const now = Date.now();
137
+ const windowStart = now - this.RPS_WINDOW_SIZE;
138
+
139
+ // Clean old timestamps
140
+ this.requestTimestamps = this.requestTimestamps.filter(ts => ts > windowStart);
141
+
142
+ // Calculate RPS based on window
143
+ const requestsInWindow = this.requestTimestamps.length;
144
+ return requestsInWindow / (this.RPS_WINDOW_SIZE / 1000);
145
+ }
146
+
147
+ /**
148
+ * Record a new request for RPS tracking
149
+ */
150
+ public recordRequest(): void {
151
+ this.requestTimestamps.push(Date.now());
152
+
153
+ // Prevent unbounded growth
154
+ if (this.requestTimestamps.length > 10000) {
155
+ this.cleanupOldRequests();
156
+ }
157
+ }
158
+
159
+ /**
160
+ * Get total throughput (bytes transferred)
161
+ */
162
+ public getThroughput(): { bytesIn: number; bytesOut: number } {
163
+ let bytesIn = 0;
164
+ let bytesOut = 0;
165
+
166
+ // Sum bytes from all active connections
167
+ for (const [_, record] of this.smartProxy.connectionManager.getConnections()) {
168
+ bytesIn += record.bytesReceived;
169
+ bytesOut += record.bytesSent;
170
+ }
171
+
172
+ return { bytesIn, bytesOut };
173
+ }
174
+
175
+ /**
176
+ * Get throughput rate (bytes per second) for last minute
177
+ */
178
+ public getThroughputRate(): { bytesInPerSec: number; bytesOutPerSec: number } {
179
+ const now = Date.now();
180
+ let recentBytesIn = 0;
181
+ let recentBytesOut = 0;
182
+
183
+ // Calculate bytes transferred in last minute from active connections
184
+ for (const [_, record] of this.smartProxy.connectionManager.getConnections()) {
185
+ const connectionAge = now - record.incomingStartTime;
186
+ if (connectionAge < 60000) { // Connection started within last minute
187
+ recentBytesIn += record.bytesReceived;
188
+ recentBytesOut += record.bytesSent;
189
+ } else {
190
+ // For older connections, estimate rate based on average
191
+ const rate = connectionAge / 60000;
192
+ recentBytesIn += record.bytesReceived / rate;
193
+ recentBytesOut += record.bytesSent / rate;
194
+ }
195
+ }
196
+
197
+ return {
198
+ bytesInPerSec: Math.round(recentBytesIn / 60),
199
+ bytesOutPerSec: Math.round(recentBytesOut / 60)
200
+ };
201
+ }
202
+
203
+ /**
204
+ * Get top IPs by connection count
205
+ */
206
+ public getTopIPs(limit: number = 10): Array<{ ip: string; connections: number }> {
207
+ const ipCounts = this.getConnectionsByIP();
208
+ const sorted = Array.from(ipCounts.entries())
209
+ .sort((a, b) => b[1] - a[1])
210
+ .slice(0, limit)
211
+ .map(([ip, connections]) => ({ ip, connections }));
212
+
213
+ return sorted;
214
+ }
215
+
216
+ /**
217
+ * Check if an IP has reached the connection limit
218
+ */
219
+ public isIPBlocked(ip: string, maxConnectionsPerIP: number): boolean {
220
+ const ipCounts = this.getConnectionsByIP();
221
+ const currentConnections = ipCounts.get(ip) || 0;
222
+ return currentConnections >= maxConnectionsPerIP;
223
+ }
224
+
225
+ /**
226
+ * Clean up old request timestamps
227
+ */
228
+ private cleanupOldRequests(): void {
229
+ const cutoff = Date.now() - this.RPS_WINDOW_SIZE;
230
+ this.requestTimestamps = this.requestTimestamps.filter(ts => ts > cutoff);
231
+ }
232
+
233
+ /**
234
+ * Start the metrics collector and set up subscriptions
235
+ */
236
+ public start(): void {
237
+ if (!this.smartProxy.routeConnectionHandler) {
238
+ throw new Error('MetricsCollector: RouteConnectionHandler not available');
239
+ }
240
+
241
+ // Subscribe to the newConnectionSubject from RouteConnectionHandler
242
+ this.connectionSubscription = this.smartProxy.routeConnectionHandler.newConnectionSubject.subscribe({
243
+ next: (record) => {
244
+ this.recordRequest();
245
+
246
+ // Optional: Log connection details
247
+ if (this.smartProxy.settings?.enableDetailedLogging) {
248
+ logger.log('debug', `MetricsCollector: New connection recorded`, {
249
+ connectionId: record.id,
250
+ remoteIP: record.remoteIP,
251
+ routeName: record.routeConfig?.name || 'unknown',
252
+ component: 'metrics'
253
+ });
254
+ }
255
+ },
256
+ error: (err) => {
257
+ logger.log('error', `MetricsCollector: Error in connection subscription`, {
258
+ error: err.message,
259
+ component: 'metrics'
260
+ });
261
+ }
262
+ });
263
+
264
+ logger.log('debug', 'MetricsCollector started', { component: 'metrics' });
265
+ }
266
+
267
+ /**
268
+ * Stop the metrics collector and clean up resources
269
+ */
270
+ public stop(): void {
271
+ if (this.connectionSubscription) {
272
+ this.connectionSubscription.unsubscribe();
273
+ this.connectionSubscription = undefined;
274
+ }
275
+
276
+ logger.log('debug', 'MetricsCollector stopped', { component: 'metrics' });
277
+ }
278
+
279
+ /**
280
+ * Alias for stop() for backward compatibility
281
+ */
282
+ public destroy(): void {
283
+ this.stop();
284
+ }
285
+ }
@@ -4,3 +4,4 @@
4
4
  // Export everything except IAcmeOptions from interfaces
5
5
  export type { ISmartProxyOptions, IConnectionRecord, TSmartProxyCertProvisionObject } from './interfaces.js';
6
6
  export * from './route-types.js';
7
+ export * from './metrics-types.js';
@@ -0,0 +1,54 @@
1
+ /**
2
+ * Interface for proxy statistics and metrics
3
+ */
4
+ export interface IProxyStats {
5
+ /**
6
+ * Get the current number of active connections
7
+ */
8
+ getActiveConnections(): number;
9
+
10
+ /**
11
+ * Get connection counts grouped by route name
12
+ */
13
+ getConnectionsByRoute(): Map<string, number>;
14
+
15
+ /**
16
+ * Get connection counts grouped by IP address
17
+ */
18
+ getConnectionsByIP(): Map<string, number>;
19
+
20
+ /**
21
+ * Get the total number of connections since proxy start
22
+ */
23
+ getTotalConnections(): number;
24
+
25
+ /**
26
+ * Get the current requests per second rate
27
+ */
28
+ getRequestsPerSecond(): number;
29
+
30
+ /**
31
+ * Get total throughput (bytes transferred)
32
+ */
33
+ getThroughput(): { bytesIn: number; bytesOut: number };
34
+ }
35
+
36
+ /**
37
+ * Extended interface for additional metrics helpers
38
+ */
39
+ export interface IProxyStatsExtended extends IProxyStats {
40
+ /**
41
+ * Get throughput rate (bytes per second) for last minute
42
+ */
43
+ getThroughputRate(): { bytesInPerSec: number; bytesOutPerSec: number };
44
+
45
+ /**
46
+ * Get top IPs by connection count
47
+ */
48
+ getTopIPs(limit?: number): Array<{ ip: string; connections: number }>;
49
+
50
+ /**
51
+ * Check if an IP has reached the connection limit
52
+ */
53
+ isIPBlocked(ip: string, maxConnectionsPerIP: number): boolean;
54
+ }
@@ -23,6 +23,9 @@ export class RouteConnectionHandler {
23
23
 
24
24
  // Cache for route contexts to avoid recreation
25
25
  private routeContextCache: Map<string, IRouteContext> = new Map();
26
+
27
+ // RxJS Subject for new connections
28
+ public newConnectionSubject = new plugins.smartrx.rxjs.Subject<IConnectionRecord>();
26
29
 
27
30
  constructor(
28
31
  settings: ISmartProxyOptions,
@@ -35,6 +38,7 @@ export class RouteConnectionHandler {
35
38
  ) {
36
39
  this.settings = settings;
37
40
  }
41
+
38
42
 
39
43
  /**
40
44
  * Create a route context object for port and host mapping functions
@@ -110,6 +114,9 @@ export class RouteConnectionHandler {
110
114
  // Connection was rejected due to limit - socket already destroyed by connection manager
111
115
  return;
112
116
  }
117
+
118
+ // Emit new connection event
119
+ this.newConnectionSubject.next(record);
113
120
  const connectionId = record.id;
114
121
 
115
122
  // Apply socket optimizations (apply to underlying socket)
@@ -640,6 +647,9 @@ export class RouteConnectionHandler {
640
647
  ): void {
641
648
  const connectionId = record.id;
642
649
  const action = route.action as IRouteAction;
650
+
651
+ // Store the route config in the connection record for metrics and other uses
652
+ record.routeConfig = route;
643
653
 
644
654
  // Check if this route uses NFTables for forwarding
645
655
  if (action.forwardingEngine === 'nftables') {
@@ -957,6 +967,9 @@ export class RouteConnectionHandler {
957
967
  ): Promise<void> {
958
968
  const connectionId = record.id;
959
969
 
970
+ // Store the route config in the connection record for metrics and other uses
971
+ record.routeConfig = route;
972
+
960
973
  if (!route.action.socketHandler) {
961
974
  logger.log('error', 'socket-handler action missing socketHandler function', {
962
975
  connectionId,
@@ -27,6 +27,10 @@ import { Mutex } from './utils/mutex.js';
27
27
  // Import ACME state manager
28
28
  import { AcmeStateManager } from './acme-state-manager.js';
29
29
 
30
+ // Import metrics collector
31
+ import { MetricsCollector } from './metrics-collector.js';
32
+ import type { IProxyStats } from './models/metrics-types.js';
33
+
30
34
  /**
31
35
  * SmartProxy - Pure route-based API
32
36
  *
@@ -47,13 +51,13 @@ export class SmartProxy extends plugins.EventEmitter {
47
51
  private isShuttingDown: boolean = false;
48
52
 
49
53
  // Component managers
50
- private connectionManager: ConnectionManager;
54
+ public connectionManager: ConnectionManager;
51
55
  private securityManager: SecurityManager;
52
56
  private tlsManager: TlsManager;
53
57
  private httpProxyBridge: HttpProxyBridge;
54
58
  private timeoutManager: TimeoutManager;
55
59
  public routeManager: RouteManager; // Made public for route management
56
- private routeConnectionHandler: RouteConnectionHandler;
60
+ public routeConnectionHandler: RouteConnectionHandler; // Made public for metrics
57
61
  private nftablesManager: NFTablesManager;
58
62
 
59
63
  // Certificate manager for ACME and static certificates
@@ -64,6 +68,9 @@ export class SmartProxy extends plugins.EventEmitter {
64
68
  private routeUpdateLock: any = null; // Will be initialized as AsyncMutex
65
69
  private acmeStateManager: AcmeStateManager;
66
70
 
71
+ // Metrics collector
72
+ private metricsCollector: MetricsCollector;
73
+
67
74
  // Track port usage across route updates
68
75
  private portUsageMap: Map<number, Set<string>> = new Map();
69
76
 
@@ -204,6 +211,9 @@ export class SmartProxy extends plugins.EventEmitter {
204
211
 
205
212
  // Initialize ACME state manager
206
213
  this.acmeStateManager = new AcmeStateManager();
214
+
215
+ // Initialize metrics collector with reference to this SmartProxy instance
216
+ this.metricsCollector = new MetricsCollector(this);
207
217
  }
208
218
 
209
219
  /**
@@ -383,6 +393,9 @@ export class SmartProxy extends plugins.EventEmitter {
383
393
  logger.log('info', 'Starting certificate provisioning now that ports are ready', { component: 'certificate-manager' });
384
394
  await this.certManager.provisionAllCertificates();
385
395
  }
396
+
397
+ // Start the metrics collector now that all components are initialized
398
+ this.metricsCollector.start();
386
399
 
387
400
  // Set up periodic connection logging and inactivity checks
388
401
  this.connectionLogger = setInterval(() => {
@@ -508,6 +521,9 @@ export class SmartProxy extends plugins.EventEmitter {
508
521
 
509
522
  // Clear ACME state manager
510
523
  this.acmeStateManager.clear();
524
+
525
+ // Stop metrics collector
526
+ this.metricsCollector.stop();
511
527
 
512
528
  logger.log('info', 'SmartProxy shutdown complete.');
513
529
  }
@@ -905,6 +921,15 @@ export class SmartProxy extends plugins.EventEmitter {
905
921
  return this.certManager.getCertificateStatus(routeName);
906
922
  }
907
923
 
924
+ /**
925
+ * Get proxy statistics and metrics
926
+ *
927
+ * @returns IProxyStats interface with various metrics methods
928
+ */
929
+ public getStats(): IProxyStats {
930
+ return this.metricsCollector;
931
+ }
932
+
908
933
  /**
909
934
  * Validates if a domain name is valid for certificate issuance
910
935
  */