@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.
- package/dist_ts/core/models/wrapped-socket.d.ts +4 -0
- package/dist_ts/core/models/wrapped-socket.js +18 -1
- package/dist_ts/core/routing/matchers/path.js +3 -2
- package/dist_ts/proxies/smart-proxy/connection-manager.d.ts +4 -7
- package/dist_ts/proxies/smart-proxy/connection-manager.js +22 -22
- package/dist_ts/proxies/smart-proxy/http-proxy-bridge.d.ts +4 -3
- package/dist_ts/proxies/smart-proxy/http-proxy-bridge.js +9 -9
- package/dist_ts/proxies/smart-proxy/metrics-collector.d.ts +68 -56
- package/dist_ts/proxies/smart-proxy/metrics-collector.js +252 -176
- package/dist_ts/proxies/smart-proxy/models/interfaces.d.ts +6 -1
- package/dist_ts/proxies/smart-proxy/models/metrics-types.d.ts +99 -47
- package/dist_ts/proxies/smart-proxy/nftables-manager.d.ts +4 -4
- package/dist_ts/proxies/smart-proxy/nftables-manager.js +6 -6
- package/dist_ts/proxies/smart-proxy/port-manager.d.ts +4 -7
- package/dist_ts/proxies/smart-proxy/port-manager.js +6 -9
- package/dist_ts/proxies/smart-proxy/route-connection-handler.d.ts +4 -15
- package/dist_ts/proxies/smart-proxy/route-connection-handler.js +133 -130
- package/dist_ts/proxies/smart-proxy/security-manager.d.ts +3 -3
- package/dist_ts/proxies/smart-proxy/security-manager.js +9 -9
- package/dist_ts/proxies/smart-proxy/smart-proxy.d.ts +20 -13
- package/dist_ts/proxies/smart-proxy/smart-proxy.js +16 -13
- package/dist_ts/proxies/smart-proxy/throughput-tracker.d.ts +36 -0
- package/dist_ts/proxies/smart-proxy/throughput-tracker.js +117 -0
- package/dist_ts/proxies/smart-proxy/timeout-manager.d.ts +5 -4
- package/dist_ts/proxies/smart-proxy/timeout-manager.js +20 -16
- package/dist_ts/proxies/smart-proxy/tls-manager.d.ts +3 -3
- package/dist_ts/proxies/smart-proxy/tls-manager.js +12 -12
- package/dist_ts/routing/router/http-router.js +2 -2
- package/package.json +1 -1
- package/readme.hints.md +0 -0
- package/readme.md +239 -73
- package/readme.plan.md +364 -0
- package/ts/core/models/wrapped-socket.ts +18 -0
- package/ts/core/routing/matchers/path.ts +2 -1
- package/ts/proxies/smart-proxy/connection-manager.ts +23 -21
- package/ts/proxies/smart-proxy/http-proxy-bridge.ts +9 -8
- package/ts/proxies/smart-proxy/metrics-collector.ts +305 -188
- package/ts/proxies/smart-proxy/models/interfaces.ts +8 -1
- package/ts/proxies/smart-proxy/models/metrics-types.ts +99 -41
- package/ts/proxies/smart-proxy/nftables-manager.ts +5 -5
- package/ts/proxies/smart-proxy/port-manager.ts +6 -14
- package/ts/proxies/smart-proxy/route-connection-handler.ts +141 -138
- package/ts/proxies/smart-proxy/security-manager.ts +8 -8
- package/ts/proxies/smart-proxy/smart-proxy.ts +26 -35
- package/ts/proxies/smart-proxy/throughput-tracker.ts +144 -0
- package/ts/proxies/smart-proxy/timeout-manager.ts +22 -16
- package/ts/proxies/smart-proxy/tls-manager.ts +11 -11
- 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 {
|
|
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
|
|
13
|
+
* Collects and provides metrics for SmartProxy with clean API
|
|
8
14
|
*/
|
|
9
|
-
export class MetricsCollector implements
|
|
10
|
-
//
|
|
11
|
-
private
|
|
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
|
-
//
|
|
16
|
-
private
|
|
17
|
-
|
|
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
|
-
|
|
23
|
+
// Connection byte tracking for per-route/IP metrics
|
|
24
|
+
private connectionByteTrackers = new Map<string, IByteTracker>();
|
|
23
25
|
|
|
24
|
-
//
|
|
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
|
-
|
|
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
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
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
|
-
|
|
53
|
-
|
|
54
|
-
|
|
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
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
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
|
-
|
|
64
|
-
|
|
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
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
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
|
-
|
|
82
|
-
|
|
83
|
-
}
|
|
88
|
+
return ipCounts;
|
|
89
|
+
},
|
|
84
90
|
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
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
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
100
|
+
// Throughput metrics implementation
|
|
101
|
+
public throughput = {
|
|
102
|
+
instant: (): IThroughputData => {
|
|
103
|
+
return this.throughputTracker.getRate(1);
|
|
104
|
+
},
|
|
96
105
|
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
return new Map(this.cachedMetrics.connectionsByIP);
|
|
101
|
-
}
|
|
106
|
+
recent: (): IThroughputData => {
|
|
107
|
+
return this.throughputTracker.getRate(10);
|
|
108
|
+
},
|
|
102
109
|
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
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
|
-
|
|
112
|
-
|
|
113
|
-
|
|
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
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
}
|
|
118
|
+
history: (seconds: number): Array<IThroughputHistoryPoint> => {
|
|
119
|
+
return this.throughputTracker.getHistory(seconds);
|
|
120
|
+
},
|
|
129
121
|
|
|
130
|
-
|
|
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
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
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
|
-
|
|
141
|
-
|
|
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
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
}
|
|
224
|
+
total: (): number => {
|
|
225
|
+
return this.totalRequests;
|
|
226
|
+
}
|
|
227
|
+
};
|
|
147
228
|
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
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
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
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
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
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
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
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
|
-
*
|
|
282
|
+
* Record a new request
|
|
181
283
|
*/
|
|
182
|
-
public
|
|
284
|
+
public recordRequest(connectionId: string, routeName: string, remoteIP: string): void {
|
|
183
285
|
const now = Date.now();
|
|
184
|
-
|
|
185
|
-
|
|
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
|
-
//
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
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
|
-
*
|
|
314
|
+
* Record bytes transferred for a connection
|
|
209
315
|
*/
|
|
210
|
-
public
|
|
211
|
-
|
|
212
|
-
|
|
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
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
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
|
|
330
|
+
* Clean up tracking for a closed connection
|
|
231
331
|
*/
|
|
232
|
-
|
|
233
|
-
|
|
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
|
|
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
|
-
//
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
|
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
|