diff --git a/ts/monitoring/classes.metricsmanager.ts b/ts/monitoring/classes.metricsmanager.ts index 6820ff7..597797c 100644 --- a/ts/monitoring/classes.metricsmanager.ts +++ b/ts/monitoring/classes.metricsmanager.ts @@ -484,40 +484,58 @@ export class MetricsManager { // Use shorter cache TTL for network stats to ensure real-time updates return this.metricsCache.get('networkStats', () => { const proxyMetrics = this.dcRouter.smartProxy ? this.dcRouter.smartProxy.getMetrics() : null; - + if (!proxyMetrics) { return { connectionsByIP: new Map(), throughputRate: { bytesInPerSecond: 0, bytesOutPerSecond: 0 }, - topIPs: [], + topIPs: [] as Array<{ ip: string; count: number }>, totalDataTransferred: { bytesIn: 0, bytesOut: 0 }, + throughputHistory: [] as Array<{ timestamp: number; in: number; out: number }>, + throughputByIP: new Map(), + requestsPerSecond: 0, + requestsTotal: 0, }; } - + // Get metrics using the new API const connectionsByIP = proxyMetrics.connections.byIP(); const instantThroughput = proxyMetrics.throughput.instant(); - + // Get throughput rate const throughputRate = { bytesInPerSecond: instantThroughput.in, bytesOutPerSecond: instantThroughput.out }; - + // Get top IPs const topIPs = proxyMetrics.connections.topIPs(10); - + // Get total data transferred const totalDataTransferred = { bytesIn: proxyMetrics.totals.bytesIn(), bytesOut: proxyMetrics.totals.bytesOut() }; - + + // Get throughput history from Rust engine (up to 300 seconds) + const throughputHistory = proxyMetrics.throughput.history(300); + + // Get per-IP throughput + const throughputByIP = proxyMetrics.throughput.byIP(); + + // Get HTTP request rates + const requestsPerSecond = proxyMetrics.requests.perSecond(); + const requestsTotal = proxyMetrics.requests.total(); + return { connectionsByIP, throughputRate, topIPs, totalDataTransferred, + throughputHistory, + throughputByIP, + requestsPerSecond, + requestsTotal, }; }, 200); // Use 200ms cache for more frequent updates } diff --git a/ts/opsserver/handlers/security.handler.ts b/ts/opsserver/handlers/security.handler.ts index 48f1844..0c20177 100644 --- a/ts/opsserver/handlers/security.handler.ts +++ b/ts/opsserver/handlers/security.handler.ts @@ -84,21 +84,37 @@ export class SecurityHandler { // Get network stats from MetricsManager if available if (this.opsServerRef.dcRouterRef.metricsManager) { const networkStats = await this.opsServerRef.dcRouterRef.metricsManager.getNetworkStats(); - + + // Convert per-IP throughput Map to serializable array + const throughputByIP: Array<{ ip: string; in: number; out: number }> = []; + if (networkStats.throughputByIP) { + for (const [ip, tp] of networkStats.throughputByIP) { + throughputByIP.push({ ip, in: tp.in, out: tp.out }); + } + } + return { connectionsByIP: Array.from(networkStats.connectionsByIP.entries()).map(([ip, count]) => ({ ip, count })), throughputRate: networkStats.throughputRate, topIPs: networkStats.topIPs, totalDataTransferred: networkStats.totalDataTransferred, + throughputHistory: networkStats.throughputHistory || [], + throughputByIP, + requestsPerSecond: networkStats.requestsPerSecond || 0, + requestsTotal: networkStats.requestsTotal || 0, }; } - + // Fallback if MetricsManager not available return { connectionsByIP: [], throughputRate: { bytesInPerSecond: 0, bytesOutPerSecond: 0 }, topIPs: [], totalDataTransferred: { bytesIn: 0, bytesOut: 0 }, + throughputHistory: [], + throughputByIP: [], + requestsPerSecond: 0, + requestsTotal: 0, }; } ) diff --git a/ts/opsserver/handlers/stats.handler.ts b/ts/opsserver/handlers/stats.handler.ts index de15e71..0cb424c 100644 --- a/ts/opsserver/handlers/stats.handler.ts +++ b/ts/opsserver/handlers/stats.handler.ts @@ -255,6 +255,14 @@ export class StatsHandler { const stats = await this.opsServerRef.dcRouterRef.metricsManager.getNetworkStats(); const serverStats = await this.collectServerStats(); + // Build per-IP bandwidth lookup from throughputByIP + const ipBandwidth = new Map(); + if (stats.throughputByIP) { + for (const [ip, tp] of stats.throughputByIP) { + ipBandwidth.set(ip, { in: tp.in, out: tp.out }); + } + } + metrics.network = { totalBandwidth: { in: stats.throughputRate.bytesInPerSecond, @@ -269,11 +277,11 @@ export class StatsHandler { topEndpoints: stats.topIPs.map(ip => ({ endpoint: ip.ip, requests: ip.count, - bandwidth: { - in: 0, - out: 0, - }, + bandwidth: ipBandwidth.get(ip.ip) || { in: 0, out: 0 }, })), + throughputHistory: stats.throughputHistory || [], + requestsPerSecond: stats.requestsPerSecond || 0, + requestsTotal: stats.requestsTotal || 0, }; })() ); diff --git a/ts_interfaces/data/stats.ts b/ts_interfaces/data/stats.ts index a8c53cb..2be4949 100644 --- a/ts_interfaces/data/stats.ts +++ b/ts_interfaces/data/stats.ts @@ -130,6 +130,9 @@ export interface INetworkMetrics { out: number; }; }>; + throughputHistory?: Array<{ timestamp: number; in: number; out: number }>; + requestsPerSecond?: number; + requestsTotal?: number; } export interface IConnectionDetails { diff --git a/ts_web/appstate.ts b/ts_web/appstate.ts index 5ed0d51..2703cac 100644 --- a/ts_web/appstate.ts +++ b/ts_web/appstate.ts @@ -49,6 +49,10 @@ export interface INetworkState { throughputRate: { bytesInPerSecond: number; bytesOutPerSecond: number }; totalBytes: { in: number; out: number }; topIPs: Array<{ ip: string; count: number }>; + throughputByIP: Array<{ ip: string; in: number; out: number }>; + throughputHistory: Array<{ timestamp: number; in: number; out: number }>; + requestsPerSecond: number; + requestsTotal: number; lastUpdated: number; isLoading: boolean; error: string | null; @@ -147,6 +151,10 @@ export const networkStatePart = await appState.getStatePart( throughputRate: { bytesInPerSecond: 0, bytesOutPerSecond: 0 }, totalBytes: { in: 0, out: 0 }, topIPs: [], + throughputByIP: [], + throughputHistory: [], + requestsPerSecond: 0, + requestsTotal: 0, lastUpdated: 0, isLoading: false, error: null, @@ -427,6 +435,10 @@ export const fetchNetworkStatsAction = networkStatePart.createAction(async (stat ? { in: networkStatsResponse.totalDataTransferred.bytesIn, out: networkStatsResponse.totalDataTransferred.bytesOut } : { in: 0, out: 0 }, topIPs: networkStatsResponse.topIPs || [], + throughputByIP: networkStatsResponse.throughputByIP || [], + throughputHistory: networkStatsResponse.throughputHistory || [], + requestsPerSecond: networkStatsResponse.requestsPerSecond || 0, + requestsTotal: networkStatsResponse.requestsTotal || 0, lastUpdated: Date.now(), isLoading: false, error: null, @@ -797,6 +809,10 @@ async function dispatchCombinedRefreshAction() { }, totalBytes: network.totalBytes || { in: 0, out: 0 }, topIPs: network.topEndpoints.map(e => ({ ip: e.endpoint, count: e.requests })), + throughputByIP: network.topEndpoints.map(e => ({ ip: e.endpoint, in: e.bandwidth?.in || 0, out: e.bandwidth?.out || 0 })), + throughputHistory: network.throughputHistory || [], + requestsPerSecond: network.requestsPerSecond || 0, + requestsTotal: network.requestsTotal || 0, lastUpdated: Date.now(), isLoading: false, error: null, @@ -813,6 +829,10 @@ async function dispatchCombinedRefreshAction() { }, totalBytes: network.totalBytes || { in: 0, out: 0 }, topIPs: network.topEndpoints.map(e => ({ ip: e.endpoint, count: e.requests })), + throughputByIP: network.topEndpoints.map(e => ({ ip: e.endpoint, in: e.bandwidth?.in || 0, out: e.bandwidth?.out || 0 })), + throughputHistory: network.throughputHistory || [], + requestsPerSecond: network.requestsPerSecond || 0, + requestsTotal: network.requestsTotal || 0, lastUpdated: Date.now(), isLoading: false, error: null, diff --git a/ts_web/elements/ops-view-network.ts b/ts_web/elements/ops-view-network.ts index 9e507a5..2e9cf3a 100644 --- a/ts_web/elements/ops-view-network.ts +++ b/ts_web/elements/ops-view-network.ts @@ -52,8 +52,7 @@ export class OpsViewNetwork extends DeesElement { private requestCountHistory = new Map(); // Track requests per time bucket private trafficUpdateTimer: any = null; private requestsPerSecHistory: number[] = []; // Track requests/sec over time for trend - - // Removed byte tracking - now using real-time data from SmartProxy + private historyLoaded = false; // Whether server-side throughput history has been loaded constructor() { super(); @@ -95,7 +94,7 @@ export class OpsViewNetwork extends DeesElement { // Fixed 5 minute time range const range = 5 * 60 * 1000; // 5 minutes const bucketSize = range / 60; // 60 data points - + // Initialize with empty data points for both in and out const emptyData = Array.from({ length: 60 }, (_, i) => { const time = now - ((59 - i) * bucketSize); @@ -104,13 +103,61 @@ export class OpsViewNetwork extends DeesElement { y: 0, }; }); - + this.trafficDataIn = [...emptyData]; this.trafficDataOut = emptyData.map(point => ({ ...point })); - + this.lastTrafficUpdateTime = now; } + /** + * Load server-side throughput history into the chart. + * Called once when history data first arrives from the Rust engine. + * This pre-populates the chart so users see historical data immediately + * instead of starting from all zeros. + */ + private loadThroughputHistory() { + const history = this.networkState.throughputHistory; + if (!history || history.length === 0) return; + + this.historyLoaded = true; + + // Convert history points to chart data format (bytes/sec → Mbit/s) + const historyIn = history.map(p => ({ + x: new Date(p.timestamp).toISOString(), + y: Math.round((p.in * 8) / 1000000 * 10) / 10, + })); + const historyOut = history.map(p => ({ + x: new Date(p.timestamp).toISOString(), + y: Math.round((p.out * 8) / 1000000 * 10) / 10, + })); + + // Use history as the chart data, keeping the most recent 60 points (5 min window) + const sliceStart = Math.max(0, historyIn.length - 60); + this.trafficDataIn = historyIn.slice(sliceStart); + this.trafficDataOut = historyOut.slice(sliceStart); + + // If fewer than 60 points, pad the front with zeros + if (this.trafficDataIn.length < 60) { + const now = Date.now(); + const range = 5 * 60 * 1000; + const bucketSize = range / 60; + const padCount = 60 - this.trafficDataIn.length; + const firstTimestamp = this.trafficDataIn.length > 0 + ? new Date(this.trafficDataIn[0].x).getTime() + : now; + + const padIn = Array.from({ length: padCount }, (_, i) => ({ + x: new Date(firstTimestamp - ((padCount - i) * bucketSize)).toISOString(), + y: 0, + })); + const padOut = padIn.map(p => ({ ...p })); + + this.trafficDataIn = [...padIn, ...this.trafficDataIn]; + this.trafficDataOut = [...padOut, ...this.trafficDataOut]; + } + } + public static styles = [ cssManager.defaultStyles, viewHostCss, @@ -352,21 +399,6 @@ export class OpsViewNetwork extends DeesElement { return `${size.toFixed(1)} ${units[unitIndex]}`; } - private calculateRequestsPerSecond(): number { - // Calculate from actual request data in the last minute - const oneMinuteAgo = Date.now() - 60000; - const recentRequests = this.networkRequests.filter(req => req.timestamp >= oneMinuteAgo); - const reqPerSec = Math.round(recentRequests.length / 60); - - // Track history for trend (keep last 20 values) - this.requestsPerSecHistory.push(reqPerSec); - if (this.requestsPerSecHistory.length > 20) { - this.requestsPerSecHistory.shift(); - } - - return reqPerSec; - } - private calculateThroughput(): { in: number; out: number } { // Use real throughput data from network state return { @@ -376,16 +408,17 @@ export class OpsViewNetwork extends DeesElement { } private renderNetworkStats(): TemplateResult { - const reqPerSec = this.calculateRequestsPerSecond(); + // Use server-side requests/sec from SmartProxy's Rust engine + const reqPerSec = this.networkState.requestsPerSecond || 0; const throughput = this.calculateThroughput(); const activeConnections = this.statsState.serverStats?.activeConnections || 0; - - // Throughput data is now available in the stats tiles - // Use request count history for the requests/sec trend + // Track requests/sec history for the trend sparkline + this.requestsPerSecHistory.push(reqPerSec); + if (this.requestsPerSecHistory.length > 20) { + this.requestsPerSecHistory.shift(); + } const trendData = [...this.requestsPerSecHistory]; - - // If we don't have enough data, pad with zeros while (trendData.length < 20) { trendData.unshift(0); } @@ -398,7 +431,7 @@ export class OpsViewNetwork extends DeesElement { type: 'number', icon: 'plug', color: activeConnections > 100 ? '#f59e0b' : '#22c55e', - description: `Total: ${this.statsState.serverStats?.totalConnections || 0}`, + description: `Total: ${this.networkState.requestsTotal || this.statsState.serverStats?.totalConnections || 0}`, actions: [ { name: 'View Details', @@ -416,7 +449,7 @@ export class OpsViewNetwork extends DeesElement { icon: 'chartLine', color: '#3b82f6', trendData: trendData, - description: `Average over last minute`, + description: `Total: ${this.formatNumber(this.networkState.requestsTotal || 0)} requests`, }, { id: 'throughputIn', @@ -462,20 +495,33 @@ export class OpsViewNetwork extends DeesElement { if (this.networkState.topIPs.length === 0) { return html``; } - + + // Build per-IP bandwidth lookup + const bandwidthByIP = new Map(); + if (this.networkState.throughputByIP) { + for (const entry of this.networkState.throughputByIP) { + bandwidthByIP.set(entry.ip, { in: entry.in, out: entry.out }); + } + } + // Calculate total connections across all top IPs const totalConnections = this.networkState.topIPs.reduce((sum, ipData) => sum + ipData.count, 0); - + return html` ({ - 'IP Address': ipData.ip, - 'Connections': ipData.count, - 'Percentage': totalConnections > 0 ? ((ipData.count / totalConnections) * 100).toFixed(1) + '%' : '0%', - })} + .displayFunction=${(ipData: { ip: string; count: number }) => { + const bw = bandwidthByIP.get(ipData.ip); + return { + 'IP Address': ipData.ip, + 'Connections': ipData.count, + 'Bandwidth In': bw ? this.formatBitsPerSecond(bw.in) : '0 bit/s', + 'Bandwidth Out': bw ? this.formatBitsPerSecond(bw.out) : '0 bit/s', + 'Share': totalConnections > 0 ? ((ipData.count / totalConnections) * 100).toFixed(1) + '%' : '0%', + }; + }} heading1="Top Connected IPs" - heading2="IPs with most active connections" + heading2="IPs with most active connections and bandwidth" .pagination=${false} dataName="ip" > @@ -515,13 +561,10 @@ export class OpsViewNetwork extends DeesElement { } } - // Generate traffic data based on request history - this.updateTrafficData(); - } - - private updateTrafficData() { - // This method is called when network data updates - // The actual chart updates are handled by the timer calling addTrafficDataPoint() + // Load server-side throughput history into chart (once) + if (!this.historyLoaded && this.networkState.throughputHistory && this.networkState.throughputHistory.length > 0) { + this.loadThroughputHistory(); + } } private startTrafficUpdateTimer() {