Compare commits

...

2 Commits

Author SHA1 Message Date
9a378ae87f v13.17.9
Some checks failed
Docker (tags) / security (push) Failing after 3s
Docker (tags) / test (push) Has been skipped
Docker (tags) / release (push) Has been skipped
Docker (tags) / metadata (push) Has been skipped
2026-04-14 09:33:41 +00:00
58fbc2b1e4 fix(monitoring): align domain activity metrics with id-keyed route data 2026-04-14 09:33:41 +00:00
8 changed files with 158 additions and 29 deletions

View File

@@ -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

View File

@@ -1,7 +1,7 @@
{
"name": "@serve.zone/dcrouter",
"private": false,
"version": "13.17.8",
"version": "13.17.9",
"description": "A multifaceted routing service handling mail and SMS delivery functions.",
"type": "module",
"exports": {
@@ -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",

10
pnpm-lock.yaml generated
View File

@@ -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

View 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();

View File

@@ -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.'
}

View File

@@ -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[]>();
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<string, string[]>();
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<string, number>();
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 };
}
}
}

View File

@@ -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.'
}

View File

@@ -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',