feat(network): add bandwidth-ranked IP and domain activity metrics to network monitoring
This commit is contained in:
@@ -1,5 +1,12 @@
|
|||||||
# Changelog
|
# Changelog
|
||||||
|
|
||||||
|
## 2026-04-13 - 13.14.0 - feat(network)
|
||||||
|
add bandwidth-ranked IP and domain activity metrics to network monitoring
|
||||||
|
|
||||||
|
- Expose top IPs by bandwidth and aggregated domain activity from route metrics.
|
||||||
|
- Replace estimated per-connection values with real per-IP throughput data in ops handlers and stats responses.
|
||||||
|
- Update the network UI to show bandwidth-ranked IPs and domain activity while removing the recent request table.
|
||||||
|
|
||||||
## 2026-04-13 - 13.13.0 - feat(dns)
|
## 2026-04-13 - 13.13.0 - feat(dns)
|
||||||
add domain migration between dcrouter and provider-managed DNS with unified ACME managed-domain handling
|
add domain migration between dcrouter and provider-managed DNS with unified ACME managed-domain handling
|
||||||
|
|
||||||
|
|||||||
@@ -3,6 +3,6 @@
|
|||||||
*/
|
*/
|
||||||
export const commitinfo = {
|
export const commitinfo = {
|
||||||
name: '@serve.zone/dcrouter',
|
name: '@serve.zone/dcrouter',
|
||||||
version: '13.13.0',
|
version: '13.14.0',
|
||||||
description: 'A multifaceted routing service handling mail and SMS delivery functions.'
|
description: 'A multifaceted routing service handling mail and SMS delivery functions.'
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -553,12 +553,14 @@ export class MetricsManager {
|
|||||||
connectionsByIP: new Map<string, number>(),
|
connectionsByIP: new Map<string, number>(),
|
||||||
throughputRate: { bytesInPerSecond: 0, bytesOutPerSecond: 0 },
|
throughputRate: { bytesInPerSecond: 0, bytesOutPerSecond: 0 },
|
||||||
topIPs: [] as Array<{ ip: string; count: number }>,
|
topIPs: [] as Array<{ ip: string; count: number }>,
|
||||||
|
topIPsByBandwidth: [] as Array<{ ip: string; count: number; bwIn: number; bwOut: number }>,
|
||||||
totalDataTransferred: { bytesIn: 0, bytesOut: 0 },
|
totalDataTransferred: { bytesIn: 0, bytesOut: 0 },
|
||||||
throughputHistory: [] as Array<{ timestamp: number; in: number; out: number }>,
|
throughputHistory: [] as Array<{ timestamp: number; in: number; out: number }>,
|
||||||
throughputByIP: new Map<string, { in: number; out: number }>(),
|
throughputByIP: new Map<string, { in: number; out: number }>(),
|
||||||
requestsPerSecond: 0,
|
requestsPerSecond: 0,
|
||||||
requestsTotal: 0,
|
requestsTotal: 0,
|
||||||
backends: [] as Array<any>,
|
backends: [] as Array<any>,
|
||||||
|
domainActivity: [] as Array<{ domain: string; bytesInPerSecond: number; bytesOutPerSecond: number; activeConnections: number; routeCount: number }>,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -572,7 +574,7 @@ export class MetricsManager {
|
|||||||
bytesOutPerSecond: instantThroughput.out
|
bytesOutPerSecond: instantThroughput.out
|
||||||
};
|
};
|
||||||
|
|
||||||
// Get top IPs
|
// Get top IPs by connection count
|
||||||
const topIPs = proxyMetrics.connections.topIPs(10);
|
const topIPs = proxyMetrics.connections.topIPs(10);
|
||||||
|
|
||||||
// Get total data transferred
|
// Get total data transferred
|
||||||
@@ -699,10 +701,83 @@ export class MetricsManager {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Build top 10 IPs by bandwidth (sorted by total throughput desc)
|
||||||
|
const allIPData = new Map<string, { count: number; bwIn: number; bwOut: number }>();
|
||||||
|
for (const [ip, count] of connectionsByIP) {
|
||||||
|
allIPData.set(ip, { count, bwIn: 0, bwOut: 0 });
|
||||||
|
}
|
||||||
|
for (const [ip, tp] of throughputByIP) {
|
||||||
|
const existing = allIPData.get(ip);
|
||||||
|
if (existing) {
|
||||||
|
existing.bwIn = tp.in;
|
||||||
|
existing.bwOut = tp.out;
|
||||||
|
} else {
|
||||||
|
allIPData.set(ip, { count: 0, bwIn: tp.in, bwOut: tp.out });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const topIPsByBandwidth = Array.from(allIPData.entries())
|
||||||
|
.sort((a, b) => (b[1].bwIn + b[1].bwOut) - (a[1].bwIn + a[1].bwOut))
|
||||||
|
.slice(0, 10)
|
||||||
|
.map(([ip, data]) => ({ ip, count: data.count, bwIn: data.bwIn, bwOut: data.bwOut }));
|
||||||
|
|
||||||
|
// Build domain activity from per-route metrics
|
||||||
|
const connectionsByRoute = proxyMetrics.connections.byRoute();
|
||||||
|
const throughputByRoute = proxyMetrics.throughput.byRoute();
|
||||||
|
|
||||||
|
// Map route name → primary domain using dcrouter's route configs
|
||||||
|
const routeToDomain = new Map<string, string>();
|
||||||
|
if (this.dcRouter.smartProxy) {
|
||||||
|
for (const route of this.dcRouter.smartProxy.routeManager.getRoutes()) {
|
||||||
|
if (!route.name || !route.match.domains) continue;
|
||||||
|
const domains = Array.isArray(route.match.domains)
|
||||||
|
? route.match.domains
|
||||||
|
: [route.match.domains];
|
||||||
|
if (domains.length > 0) {
|
||||||
|
routeToDomain.set(route.name, domains[0]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Aggregate metrics by domain
|
||||||
|
const domainAgg = new Map<string, {
|
||||||
|
activeConnections: number;
|
||||||
|
bytesInPerSec: number;
|
||||||
|
bytesOutPerSec: number;
|
||||||
|
routeCount: number;
|
||||||
|
}>();
|
||||||
|
for (const [routeName, activeConns] of connectionsByRoute) {
|
||||||
|
const domain = routeToDomain.get(routeName) || routeName;
|
||||||
|
const tp = throughputByRoute.get(routeName) || { in: 0, out: 0 };
|
||||||
|
const existing = domainAgg.get(domain);
|
||||||
|
if (existing) {
|
||||||
|
existing.activeConnections += activeConns;
|
||||||
|
existing.bytesInPerSec += tp.in;
|
||||||
|
existing.bytesOutPerSec += tp.out;
|
||||||
|
existing.routeCount++;
|
||||||
|
} else {
|
||||||
|
domainAgg.set(domain, {
|
||||||
|
activeConnections: activeConns,
|
||||||
|
bytesInPerSec: tp.in,
|
||||||
|
bytesOutPerSec: tp.out,
|
||||||
|
routeCount: 1,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const domainActivity = Array.from(domainAgg.entries())
|
||||||
|
.map(([domain, data]) => ({
|
||||||
|
domain,
|
||||||
|
bytesInPerSecond: data.bytesInPerSec,
|
||||||
|
bytesOutPerSecond: data.bytesOutPerSec,
|
||||||
|
activeConnections: data.activeConnections,
|
||||||
|
routeCount: data.routeCount,
|
||||||
|
}))
|
||||||
|
.sort((a, b) => (b.bytesInPerSecond + b.bytesOutPerSecond) - (a.bytesInPerSecond + a.bytesOutPerSecond));
|
||||||
|
|
||||||
return {
|
return {
|
||||||
connectionsByIP,
|
connectionsByIP,
|
||||||
throughputRate,
|
throughputRate,
|
||||||
topIPs,
|
topIPs,
|
||||||
|
topIPsByBandwidth,
|
||||||
totalDataTransferred,
|
totalDataTransferred,
|
||||||
throughputHistory,
|
throughputHistory,
|
||||||
throughputByIP,
|
throughputByIP,
|
||||||
@@ -711,6 +786,7 @@ export class MetricsManager {
|
|||||||
backends,
|
backends,
|
||||||
frontendProtocols,
|
frontendProtocols,
|
||||||
backendProtocols,
|
backendProtocols,
|
||||||
|
domainActivity,
|
||||||
};
|
};
|
||||||
}, 1000); // 1s cache — matches typical dashboard poll interval
|
}, 1000); // 1s cache — matches typical dashboard poll interval
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -51,8 +51,8 @@ export class SecurityHandler {
|
|||||||
startTime: conn.startTime,
|
startTime: conn.startTime,
|
||||||
protocol: conn.type === 'http' ? 'https' : conn.type as any,
|
protocol: conn.type === 'http' ? 'https' : conn.type as any,
|
||||||
state: conn.status as any,
|
state: conn.status as any,
|
||||||
bytesReceived: Math.floor(conn.bytesTransferred / 2),
|
bytesReceived: (conn as any)._throughputIn || 0,
|
||||||
bytesSent: Math.floor(conn.bytesTransferred / 2),
|
bytesSent: (conn as any)._throughputOut || 0,
|
||||||
}));
|
}));
|
||||||
|
|
||||||
const summary = {
|
const summary = {
|
||||||
@@ -96,9 +96,11 @@ export class SecurityHandler {
|
|||||||
connectionsByIP: Array.from(networkStats.connectionsByIP.entries()).map(([ip, count]) => ({ ip, count })),
|
connectionsByIP: Array.from(networkStats.connectionsByIP.entries()).map(([ip, count]) => ({ ip, count })),
|
||||||
throughputRate: networkStats.throughputRate,
|
throughputRate: networkStats.throughputRate,
|
||||||
topIPs: networkStats.topIPs,
|
topIPs: networkStats.topIPs,
|
||||||
|
topIPsByBandwidth: networkStats.topIPsByBandwidth,
|
||||||
totalDataTransferred: networkStats.totalDataTransferred,
|
totalDataTransferred: networkStats.totalDataTransferred,
|
||||||
throughputHistory: networkStats.throughputHistory || [],
|
throughputHistory: networkStats.throughputHistory || [],
|
||||||
throughputByIP,
|
throughputByIP,
|
||||||
|
domainActivity: networkStats.domainActivity || [],
|
||||||
requestsPerSecond: networkStats.requestsPerSecond || 0,
|
requestsPerSecond: networkStats.requestsPerSecond || 0,
|
||||||
requestsTotal: networkStats.requestsTotal || 0,
|
requestsTotal: networkStats.requestsTotal || 0,
|
||||||
backends: networkStats.backends || [],
|
backends: networkStats.backends || [],
|
||||||
@@ -110,9 +112,11 @@ export class SecurityHandler {
|
|||||||
connectionsByIP: [],
|
connectionsByIP: [],
|
||||||
throughputRate: { bytesInPerSecond: 0, bytesOutPerSecond: 0 },
|
throughputRate: { bytesInPerSecond: 0, bytesOutPerSecond: 0 },
|
||||||
topIPs: [],
|
topIPs: [],
|
||||||
|
topIPsByBandwidth: [],
|
||||||
totalDataTransferred: { bytesIn: 0, bytesOut: 0 },
|
totalDataTransferred: { bytesIn: 0, bytesOut: 0 },
|
||||||
throughputHistory: [],
|
throughputHistory: [],
|
||||||
throughputByIP: [],
|
throughputByIP: [],
|
||||||
|
domainActivity: [],
|
||||||
requestsPerSecond: 0,
|
requestsPerSecond: 0,
|
||||||
requestsTotal: 0,
|
requestsTotal: 0,
|
||||||
backends: [],
|
backends: [],
|
||||||
@@ -251,31 +255,31 @@ export class SecurityHandler {
|
|||||||
const connectionInfo = await this.opsServerRef.dcRouterRef.metricsManager.getConnectionInfo();
|
const connectionInfo = await this.opsServerRef.dcRouterRef.metricsManager.getConnectionInfo();
|
||||||
const networkStats = await this.opsServerRef.dcRouterRef.metricsManager.getNetworkStats();
|
const networkStats = await this.opsServerRef.dcRouterRef.metricsManager.getNetworkStats();
|
||||||
|
|
||||||
// Use IP-based connection data from the new metrics API
|
// One aggregate row per IP with real throughput data
|
||||||
if (networkStats.connectionsByIP && networkStats.connectionsByIP.size > 0) {
|
if (networkStats.connectionsByIP && networkStats.connectionsByIP.size > 0) {
|
||||||
let connIndex = 0;
|
let connIndex = 0;
|
||||||
const publicIp = this.opsServerRef.dcRouterRef.options.publicIp || 'server';
|
const publicIp = this.opsServerRef.dcRouterRef.options.publicIp || 'server';
|
||||||
|
|
||||||
for (const [ip, count] of networkStats.connectionsByIP) {
|
for (const [ip, count] of networkStats.connectionsByIP) {
|
||||||
// Create a connection entry for each active IP connection
|
const tp = networkStats.throughputByIP?.get(ip);
|
||||||
for (let i = 0; i < Math.min(count, 5); i++) { // Limit to 5 connections per IP for UI performance
|
connections.push({
|
||||||
connections.push({
|
id: `ip-${connIndex++}`,
|
||||||
id: `conn-${connIndex++}`,
|
type: 'http',
|
||||||
type: 'http',
|
source: {
|
||||||
source: {
|
ip: ip,
|
||||||
ip: ip,
|
port: 0,
|
||||||
port: Math.floor(Math.random() * 50000) + 10000, // High port range
|
},
|
||||||
},
|
destination: {
|
||||||
destination: {
|
ip: publicIp,
|
||||||
ip: publicIp,
|
port: 443,
|
||||||
port: 443,
|
service: 'proxy',
|
||||||
service: 'proxy',
|
},
|
||||||
},
|
startTime: 0,
|
||||||
startTime: Date.now() - Math.floor(Math.random() * 3600000), // Within last hour
|
bytesTransferred: count, // Store connection count here
|
||||||
bytesTransferred: Math.floor(networkStats.totalDataTransferred.bytesIn / networkStats.connectionsByIP.size),
|
status: 'active',
|
||||||
status: 'active',
|
// Attach real throughput for the handler mapping
|
||||||
});
|
...(tp ? { _throughputIn: tp.in, _throughputOut: tp.out } : {}),
|
||||||
}
|
} as any);
|
||||||
}
|
}
|
||||||
} else if (connectionInfo.length > 0) {
|
} else if (connectionInfo.length > 0) {
|
||||||
// Fallback to route-based connection info if no IP data available
|
// Fallback to route-based connection info if no IP data available
|
||||||
|
|||||||
@@ -291,6 +291,20 @@ export class StatsHandler {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Build connectionDetails from real per-IP data
|
||||||
|
const connectionDetails: interfaces.data.IConnectionDetails[] = [];
|
||||||
|
for (const [ip, count] of stats.connectionsByIP) {
|
||||||
|
const tp = stats.throughputByIP?.get(ip);
|
||||||
|
connectionDetails.push({
|
||||||
|
remoteAddress: ip,
|
||||||
|
protocol: 'https',
|
||||||
|
state: 'connected',
|
||||||
|
startTime: 0,
|
||||||
|
bytesIn: tp?.in || 0,
|
||||||
|
bytesOut: tp?.out || 0,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
metrics.network = {
|
metrics.network = {
|
||||||
totalBandwidth: {
|
totalBandwidth: {
|
||||||
in: stats.throughputRate.bytesInPerSecond,
|
in: stats.throughputRate.bytesInPerSecond,
|
||||||
@@ -301,12 +315,18 @@ export class StatsHandler {
|
|||||||
out: stats.totalDataTransferred.bytesOut,
|
out: stats.totalDataTransferred.bytesOut,
|
||||||
},
|
},
|
||||||
activeConnections: serverStats.activeConnections,
|
activeConnections: serverStats.activeConnections,
|
||||||
connectionDetails: [],
|
connectionDetails,
|
||||||
topEndpoints: stats.topIPs.map(ip => ({
|
topEndpoints: stats.topIPs.map(ip => ({
|
||||||
endpoint: ip.ip,
|
endpoint: ip.ip,
|
||||||
requests: ip.count,
|
connections: ip.count,
|
||||||
bandwidth: ipBandwidth.get(ip.ip) || { in: 0, out: 0 },
|
bandwidth: ipBandwidth.get(ip.ip) || { in: 0, out: 0 },
|
||||||
})),
|
})),
|
||||||
|
topEndpointsByBandwidth: stats.topIPsByBandwidth.map(ip => ({
|
||||||
|
endpoint: ip.ip,
|
||||||
|
connections: ip.count,
|
||||||
|
bandwidth: { in: ip.bwIn, out: ip.bwOut },
|
||||||
|
})),
|
||||||
|
domainActivity: stats.domainActivity || [],
|
||||||
throughputHistory: stats.throughputHistory || [],
|
throughputHistory: stats.throughputHistory || [],
|
||||||
requestsPerSecond: stats.requestsPerSecond || 0,
|
requestsPerSecond: stats.requestsPerSecond || 0,
|
||||||
requestsTotal: stats.requestsTotal || 0,
|
requestsTotal: stats.requestsTotal || 0,
|
||||||
|
|||||||
@@ -143,6 +143,14 @@ export interface IHealthStatus {
|
|||||||
version?: string;
|
version?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface IDomainActivity {
|
||||||
|
domain: string;
|
||||||
|
bytesInPerSecond: number;
|
||||||
|
bytesOutPerSecond: number;
|
||||||
|
activeConnections: number;
|
||||||
|
routeCount: number;
|
||||||
|
}
|
||||||
|
|
||||||
export interface INetworkMetrics {
|
export interface INetworkMetrics {
|
||||||
totalBandwidth: {
|
totalBandwidth: {
|
||||||
in: number;
|
in: number;
|
||||||
@@ -156,12 +164,21 @@ export interface INetworkMetrics {
|
|||||||
connectionDetails: IConnectionDetails[];
|
connectionDetails: IConnectionDetails[];
|
||||||
topEndpoints: Array<{
|
topEndpoints: Array<{
|
||||||
endpoint: string;
|
endpoint: string;
|
||||||
requests: number;
|
connections: number;
|
||||||
bandwidth: {
|
bandwidth: {
|
||||||
in: number;
|
in: number;
|
||||||
out: number;
|
out: number;
|
||||||
};
|
};
|
||||||
}>;
|
}>;
|
||||||
|
topEndpointsByBandwidth: Array<{
|
||||||
|
endpoint: string;
|
||||||
|
connections: number;
|
||||||
|
bandwidth: {
|
||||||
|
in: number;
|
||||||
|
out: number;
|
||||||
|
};
|
||||||
|
}>;
|
||||||
|
domainActivity: IDomainActivity[];
|
||||||
throughputHistory?: Array<{ timestamp: number; in: number; out: number }>;
|
throughputHistory?: Array<{ timestamp: number; in: number; out: number }>;
|
||||||
requestsPerSecond?: number;
|
requestsPerSecond?: number;
|
||||||
requestsTotal?: number;
|
requestsTotal?: number;
|
||||||
|
|||||||
@@ -3,6 +3,6 @@
|
|||||||
*/
|
*/
|
||||||
export const commitinfo = {
|
export const commitinfo = {
|
||||||
name: '@serve.zone/dcrouter',
|
name: '@serve.zone/dcrouter',
|
||||||
version: '13.13.0',
|
version: '13.14.0',
|
||||||
description: 'A multifaceted routing service handling mail and SMS delivery functions.'
|
description: 'A multifaceted routing service handling mail and SMS delivery functions.'
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -52,7 +52,9 @@ export interface INetworkState {
|
|||||||
throughputRate: { bytesInPerSecond: number; bytesOutPerSecond: number };
|
throughputRate: { bytesInPerSecond: number; bytesOutPerSecond: number };
|
||||||
totalBytes: { in: number; out: number };
|
totalBytes: { in: number; out: number };
|
||||||
topIPs: Array<{ ip: string; count: number }>;
|
topIPs: Array<{ ip: string; count: number }>;
|
||||||
|
topIPsByBandwidth: Array<{ ip: string; count: number; bwIn: number; bwOut: number }>;
|
||||||
throughputByIP: Array<{ ip: string; in: number; out: number }>;
|
throughputByIP: Array<{ ip: string; in: number; out: number }>;
|
||||||
|
domainActivity: interfaces.data.IDomainActivity[];
|
||||||
throughputHistory: Array<{ timestamp: number; in: number; out: number }>;
|
throughputHistory: Array<{ timestamp: number; in: number; out: number }>;
|
||||||
requestsPerSecond: number;
|
requestsPerSecond: number;
|
||||||
requestsTotal: number;
|
requestsTotal: number;
|
||||||
@@ -160,7 +162,9 @@ export const networkStatePart = await appState.getStatePart<INetworkState>(
|
|||||||
throughputRate: { bytesInPerSecond: 0, bytesOutPerSecond: 0 },
|
throughputRate: { bytesInPerSecond: 0, bytesOutPerSecond: 0 },
|
||||||
totalBytes: { in: 0, out: 0 },
|
totalBytes: { in: 0, out: 0 },
|
||||||
topIPs: [],
|
topIPs: [],
|
||||||
|
topIPsByBandwidth: [],
|
||||||
throughputByIP: [],
|
throughputByIP: [],
|
||||||
|
domainActivity: [],
|
||||||
throughputHistory: [],
|
throughputHistory: [],
|
||||||
requestsPerSecond: 0,
|
requestsPerSecond: 0,
|
||||||
requestsTotal: 0,
|
requestsTotal: 0,
|
||||||
@@ -552,7 +556,9 @@ export const fetchNetworkStatsAction = networkStatePart.createAction(async (stat
|
|||||||
? { in: networkStatsResponse.totalDataTransferred.bytesIn, out: networkStatsResponse.totalDataTransferred.bytesOut }
|
? { in: networkStatsResponse.totalDataTransferred.bytesIn, out: networkStatsResponse.totalDataTransferred.bytesOut }
|
||||||
: { in: 0, out: 0 },
|
: { in: 0, out: 0 },
|
||||||
topIPs: networkStatsResponse.topIPs || [],
|
topIPs: networkStatsResponse.topIPs || [],
|
||||||
|
topIPsByBandwidth: networkStatsResponse.topIPsByBandwidth || [],
|
||||||
throughputByIP: networkStatsResponse.throughputByIP || [],
|
throughputByIP: networkStatsResponse.throughputByIP || [],
|
||||||
|
domainActivity: networkStatsResponse.domainActivity || [],
|
||||||
throughputHistory: networkStatsResponse.throughputHistory || [],
|
throughputHistory: networkStatsResponse.throughputHistory || [],
|
||||||
requestsPerSecond: networkStatsResponse.requestsPerSecond || 0,
|
requestsPerSecond: networkStatsResponse.requestsPerSecond || 0,
|
||||||
requestsTotal: networkStatsResponse.requestsTotal || 0,
|
requestsTotal: networkStatsResponse.requestsTotal || 0,
|
||||||
@@ -2650,66 +2656,51 @@ async function dispatchCombinedRefreshActionInner() {
|
|||||||
const network = combinedResponse.metrics.network;
|
const network = combinedResponse.metrics.network;
|
||||||
const connectionsByIP: { [ip: string]: number } = {};
|
const connectionsByIP: { [ip: string]: number } = {};
|
||||||
|
|
||||||
// Convert connection details to IP counts
|
// Build connectionsByIP from connectionDetails (now populated with real per-IP data)
|
||||||
network.connectionDetails.forEach(conn => {
|
network.connectionDetails.forEach(conn => {
|
||||||
connectionsByIP[conn.remoteAddress] = (connectionsByIP[conn.remoteAddress] || 0) + 1;
|
connectionsByIP[conn.remoteAddress] = (connectionsByIP[conn.remoteAddress] || 0) + 1;
|
||||||
});
|
});
|
||||||
|
|
||||||
// Fetch detailed connections for the network view
|
// Build connections from connectionDetails (real per-IP aggregates)
|
||||||
try {
|
const connections: interfaces.data.IConnectionInfo[] = network.connectionDetails.map((conn, i) => ({
|
||||||
const connectionsRequest = new plugins.domtools.plugins.typedrequest.TypedRequest<
|
id: `ip-${conn.remoteAddress}`,
|
||||||
interfaces.requests.IReq_GetActiveConnections
|
remoteAddress: conn.remoteAddress,
|
||||||
>('/typedrequest', 'getActiveConnections');
|
localAddress: 'server',
|
||||||
|
startTime: conn.startTime,
|
||||||
|
protocol: conn.protocol as any,
|
||||||
|
state: conn.state as any,
|
||||||
|
bytesReceived: conn.bytesIn,
|
||||||
|
bytesSent: conn.bytesOut,
|
||||||
|
}));
|
||||||
|
|
||||||
const connectionsResponse = await connectionsRequest.fire({
|
networkStatePart.setState({
|
||||||
identity: context.identity,
|
...networkStatePart.getState()!,
|
||||||
});
|
connections,
|
||||||
|
connectionsByIP,
|
||||||
networkStatePart.setState({
|
throughputRate: {
|
||||||
...networkStatePart.getState()!,
|
bytesInPerSecond: network.totalBandwidth.in,
|
||||||
connections: connectionsResponse.connections,
|
bytesOutPerSecond: network.totalBandwidth.out,
|
||||||
connectionsByIP,
|
},
|
||||||
throughputRate: {
|
totalBytes: network.totalBytes || { in: 0, out: 0 },
|
||||||
bytesInPerSecond: network.totalBandwidth.in,
|
topIPs: network.topEndpoints.map(e => ({ ip: e.endpoint, count: e.connections })),
|
||||||
bytesOutPerSecond: network.totalBandwidth.out
|
topIPsByBandwidth: (network.topEndpointsByBandwidth || []).map(e => ({
|
||||||
},
|
ip: e.endpoint,
|
||||||
totalBytes: network.totalBytes || { in: 0, out: 0 },
|
count: e.connections,
|
||||||
topIPs: network.topEndpoints.map(e => ({ ip: e.endpoint, count: e.requests })),
|
bwIn: e.bandwidth?.in || 0,
|
||||||
throughputByIP: network.topEndpoints.map(e => ({ ip: e.endpoint, in: e.bandwidth?.in || 0, out: e.bandwidth?.out || 0 })),
|
bwOut: e.bandwidth?.out || 0,
|
||||||
throughputHistory: network.throughputHistory || [],
|
})),
|
||||||
requestsPerSecond: network.requestsPerSecond || 0,
|
throughputByIP: network.topEndpoints.map(e => ({ ip: e.endpoint, in: e.bandwidth?.in || 0, out: e.bandwidth?.out || 0 })),
|
||||||
requestsTotal: network.requestsTotal || 0,
|
domainActivity: network.domainActivity || [],
|
||||||
backends: network.backends || [],
|
throughputHistory: network.throughputHistory || [],
|
||||||
frontendProtocols: network.frontendProtocols || null,
|
requestsPerSecond: network.requestsPerSecond || 0,
|
||||||
backendProtocols: network.backendProtocols || null,
|
requestsTotal: network.requestsTotal || 0,
|
||||||
lastUpdated: Date.now(),
|
backends: network.backends || [],
|
||||||
isLoading: false,
|
frontendProtocols: network.frontendProtocols || null,
|
||||||
error: null,
|
backendProtocols: network.backendProtocols || null,
|
||||||
});
|
lastUpdated: Date.now(),
|
||||||
} catch (error: unknown) {
|
isLoading: false,
|
||||||
console.error('Failed to fetch connections:', error);
|
error: null,
|
||||||
networkStatePart.setState({
|
});
|
||||||
...networkStatePart.getState()!,
|
|
||||||
connections: [],
|
|
||||||
connectionsByIP,
|
|
||||||
throughputRate: {
|
|
||||||
bytesInPerSecond: network.totalBandwidth.in,
|
|
||||||
bytesOutPerSecond: network.totalBandwidth.out
|
|
||||||
},
|
|
||||||
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,
|
|
||||||
backends: network.backends || [],
|
|
||||||
frontendProtocols: network.frontendProtocols || null,
|
|
||||||
backendProtocols: network.backendProtocols || null,
|
|
||||||
lastUpdated: Date.now(),
|
|
||||||
isLoading: false,
|
|
||||||
error: null,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Refresh certificate data if on Domains > Certificates subview
|
// Refresh certificate data if on Domains > Certificates subview
|
||||||
|
|||||||
@@ -323,16 +323,14 @@ export class OpsViewDomains extends DeesElement {
|
|||||||
|
|
||||||
// Build target options based on current source
|
// Build target options based on current source
|
||||||
const targetOptions: { option: string; key: string }[] = [];
|
const targetOptions: { option: string; key: string }[] = [];
|
||||||
if (domain.source === 'provider') {
|
|
||||||
targetOptions.push({ option: 'DcRouter (authoritative)', key: 'dcrouter' });
|
|
||||||
}
|
|
||||||
// Add all providers (except the current one if already provider-managed)
|
|
||||||
for (const p of providers) {
|
for (const p of providers) {
|
||||||
if (domain.source === 'provider' && domain.providerId === p.id) continue;
|
// Skip current source
|
||||||
targetOptions.push({ option: `${p.name} (${p.type})`, key: `provider:${p.id}` });
|
if (p.builtIn && domain.source === 'dcrouter') continue;
|
||||||
}
|
if (!p.builtIn && domain.source === 'provider' && domain.providerId === p.id) continue;
|
||||||
if (domain.source === 'dcrouter') {
|
|
||||||
targetOptions.unshift({ option: 'DcRouter (authoritative)', key: 'dcrouter' });
|
const label = p.builtIn ? 'DcRouter (self)' : `${p.name} (${p.type})`;
|
||||||
|
const key = p.builtIn ? 'dcrouter' : `provider:${p.id}`;
|
||||||
|
targetOptions.push({ option: label, key });
|
||||||
}
|
}
|
||||||
|
|
||||||
if (targetOptions.length === 0) {
|
if (targetOptions.length === 0) {
|
||||||
@@ -345,7 +343,7 @@ export class OpsViewDomains extends DeesElement {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const currentLabel = domain.source === 'dcrouter'
|
const currentLabel = domain.source === 'dcrouter'
|
||||||
? 'DcRouter (authoritative)'
|
? 'DcRouter (self)'
|
||||||
: providers.find((p) => p.id === domain.providerId)?.name || 'Provider';
|
: providers.find((p) => p.id === domain.providerId)?.name || 'Provider';
|
||||||
|
|
||||||
DeesModal.createAndShow({
|
DeesModal.createAndShow({
|
||||||
|
|||||||
@@ -10,22 +10,6 @@ declare global {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
interface INetworkRequest {
|
|
||||||
id: string;
|
|
||||||
timestamp: number;
|
|
||||||
method: string;
|
|
||||||
url: string;
|
|
||||||
hostname: string;
|
|
||||||
port: number;
|
|
||||||
protocol: 'http' | 'https' | 'tcp' | 'udp';
|
|
||||||
statusCode?: number;
|
|
||||||
duration: number;
|
|
||||||
bytesIn: number;
|
|
||||||
bytesOut: number;
|
|
||||||
remoteIp: string;
|
|
||||||
route?: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
@customElement('ops-view-network-activity')
|
@customElement('ops-view-network-activity')
|
||||||
export class OpsViewNetworkActivity extends DeesElement {
|
export class OpsViewNetworkActivity extends DeesElement {
|
||||||
/** How far back the traffic chart shows */
|
/** How far back the traffic chart shows */
|
||||||
@@ -42,9 +26,6 @@ export class OpsViewNetworkActivity extends DeesElement {
|
|||||||
accessor networkState = appstate.networkStatePart.getState()!;
|
accessor networkState = appstate.networkStatePart.getState()!;
|
||||||
|
|
||||||
|
|
||||||
@state()
|
|
||||||
accessor networkRequests: INetworkRequest[] = [];
|
|
||||||
|
|
||||||
@state()
|
@state()
|
||||||
accessor trafficDataIn: Array<{ x: string | number; y: number }> = [];
|
accessor trafficDataIn: Array<{ x: string | number; y: number }> = [];
|
||||||
|
|
||||||
@@ -314,108 +295,21 @@ export class OpsViewNetworkActivity extends DeesElement {
|
|||||||
<!-- Protocol Distribution Charts -->
|
<!-- Protocol Distribution Charts -->
|
||||||
${this.renderProtocolCharts()}
|
${this.renderProtocolCharts()}
|
||||||
|
|
||||||
<!-- Top IPs Section -->
|
<!-- Top IPs by Connection Count -->
|
||||||
${this.renderTopIPs()}
|
${this.renderTopIPs()}
|
||||||
|
|
||||||
|
<!-- Top IPs by Bandwidth -->
|
||||||
|
${this.renderTopIPsByBandwidth()}
|
||||||
|
|
||||||
|
<!-- Domain Activity -->
|
||||||
|
${this.renderDomainActivity()}
|
||||||
|
|
||||||
<!-- Backend Protocols Section -->
|
<!-- Backend Protocols Section -->
|
||||||
${this.renderBackendProtocols()}
|
${this.renderBackendProtocols()}
|
||||||
|
|
||||||
<!-- Requests Table -->
|
|
||||||
<dees-table
|
|
||||||
.data=${this.networkRequests}
|
|
||||||
.rowKey=${'id'}
|
|
||||||
.highlightUpdates=${'flash'}
|
|
||||||
.displayFunction=${(req: INetworkRequest) => ({
|
|
||||||
Time: new Date(req.timestamp).toLocaleTimeString(),
|
|
||||||
Protocol: html`<span class="protocolBadge ${req.protocol}">${req.protocol.toUpperCase()}</span>`,
|
|
||||||
Method: req.method,
|
|
||||||
'Host:Port': `${req.hostname}:${req.port}`,
|
|
||||||
Path: this.truncateUrl(req.url),
|
|
||||||
Status: this.renderStatus(req.statusCode),
|
|
||||||
Duration: `${req.duration}ms`,
|
|
||||||
'In/Out': `${this.formatBytes(req.bytesIn)} / ${this.formatBytes(req.bytesOut)}`,
|
|
||||||
'Remote IP': req.remoteIp,
|
|
||||||
})}
|
|
||||||
.dataActions=${[
|
|
||||||
{
|
|
||||||
name: 'View Details',
|
|
||||||
iconName: 'fa:magnifyingGlass',
|
|
||||||
type: ['inRow', 'doubleClick', 'contextmenu'],
|
|
||||||
actionFunc: async (actionData) => {
|
|
||||||
await this.showRequestDetails(actionData.item);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
]}
|
|
||||||
heading1="Recent Network Activity"
|
|
||||||
heading2="Recent network requests"
|
|
||||||
searchable
|
|
||||||
.showColumnFilters=${true}
|
|
||||||
.pagination=${true}
|
|
||||||
.paginationSize=${50}
|
|
||||||
dataName="request"
|
|
||||||
></dees-table>
|
|
||||||
</div>
|
</div>
|
||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
|
|
||||||
private async showRequestDetails(request: INetworkRequest) {
|
|
||||||
const { DeesModal } = await import('@design.estate/dees-catalog');
|
|
||||||
|
|
||||||
await DeesModal.createAndShow({
|
|
||||||
heading: 'Request Details',
|
|
||||||
content: html`
|
|
||||||
<div style="padding: 20px;">
|
|
||||||
<dees-dataview-codebox
|
|
||||||
.heading=${'Request Information'}
|
|
||||||
progLang="json"
|
|
||||||
.codeToDisplay=${JSON.stringify({
|
|
||||||
id: request.id,
|
|
||||||
timestamp: new Date(request.timestamp).toISOString(),
|
|
||||||
protocol: request.protocol,
|
|
||||||
method: request.method,
|
|
||||||
url: request.url,
|
|
||||||
hostname: request.hostname,
|
|
||||||
port: request.port,
|
|
||||||
statusCode: request.statusCode,
|
|
||||||
duration: `${request.duration}ms`,
|
|
||||||
bytesIn: request.bytesIn,
|
|
||||||
bytesOut: request.bytesOut,
|
|
||||||
remoteIp: request.remoteIp,
|
|
||||||
route: request.route,
|
|
||||||
}, null, 2)}
|
|
||||||
></dees-dataview-codebox>
|
|
||||||
</div>
|
|
||||||
`,
|
|
||||||
menuOptions: [
|
|
||||||
{
|
|
||||||
name: 'Copy Request ID',
|
|
||||||
iconName: 'lucide:Copy',
|
|
||||||
action: async () => {
|
|
||||||
await navigator.clipboard.writeText(request.id);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
]
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
private renderStatus(statusCode?: number): TemplateResult {
|
|
||||||
if (!statusCode) {
|
|
||||||
return html`<span class="statusBadge warning">N/A</span>`;
|
|
||||||
}
|
|
||||||
|
|
||||||
const statusClass = statusCode >= 200 && statusCode < 300 ? 'success' :
|
|
||||||
statusCode >= 400 ? 'error' : 'warning';
|
|
||||||
|
|
||||||
return html`<span class="statusBadge ${statusClass}">${statusCode}</span>`;
|
|
||||||
}
|
|
||||||
|
|
||||||
private truncateUrl(url: string, maxLength = 50): string {
|
|
||||||
if (url.length <= maxLength) return url;
|
|
||||||
return url.substring(0, maxLength - 3) + '...';
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
private formatNumber(num: number): string {
|
private formatNumber(num: number): string {
|
||||||
if (num >= 1000000) {
|
if (num >= 1000000) {
|
||||||
return (num / 1000000).toFixed(1) + 'M';
|
return (num / 1000000).toFixed(1) + 'M';
|
||||||
@@ -619,6 +513,66 @@ export class OpsViewNetworkActivity extends DeesElement {
|
|||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private renderTopIPsByBandwidth(): TemplateResult {
|
||||||
|
if (!this.networkState.topIPsByBandwidth || this.networkState.topIPsByBandwidth.length === 0) {
|
||||||
|
return html``;
|
||||||
|
}
|
||||||
|
|
||||||
|
return html`
|
||||||
|
<dees-table
|
||||||
|
.data=${this.networkState.topIPsByBandwidth}
|
||||||
|
.rowKey=${'ip'}
|
||||||
|
.highlightUpdates=${'flash'}
|
||||||
|
.displayFunction=${(ipData: { ip: string; count: number; bwIn: number; bwOut: number }) => {
|
||||||
|
return {
|
||||||
|
'IP Address': ipData.ip,
|
||||||
|
'Bandwidth In': this.formatBitsPerSecond(ipData.bwIn),
|
||||||
|
'Bandwidth Out': this.formatBitsPerSecond(ipData.bwOut),
|
||||||
|
'Total Bandwidth': this.formatBitsPerSecond(ipData.bwIn + ipData.bwOut),
|
||||||
|
'Connections': ipData.count,
|
||||||
|
};
|
||||||
|
}}
|
||||||
|
heading1="Top IPs by Bandwidth"
|
||||||
|
heading2="IPs with highest throughput"
|
||||||
|
searchable
|
||||||
|
.showColumnFilters=${true}
|
||||||
|
.pagination=${false}
|
||||||
|
dataName="ip"
|
||||||
|
></dees-table>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
private renderDomainActivity(): TemplateResult {
|
||||||
|
if (!this.networkState.domainActivity || this.networkState.domainActivity.length === 0) {
|
||||||
|
return html``;
|
||||||
|
}
|
||||||
|
|
||||||
|
return html`
|
||||||
|
<dees-table
|
||||||
|
.data=${this.networkState.domainActivity}
|
||||||
|
.rowKey=${'domain'}
|
||||||
|
.highlightUpdates=${'flash'}
|
||||||
|
.displayFunction=${(item: interfaces.data.IDomainActivity) => {
|
||||||
|
const totalBytesPerMin = (item.bytesInPerSecond + item.bytesOutPerSecond) * 60;
|
||||||
|
return {
|
||||||
|
'Domain': item.domain,
|
||||||
|
'Throughput In': this.formatBitsPerSecond(item.bytesInPerSecond),
|
||||||
|
'Throughput Out': this.formatBitsPerSecond(item.bytesOutPerSecond),
|
||||||
|
'Transferred / min': this.formatBytes(totalBytesPerMin),
|
||||||
|
'Connections': item.activeConnections,
|
||||||
|
'Routes': item.routeCount,
|
||||||
|
};
|
||||||
|
}}
|
||||||
|
heading1="Domain Activity"
|
||||||
|
heading2="Per-domain network activity aggregated from route metrics"
|
||||||
|
searchable
|
||||||
|
.showColumnFilters=${true}
|
||||||
|
.pagination=${false}
|
||||||
|
dataName="domain"
|
||||||
|
></dees-table>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
private renderBackendProtocols(): TemplateResult {
|
private renderBackendProtocols(): TemplateResult {
|
||||||
const backends = this.networkState.backends;
|
const backends = this.networkState.backends;
|
||||||
if (!backends || backends.length === 0) {
|
if (!backends || backends.length === 0) {
|
||||||
@@ -730,25 +684,6 @@ export class OpsViewNetworkActivity extends DeesElement {
|
|||||||
this.requestsPerSecHistory.shift();
|
this.requestsPerSecHistory.shift();
|
||||||
}
|
}
|
||||||
|
|
||||||
// Reassign unconditionally so dees-table's flash diff can compare per-cell
|
|
||||||
// values against the previous snapshot. Row identity is preserved via
|
|
||||||
// rowKey='id', so DOM nodes are reused across ticks.
|
|
||||||
this.networkRequests = this.networkState.connections.map((conn) => ({
|
|
||||||
id: conn.id,
|
|
||||||
timestamp: conn.startTime,
|
|
||||||
method: 'GET', // Default method for proxy connections
|
|
||||||
url: '/',
|
|
||||||
hostname: conn.remoteAddress,
|
|
||||||
port: conn.protocol === 'https' ? 443 : 80,
|
|
||||||
protocol: conn.protocol === 'https' || conn.protocol === 'http' ? conn.protocol : 'tcp',
|
|
||||||
statusCode: conn.state === 'connected' ? 200 : undefined,
|
|
||||||
duration: Date.now() - conn.startTime,
|
|
||||||
bytesIn: conn.bytesReceived,
|
|
||||||
bytesOut: conn.bytesSent,
|
|
||||||
remoteIp: conn.remoteAddress,
|
|
||||||
route: 'proxy',
|
|
||||||
}));
|
|
||||||
|
|
||||||
// Load server-side throughput history into chart (once)
|
// Load server-side throughput history into chart (once)
|
||||||
if (!this.historyLoaded && this.networkState.throughputHistory && this.networkState.throughputHistory.length > 0) {
|
if (!this.historyLoaded && this.networkState.throughputHistory && this.networkState.throughputHistory.length > 0) {
|
||||||
this.loadThroughputHistory();
|
this.loadThroughputHistory();
|
||||||
|
|||||||
Reference in New Issue
Block a user