diff --git a/changelog.md b/changelog.md index 4221b30..be348f0 100644 --- a/changelog.md +++ b/changelog.md @@ -1,5 +1,13 @@ # Changelog +## 2026-04-14 - 13.17.9 - fix(monitoring) +align domain activity metrics with id-keyed route data + +- Use route id as a fallback canonical key when matching route metrics to configured domains in MetricsManager. +- Add a regression test covering domain activity aggregation for routes identified only by id. +- Update the network activity UI to show formatted total connection counts in the active connections card. +- Bump @push.rocks/smartproxy from ^27.7.3 to ^27.7.4. + ## 2026-04-14 - 13.17.8 - fix(opsserver) align certificate status handling with the updated smartproxy response format diff --git a/package.json b/package.json index cb573e2..9c33df6 100644 --- a/package.json +++ b/package.json @@ -54,7 +54,7 @@ "@push.rocks/smartnetwork": "^4.6.0", "@push.rocks/smartpath": "^6.0.0", "@push.rocks/smartpromise": "^4.2.3", - "@push.rocks/smartproxy": "^27.7.3", + "@push.rocks/smartproxy": "^27.7.4", "@push.rocks/smartradius": "^1.1.1", "@push.rocks/smartrequest": "^5.0.1", "@push.rocks/smartrx": "^3.0.10", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index ce93ec7..bd9defe 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -81,8 +81,8 @@ importers: specifier: ^4.2.3 version: 4.2.3 '@push.rocks/smartproxy': - specifier: ^27.7.3 - version: 27.7.3 + specifier: ^27.7.4 + version: 27.7.4 '@push.rocks/smartradius': specifier: ^1.1.1 version: 1.1.1 @@ -1284,8 +1284,8 @@ packages: '@push.rocks/smartpromise@4.2.3': resolution: {integrity: sha512-Ycg/TJR+tMt+S3wSFurOpEoW6nXv12QBtKXgBcjMZ4RsdO28geN46U09osPn9N9WuwQy1PkmTV5J/V4F9U8qEw==} - '@push.rocks/smartproxy@27.7.3': - resolution: {integrity: sha512-1NER+2nM2GWzrMB1xoCzkV9in8a1gWNwOimgE/f2kPYnhy5DsA2XX/KiLGJ0ge3cpCVgoK1nNe3byXQHWvmPbA==} + '@push.rocks/smartproxy@27.7.4': + resolution: {integrity: sha512-WY9Jp6Jtqo5WbW29XpATuxzGyLs8LGkAlrycgMN/IdYfvgtEB2HWuztBZCDLFMuD3Qnv4vVdci9s0nF0ZPyJcQ==} '@push.rocks/smartpuppeteer@2.0.5': resolution: {integrity: sha512-yK/qSeWVHIGWRp3c8S5tfdGP6WCKllZC4DR8d8CQlEjszOSBmHtlTdyyqOMBZ/BA4kd+eU5f3A1r4K2tGYty1g==} @@ -6506,7 +6506,7 @@ snapshots: '@push.rocks/smartpromise@4.2.3': {} - '@push.rocks/smartproxy@27.7.3': + '@push.rocks/smartproxy@27.7.4': dependencies: '@push.rocks/smartcrypto': 2.0.4 '@push.rocks/smartlog': 3.2.2 diff --git a/test/test.metricsmanager.route-keys.node.ts b/test/test.metricsmanager.route-keys.node.ts new file mode 100644 index 0000000..05c2875 --- /dev/null +++ b/test/test.metricsmanager.route-keys.node.ts @@ -0,0 +1,120 @@ +import { expect, tap } from '@git.zone/tstest/tapbundle'; +import { MetricsManager } from '../ts/monitoring/classes.metricsmanager.js'; + +const emptyProtocolDistribution = { + h1Active: 0, + h1Total: 0, + h2Active: 0, + h2Total: 0, + h3Active: 0, + h3Total: 0, + wsActive: 0, + wsTotal: 0, + otherActive: 0, + otherTotal: 0, +}; + +function createProxyMetrics(args: { + connectionsByRoute: Map; + throughputByRoute: Map; + domainRequestsByIP: Map>; + requestsTotal?: number; +}) { + return { + connections: { + active: () => 0, + total: () => 0, + byRoute: () => args.connectionsByRoute, + byIP: () => new Map(), + topIPs: () => [], + domainRequestsByIP: () => args.domainRequestsByIP, + topDomainRequests: () => [], + frontendProtocols: () => emptyProtocolDistribution, + backendProtocols: () => emptyProtocolDistribution, + }, + throughput: { + instant: () => ({ in: 0, out: 0 }), + recent: () => ({ in: 0, out: 0 }), + average: () => ({ in: 0, out: 0 }), + custom: () => ({ in: 0, out: 0 }), + history: () => [], + byRoute: () => args.throughputByRoute, + byIP: () => new Map(), + }, + requests: { + perSecond: () => 0, + perMinute: () => 0, + total: () => args.requestsTotal || 0, + }, + totals: { + bytesIn: () => 0, + bytesOut: () => 0, + connections: () => 0, + }, + backends: { + byBackend: () => new Map(), + protocols: () => new Map(), + topByErrors: () => [], + detectedProtocols: () => [], + }, + }; +} + +tap.test('MetricsManager joins domain activity to id-keyed route metrics', async () => { + const proxyMetrics = createProxyMetrics({ + connectionsByRoute: new Map([ + ['route-id-only', 4], + ]), + throughputByRoute: new Map([ + ['route-id-only', { in: 1200, out: 2400 }], + ]), + domainRequestsByIP: new Map([ + ['192.0.2.10', new Map([ + ['alpha.example.com', 3], + ['beta.example.com', 1], + ])], + ]), + requestsTotal: 4, + }); + + const smartProxy = { + getMetrics: () => proxyMetrics, + routeManager: { + getRoutes: () => [ + { + id: 'route-id-only', + match: { + ports: [443], + domains: ['alpha.example.com', 'beta.example.com'], + }, + action: { + type: 'forward', + targets: [{ host: '127.0.0.1', port: 8443 }], + }, + }, + ], + }, + }; + + const manager = new MetricsManager({ smartProxy } as any); + const stats = await manager.getNetworkStats(); + const alpha = stats.domainActivity.find((item) => item.domain === 'alpha.example.com'); + const beta = stats.domainActivity.find((item) => item.domain === 'beta.example.com'); + + expect(alpha).toBeDefined(); + expect(beta).toBeDefined(); + + expect(alpha!.requestCount).toEqual(3); + expect(alpha!.routeCount).toEqual(1); + expect(alpha!.activeConnections).toEqual(3); + expect(alpha!.bytesInPerSecond).toEqual(900); + expect(alpha!.bytesOutPerSecond).toEqual(1800); + + expect(beta!.requestCount).toEqual(1); + expect(beta!.routeCount).toEqual(1); + expect(beta!.activeConnections).toEqual(1); + expect(beta!.bytesInPerSecond).toEqual(300); + expect(beta!.bytesOutPerSecond).toEqual(600); +}); + +export default tap.start(); diff --git a/ts/00_commitinfo_data.ts b/ts/00_commitinfo_data.ts index e77a717..597917b 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.17.8', + version: '13.17.9', 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 125cc96..f4e2053 100644 --- a/ts/monitoring/classes.metricsmanager.ts +++ b/ts/monitoring/classes.metricsmanager.ts @@ -733,16 +733,17 @@ export class MetricsManager { } } - // Map route name → domains from route config + // Map canonical route key → domains from route config const routeDomains = new Map(); if (this.dcRouter.smartProxy) { for (const route of this.dcRouter.smartProxy.routeManager.getRoutes()) { - if (!route.name || !route.match.domains) continue; + const routeKey = route.name || route.id; + if (!routeKey || !route.match.domains) continue; const domains = Array.isArray(route.match.domains) ? route.match.domains : [route.match.domains]; if (domains.length > 0) { - routeDomains.set(route.name, domains); + routeDomains.set(routeKey, domains); } } } @@ -753,23 +754,23 @@ export class MetricsManager { if (entry.domain) allKnownDomains.add(entry.domain); } - // Build reverse map: concrete domain → route name(s) + // Build reverse map: concrete domain → canonical route key(s) const domainToRoutes = new Map(); - for (const [routeName, domains] of routeDomains) { + for (const [routeKey, domains] of routeDomains) { for (const pattern of domains) { if (pattern.includes('*')) { const regex = new RegExp('^' + pattern.replace(/\./g, '\\.').replace(/\*/g, '[^.]+') + '$'); for (const knownDomain of allKnownDomains) { if (regex.test(knownDomain)) { const existing = domainToRoutes.get(knownDomain); - if (existing) { existing.push(routeName); } - else { domainToRoutes.set(knownDomain, [routeName]); } + if (existing) { existing.push(routeKey); } + else { domainToRoutes.set(knownDomain, [routeKey]); } } } } else { const existing = domainToRoutes.get(pattern); - if (existing) { existing.push(routeName); } - else { domainToRoutes.set(pattern, [routeName]); } + if (existing) { existing.push(routeKey); } + else { domainToRoutes.set(pattern, [routeKey]); } } } } @@ -777,10 +778,10 @@ export class MetricsManager { // 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) { + for (const [domain, routeKeys] of domainToRoutes) { const reqs = domainRequestTotals.get(domain) || 0; - for (const routeName of routeNames) { - routeTotalRequests.set(routeName, (routeTotalRequests.get(routeName) || 0) + reqs); + for (const routeKey of routeKeys) { + routeTotalRequests.set(routeKey, (routeTotalRequests.get(routeKey) || 0) + reqs); } } @@ -793,16 +794,16 @@ export class MetricsManager { requestCount: number; }>(); - for (const [domain, routeNames] of domainToRoutes) { + for (const [domain, routeKeys] of domainToRoutes) { const domainReqs = domainRequestTotals.get(domain) || 0; let totalConns = 0; let totalIn = 0; let totalOut = 0; - for (const routeName of routeNames) { - const conns = connectionsByRoute.get(routeName) || 0; - const tp = throughputByRoute.get(routeName) || { in: 0, out: 0 }; - const routeTotal = routeTotalRequests.get(routeName) || 0; + for (const routeKey of routeKeys) { + const conns = connectionsByRoute.get(routeKey) || 0; + const tp = throughputByRoute.get(routeKey) || { in: 0, out: 0 }; + const routeTotal = routeTotalRequests.get(routeKey) || 0; const share = routeTotal > 0 ? domainReqs / routeTotal : 0; totalConns += conns * share; @@ -814,7 +815,7 @@ export class MetricsManager { activeConnections: Math.round(totalConns), bytesInPerSec: totalIn, bytesOutPerSec: totalOut, - routeCount: routeNames.length, + routeCount: routeKeys.length, requestCount: domainReqs, }); } @@ -990,4 +991,4 @@ export class MetricsManager { return { queries }; } -} \ No newline at end of file +} diff --git a/ts_web/00_commitinfo_data.ts b/ts_web/00_commitinfo_data.ts index e77a717..597917b 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.17.8', + version: '13.17.9', 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 9048604..45650ba 100644 --- a/ts_web/elements/network/ops-view-network-activity.ts +++ b/ts_web/elements/network/ops-view-network-activity.ts @@ -374,7 +374,7 @@ export class OpsViewNetworkActivity extends DeesElement { type: 'number', icon: 'lucide:Plug', color: activeConnections > 100 ? '#f59e0b' : '#22c55e', - description: `Total: ${this.networkState.requestsTotal || this.statsState.serverStats?.totalConnections || 0}`, + description: `Total: ${this.formatNumber(this.statsState.serverStats?.totalConnections || 0)} connections`, actions: [ { name: 'View Details',