fix(monitoring): improve domain activity aggregation for multi-domain and wildcard routes

This commit is contained in:
2026-04-13 12:15:11 +00:00
parent 501f4f9de6
commit cfcb66f1ee
4 changed files with 89 additions and 9 deletions

View File

@@ -1,5 +1,12 @@
# Changelog # Changelog
## 2026-04-13 - 13.15.1 - fix(monitoring)
improve domain activity aggregation for multi-domain and wildcard routes
- map route metrics across all configured domains instead of only the first domain
- resolve wildcard domain patterns against active protocol cache entries
- distribute shared route traffic across matched domains and preserve fallback reporting for routes without domain configuration
## 2026-04-13 - 13.15.0 - feat(stats) ## 2026-04-13 - 13.15.0 - feat(stats)
add typed network stats response fields for bandwidth, domain activity, and protocol distribution add typed network stats response fields for bandwidth, domain activity, and protocol distribution

View File

@@ -3,6 +3,6 @@
*/ */
export const commitinfo = { export const commitinfo = {
name: '@serve.zone/dcrouter', name: '@serve.zone/dcrouter',
version: '13.15.0', version: '13.15.1',
description: 'A multifaceted routing service handling mail and SMS delivery functions.' description: 'A multifaceted routing service handling mail and SMS delivery functions.'
} }

View File

@@ -724,8 +724,8 @@ export class MetricsManager {
const connectionsByRoute = proxyMetrics.connections.byRoute(); const connectionsByRoute = proxyMetrics.connections.byRoute();
const throughputByRoute = proxyMetrics.throughput.byRoute(); const throughputByRoute = proxyMetrics.throughput.byRoute();
// Map route name → primary domain using dcrouter's route configs // Map route name → ALL its domains (not just the first one)
const routeToDomain = new Map<string, string>(); const routeDomains = new Map<string, string[]>();
if (this.dcRouter.smartProxy) { if (this.dcRouter.smartProxy) {
for (const route of this.dcRouter.smartProxy.routeManager.getRoutes()) { for (const route of this.dcRouter.smartProxy.routeManager.getRoutes()) {
if (!route.name || !route.match.domains) continue; if (!route.name || !route.match.domains) continue;
@@ -733,29 +733,101 @@ export class MetricsManager {
? route.match.domains ? route.match.domains
: [route.match.domains]; : [route.match.domains];
if (domains.length > 0) { if (domains.length > 0) {
routeToDomain.set(route.name, domains[0]); routeDomains.set(route.name, domains);
} }
} }
} }
// Aggregate metrics by domain // Use protocol cache to discover actual active domains (resolves wildcards)
const activeDomains = new Set<string>();
const domainToBackend = new Map<string, string>(); // domain → host:port
for (const entry of protocolCache) {
if (entry.domain) {
activeDomains.add(entry.domain);
domainToBackend.set(entry.domain, `${entry.host}:${entry.port}`);
}
}
// 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
const domainToRoutes = new Map<string, string[]>();
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);
if (existing) { existing.push(routeName); }
else { domainToRoutes.set(activeDomain, [routeName]); }
}
}
} else {
// Concrete domain
const existing = domainToRoutes.get(pattern);
if (existing) { existing.push(routeName); }
else { domainToRoutes.set(pattern, [routeName]); }
}
}
}
// Aggregate metrics per domain
// For each domain, sum metrics from all routes that serve it,
// divided by the number of domains each route serves
const domainAgg = new Map<string, { const domainAgg = new Map<string, {
activeConnections: number; activeConnections: number;
bytesInPerSec: number; bytesInPerSec: number;
bytesOutPerSec: number; bytesOutPerSec: number;
routeCount: number; routeCount: number;
}>(); }>();
// Track which routes are accounted for
const accountedRoutes = new Set<string>();
for (const [domain, routeNames] of domainToRoutes) {
let totalConns = 0;
let totalIn = 0;
let totalOut = 0;
for (const routeName of routeNames) {
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 share = Math.max(domainsInRoute, 1);
totalConns += conns / share;
totalIn += tp.in / share;
totalOut += tp.out / share;
}
domainAgg.set(domain, {
activeConnections: Math.round(totalConns),
bytesInPerSec: totalIn,
bytesOutPerSec: totalOut,
routeCount: routeNames.length,
});
}
// Include routes with no domain config (fallback: use route name)
for (const [routeName, activeConns] of connectionsByRoute) { for (const [routeName, activeConns] of connectionsByRoute) {
const domain = routeToDomain.get(routeName) || routeName; if (accountedRoutes.has(routeName)) continue;
if (routeDomains.has(routeName)) continue; // has domains but no traffic matched
const tp = throughputByRoute.get(routeName) || { in: 0, out: 0 }; const tp = throughputByRoute.get(routeName) || { in: 0, out: 0 };
const existing = domainAgg.get(domain); if (activeConns === 0 && tp.in === 0 && tp.out === 0) continue;
const existing = domainAgg.get(routeName);
if (existing) { if (existing) {
existing.activeConnections += activeConns; existing.activeConnections += activeConns;
existing.bytesInPerSec += tp.in; existing.bytesInPerSec += tp.in;
existing.bytesOutPerSec += tp.out; existing.bytesOutPerSec += tp.out;
existing.routeCount++; existing.routeCount++;
} else { } else {
domainAgg.set(domain, { domainAgg.set(routeName, {
activeConnections: activeConns, activeConnections: activeConns,
bytesInPerSec: tp.in, bytesInPerSec: tp.in,
bytesOutPerSec: tp.out, bytesOutPerSec: tp.out,
@@ -763,6 +835,7 @@ export class MetricsManager {
}); });
} }
} }
const domainActivity = Array.from(domainAgg.entries()) const domainActivity = Array.from(domainAgg.entries())
.map(([domain, data]) => ({ .map(([domain, data]) => ({
domain, domain,

View File

@@ -3,6 +3,6 @@
*/ */
export const commitinfo = { export const commitinfo = {
name: '@serve.zone/dcrouter', name: '@serve.zone/dcrouter',
version: '13.15.0', version: '13.15.1',
description: 'A multifaceted routing service handling mail and SMS delivery functions.' description: 'A multifaceted routing service handling mail and SMS delivery functions.'
} }