Compare commits
2 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 9a378ae87f | |||
| 58fbc2b1e4 |
@@ -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
|
||||
|
||||
|
||||
@@ -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
10
pnpm-lock.yaml
generated
@@ -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
|
||||
|
||||
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 = {
|
||||
name: '@serve.zone/dcrouter',
|
||||
version: '13.17.8',
|
||||
version: '13.17.9',
|
||||
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[]>();
|
||||
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 };
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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.'
|
||||
}
|
||||
|
||||
@@ -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',
|
||||
|
||||
Reference in New Issue
Block a user