feat(dashboard): use SmartProxy server-side throughput history and per-IP bandwidth in network view
This commit is contained in:
@@ -489,8 +489,12 @@ export class MetricsManager {
|
||||
return {
|
||||
connectionsByIP: new Map<string, number>(),
|
||||
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<string, { in: number; out: number }>(),
|
||||
requestsPerSecond: 0,
|
||||
requestsTotal: 0,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -513,11 +517,25 @@ export class MetricsManager {
|
||||
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
|
||||
}
|
||||
|
||||
@@ -85,11 +85,23 @@ export class SecurityHandler {
|
||||
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,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -99,6 +111,10 @@ export class SecurityHandler {
|
||||
throughputRate: { bytesInPerSecond: 0, bytesOutPerSecond: 0 },
|
||||
topIPs: [],
|
||||
totalDataTransferred: { bytesIn: 0, bytesOut: 0 },
|
||||
throughputHistory: [],
|
||||
throughputByIP: [],
|
||||
requestsPerSecond: 0,
|
||||
requestsTotal: 0,
|
||||
};
|
||||
}
|
||||
)
|
||||
|
||||
@@ -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<string, { in: number; out: number }>();
|
||||
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,
|
||||
};
|
||||
})()
|
||||
);
|
||||
|
||||
@@ -130,6 +130,9 @@ export interface INetworkMetrics {
|
||||
out: number;
|
||||
};
|
||||
}>;
|
||||
throughputHistory?: Array<{ timestamp: number; in: number; out: number }>;
|
||||
requestsPerSecond?: number;
|
||||
requestsTotal?: number;
|
||||
}
|
||||
|
||||
export interface IConnectionDetails {
|
||||
|
||||
@@ -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<INetworkState>(
|
||||
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,
|
||||
|
||||
@@ -52,8 +52,7 @@ export class OpsViewNetwork extends DeesElement {
|
||||
private requestCountHistory = new Map<number, number>(); // 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();
|
||||
@@ -111,6 +110,54 @@ export class OpsViewNetwork extends DeesElement {
|
||||
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',
|
||||
@@ -463,19 +496,32 @@ export class OpsViewNetwork extends DeesElement {
|
||||
return html``;
|
||||
}
|
||||
|
||||
// Build per-IP bandwidth lookup
|
||||
const bandwidthByIP = new Map<string, { in: number; out: number }>();
|
||||
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`
|
||||
<dees-table
|
||||
.data=${this.networkState.topIPs}
|
||||
.displayFunction=${(ipData: { ip: string; count: number }) => ({
|
||||
.displayFunction=${(ipData: { ip: string; count: number }) => {
|
||||
const bw = bandwidthByIP.get(ipData.ip);
|
||||
return {
|
||||
'IP Address': ipData.ip,
|
||||
'Connections': ipData.count,
|
||||
'Percentage': totalConnections > 0 ? ((ipData.count / totalConnections) * 100).toFixed(1) + '%' : '0%',
|
||||
})}
|
||||
'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"
|
||||
></dees-table>
|
||||
@@ -515,13 +561,10 @@ export class OpsViewNetwork extends DeesElement {
|
||||
}
|
||||
}
|
||||
|
||||
// Generate traffic data based on request history
|
||||
this.updateTrafficData();
|
||||
// Load server-side throughput history into chart (once)
|
||||
if (!this.historyLoaded && this.networkState.throughputHistory && this.networkState.throughputHistory.length > 0) {
|
||||
this.loadThroughputHistory();
|
||||
}
|
||||
|
||||
private updateTrafficData() {
|
||||
// This method is called when network data updates
|
||||
// The actual chart updates are handled by the timer calling addTrafficDataPoint()
|
||||
}
|
||||
|
||||
private startTrafficUpdateTimer() {
|
||||
|
||||
Reference in New Issue
Block a user