diff --git a/certs/static-route/meta.json b/certs/static-route/meta.json index 021d8de..c82f080 100644 --- a/certs/static-route/meta.json +++ b/certs/static-route/meta.json @@ -1,5 +1,5 @@ { - "expiryDate": "2025-09-20T22:26:01.547Z", - "issueDate": "2025-06-22T22:26:01.547Z", - "savedAt": "2025-06-22T22:26:01.548Z" + "expiryDate": "2025-09-20T22:46:46.609Z", + "issueDate": "2025-06-22T22:46:46.609Z", + "savedAt": "2025-06-22T22:46:46.610Z" } \ No newline at end of file diff --git a/readme.md b/readme.md index 8100746..05f7640 100644 --- a/readme.md +++ b/readme.md @@ -1576,150 +1576,316 @@ Available helper functions: ## 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 ```typescript -const proxy = new SmartProxy({ /* config */ }); -await proxy.start(); +// Access metrics through the getMetrics() method +const metrics = proxy.getMetrics(); -// Access metrics through the getStats() method -const stats = proxy.getStats(); +// The metrics object provides grouped methods for different categories +``` +### Connection Metrics + +Monitor active connections, total connections, and connection distribution: + +```typescript // Get current active connections -console.log(`Active connections: ${stats.getActiveConnections()}`); +console.log(`Active connections: ${metrics.connections.active()}`); // Get total connections since start -console.log(`Total connections: ${stats.getTotalConnections()}`); - -// 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}`); +console.log(`Total connections: ${metrics.connections.total()}`); // Get connections by route -const routeConnections = stats.getConnectionsByRoute(); +const routeConnections = metrics.connections.byRoute(); for (const [route, count] of routeConnections) { console.log(`Route ${route}: ${count} connections`); } // Get connections by IP address -const ipConnections = stats.getConnectionsByIP(); +const ipConnections = metrics.connections.byIP(); for (const [ip, count] of ipConnections) { 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: - -- `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 +Real-time and historical throughput data with customizable time windows: ```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 -const rate = stats.getThroughputRate(); -console.log(`Incoming: ${rate.bytesInPerSec} bytes/sec`); -console.log(`Outgoing: ${rate.bytesOutPerSec} bytes/sec`); +// Get recent throughput (last 10 seconds average) +const recent = metrics.throughput.recent(); +console.log(`Recent: ${recent.in} bytes/sec in, ${recent.out} bytes/sec out`); -// Get top 10 IPs by connection count -const topIPs = stats.getTopIPs(10); -topIPs.forEach(({ ip, connections }) => { - console.log(`${ip}: ${connections} connections`); +// Get average throughput (last 60 seconds) +const average = metrics.throughput.average(); +console.log(`Average: ${average.in} bytes/sec in, ${average.out} bytes/sec out`); + +// 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 -if (stats.isIPBlocked('192.168.1.100', 100)) { - console.log('IP has too many connections'); -} +// Get throughput by route +const routeThroughput = metrics.throughput.byRoute(60); // Last 60 seconds +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 -// 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(() => { - const stats = proxy.getStats(); + const metrics = proxy.getMetrics(); // Log key metrics console.log({ timestamp: new Date().toISOString(), - activeConnections: stats.getActiveConnections(), - rps: stats.getRequestsPerSecond(), - throughput: stats.getThroughput() + connections: { + active: metrics.connections.active(), + 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 - const ipConnections = stats.getConnectionsByIP(); - for (const [ip, count] of ipConnections) { + // Alert on high connection counts + const topIPs = metrics.connections.topIPs(5); + topIPs.forEach(({ ip, count }) => { if (count > 100) { 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 ``` ### Exporting Metrics -You can export metrics in various formats for external monitoring systems: +Export metrics in various formats for external monitoring systems: ```typescript // Export as JSON app.get('/metrics.json', (req, res) => { - const stats = proxy.getStats(); + const metrics = proxy.getMetrics(); res.json({ - activeConnections: stats.getActiveConnections(), - totalConnections: stats.getTotalConnections(), - requestsPerSecond: stats.getRequestsPerSecond(), - throughput: stats.getThroughput(), - connectionsByRoute: Object.fromEntries(stats.getConnectionsByRoute()), - connectionsByIP: Object.fromEntries(stats.getConnectionsByIP()) + connections: { + active: metrics.connections.active(), + total: metrics.connections.total(), + byRoute: Object.fromEntries(metrics.connections.byRoute()), + byIP: Object.fromEntries(metrics.connections.byIP()) + }, + 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 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.send(` -# HELP smartproxy_active_connections Current active connections -# TYPE smartproxy_active_connections gauge -smartproxy_active_connections ${stats.getActiveConnections()} +# HELP smartproxy_connections_active Current active connections +# TYPE smartproxy_connections_active gauge +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 # 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 -# TYPE smartproxy_bytes_in counter -smartproxy_bytes_in ${stats.getThroughput().bytesIn} - -# HELP smartproxy_bytes_out Total bytes sent -# TYPE smartproxy_bytes_out counter -smartproxy_bytes_out ${stats.getThroughput().bytesOut} +# HELP smartproxy_bytes_total Total bytes transferred +# TYPE smartproxy_bytes_total counter +smartproxy_bytes_total{direction="in"} ${metrics.totals.bytesIn()} +smartproxy_bytes_total{direction="out"} ${metrics.totals.bytesOut()} `); }); ``` +### Metrics API Reference + +The metrics API is organized into logical groups: + +```typescript +interface IMetrics { + connections: { + active(): number; + total(): number; + byRoute(): Map; + byIP(): Map; + 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; + byRoute(windowSeconds?: number): Map; + byIP(windowSeconds?: number): Map; + }; + + 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 While SmartProxy provides a unified API for most needs, you can also use individual components: diff --git a/readme.plan.md b/readme.plan.md index 45884df..ca4eaea 100644 --- a/readme.plan.md +++ b/readme.plan.md @@ -311,11 +311,17 @@ smartproxy_connection_duration_seconds_count 850 - Provide clear migration guide in documentation ### Implementation Approach -1. Create new `ThroughputTracker` class for time-series data -2. Implement new `IMetrics` interface with clean API -3. Replace `MetricsCollector` implementation entirely -4. Update all references to use new API -5. Add comprehensive tests for accuracy validation +1. ✅ Create new `ThroughputTracker` class for time-series data +2. ✅ Implement new `IMetrics` interface with clean API +3. ✅ Replace `MetricsCollector` implementation entirely +4. ✅ Update all references to use new API +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 diff --git a/test/test.cleanup-queue-bug.node.ts b/test/test.cleanup-queue-bug.node.ts index d9fc896..9bd40a8 100644 --- a/test/test.cleanup-queue-bug.node.ts +++ b/test/test.cleanup-queue-bug.node.ts @@ -30,10 +30,27 @@ tap.test('cleanup queue bug - verify queue processing handles more than batch si const mockConnections: any[] = []; 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 = { id: `mock-${i}`, - incoming: { destroyed: true, remoteAddress: '127.0.0.1' }, - outgoing: { destroyed: true }, + incoming: mockIncoming, + outgoing: mockOutgoing, connectionClosed: false, incomingStartTime: 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 console.log('\n--- Waiting for cleanup batches to process ---'); - // The first batch should process immediately (100 connections) - // Then additional batches should be scheduled - await new Promise(resolve => setTimeout(resolve, 500)); + // The cleanup happens in batches, wait for all to complete + let waitTime = 0; + 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 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); // Cleanup + console.log('\n--- Stopping proxy ---'); await proxy.stop(); console.log('\n✓ Test complete: Cleanup queue now correctly processes all connections'); diff --git a/ts/proxies/smart-proxy/models/interfaces.ts b/ts/proxies/smart-proxy/models/interfaces.ts index c395d2d..77a0f08 100644 --- a/ts/proxies/smart-proxy/models/interfaces.ts +++ b/ts/proxies/smart-proxy/models/interfaces.ts @@ -105,6 +105,13 @@ export interface ISmartProxyOptions { useHttpProxy?: number[]; // Array of ports to forward to HttpProxy httpProxyPort?: number; // Port where HttpProxy is listening (default: 8443) + // Metrics configuration + metrics?: { + enabled?: boolean; + sampleIntervalMs?: number; + retentionSeconds?: number; + }; + /** * Global ACME configuration options for SmartProxy * diff --git a/ts/proxies/smart-proxy/smart-proxy.ts b/ts/proxies/smart-proxy/smart-proxy.ts index f1c11ef..54c2d44 100644 --- a/ts/proxies/smart-proxy/smart-proxy.ts +++ b/ts/proxies/smart-proxy/smart-proxy.ts @@ -201,7 +201,10 @@ export class SmartProxy extends plugins.EventEmitter { this.acmeStateManager = new AcmeStateManager(); // 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 + }); } /**