feat(network): add bandwidth-ranked IP and domain activity metrics to network monitoring

This commit is contained in:
2026-04-13 11:04:15 +00:00
parent 07a3365496
commit 035173702d
10 changed files with 275 additions and 227 deletions

View File

@@ -323,16 +323,14 @@ export class OpsViewDomains extends DeesElement {
// Build target options based on current source
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) {
if (domain.source === 'provider' && domain.providerId === p.id) continue;
targetOptions.push({ option: `${p.name} (${p.type})`, key: `provider:${p.id}` });
}
if (domain.source === 'dcrouter') {
targetOptions.unshift({ option: 'DcRouter (authoritative)', key: 'dcrouter' });
// Skip current source
if (p.builtIn && domain.source === 'dcrouter') continue;
if (!p.builtIn && domain.source === 'provider' && domain.providerId === p.id) continue;
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) {
@@ -345,7 +343,7 @@ export class OpsViewDomains extends DeesElement {
}
const currentLabel = domain.source === 'dcrouter'
? 'DcRouter (authoritative)'
? 'DcRouter (self)'
: providers.find((p) => p.id === domain.providerId)?.name || 'Provider';
DeesModal.createAndShow({

View File

@@ -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')
export class OpsViewNetworkActivity extends DeesElement {
/** How far back the traffic chart shows */
@@ -42,9 +26,6 @@ export class OpsViewNetworkActivity extends DeesElement {
accessor networkState = appstate.networkStatePart.getState()!;
@state()
accessor networkRequests: INetworkRequest[] = [];
@state()
accessor trafficDataIn: Array<{ x: string | number; y: number }> = [];
@@ -314,108 +295,21 @@ export class OpsViewNetworkActivity extends DeesElement {
<!-- Protocol Distribution Charts -->
${this.renderProtocolCharts()}
<!-- Top IPs Section -->
<!-- Top IPs by Connection Count -->
${this.renderTopIPs()}
<!-- Top IPs by Bandwidth -->
${this.renderTopIPsByBandwidth()}
<!-- Domain Activity -->
${this.renderDomainActivity()}
<!-- Backend Protocols Section -->
${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>
`;
}
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 {
if (num >= 1000000) {
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 {
const backends = this.networkState.backends;
if (!backends || backends.length === 0) {
@@ -730,25 +684,6 @@ export class OpsViewNetworkActivity extends DeesElement {
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)
if (!this.historyLoaded && this.networkState.throughputHistory && this.networkState.throughputHistory.length > 0) {
this.loadThroughputHistory();