fix(monitoring): align domain activity metrics with id-keyed route data
This commit is contained in:
@@ -1,5 +1,13 @@
|
|||||||
# Changelog
|
# 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)
|
## 2026-04-14 - 13.17.8 - fix(opsserver)
|
||||||
align certificate status handling with the updated smartproxy response format
|
align certificate status handling with the updated smartproxy response format
|
||||||
|
|
||||||
|
|||||||
@@ -54,7 +54,7 @@
|
|||||||
"@push.rocks/smartnetwork": "^4.6.0",
|
"@push.rocks/smartnetwork": "^4.6.0",
|
||||||
"@push.rocks/smartpath": "^6.0.0",
|
"@push.rocks/smartpath": "^6.0.0",
|
||||||
"@push.rocks/smartpromise": "^4.2.3",
|
"@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/smartradius": "^1.1.1",
|
||||||
"@push.rocks/smartrequest": "^5.0.1",
|
"@push.rocks/smartrequest": "^5.0.1",
|
||||||
"@push.rocks/smartrx": "^3.0.10",
|
"@push.rocks/smartrx": "^3.0.10",
|
||||||
|
|||||||
10
pnpm-lock.yaml
generated
10
pnpm-lock.yaml
generated
@@ -81,8 +81,8 @@ importers:
|
|||||||
specifier: ^4.2.3
|
specifier: ^4.2.3
|
||||||
version: 4.2.3
|
version: 4.2.3
|
||||||
'@push.rocks/smartproxy':
|
'@push.rocks/smartproxy':
|
||||||
specifier: ^27.7.3
|
specifier: ^27.7.4
|
||||||
version: 27.7.3
|
version: 27.7.4
|
||||||
'@push.rocks/smartradius':
|
'@push.rocks/smartradius':
|
||||||
specifier: ^1.1.1
|
specifier: ^1.1.1
|
||||||
version: 1.1.1
|
version: 1.1.1
|
||||||
@@ -1284,8 +1284,8 @@ packages:
|
|||||||
'@push.rocks/smartpromise@4.2.3':
|
'@push.rocks/smartpromise@4.2.3':
|
||||||
resolution: {integrity: sha512-Ycg/TJR+tMt+S3wSFurOpEoW6nXv12QBtKXgBcjMZ4RsdO28geN46U09osPn9N9WuwQy1PkmTV5J/V4F9U8qEw==}
|
resolution: {integrity: sha512-Ycg/TJR+tMt+S3wSFurOpEoW6nXv12QBtKXgBcjMZ4RsdO28geN46U09osPn9N9WuwQy1PkmTV5J/V4F9U8qEw==}
|
||||||
|
|
||||||
'@push.rocks/smartproxy@27.7.3':
|
'@push.rocks/smartproxy@27.7.4':
|
||||||
resolution: {integrity: sha512-1NER+2nM2GWzrMB1xoCzkV9in8a1gWNwOimgE/f2kPYnhy5DsA2XX/KiLGJ0ge3cpCVgoK1nNe3byXQHWvmPbA==}
|
resolution: {integrity: sha512-WY9Jp6Jtqo5WbW29XpATuxzGyLs8LGkAlrycgMN/IdYfvgtEB2HWuztBZCDLFMuD3Qnv4vVdci9s0nF0ZPyJcQ==}
|
||||||
|
|
||||||
'@push.rocks/smartpuppeteer@2.0.5':
|
'@push.rocks/smartpuppeteer@2.0.5':
|
||||||
resolution: {integrity: sha512-yK/qSeWVHIGWRp3c8S5tfdGP6WCKllZC4DR8d8CQlEjszOSBmHtlTdyyqOMBZ/BA4kd+eU5f3A1r4K2tGYty1g==}
|
resolution: {integrity: sha512-yK/qSeWVHIGWRp3c8S5tfdGP6WCKllZC4DR8d8CQlEjszOSBmHtlTdyyqOMBZ/BA4kd+eU5f3A1r4K2tGYty1g==}
|
||||||
@@ -6506,7 +6506,7 @@ snapshots:
|
|||||||
|
|
||||||
'@push.rocks/smartpromise@4.2.3': {}
|
'@push.rocks/smartpromise@4.2.3': {}
|
||||||
|
|
||||||
'@push.rocks/smartproxy@27.7.3':
|
'@push.rocks/smartproxy@27.7.4':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@push.rocks/smartcrypto': 2.0.4
|
'@push.rocks/smartcrypto': 2.0.4
|
||||||
'@push.rocks/smartlog': 3.2.2
|
'@push.rocks/smartlog': 3.2.2
|
||||||
|
|||||||
120
test/test.metricsmanager.route-keys.node.ts
Normal file
120
test/test.metricsmanager.route-keys.node.ts
Normal file
@@ -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<string, number>;
|
||||||
|
throughputByRoute: Map<string, { in: number; out: number }>;
|
||||||
|
domainRequestsByIP: Map<string, Map<string, number>>;
|
||||||
|
requestsTotal?: number;
|
||||||
|
}) {
|
||||||
|
return {
|
||||||
|
connections: {
|
||||||
|
active: () => 0,
|
||||||
|
total: () => 0,
|
||||||
|
byRoute: () => args.connectionsByRoute,
|
||||||
|
byIP: () => new Map<string, number>(),
|
||||||
|
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<string, { in: number; out: number }>(),
|
||||||
|
},
|
||||||
|
requests: {
|
||||||
|
perSecond: () => 0,
|
||||||
|
perMinute: () => 0,
|
||||||
|
total: () => args.requestsTotal || 0,
|
||||||
|
},
|
||||||
|
totals: {
|
||||||
|
bytesIn: () => 0,
|
||||||
|
bytesOut: () => 0,
|
||||||
|
connections: () => 0,
|
||||||
|
},
|
||||||
|
backends: {
|
||||||
|
byBackend: () => new Map<string, any>(),
|
||||||
|
protocols: () => new Map<string, string>(),
|
||||||
|
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();
|
||||||
@@ -3,6 +3,6 @@
|
|||||||
*/
|
*/
|
||||||
export const commitinfo = {
|
export const commitinfo = {
|
||||||
name: '@serve.zone/dcrouter',
|
name: '@serve.zone/dcrouter',
|
||||||
version: '13.17.8',
|
version: '13.17.9',
|
||||||
description: 'A multifaceted routing service handling mail and SMS delivery functions.'
|
description: 'A multifaceted routing service handling mail and SMS delivery functions.'
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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<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;
|
const routeKey = route.name || route.id;
|
||||||
|
if (!routeKey || !route.match.domains) continue;
|
||||||
const domains = Array.isArray(route.match.domains)
|
const domains = Array.isArray(route.match.domains)
|
||||||
? route.match.domains
|
? route.match.domains
|
||||||
: [route.match.domains];
|
: [route.match.domains];
|
||||||
if (domains.length > 0) {
|
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);
|
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<string, string[]>();
|
const domainToRoutes = new Map<string, string[]>();
|
||||||
for (const [routeName, domains] of routeDomains) {
|
for (const [routeKey, domains] of routeDomains) {
|
||||||
for (const pattern of domains) {
|
for (const pattern of domains) {
|
||||||
if (pattern.includes('*')) {
|
if (pattern.includes('*')) {
|
||||||
const regex = new RegExp('^' + pattern.replace(/\./g, '\\.').replace(/\*/g, '[^.]+') + '$');
|
const regex = new RegExp('^' + pattern.replace(/\./g, '\\.').replace(/\*/g, '[^.]+') + '$');
|
||||||
for (const knownDomain of allKnownDomains) {
|
for (const knownDomain of allKnownDomains) {
|
||||||
if (regex.test(knownDomain)) {
|
if (regex.test(knownDomain)) {
|
||||||
const existing = domainToRoutes.get(knownDomain);
|
const existing = domainToRoutes.get(knownDomain);
|
||||||
if (existing) { existing.push(routeName); }
|
if (existing) { existing.push(routeKey); }
|
||||||
else { domainToRoutes.set(knownDomain, [routeName]); }
|
else { domainToRoutes.set(knownDomain, [routeKey]); }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
const existing = domainToRoutes.get(pattern);
|
const existing = domainToRoutes.get(pattern);
|
||||||
if (existing) { existing.push(routeName); }
|
if (existing) { existing.push(routeKey); }
|
||||||
else { domainToRoutes.set(pattern, [routeName]); }
|
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
|
// For each route, compute the total request count across all its resolved domains
|
||||||
// so we can distribute throughput/connections proportionally
|
// so we can distribute throughput/connections proportionally
|
||||||
const routeTotalRequests = new Map<string, number>();
|
const routeTotalRequests = new Map<string, number>();
|
||||||
for (const [domain, routeNames] of domainToRoutes) {
|
for (const [domain, routeKeys] of domainToRoutes) {
|
||||||
const reqs = domainRequestTotals.get(domain) || 0;
|
const reqs = domainRequestTotals.get(domain) || 0;
|
||||||
for (const routeName of routeNames) {
|
for (const routeKey of routeKeys) {
|
||||||
routeTotalRequests.set(routeName, (routeTotalRequests.get(routeName) || 0) + reqs);
|
routeTotalRequests.set(routeKey, (routeTotalRequests.get(routeKey) || 0) + reqs);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -793,16 +794,16 @@ export class MetricsManager {
|
|||||||
requestCount: number;
|
requestCount: number;
|
||||||
}>();
|
}>();
|
||||||
|
|
||||||
for (const [domain, routeNames] of domainToRoutes) {
|
for (const [domain, routeKeys] of domainToRoutes) {
|
||||||
const domainReqs = domainRequestTotals.get(domain) || 0;
|
const domainReqs = domainRequestTotals.get(domain) || 0;
|
||||||
let totalConns = 0;
|
let totalConns = 0;
|
||||||
let totalIn = 0;
|
let totalIn = 0;
|
||||||
let totalOut = 0;
|
let totalOut = 0;
|
||||||
|
|
||||||
for (const routeName of routeNames) {
|
for (const routeKey of routeKeys) {
|
||||||
const conns = connectionsByRoute.get(routeName) || 0;
|
const conns = connectionsByRoute.get(routeKey) || 0;
|
||||||
const tp = throughputByRoute.get(routeName) || { in: 0, out: 0 };
|
const tp = throughputByRoute.get(routeKey) || { in: 0, out: 0 };
|
||||||
const routeTotal = routeTotalRequests.get(routeName) || 0;
|
const routeTotal = routeTotalRequests.get(routeKey) || 0;
|
||||||
|
|
||||||
const share = routeTotal > 0 ? domainReqs / routeTotal : 0;
|
const share = routeTotal > 0 ? domainReqs / routeTotal : 0;
|
||||||
totalConns += conns * share;
|
totalConns += conns * share;
|
||||||
@@ -814,7 +815,7 @@ export class MetricsManager {
|
|||||||
activeConnections: Math.round(totalConns),
|
activeConnections: Math.round(totalConns),
|
||||||
bytesInPerSec: totalIn,
|
bytesInPerSec: totalIn,
|
||||||
bytesOutPerSec: totalOut,
|
bytesOutPerSec: totalOut,
|
||||||
routeCount: routeNames.length,
|
routeCount: routeKeys.length,
|
||||||
requestCount: domainReqs,
|
requestCount: domainReqs,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,6 +3,6 @@
|
|||||||
*/
|
*/
|
||||||
export const commitinfo = {
|
export const commitinfo = {
|
||||||
name: '@serve.zone/dcrouter',
|
name: '@serve.zone/dcrouter',
|
||||||
version: '13.17.8',
|
version: '13.17.9',
|
||||||
description: 'A multifaceted routing service handling mail and SMS delivery functions.'
|
description: 'A multifaceted routing service handling mail and SMS delivery functions.'
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -374,7 +374,7 @@ export class OpsViewNetworkActivity extends DeesElement {
|
|||||||
type: 'number',
|
type: 'number',
|
||||||
icon: 'lucide:Plug',
|
icon: 'lucide:Plug',
|
||||||
color: activeConnections > 100 ? '#f59e0b' : '#22c55e',
|
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: [
|
actions: [
|
||||||
{
|
{
|
||||||
name: 'View Details',
|
name: 'View Details',
|
||||||
|
|||||||
Reference in New Issue
Block a user