fix(tests): fix tests

This commit is contained in:
Juergen Kunz
2025-06-22 23:10:56 +00:00
parent 131a454b28
commit e5ec48abd3
6 changed files with 298 additions and 90 deletions

View File

@ -1,5 +1,5 @@
{ {
"expiryDate": "2025-09-20T22:26:01.547Z", "expiryDate": "2025-09-20T22:46:46.609Z",
"issueDate": "2025-06-22T22:26:01.547Z", "issueDate": "2025-06-22T22:46:46.609Z",
"savedAt": "2025-06-22T22:26:01.548Z" "savedAt": "2025-06-22T22:46:46.610Z"
} }

318
readme.md
View File

@ -1576,150 +1576,316 @@ Available helper functions:
## Metrics and Monitoring ## Metrics and Monitoring
SmartProxy includes a comprehensive metrics collection system that provides real-time insights into proxy performance, connection statistics, and throughput data. SmartProxy includes a comprehensive metrics collection system that provides real-time insights into proxy performance, connection statistics, and throughput data. The metrics system uses a clean, grouped API design for intuitive access to different metric categories.
### Enabling Metrics
```typescript
const proxy = new SmartProxy({
// Enable metrics collection
metrics: {
enabled: true,
sampleIntervalMs: 1000, // Sample throughput every second
retentionSeconds: 3600 // Keep 1 hour of history
},
routes: [/* your routes */]
});
await proxy.start();
```
### Getting Metrics ### Getting Metrics
```typescript ```typescript
const proxy = new SmartProxy({ /* config */ }); // Access metrics through the getMetrics() method
await proxy.start(); const metrics = proxy.getMetrics();
// Access metrics through the getStats() method // The metrics object provides grouped methods for different categories
const stats = proxy.getStats(); ```
### Connection Metrics
Monitor active connections, total connections, and connection distribution:
```typescript
// Get current active connections // Get current active connections
console.log(`Active connections: ${stats.getActiveConnections()}`); console.log(`Active connections: ${metrics.connections.active()}`);
// Get total connections since start // Get total connections since start
console.log(`Total connections: ${stats.getTotalConnections()}`); console.log(`Total connections: ${metrics.connections.total()}`);
// Get requests per second (RPS)
console.log(`Current RPS: ${stats.getRequestsPerSecond()}`);
// Get throughput data
const throughput = stats.getThroughput();
console.log(`Bytes received: ${throughput.bytesIn}`);
console.log(`Bytes sent: ${throughput.bytesOut}`);
// Get connections by route // Get connections by route
const routeConnections = stats.getConnectionsByRoute(); const routeConnections = metrics.connections.byRoute();
for (const [route, count] of routeConnections) { for (const [route, count] of routeConnections) {
console.log(`Route ${route}: ${count} connections`); console.log(`Route ${route}: ${count} connections`);
} }
// Get connections by IP address // Get connections by IP address
const ipConnections = stats.getConnectionsByIP(); const ipConnections = metrics.connections.byIP();
for (const [ip, count] of ipConnections) { for (const [ip, count] of ipConnections) {
console.log(`IP ${ip}: ${count} connections`); console.log(`IP ${ip}: ${count} connections`);
} }
// Get top IPs by connection count
const topIPs = metrics.connections.topIPs(10);
topIPs.forEach(({ ip, count }) => {
console.log(`${ip}: ${count} connections`);
});
``` ```
### Available Metrics ### Throughput Metrics
The `IProxyStats` interface provides the following methods: Real-time and historical throughput data with customizable time windows:
- `getActiveConnections()`: Current number of active connections
- `getTotalConnections()`: Total connections handled since proxy start
- `getRequestsPerSecond()`: Current requests per second (1-minute average)
- `getThroughput()`: Total bytes transferred (in/out)
- `getConnectionsByRoute()`: Connection count per route
- `getConnectionsByIP()`: Connection count per client IP
Additional extended methods available:
- `getThroughputRate()`: Bytes per second rate for the last minute
- `getTopIPs(limit?: number)`: Get top IPs by connection count
- `isIPBlocked(ip: string, maxConnectionsPerIP: number)`: Check if an IP has reached the connection limit
### Extended Metrics Example
```typescript ```typescript
const stats = proxy.getStats() as any; // Extended methods are available // Get instant throughput (last 1 second)
const instant = metrics.throughput.instant();
console.log(`Current: ${instant.in} bytes/sec in, ${instant.out} bytes/sec out`);
// Get throughput rate // Get recent throughput (last 10 seconds average)
const rate = stats.getThroughputRate(); const recent = metrics.throughput.recent();
console.log(`Incoming: ${rate.bytesInPerSec} bytes/sec`); console.log(`Recent: ${recent.in} bytes/sec in, ${recent.out} bytes/sec out`);
console.log(`Outgoing: ${rate.bytesOutPerSec} bytes/sec`);
// Get top 10 IPs by connection count // Get average throughput (last 60 seconds)
const topIPs = stats.getTopIPs(10); const average = metrics.throughput.average();
topIPs.forEach(({ ip, connections }) => { console.log(`Average: ${average.in} bytes/sec in, ${average.out} bytes/sec out`);
console.log(`${ip}: ${connections} connections`);
// Get custom time window (e.g., last 5 minutes)
const custom = metrics.throughput.custom(300);
console.log(`5-min avg: ${custom.in} bytes/sec in, ${custom.out} bytes/sec out`);
// Get throughput history for graphing
const history = metrics.throughput.history(300); // Last 5 minutes
history.forEach(point => {
console.log(`${new Date(point.timestamp)}: ${point.in} in, ${point.out} out`);
}); });
// Check if an IP should be rate limited // Get throughput by route
if (stats.isIPBlocked('192.168.1.100', 100)) { const routeThroughput = metrics.throughput.byRoute(60); // Last 60 seconds
console.log('IP has too many connections'); routeThroughput.forEach((stats, route) => {
} console.log(`Route ${route}: ${stats.in} in, ${stats.out} out bytes/sec`);
});
// Get throughput by IP
const ipThroughput = metrics.throughput.byIP(60);
ipThroughput.forEach((stats, ip) => {
console.log(`IP ${ip}: ${stats.in} in, ${stats.out} out bytes/sec`);
});
``` ```
### Monitoring Example ### Request Metrics
Track request rates:
```typescript ```typescript
// Create a monitoring loop // Get requests per second
console.log(`RPS: ${metrics.requests.perSecond()}`);
// Get requests per minute
console.log(`RPM: ${metrics.requests.perMinute()}`);
// Get total requests
console.log(`Total requests: ${metrics.requests.total()}`);
```
### Cumulative Totals
Track total bytes transferred and connections:
```typescript
// Get total bytes
console.log(`Total bytes in: ${metrics.totals.bytesIn()}`);
console.log(`Total bytes out: ${metrics.totals.bytesOut()}`);
console.log(`Total connections: ${metrics.totals.connections()}`);
```
### Performance Percentiles
Get percentile statistics (when implemented):
```typescript
// Connection duration percentiles
const durations = metrics.percentiles.connectionDuration();
console.log(`Connection durations - P50: ${durations.p50}ms, P95: ${durations.p95}ms, P99: ${durations.p99}ms`);
// Bytes transferred percentiles
const bytes = metrics.percentiles.bytesTransferred();
console.log(`Bytes in - P50: ${bytes.in.p50}, P95: ${bytes.in.p95}, P99: ${bytes.in.p99}`);
console.log(`Bytes out - P50: ${bytes.out.p50}, P95: ${bytes.out.p95}, P99: ${bytes.out.p99}`);
```
### Complete Monitoring Example
```typescript
// Create a monitoring dashboard
setInterval(() => { setInterval(() => {
const stats = proxy.getStats(); const metrics = proxy.getMetrics();
// Log key metrics // Log key metrics
console.log({ console.log({
timestamp: new Date().toISOString(), timestamp: new Date().toISOString(),
activeConnections: stats.getActiveConnections(), connections: {
rps: stats.getRequestsPerSecond(), active: metrics.connections.active(),
throughput: stats.getThroughput() total: metrics.connections.total()
},
throughput: {
instant: metrics.throughput.instant(),
average: metrics.throughput.average()
},
requests: {
rps: metrics.requests.perSecond(),
total: metrics.requests.total()
},
totals: {
bytesIn: metrics.totals.bytesIn(),
bytesOut: metrics.totals.bytesOut()
}
}); });
// Check for high connection counts from specific IPs // Alert on high connection counts
const ipConnections = stats.getConnectionsByIP(); const topIPs = metrics.connections.topIPs(5);
for (const [ip, count] of ipConnections) { topIPs.forEach(({ ip, count }) => {
if (count > 100) { if (count > 100) {
console.warn(`High connection count from ${ip}: ${count}`); console.warn(`High connection count from ${ip}: ${count}`);
} }
});
// Alert on high throughput
const instant = metrics.throughput.instant();
if (instant.in > 100_000_000) { // 100 MB/s
console.warn(`High incoming throughput: ${instant.in} bytes/sec`);
} }
}, 10000); // Every 10 seconds }, 10000); // Every 10 seconds
``` ```
### Exporting Metrics ### Exporting Metrics
You can export metrics in various formats for external monitoring systems: Export metrics in various formats for external monitoring systems:
```typescript ```typescript
// Export as JSON // Export as JSON
app.get('/metrics.json', (req, res) => { app.get('/metrics.json', (req, res) => {
const stats = proxy.getStats(); const metrics = proxy.getMetrics();
res.json({ res.json({
activeConnections: stats.getActiveConnections(), connections: {
totalConnections: stats.getTotalConnections(), active: metrics.connections.active(),
requestsPerSecond: stats.getRequestsPerSecond(), total: metrics.connections.total(),
throughput: stats.getThroughput(), byRoute: Object.fromEntries(metrics.connections.byRoute()),
connectionsByRoute: Object.fromEntries(stats.getConnectionsByRoute()), byIP: Object.fromEntries(metrics.connections.byIP())
connectionsByIP: Object.fromEntries(stats.getConnectionsByIP()) },
throughput: {
instant: metrics.throughput.instant(),
recent: metrics.throughput.recent(),
average: metrics.throughput.average()
},
requests: {
perSecond: metrics.requests.perSecond(),
perMinute: metrics.requests.perMinute(),
total: metrics.requests.total()
},
totals: {
bytesIn: metrics.totals.bytesIn(),
bytesOut: metrics.totals.bytesOut(),
connections: metrics.totals.connections()
}
}); });
}); });
// Export as Prometheus format // Export as Prometheus format
app.get('/metrics', (req, res) => { app.get('/metrics', (req, res) => {
const stats = proxy.getStats(); const metrics = proxy.getMetrics();
const instant = metrics.throughput.instant();
res.set('Content-Type', 'text/plain'); res.set('Content-Type', 'text/plain');
res.send(` res.send(`
# HELP smartproxy_active_connections Current active connections # HELP smartproxy_connections_active Current active connections
# TYPE smartproxy_active_connections gauge # TYPE smartproxy_connections_active gauge
smartproxy_active_connections ${stats.getActiveConnections()} smartproxy_connections_active ${metrics.connections.active()}
# HELP smartproxy_connections_total Total connections since start
# TYPE smartproxy_connections_total counter
smartproxy_connections_total ${metrics.connections.total()}
# HELP smartproxy_throughput_bytes_per_second Current throughput in bytes per second
# TYPE smartproxy_throughput_bytes_per_second gauge
smartproxy_throughput_bytes_per_second{direction="in"} ${instant.in}
smartproxy_throughput_bytes_per_second{direction="out"} ${instant.out}
# HELP smartproxy_requests_per_second Current requests per second # HELP smartproxy_requests_per_second Current requests per second
# TYPE smartproxy_requests_per_second gauge # TYPE smartproxy_requests_per_second gauge
smartproxy_requests_per_second ${stats.getRequestsPerSecond()} smartproxy_requests_per_second ${metrics.requests.perSecond()}
# HELP smartproxy_bytes_in Total bytes received # HELP smartproxy_bytes_total Total bytes transferred
# TYPE smartproxy_bytes_in counter # TYPE smartproxy_bytes_total counter
smartproxy_bytes_in ${stats.getThroughput().bytesIn} smartproxy_bytes_total{direction="in"} ${metrics.totals.bytesIn()}
smartproxy_bytes_total{direction="out"} ${metrics.totals.bytesOut()}
# HELP smartproxy_bytes_out Total bytes sent
# TYPE smartproxy_bytes_out counter
smartproxy_bytes_out ${stats.getThroughput().bytesOut}
`); `);
}); });
``` ```
### Metrics API Reference
The metrics API is organized into logical groups:
```typescript
interface IMetrics {
connections: {
active(): number;
total(): number;
byRoute(): Map<string, number>;
byIP(): Map<string, number>;
topIPs(limit?: number): Array<{ ip: string; count: number }>;
};
throughput: {
instant(): IThroughputData; // Last 1 second
recent(): IThroughputData; // Last 10 seconds
average(): IThroughputData; // Last 60 seconds
custom(seconds: number): IThroughputData;
history(seconds: number): Array<IThroughputHistoryPoint>;
byRoute(windowSeconds?: number): Map<string, IThroughputData>;
byIP(windowSeconds?: number): Map<string, IThroughputData>;
};
requests: {
perSecond(): number;
perMinute(): number;
total(): number;
};
totals: {
bytesIn(): number;
bytesOut(): number;
connections(): number;
};
percentiles: {
connectionDuration(): { p50: number; p95: number; p99: number };
bytesTransferred(): {
in: { p50: number; p95: number; p99: number };
out: { p50: number; p95: number; p99: number };
};
};
}
```
Where `IThroughputData` is:
```typescript
interface IThroughputData {
in: number; // Bytes per second incoming
out: number; // Bytes per second outgoing
}
```
And `IThroughputHistoryPoint` is:
```typescript
interface IThroughputHistoryPoint {
timestamp: number; // Unix timestamp in milliseconds
in: number; // Bytes per second at this point
out: number; // Bytes per second at this point
}
```
## Other Components ## Other Components
While SmartProxy provides a unified API for most needs, you can also use individual components: While SmartProxy provides a unified API for most needs, you can also use individual components:

View File

@ -311,11 +311,17 @@ smartproxy_connection_duration_seconds_count 850
- Provide clear migration guide in documentation - Provide clear migration guide in documentation
### Implementation Approach ### Implementation Approach
1. Create new `ThroughputTracker` class for time-series data 1. Create new `ThroughputTracker` class for time-series data
2. Implement new `IMetrics` interface with clean API 2. Implement new `IMetrics` interface with clean API
3. Replace `MetricsCollector` implementation entirely 3. Replace `MetricsCollector` implementation entirely
4. Update all references to use new API 4. Update all references to use new API
5. Add comprehensive tests for accuracy validation 5. ⚠️ Add comprehensive tests for accuracy validation (partial)
### Additional Refactoring Completed
- Refactored all SmartProxy components to use cleaner dependency pattern
- Components now receive only `SmartProxy` instance instead of individual dependencies
- Access to other components via `this.smartProxy.componentName`
- Significantly simplified constructor signatures across the codebase
## 9. Success Metrics ## 9. Success Metrics

View File

@ -30,10 +30,27 @@ tap.test('cleanup queue bug - verify queue processing handles more than batch si
const mockConnections: any[] = []; const mockConnections: any[] = [];
for (let i = 0; i < 150; i++) { for (let i = 0; i < 150; i++) {
// Create mock socket objects with necessary methods
const mockIncoming = {
destroyed: true,
writable: false,
remoteAddress: '127.0.0.1',
removeAllListeners: () => {},
destroy: () => {},
end: () => {}
};
const mockOutgoing = {
destroyed: true,
writable: false,
destroy: () => {},
end: () => {}
};
const mockRecord = { const mockRecord = {
id: `mock-${i}`, id: `mock-${i}`,
incoming: { destroyed: true, remoteAddress: '127.0.0.1' }, incoming: mockIncoming,
outgoing: { destroyed: true }, outgoing: mockOutgoing,
connectionClosed: false, connectionClosed: false,
incomingStartTime: Date.now(), incomingStartTime: Date.now(),
lastActivity: Date.now(), lastActivity: Date.now(),
@ -66,9 +83,17 @@ tap.test('cleanup queue bug - verify queue processing handles more than batch si
// Wait for cleanup to complete // Wait for cleanup to complete
console.log('\n--- Waiting for cleanup batches to process ---'); console.log('\n--- Waiting for cleanup batches to process ---');
// The first batch should process immediately (100 connections) // The cleanup happens in batches, wait for all to complete
// Then additional batches should be scheduled let waitTime = 0;
await new Promise(resolve => setTimeout(resolve, 500)); while (cm.getConnectionCount() > 0 || cm.cleanupQueue.size > 0) {
await new Promise(resolve => setTimeout(resolve, 100));
waitTime += 100;
if (waitTime > 5000) {
console.log('Timeout waiting for cleanup to complete');
break;
}
}
console.log(`Cleanup completed in ${waitTime}ms`);
// Check final state // Check final state
const finalCount = cm.getConnectionCount(); const finalCount = cm.getConnectionCount();
@ -85,6 +110,7 @@ tap.test('cleanup queue bug - verify queue processing handles more than batch si
expect(stats.incoming.test_cleanup).toEqual(150); expect(stats.incoming.test_cleanup).toEqual(150);
// Cleanup // Cleanup
console.log('\n--- Stopping proxy ---');
await proxy.stop(); await proxy.stop();
console.log('\n✓ Test complete: Cleanup queue now correctly processes all connections'); console.log('\n✓ Test complete: Cleanup queue now correctly processes all connections');

View File

@ -105,6 +105,13 @@ export interface ISmartProxyOptions {
useHttpProxy?: number[]; // Array of ports to forward to HttpProxy useHttpProxy?: number[]; // Array of ports to forward to HttpProxy
httpProxyPort?: number; // Port where HttpProxy is listening (default: 8443) httpProxyPort?: number; // Port where HttpProxy is listening (default: 8443)
// Metrics configuration
metrics?: {
enabled?: boolean;
sampleIntervalMs?: number;
retentionSeconds?: number;
};
/** /**
* Global ACME configuration options for SmartProxy * Global ACME configuration options for SmartProxy
* *

View File

@ -201,7 +201,10 @@ export class SmartProxy extends plugins.EventEmitter {
this.acmeStateManager = new AcmeStateManager(); this.acmeStateManager = new AcmeStateManager();
// Initialize metrics collector with reference to this SmartProxy instance // Initialize metrics collector with reference to this SmartProxy instance
this.metricsCollector = new MetricsCollector(this); this.metricsCollector = new MetricsCollector(this, {
sampleIntervalMs: this.settings.metrics?.sampleIntervalMs,
retentionSeconds: this.settings.metrics?.retentionSeconds
});
} }
/** /**