diff --git a/changelog.md b/changelog.md index 8b9f83c..0c35545 100644 --- a/changelog.md +++ b/changelog.md @@ -1,5 +1,12 @@ # Changelog +## 2026-04-13 - 13.17.0 - feat(monitoring,network-ui,routes) +add request-based domain activity metrics and split routes into user and system views + +- Domain activity now includes per-domain request counts and distributes route throughput and connections using request-level metrics instead of equal route sharing. +- Network activity UI displays request counts and updates the domain activity description to reflect request-level aggregation. +- Routes UI adds a toggle to filter between user-created and system-generated routes, updates summary card labels, and adjusts empty states accordingly. + ## 2026-04-13 - 13.16.2 - fix(deps) bump @push.rocks/smartproxy to ^27.6.0 diff --git a/ts/00_commitinfo_data.ts b/ts/00_commitinfo_data.ts index 3900517..2673fe3 100644 --- a/ts/00_commitinfo_data.ts +++ b/ts/00_commitinfo_data.ts @@ -3,6 +3,6 @@ */ export const commitinfo = { name: '@serve.zone/dcrouter', - version: '13.16.2', + version: '13.17.0', description: 'A multifaceted routing service handling mail and SMS delivery functions.' } diff --git a/ts/monitoring/classes.metricsmanager.ts b/ts/monitoring/classes.metricsmanager.ts index 05bcac2..a3ab69b 100644 --- a/ts/monitoring/classes.metricsmanager.ts +++ b/ts/monitoring/classes.metricsmanager.ts @@ -560,7 +560,7 @@ export class MetricsManager { requestsPerSecond: 0, requestsTotal: 0, backends: [] as Array, - domainActivity: [] as Array<{ domain: string; bytesInPerSecond: number; bytesOutPerSecond: number; activeConnections: number; routeCount: number }>, + domainActivity: [] as Array<{ domain: string; bytesInPerSecond: number; bytesOutPerSecond: number; activeConnections: number; routeCount: number; requestCount: number }>, }; } @@ -720,11 +720,20 @@ export class MetricsManager { .slice(0, 10) .map(([ip, data]) => ({ ip, count: data.count, bwIn: data.bwIn, bwOut: data.bwOut })); - // Build domain activity from per-route metrics + // Build domain activity using per-IP domain request counts from Rust engine const connectionsByRoute = proxyMetrics.connections.byRoute(); const throughputByRoute = proxyMetrics.throughput.byRoute(); - // Map route name → ALL its domains (not just the first one) + // Aggregate per-IP domain request counts into per-domain totals + const domainRequestTotals = new Map(); + const domainRequestsByIP = proxyMetrics.connections.domainRequestsByIP(); + for (const [, domainMap] of domainRequestsByIP) { + for (const [domain, count] of domainMap) { + domainRequestTotals.set(domain, (domainRequestTotals.get(domain) || 0) + count); + } + } + + // Map route name → domains from route config const routeDomains = new Map(); if (this.dcRouter.smartProxy) { for (const route of this.dcRouter.smartProxy.routeManager.getRoutes()) { @@ -738,34 +747,26 @@ export class MetricsManager { } } - // Use protocol cache to discover actual active domains (resolves wildcards) - const activeDomains = new Set(); - const domainToBackend = new Map(); // domain → host:port + // Resolve wildcards using domains seen in request metrics + const allKnownDomains = new Set(domainRequestTotals.keys()); for (const entry of protocolCache) { - if (entry.domain) { - activeDomains.add(entry.domain); - domainToBackend.set(entry.domain, `${entry.host}:${entry.port}`); - } + if (entry.domain) allKnownDomains.add(entry.domain); } - // Build reverse map: domain → route name(s) that handle it - // For concrete domains: direct lookup from route config - // For wildcard patterns: match active domains from protocol cache + // Build reverse map: concrete domain → route name(s) const domainToRoutes = new Map(); for (const [routeName, domains] of routeDomains) { for (const pattern of domains) { if (pattern.includes('*')) { - // Wildcard pattern — match against active domains from protocol cache const regex = new RegExp('^' + pattern.replace(/\./g, '\\.').replace(/\*/g, '[^.]+') + '$'); - for (const activeDomain of activeDomains) { - if (regex.test(activeDomain)) { - const existing = domainToRoutes.get(activeDomain); + for (const knownDomain of allKnownDomains) { + if (regex.test(knownDomain)) { + const existing = domainToRoutes.get(knownDomain); if (existing) { existing.push(routeName); } - else { domainToRoutes.set(activeDomain, [routeName]); } + else { domainToRoutes.set(knownDomain, [routeName]); } } } } else { - // Concrete domain const existing = domainToRoutes.get(pattern); if (existing) { existing.push(routeName); } else { domainToRoutes.set(pattern, [routeName]); } @@ -773,20 +774,28 @@ export class MetricsManager { } } - // Aggregate metrics per domain - // For each domain, sum metrics from all routes that serve it, - // divided by the number of domains each route serves + // For each route, compute the total request count across all its resolved domains + // so we can distribute throughput/connections proportionally + const routeTotalRequests = new Map(); + for (const [domain, routeNames] of domainToRoutes) { + const reqs = domainRequestTotals.get(domain) || 0; + for (const routeName of routeNames) { + routeTotalRequests.set(routeName, (routeTotalRequests.get(routeName) || 0) + reqs); + } + } + + // Aggregate metrics per domain using request-count-proportional splitting const domainAgg = new Map(); - - // Track which routes are accounted for const accountedRoutes = new Set(); for (const [domain, routeNames] of domainToRoutes) { + const domainReqs = domainRequestTotals.get(domain) || 0; let totalConns = 0; let totalIn = 0; let totalOut = 0; @@ -795,15 +804,25 @@ export class MetricsManager { accountedRoutes.add(routeName); const conns = connectionsByRoute.get(routeName) || 0; const tp = throughputByRoute.get(routeName) || { in: 0, out: 0 }; - // Count how many resolved domains share this route - let domainsInRoute = 0; - for (const [, routes] of domainToRoutes) { - if (routes.includes(routeName)) domainsInRoute++; + const routeTotal = routeTotalRequests.get(routeName) || 0; + + // Proportional share based on actual request counts + // Fall back to equal split only when no request data exists + let share: number; + if (routeTotal > 0 && domainReqs > 0) { + share = domainReqs / routeTotal; + } else { + // Count how many resolved domains share this route + let domainsInRoute = 0; + for (const [, routes] of domainToRoutes) { + if (routes.includes(routeName)) domainsInRoute++; + } + share = 1 / Math.max(domainsInRoute, 1); } - const share = Math.max(domainsInRoute, 1); - totalConns += conns / share; - totalIn += tp.in / share; - totalOut += tp.out / share; + + totalConns += conns * share; + totalIn += tp.in * share; + totalOut += tp.out * share; } domainAgg.set(domain, { @@ -811,13 +830,14 @@ export class MetricsManager { bytesInPerSec: totalIn, bytesOutPerSec: totalOut, routeCount: routeNames.length, + requestCount: domainReqs, }); } // Include routes with no domain config (fallback: use route name) for (const [routeName, activeConns] of connectionsByRoute) { if (accountedRoutes.has(routeName)) continue; - if (routeDomains.has(routeName)) continue; // has domains but no traffic matched + if (routeDomains.has(routeName)) continue; const tp = throughputByRoute.get(routeName) || { in: 0, out: 0 }; if (activeConns === 0 && tp.in === 0 && tp.out === 0) continue; const existing = domainAgg.get(routeName); @@ -832,6 +852,7 @@ export class MetricsManager { bytesInPerSec: tp.in, bytesOutPerSec: tp.out, routeCount: 1, + requestCount: 0, }); } } @@ -843,6 +864,7 @@ export class MetricsManager { bytesOutPerSecond: data.bytesOutPerSec, activeConnections: data.activeConnections, routeCount: data.routeCount, + requestCount: data.requestCount, })) .sort((a, b) => (b.bytesInPerSecond + b.bytesOutPerSecond) - (a.bytesInPerSecond + a.bytesOutPerSecond)); diff --git a/ts_interfaces/data/stats.ts b/ts_interfaces/data/stats.ts index b09a998..cbec962 100644 --- a/ts_interfaces/data/stats.ts +++ b/ts_interfaces/data/stats.ts @@ -149,6 +149,7 @@ export interface IDomainActivity { bytesOutPerSecond: number; activeConnections: number; routeCount: number; + requestCount: number; } export interface INetworkMetrics { diff --git a/ts_web/00_commitinfo_data.ts b/ts_web/00_commitinfo_data.ts index 3900517..2673fe3 100644 --- a/ts_web/00_commitinfo_data.ts +++ b/ts_web/00_commitinfo_data.ts @@ -3,6 +3,6 @@ */ export const commitinfo = { name: '@serve.zone/dcrouter', - version: '13.16.2', + version: '13.17.0', description: 'A multifaceted routing service handling mail and SMS delivery functions.' } diff --git a/ts_web/elements/network/ops-view-network-activity.ts b/ts_web/elements/network/ops-view-network-activity.ts index 1984838..9048604 100644 --- a/ts_web/elements/network/ops-view-network-activity.ts +++ b/ts_web/elements/network/ops-view-network-activity.ts @@ -560,11 +560,12 @@ export class OpsViewNetworkActivity extends DeesElement { 'Throughput Out': this.formatBitsPerSecond(item.bytesOutPerSecond), 'Transferred / min': this.formatBytes(totalBytesPerMin), 'Connections': item.activeConnections, + 'Requests': item.requestCount?.toLocaleString() ?? '0', 'Routes': item.routeCount, }; }} heading1="Domain Activity" - heading2="Per-domain network activity aggregated from route metrics" + heading2="Per-domain network activity from request-level metrics" searchable .showColumnFilters=${true} .pagination=${false} diff --git a/ts_web/elements/network/ops-view-routes.ts b/ts_web/elements/network/ops-view-routes.ts index a1153e2..6d5e849 100644 --- a/ts_web/elements/network/ops-view-routes.ts +++ b/ts_web/elements/network/ops-view-routes.ts @@ -49,6 +49,8 @@ function setupTlsVisibility(formEl: any) { @customElement('ops-view-routes') export class OpsViewRoutes extends DeesElement { + @state() accessor routeFilter: 'User Routes' | 'System Routes' = 'User Routes'; + @state() accessor routeState: appstate.IRouteManagementState = { mergedRoutes: [], warnings: [], @@ -156,20 +158,20 @@ export class OpsViewRoutes extends DeesElement { }, { id: 'configRoutes', - title: 'From Config', + title: 'System Routes', type: 'number', value: configCount, icon: 'lucide:settings', - description: 'Seeded from config/email/DNS', + description: 'From config, email, and DNS', color: '#8b5cf6', }, { id: 'apiRoutes', - title: 'API Created', + title: 'User Routes', type: 'number', value: apiCount, icon: 'lucide:code', - description: 'Routes added via API', + description: 'Created via API', color: '#0ea5e9', }, { @@ -183,8 +185,14 @@ export class OpsViewRoutes extends DeesElement { }, ]; - // Map merged routes to sz-route-list-view format - const szRoutes = mergedRoutes.map((mr) => { + // Filter routes based on selected tab + const isUserRoutes = this.routeFilter === 'User Routes'; + const filteredRoutes = mergedRoutes.filter((mr) => + isUserRoutes ? mr.origin === 'api' : mr.origin !== 'api' + ); + + // Map filtered routes to sz-route-list-view format + const szRoutes = filteredRoutes.map((mr) => { const tags = [...(mr.route.tags || [])]; tags.push(mr.origin); if (!mr.enabled) tags.push('disabled'); @@ -218,6 +226,13 @@ export class OpsViewRoutes extends DeesElement { ]} > + { this.routeFilter = e.target.value || e.target.selectedOption; }} + > + ${warnings.length > 0 ? html`
@@ -237,6 +252,7 @@ export class OpsViewRoutes extends DeesElement { ? html` true : () => false} @route-click=${(e: CustomEvent) => this.handleRouteClick(e)} @route-edit=${(e: CustomEvent) => this.handleRouteEdit(e)} @route-delete=${(e: CustomEvent) => this.handleRouteDelete(e)} @@ -244,8 +260,8 @@ export class OpsViewRoutes extends DeesElement { ` : html`
-

No routes configured

-

Add a route to get started.

+

No ${isUserRoutes ? 'user' : 'system'} routes

+

${isUserRoutes ? 'Add a route to get started.' : 'System routes are generated from config, email, and DNS settings.'}

`}