diff --git a/changelog.md b/changelog.md index 03a49bd..e72ba64 100644 --- a/changelog.md +++ b/changelog.md @@ -1,5 +1,12 @@ # 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) add typed network stats response fields for bandwidth, domain activity, and protocol distribution diff --git a/ts/00_commitinfo_data.ts b/ts/00_commitinfo_data.ts index 3d70bd9..8b12c34 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.15.0', + version: '13.15.1', 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 ac3e30c..05bcac2 100644 --- a/ts/monitoring/classes.metricsmanager.ts +++ b/ts/monitoring/classes.metricsmanager.ts @@ -724,8 +724,8 @@ export class MetricsManager { const connectionsByRoute = proxyMetrics.connections.byRoute(); const throughputByRoute = proxyMetrics.throughput.byRoute(); - // Map route name → primary domain using dcrouter's route configs - const routeToDomain = new Map(); + // Map route name → ALL its domains (not just the first one) + const routeDomains = new Map(); if (this.dcRouter.smartProxy) { for (const route of this.dcRouter.smartProxy.routeManager.getRoutes()) { if (!route.name || !route.match.domains) continue; @@ -733,29 +733,101 @@ export class MetricsManager { ? route.match.domains : [route.match.domains]; 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(); + const domainToBackend = new Map(); // 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(); + 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(); + + // Track which routes are accounted for + const accountedRoutes = new Set(); + + 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) { - 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 existing = domainAgg.get(domain); + if (activeConns === 0 && tp.in === 0 && tp.out === 0) continue; + const existing = domainAgg.get(routeName); if (existing) { existing.activeConnections += activeConns; existing.bytesInPerSec += tp.in; existing.bytesOutPerSec += tp.out; existing.routeCount++; } else { - domainAgg.set(domain, { + domainAgg.set(routeName, { activeConnections: activeConns, bytesInPerSec: tp.in, bytesOutPerSec: tp.out, @@ -763,6 +835,7 @@ export class MetricsManager { }); } } + const domainActivity = Array.from(domainAgg.entries()) .map(([domain, data]) => ({ domain, diff --git a/ts_web/00_commitinfo_data.ts b/ts_web/00_commitinfo_data.ts index 3d70bd9..8b12c34 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.15.0', + version: '13.15.1', description: 'A multifaceted routing service handling mail and SMS delivery functions.' }