feat(monitoring): improve network activity metrics with live domain request rates and backend identifiers
This commit is contained in:
@@ -1,5 +1,12 @@
|
|||||||
# Changelog
|
# Changelog
|
||||||
|
|
||||||
|
## 2026-04-25 - 13.21.0 - feat(monitoring)
|
||||||
|
improve network activity metrics with live domain request rates and backend identifiers
|
||||||
|
|
||||||
|
- use SmartProxy per-domain live request rates to rank and attribute domain activity metrics, while retaining lifetime request totals as fallback data
|
||||||
|
- separate aggregate backend rows from protocol cache rows with stable ids so cached protocol entries no longer duplicate active backend connection counts
|
||||||
|
- expose frontend and backend protocol distributions plus aggregated connectionCount fields through ops and web network views
|
||||||
|
|
||||||
## 2026-04-17 - 13.20.2 - fix(vpn)
|
## 2026-04-17 - 13.20.2 - fix(vpn)
|
||||||
handle VPN forwarding mode downgrades and support runtime VPN config updates
|
handle VPN forwarding mode downgrades and support runtime VPN config updates
|
||||||
|
|
||||||
|
|||||||
+1
-1
@@ -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.4",
|
"@push.rocks/smartproxy": "^27.8.0",
|
||||||
"@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",
|
||||||
|
|||||||
Generated
+5
-5
@@ -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.4
|
specifier: ^27.8.0
|
||||||
version: 27.7.4
|
version: 27.8.0
|
||||||
'@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.4':
|
'@push.rocks/smartproxy@27.8.0':
|
||||||
resolution: {integrity: sha512-WY9Jp6Jtqo5WbW29XpATuxzGyLs8LGkAlrycgMN/IdYfvgtEB2HWuztBZCDLFMuD3Qnv4vVdci9s0nF0ZPyJcQ==}
|
resolution: {integrity: sha512-/+rfSAz9hRopuRRBwaI/VVtFTLNemnh9RIf0YAPRhrLCL4WGJXkjnpX4Zi6W1AAPDU2wlz7Zm0F6TO+nXLqP5w==}
|
||||||
|
|
||||||
'@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==}
|
||||||
@@ -6512,7 +6512,7 @@ snapshots:
|
|||||||
|
|
||||||
'@push.rocks/smartpromise@4.2.3': {}
|
'@push.rocks/smartpromise@4.2.3': {}
|
||||||
|
|
||||||
'@push.rocks/smartproxy@27.7.4':
|
'@push.rocks/smartproxy@27.8.0':
|
||||||
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
|
||||||
|
|||||||
@@ -101,7 +101,13 @@ tap.test('should login as admin for email API tests', async () => {
|
|||||||
password: 'admin',
|
password: 'admin',
|
||||||
});
|
});
|
||||||
|
|
||||||
adminIdentity = response.identity;
|
const responseIdentity = response.identity;
|
||||||
|
expect(responseIdentity).toBeDefined();
|
||||||
|
if (!responseIdentity) {
|
||||||
|
throw new Error('Expected admin login response to include identity');
|
||||||
|
}
|
||||||
|
|
||||||
|
adminIdentity = responseIdentity;
|
||||||
expect(adminIdentity.jwt).toBeTruthy();
|
expect(adminIdentity.jwt).toBeTruthy();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -103,6 +103,9 @@ tap.test('ErrorHandler should properly handle and format errors', async () => {
|
|||||||
}, 'TEST_EXECUTION_ERROR', { operation: 'testExecution' });
|
}, 'TEST_EXECUTION_ERROR', { operation: 'testExecution' });
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
expect(error).toBeInstanceOf(PlatformError);
|
expect(error).toBeInstanceOf(PlatformError);
|
||||||
|
if (!(error instanceof PlatformError)) {
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
expect(error.code).toEqual('TEST_EXECUTION_ERROR');
|
expect(error.code).toEqual('TEST_EXECUTION_ERROR');
|
||||||
expect(error.context.operation).toEqual('testExecution');
|
expect(error.context.operation).toEqual('testExecution');
|
||||||
}
|
}
|
||||||
@@ -197,6 +200,9 @@ tap.test('Error retry utilities should work correctly', async () => {
|
|||||||
}
|
}
|
||||||
);
|
);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
if (!(error instanceof Error)) {
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
expect(error.message).toEqual('Critical error');
|
expect(error.message).toEqual('Critical error');
|
||||||
expect(attempts).toEqual(1); // Should only attempt once
|
expect(attempts).toEqual(1); // Should only attempt once
|
||||||
}
|
}
|
||||||
@@ -262,6 +268,9 @@ tap.test('Error handling can be combined with retry for robust operations', asyn
|
|||||||
// Should not reach here
|
// Should not reach here
|
||||||
expect(false).toEqual(true);
|
expect(false).toEqual(true);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
if (!(error instanceof Error)) {
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
expect(error.message).toContain('Flaky failure');
|
expect(error.message).toContain('Flaky failure');
|
||||||
expect(flaky.counter).toEqual(3); // Initial + 2 retries = 3 attempts
|
expect(flaky.counter).toEqual(3); // Initial + 2 retries = 3 attempts
|
||||||
}
|
}
|
||||||
|
|||||||
+25
-13
@@ -27,16 +27,20 @@ tap.test('should login with admin credentials and receive JWT', async () => {
|
|||||||
username: 'admin',
|
username: 'admin',
|
||||||
password: 'admin'
|
password: 'admin'
|
||||||
});
|
});
|
||||||
|
|
||||||
expect(response).toHaveProperty('identity');
|
expect(response).toHaveProperty('identity');
|
||||||
expect(response.identity).toHaveProperty('jwt');
|
const responseIdentity = response.identity;
|
||||||
expect(response.identity).toHaveProperty('userId');
|
if (!responseIdentity) {
|
||||||
expect(response.identity).toHaveProperty('name');
|
throw new Error('Expected admin login response to include identity');
|
||||||
expect(response.identity).toHaveProperty('expiresAt');
|
}
|
||||||
expect(response.identity).toHaveProperty('role');
|
expect(responseIdentity).toHaveProperty('jwt');
|
||||||
expect(response.identity.role).toEqual('admin');
|
expect(responseIdentity).toHaveProperty('userId');
|
||||||
|
expect(responseIdentity).toHaveProperty('name');
|
||||||
identity = response.identity;
|
expect(responseIdentity).toHaveProperty('expiresAt');
|
||||||
|
expect(responseIdentity).toHaveProperty('role');
|
||||||
|
expect(responseIdentity.role).toEqual('admin');
|
||||||
|
|
||||||
|
identity = responseIdentity;
|
||||||
console.log('JWT:', identity.jwt);
|
console.log('JWT:', identity.jwt);
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -53,7 +57,11 @@ tap.test('should verify valid JWT identity', async () => {
|
|||||||
expect(response).toHaveProperty('valid');
|
expect(response).toHaveProperty('valid');
|
||||||
expect(response.valid).toBeTrue();
|
expect(response.valid).toBeTrue();
|
||||||
expect(response).toHaveProperty('identity');
|
expect(response).toHaveProperty('identity');
|
||||||
expect(response.identity.userId).toEqual(identity.userId);
|
const responseIdentity = response.identity;
|
||||||
|
if (!responseIdentity) {
|
||||||
|
throw new Error('Expected verify response to include identity');
|
||||||
|
}
|
||||||
|
expect(responseIdentity.userId).toEqual(identity.userId);
|
||||||
});
|
});
|
||||||
|
|
||||||
tap.test('should reject invalid JWT', async () => {
|
tap.test('should reject invalid JWT', async () => {
|
||||||
@@ -86,8 +94,12 @@ tap.test('should verify JWT matches identity data', async () => {
|
|||||||
|
|
||||||
expect(response).toHaveProperty('valid');
|
expect(response).toHaveProperty('valid');
|
||||||
expect(response.valid).toBeTrue();
|
expect(response.valid).toBeTrue();
|
||||||
expect(response.identity.expiresAt).toEqual(identity.expiresAt);
|
const responseIdentity = response.identity;
|
||||||
expect(response.identity.userId).toEqual(identity.userId);
|
if (!responseIdentity) {
|
||||||
|
throw new Error('Expected verify response to include identity');
|
||||||
|
}
|
||||||
|
expect(responseIdentity.expiresAt).toEqual(identity.expiresAt);
|
||||||
|
expect(responseIdentity.userId).toEqual(identity.userId);
|
||||||
});
|
});
|
||||||
|
|
||||||
tap.test('should handle logout', async () => {
|
tap.test('should handle logout', async () => {
|
||||||
@@ -129,4 +141,4 @@ tap.test('should stop DCRouter', async () => {
|
|||||||
await testDcRouter.stop();
|
await testDcRouter.stop();
|
||||||
});
|
});
|
||||||
|
|
||||||
export default tap.start();
|
export default tap.start();
|
||||||
|
|||||||
@@ -18,6 +18,9 @@ function createProxyMetrics(args: {
|
|||||||
connectionsByRoute: Map<string, number>;
|
connectionsByRoute: Map<string, number>;
|
||||||
throughputByRoute: Map<string, { in: number; out: number }>;
|
throughputByRoute: Map<string, { in: number; out: number }>;
|
||||||
domainRequestsByIP: Map<string, Map<string, number>>;
|
domainRequestsByIP: Map<string, Map<string, number>>;
|
||||||
|
domainRequestRates?: Map<string, { perSecond: number; lastMinute: number }>;
|
||||||
|
backendMetrics?: Map<string, any>;
|
||||||
|
protocolCache?: any[];
|
||||||
requestsTotal?: number;
|
requestsTotal?: number;
|
||||||
}) {
|
}) {
|
||||||
return {
|
return {
|
||||||
@@ -45,6 +48,7 @@ function createProxyMetrics(args: {
|
|||||||
perSecond: () => 0,
|
perSecond: () => 0,
|
||||||
perMinute: () => 0,
|
perMinute: () => 0,
|
||||||
total: () => args.requestsTotal || 0,
|
total: () => args.requestsTotal || 0,
|
||||||
|
byDomain: () => args.domainRequestRates || new Map<string, { perSecond: number; lastMinute: number }>(),
|
||||||
},
|
},
|
||||||
totals: {
|
totals: {
|
||||||
bytesIn: () => 0,
|
bytesIn: () => 0,
|
||||||
@@ -52,10 +56,10 @@ function createProxyMetrics(args: {
|
|||||||
connections: () => 0,
|
connections: () => 0,
|
||||||
},
|
},
|
||||||
backends: {
|
backends: {
|
||||||
byBackend: () => new Map<string, any>(),
|
byBackend: () => args.backendMetrics || new Map<string, any>(),
|
||||||
protocols: () => new Map<string, string>(),
|
protocols: () => new Map<string, string>(),
|
||||||
topByErrors: () => [],
|
topByErrors: () => [],
|
||||||
detectedProtocols: () => [],
|
detectedProtocols: () => args.protocolCache || [],
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@@ -117,4 +121,122 @@ tap.test('MetricsManager joins domain activity to id-keyed route metrics', async
|
|||||||
expect(beta!.bytesOutPerSecond).toEqual(600);
|
expect(beta!.bytesOutPerSecond).toEqual(600);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
tap.test('MetricsManager prefers live domain request rates for current activity', async () => {
|
||||||
|
const proxyMetrics = createProxyMetrics({
|
||||||
|
connectionsByRoute: new Map([
|
||||||
|
['route-id-only', 10],
|
||||||
|
]),
|
||||||
|
throughputByRoute: new Map([
|
||||||
|
['route-id-only', { in: 1000, out: 1000 }],
|
||||||
|
]),
|
||||||
|
domainRequestsByIP: new Map([
|
||||||
|
['192.0.2.10', new Map([
|
||||||
|
['alpha.example.com', 1000],
|
||||||
|
['beta.example.com', 1],
|
||||||
|
])],
|
||||||
|
]),
|
||||||
|
domainRequestRates: new Map([
|
||||||
|
['alpha.example.com', { perSecond: 0, lastMinute: 0 }],
|
||||||
|
['beta.example.com', { perSecond: 5, lastMinute: 60 }],
|
||||||
|
]),
|
||||||
|
});
|
||||||
|
|
||||||
|
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!.activeConnections).toEqual(0);
|
||||||
|
expect(alpha!.requestsPerSecond).toEqual(0);
|
||||||
|
expect(beta!.activeConnections).toEqual(10);
|
||||||
|
expect(beta!.requestsPerSecond).toEqual(5);
|
||||||
|
expect(beta!.bytesInPerSecond).toEqual(1000);
|
||||||
|
});
|
||||||
|
|
||||||
|
tap.test('MetricsManager does not duplicate backend active counts onto protocol cache rows', async () => {
|
||||||
|
const proxyMetrics = createProxyMetrics({
|
||||||
|
connectionsByRoute: new Map(),
|
||||||
|
throughputByRoute: new Map(),
|
||||||
|
domainRequestsByIP: new Map(),
|
||||||
|
backendMetrics: new Map([
|
||||||
|
['192.0.2.1:443', {
|
||||||
|
protocol: 'h2',
|
||||||
|
activeConnections: 257,
|
||||||
|
totalConnections: 1000,
|
||||||
|
connectErrors: 1,
|
||||||
|
handshakeErrors: 2,
|
||||||
|
requestErrors: 3,
|
||||||
|
avgConnectTimeMs: 4,
|
||||||
|
poolHitRate: 0.9,
|
||||||
|
h2Failures: 5,
|
||||||
|
}],
|
||||||
|
]),
|
||||||
|
protocolCache: [
|
||||||
|
{
|
||||||
|
host: '192.0.2.1',
|
||||||
|
port: 443,
|
||||||
|
domain: 'alpha.example.com',
|
||||||
|
protocol: 'h2',
|
||||||
|
h2Suppressed: false,
|
||||||
|
h3Suppressed: false,
|
||||||
|
h2CooldownRemainingSecs: null,
|
||||||
|
h3CooldownRemainingSecs: null,
|
||||||
|
h2ConsecutiveFailures: null,
|
||||||
|
h3ConsecutiveFailures: null,
|
||||||
|
h3Port: null,
|
||||||
|
ageSecs: 1,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
host: '192.0.2.1',
|
||||||
|
port: 443,
|
||||||
|
domain: 'beta.example.com',
|
||||||
|
protocol: 'h2',
|
||||||
|
h2Suppressed: false,
|
||||||
|
h3Suppressed: false,
|
||||||
|
h2CooldownRemainingSecs: null,
|
||||||
|
h3CooldownRemainingSecs: null,
|
||||||
|
h2ConsecutiveFailures: null,
|
||||||
|
h3ConsecutiveFailures: null,
|
||||||
|
h3Port: null,
|
||||||
|
ageSecs: 1,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
});
|
||||||
|
|
||||||
|
const smartProxy = {
|
||||||
|
getMetrics: () => proxyMetrics,
|
||||||
|
routeManager: {
|
||||||
|
getRoutes: () => [],
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
const manager = new MetricsManager({ smartProxy } as any);
|
||||||
|
const stats = await manager.getNetworkStats();
|
||||||
|
const aggregate = stats.backends.find((item) => item.id === 'backend:192.0.2.1:443');
|
||||||
|
const cacheRows = stats.backends.filter((item) => item.id?.startsWith('cache:'));
|
||||||
|
|
||||||
|
expect(aggregate!.activeConnections).toEqual(257);
|
||||||
|
expect(cacheRows.length).toEqual(2);
|
||||||
|
expect(cacheRows.every((item) => item.activeConnections === 0)).toBeTrue();
|
||||||
|
});
|
||||||
|
|
||||||
export default tap.start();
|
export default tap.start();
|
||||||
|
|||||||
@@ -29,7 +29,11 @@ tap.test('should login as admin', async () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
expect(response).toHaveProperty('identity');
|
expect(response).toHaveProperty('identity');
|
||||||
adminIdentity = response.identity;
|
const responseIdentity = response.identity;
|
||||||
|
if (!responseIdentity) {
|
||||||
|
throw new Error('Expected admin login response to include identity');
|
||||||
|
}
|
||||||
|
adminIdentity = responseIdentity;
|
||||||
});
|
});
|
||||||
|
|
||||||
tap.test('should respond to health status request', async () => {
|
tap.test('should respond to health status request', async () => {
|
||||||
|
|||||||
@@ -29,7 +29,11 @@ tap.test('should login as admin', async () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
expect(response).toHaveProperty('identity');
|
expect(response).toHaveProperty('identity');
|
||||||
adminIdentity = response.identity;
|
const responseIdentity = response.identity;
|
||||||
|
if (!responseIdentity) {
|
||||||
|
throw new Error('Expected admin login response to include identity');
|
||||||
|
}
|
||||||
|
adminIdentity = responseIdentity;
|
||||||
console.log('Admin logged in with JWT');
|
console.log('Admin logged in with JWT');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -35,7 +35,11 @@ tap.test('should login as admin', async () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
expect(response).toHaveProperty('identity');
|
expect(response).toHaveProperty('identity');
|
||||||
adminIdentity = response.identity;
|
const responseIdentity = response.identity;
|
||||||
|
if (!responseIdentity) {
|
||||||
|
throw new Error('Expected admin login response to include identity');
|
||||||
|
}
|
||||||
|
adminIdentity = responseIdentity;
|
||||||
});
|
});
|
||||||
|
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
|
|||||||
@@ -3,6 +3,6 @@
|
|||||||
*/
|
*/
|
||||||
export const commitinfo = {
|
export const commitinfo = {
|
||||||
name: '@serve.zone/dcrouter',
|
name: '@serve.zone/dcrouter',
|
||||||
version: '13.20.2',
|
version: '13.21.0',
|
||||||
description: 'A multifaceted routing service handling mail and SMS delivery functions.'
|
description: 'A multifaceted routing service handling mail and SMS delivery functions.'
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -560,7 +560,9 @@ export class MetricsManager {
|
|||||||
requestsPerSecond: 0,
|
requestsPerSecond: 0,
|
||||||
requestsTotal: 0,
|
requestsTotal: 0,
|
||||||
backends: [] as Array<any>,
|
backends: [] as Array<any>,
|
||||||
domainActivity: [] as Array<{ domain: string; bytesInPerSecond: number; bytesOutPerSecond: number; activeConnections: number; routeCount: number; requestCount: number }>,
|
domainActivity: [] as Array<{ domain: string; bytesInPerSecond: number; bytesOutPerSecond: number; activeConnections: number; routeCount: number; requestCount: number; requestsPerSecond?: number; requestsLastMinute?: number }>,
|
||||||
|
frontendProtocols: null,
|
||||||
|
backendProtocols: null,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -592,6 +594,7 @@ export class MetricsManager {
|
|||||||
// Get HTTP request rates
|
// Get HTTP request rates
|
||||||
const requestsPerSecond = proxyMetrics.requests.perSecond();
|
const requestsPerSecond = proxyMetrics.requests.perSecond();
|
||||||
const requestsTotal = proxyMetrics.requests.total();
|
const requestsTotal = proxyMetrics.requests.total();
|
||||||
|
const domainRequestRates = proxyMetrics.requests.byDomain();
|
||||||
|
|
||||||
// Get frontend/backend protocol distribution
|
// Get frontend/backend protocol distribution
|
||||||
const frontendProtocols = proxyMetrics.connections.frontendProtocols() ?? null;
|
const frontendProtocols = proxyMetrics.connections.frontendProtocols() ?? null;
|
||||||
@@ -619,47 +622,48 @@ export class MetricsManager {
|
|||||||
const seenCacheKeys = new Set<string>();
|
const seenCacheKeys = new Set<string>();
|
||||||
|
|
||||||
for (const [key, bm] of backendMetrics) {
|
for (const [key, bm] of backendMetrics) {
|
||||||
|
backends.push({
|
||||||
|
id: `backend:${key}`,
|
||||||
|
backend: key,
|
||||||
|
domain: null,
|
||||||
|
protocol: bm.protocol,
|
||||||
|
activeConnections: bm.activeConnections,
|
||||||
|
totalConnections: bm.totalConnections,
|
||||||
|
connectErrors: bm.connectErrors,
|
||||||
|
handshakeErrors: bm.handshakeErrors,
|
||||||
|
requestErrors: bm.requestErrors,
|
||||||
|
avgConnectTimeMs: Math.round(bm.avgConnectTimeMs * 10) / 10,
|
||||||
|
poolHitRate: Math.round(bm.poolHitRate * 1000) / 1000,
|
||||||
|
h2Failures: bm.h2Failures,
|
||||||
|
h2Suppressed: false,
|
||||||
|
h3Suppressed: false,
|
||||||
|
h2CooldownRemainingSecs: null,
|
||||||
|
h3CooldownRemainingSecs: null,
|
||||||
|
h2ConsecutiveFailures: null,
|
||||||
|
h3ConsecutiveFailures: null,
|
||||||
|
h3Port: null,
|
||||||
|
cacheAgeSecs: null,
|
||||||
|
});
|
||||||
|
|
||||||
const cacheEntries = cacheByBackend.get(key);
|
const cacheEntries = cacheByBackend.get(key);
|
||||||
if (!cacheEntries || cacheEntries.length === 0) {
|
if (cacheEntries && cacheEntries.length > 0) {
|
||||||
// No protocol cache entry — emit one row with backend metrics only
|
// Protocol cache rows are domain-scoped metadata, not live backend connections.
|
||||||
backends.push({
|
|
||||||
backend: key,
|
|
||||||
domain: null,
|
|
||||||
protocol: bm.protocol,
|
|
||||||
activeConnections: bm.activeConnections,
|
|
||||||
totalConnections: bm.totalConnections,
|
|
||||||
connectErrors: bm.connectErrors,
|
|
||||||
handshakeErrors: bm.handshakeErrors,
|
|
||||||
requestErrors: bm.requestErrors,
|
|
||||||
avgConnectTimeMs: Math.round(bm.avgConnectTimeMs * 10) / 10,
|
|
||||||
poolHitRate: Math.round(bm.poolHitRate * 1000) / 1000,
|
|
||||||
h2Failures: bm.h2Failures,
|
|
||||||
h2Suppressed: false,
|
|
||||||
h3Suppressed: false,
|
|
||||||
h2CooldownRemainingSecs: null,
|
|
||||||
h3CooldownRemainingSecs: null,
|
|
||||||
h2ConsecutiveFailures: null,
|
|
||||||
h3ConsecutiveFailures: null,
|
|
||||||
h3Port: null,
|
|
||||||
cacheAgeSecs: null,
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
// One row per domain, each enriched with the shared backend metrics
|
|
||||||
for (const cache of cacheEntries) {
|
for (const cache of cacheEntries) {
|
||||||
const compositeKey = `${cache.host}:${cache.port}:${cache.domain ?? ''}`;
|
const compositeKey = `${cache.host}:${cache.port}:${cache.domain ?? ''}`;
|
||||||
seenCacheKeys.add(compositeKey);
|
seenCacheKeys.add(compositeKey);
|
||||||
backends.push({
|
backends.push({
|
||||||
|
id: `cache:${compositeKey}`,
|
||||||
backend: key,
|
backend: key,
|
||||||
domain: cache.domain ?? null,
|
domain: cache.domain ?? null,
|
||||||
protocol: cache.protocol ?? bm.protocol,
|
protocol: cache.protocol ?? bm.protocol,
|
||||||
activeConnections: bm.activeConnections,
|
activeConnections: 0,
|
||||||
totalConnections: bm.totalConnections,
|
totalConnections: 0,
|
||||||
connectErrors: bm.connectErrors,
|
connectErrors: 0,
|
||||||
handshakeErrors: bm.handshakeErrors,
|
handshakeErrors: 0,
|
||||||
requestErrors: bm.requestErrors,
|
requestErrors: 0,
|
||||||
avgConnectTimeMs: Math.round(bm.avgConnectTimeMs * 10) / 10,
|
avgConnectTimeMs: 0,
|
||||||
poolHitRate: Math.round(bm.poolHitRate * 1000) / 1000,
|
poolHitRate: 0,
|
||||||
h2Failures: bm.h2Failures,
|
h2Failures: 0,
|
||||||
h2Suppressed: cache.h2Suppressed,
|
h2Suppressed: cache.h2Suppressed,
|
||||||
h3Suppressed: cache.h3Suppressed,
|
h3Suppressed: cache.h3Suppressed,
|
||||||
h2CooldownRemainingSecs: cache.h2CooldownRemainingSecs,
|
h2CooldownRemainingSecs: cache.h2CooldownRemainingSecs,
|
||||||
@@ -678,6 +682,7 @@ export class MetricsManager {
|
|||||||
const compositeKey = `${entry.host}:${entry.port}:${entry.domain ?? ''}`;
|
const compositeKey = `${entry.host}:${entry.port}:${entry.domain ?? ''}`;
|
||||||
if (!seenCacheKeys.has(compositeKey)) {
|
if (!seenCacheKeys.has(compositeKey)) {
|
||||||
backends.push({
|
backends.push({
|
||||||
|
id: `cache:${compositeKey}`,
|
||||||
backend: `${entry.host}:${entry.port}`,
|
backend: `${entry.host}:${entry.port}`,
|
||||||
domain: entry.domain,
|
domain: entry.domain,
|
||||||
protocol: entry.protocol,
|
protocol: entry.protocol,
|
||||||
@@ -750,6 +755,9 @@ export class MetricsManager {
|
|||||||
|
|
||||||
// Resolve wildcards using domains seen in request metrics
|
// Resolve wildcards using domains seen in request metrics
|
||||||
const allKnownDomains = new Set<string>(domainRequestTotals.keys());
|
const allKnownDomains = new Set<string>(domainRequestTotals.keys());
|
||||||
|
for (const domain of domainRequestRates.keys()) {
|
||||||
|
allKnownDomains.add(domain);
|
||||||
|
}
|
||||||
for (const entry of protocolCache) {
|
for (const entry of protocolCache) {
|
||||||
if (entry.domain) allKnownDomains.add(entry.domain);
|
if (entry.domain) allKnownDomains.add(entry.domain);
|
||||||
}
|
}
|
||||||
@@ -775,11 +783,20 @@ export class MetricsManager {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// For each route, compute the total request count across all its resolved domains
|
const hasLiveDomainRates = domainRequestRates.size > 0;
|
||||||
// so we can distribute throughput/connections proportionally
|
const getDomainWeight = (domain: string): number => {
|
||||||
|
const liveRate = domainRequestRates.get(domain);
|
||||||
|
return hasLiveDomainRates
|
||||||
|
? (liveRate?.lastMinute ?? 0)
|
||||||
|
: (domainRequestTotals.get(domain) || 0);
|
||||||
|
};
|
||||||
|
|
||||||
|
// For each route, compute the total activity weight across all resolved domains
|
||||||
|
// so we can distribute route-level throughput/connections. Prefer live domain
|
||||||
|
// request rates from SmartProxy 27.8+, falling back to lifetime counters.
|
||||||
const routeTotalRequests = new Map<string, number>();
|
const routeTotalRequests = new Map<string, number>();
|
||||||
for (const [domain, routeKeys] of domainToRoutes) {
|
for (const [domain, routeKeys] of domainToRoutes) {
|
||||||
const reqs = domainRequestTotals.get(domain) || 0;
|
const reqs = getDomainWeight(domain);
|
||||||
for (const routeKey of routeKeys) {
|
for (const routeKey of routeKeys) {
|
||||||
routeTotalRequests.set(routeKey, (routeTotalRequests.get(routeKey) || 0) + reqs);
|
routeTotalRequests.set(routeKey, (routeTotalRequests.get(routeKey) || 0) + reqs);
|
||||||
}
|
}
|
||||||
@@ -792,10 +809,13 @@ export class MetricsManager {
|
|||||||
bytesOutPerSec: number;
|
bytesOutPerSec: number;
|
||||||
routeCount: number;
|
routeCount: number;
|
||||||
requestCount: number;
|
requestCount: number;
|
||||||
|
requestsPerSecond: number;
|
||||||
|
requestsLastMinute: number;
|
||||||
}>();
|
}>();
|
||||||
|
|
||||||
for (const [domain, routeKeys] of domainToRoutes) {
|
for (const [domain, routeKeys] of domainToRoutes) {
|
||||||
const domainReqs = domainRequestTotals.get(domain) || 0;
|
const domainReqs = getDomainWeight(domain);
|
||||||
|
const requestRate = domainRequestRates.get(domain);
|
||||||
let totalConns = 0;
|
let totalConns = 0;
|
||||||
let totalIn = 0;
|
let totalIn = 0;
|
||||||
let totalOut = 0;
|
let totalOut = 0;
|
||||||
@@ -816,7 +836,9 @@ export class MetricsManager {
|
|||||||
bytesInPerSec: totalIn,
|
bytesInPerSec: totalIn,
|
||||||
bytesOutPerSec: totalOut,
|
bytesOutPerSec: totalOut,
|
||||||
routeCount: routeKeys.length,
|
routeCount: routeKeys.length,
|
||||||
requestCount: domainReqs,
|
requestCount: domainRequestTotals.get(domain) || 0,
|
||||||
|
requestsPerSecond: requestRate?.perSecond ?? 0,
|
||||||
|
requestsLastMinute: requestRate?.lastMinute ?? 0,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -828,8 +850,17 @@ export class MetricsManager {
|
|||||||
activeConnections: data.activeConnections,
|
activeConnections: data.activeConnections,
|
||||||
routeCount: data.routeCount,
|
routeCount: data.routeCount,
|
||||||
requestCount: data.requestCount,
|
requestCount: data.requestCount,
|
||||||
|
requestsPerSecond: data.requestsPerSecond,
|
||||||
|
requestsLastMinute: data.requestsLastMinute,
|
||||||
}))
|
}))
|
||||||
.sort((a, b) => (b.bytesInPerSecond + b.bytesOutPerSecond) - (a.bytesInPerSecond + a.bytesOutPerSecond));
|
.sort((a, b) => {
|
||||||
|
if (hasLiveDomainRates) {
|
||||||
|
return (b.requestsPerSecond - a.requestsPerSecond) ||
|
||||||
|
(b.requestsLastMinute - a.requestsLastMinute) ||
|
||||||
|
((b.bytesInPerSecond + b.bytesOutPerSecond) - (a.bytesInPerSecond + a.bytesOutPerSecond));
|
||||||
|
}
|
||||||
|
return (b.bytesInPerSecond + b.bytesOutPerSecond) - (a.bytesInPerSecond + a.bytesOutPerSecond);
|
||||||
|
});
|
||||||
|
|
||||||
return {
|
return {
|
||||||
connectionsByIP,
|
connectionsByIP,
|
||||||
|
|||||||
@@ -50,19 +50,21 @@ export class SecurityHandler {
|
|||||||
localAddress: conn.destination.ip,
|
localAddress: conn.destination.ip,
|
||||||
startTime: conn.startTime,
|
startTime: conn.startTime,
|
||||||
protocol: conn.type === 'http' ? 'https' : conn.type as any,
|
protocol: conn.type === 'http' ? 'https' : conn.type as any,
|
||||||
state: conn.status as any,
|
state: conn.status === 'active' ? 'connected' : conn.status as any,
|
||||||
bytesReceived: (conn as any)._throughputIn || 0,
|
bytesReceived: (conn as any)._throughputIn || 0,
|
||||||
bytesSent: (conn as any)._throughputOut || 0,
|
bytesSent: (conn as any)._throughputOut || 0,
|
||||||
|
connectionCount: conn.bytesTransferred || 1,
|
||||||
}));
|
}));
|
||||||
|
const totalConnections = connectionInfos.reduce((sum, conn) => sum + (conn.connectionCount || 1), 0);
|
||||||
|
|
||||||
const summary = {
|
const summary = {
|
||||||
total: connectionInfos.length,
|
total: totalConnections,
|
||||||
byProtocol: connectionInfos.reduce((acc, conn) => {
|
byProtocol: connectionInfos.reduce((acc, conn) => {
|
||||||
acc[conn.protocol] = (acc[conn.protocol] || 0) + 1;
|
acc[conn.protocol] = (acc[conn.protocol] || 0) + (conn.connectionCount || 1);
|
||||||
return acc;
|
return acc;
|
||||||
}, {} as { [protocol: string]: number }),
|
}, {} as { [protocol: string]: number }),
|
||||||
byState: connectionInfos.reduce((acc, conn) => {
|
byState: connectionInfos.reduce((acc, conn) => {
|
||||||
acc[conn.state] = (acc[conn.state] || 0) + 1;
|
acc[conn.state] = (acc[conn.state] || 0) + (conn.connectionCount || 1);
|
||||||
return acc;
|
return acc;
|
||||||
}, {} as { [state: string]: number }),
|
}, {} as { [state: string]: number }),
|
||||||
};
|
};
|
||||||
@@ -104,6 +106,8 @@ export class SecurityHandler {
|
|||||||
requestsPerSecond: networkStats.requestsPerSecond || 0,
|
requestsPerSecond: networkStats.requestsPerSecond || 0,
|
||||||
requestsTotal: networkStats.requestsTotal || 0,
|
requestsTotal: networkStats.requestsTotal || 0,
|
||||||
backends: networkStats.backends || [],
|
backends: networkStats.backends || [],
|
||||||
|
frontendProtocols: networkStats.frontendProtocols || null,
|
||||||
|
backendProtocols: networkStats.backendProtocols || null,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -120,6 +124,8 @@ export class SecurityHandler {
|
|||||||
requestsPerSecond: 0,
|
requestsPerSecond: 0,
|
||||||
requestsTotal: 0,
|
requestsTotal: 0,
|
||||||
backends: [],
|
backends: [],
|
||||||
|
frontendProtocols: null,
|
||||||
|
backendProtocols: null,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
@@ -335,4 +341,4 @@ export class SecurityHandler {
|
|||||||
limits: [],
|
limits: [],
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -302,6 +302,7 @@ export class StatsHandler {
|
|||||||
startTime: 0,
|
startTime: 0,
|
||||||
bytesIn: tp?.in || 0,
|
bytesIn: tp?.in || 0,
|
||||||
bytesOut: tp?.out || 0,
|
bytesOut: tp?.out || 0,
|
||||||
|
connectionCount: count,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -119,6 +119,8 @@ export interface IConnectionInfo {
|
|||||||
state: 'connecting' | 'connected' | 'authenticated' | 'transmitting' | 'closing';
|
state: 'connecting' | 'connected' | 'authenticated' | 'transmitting' | 'closing';
|
||||||
bytesReceived: number;
|
bytesReceived: number;
|
||||||
bytesSent: number;
|
bytesSent: number;
|
||||||
|
/** Present when the row is an aggregate, e.g. one row per remote IP. */
|
||||||
|
connectionCount?: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface IQueueStatus {
|
export interface IQueueStatus {
|
||||||
@@ -149,7 +151,12 @@ export interface IDomainActivity {
|
|||||||
bytesOutPerSecond: number;
|
bytesOutPerSecond: number;
|
||||||
activeConnections: number;
|
activeConnections: number;
|
||||||
routeCount: number;
|
routeCount: number;
|
||||||
|
/** Lifetime request count when available from SmartProxy. */
|
||||||
requestCount: number;
|
requestCount: number;
|
||||||
|
/** Live HTTP request rate when SmartProxy exposes per-domain rates. */
|
||||||
|
requestsPerSecond?: number;
|
||||||
|
/** HTTP requests over the last minute when SmartProxy exposes per-domain rates. */
|
||||||
|
requestsLastMinute?: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface INetworkMetrics {
|
export interface INetworkMetrics {
|
||||||
@@ -208,9 +215,12 @@ export interface IConnectionDetails {
|
|||||||
startTime: number;
|
startTime: number;
|
||||||
bytesIn: number;
|
bytesIn: number;
|
||||||
bytesOut: number;
|
bytesOut: number;
|
||||||
|
/** Present when the row is an aggregate, e.g. one row per remote IP. */
|
||||||
|
connectionCount?: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface IBackendInfo {
|
export interface IBackendInfo {
|
||||||
|
id?: string;
|
||||||
backend: string;
|
backend: string;
|
||||||
domain: string | null;
|
domain: string | null;
|
||||||
protocol: string;
|
protocol: string;
|
||||||
@@ -250,4 +260,4 @@ export interface IVpnStats {
|
|||||||
registeredClients: number;
|
registeredClients: number;
|
||||||
connectedClients: number;
|
connectedClients: number;
|
||||||
wgListenPort: number;
|
wgListenPort: number;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,6 +3,6 @@
|
|||||||
*/
|
*/
|
||||||
export const commitinfo = {
|
export const commitinfo = {
|
||||||
name: '@serve.zone/dcrouter',
|
name: '@serve.zone/dcrouter',
|
||||||
version: '13.20.2',
|
version: '13.21.0',
|
||||||
description: 'A multifaceted routing service handling mail and SMS delivery functions.'
|
description: 'A multifaceted routing service handling mail and SMS delivery functions.'
|
||||||
}
|
}
|
||||||
|
|||||||
+23
-18
@@ -512,15 +512,6 @@ export const fetchNetworkStatsAction = networkStatePart.createAction(async (stat
|
|||||||
if (!context.identity) return currentState;
|
if (!context.identity) return currentState;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// Fetch active connections using the existing endpoint
|
|
||||||
const connectionsRequest = new plugins.domtools.plugins.typedrequest.TypedRequest<
|
|
||||||
interfaces.requests.IReq_GetActiveConnections
|
|
||||||
>('/typedrequest', 'getActiveConnections');
|
|
||||||
|
|
||||||
const connectionsResponse = await connectionsRequest.fire({
|
|
||||||
identity: context.identity,
|
|
||||||
});
|
|
||||||
|
|
||||||
// Get network stats for throughput and IP data
|
// Get network stats for throughput and IP data
|
||||||
const networkStatsRequest = new plugins.domtools.plugins.typedrequest.TypedRequest<
|
const networkStatsRequest = new plugins.domtools.plugins.typedrequest.TypedRequest<
|
||||||
interfaces.requests.IReq_GetNetworkStats
|
interfaces.requests.IReq_GetNetworkStats
|
||||||
@@ -533,22 +524,35 @@ export const fetchNetworkStatsAction = networkStatePart.createAction(async (stat
|
|||||||
// Use the connections data for the connection list
|
// Use the connections data for the connection list
|
||||||
// and network stats for throughput and IP analytics
|
// and network stats for throughput and IP analytics
|
||||||
const connectionsByIP: { [ip: string]: number } = {};
|
const connectionsByIP: { [ip: string]: number } = {};
|
||||||
|
const throughputByIP = new Map<string, { in: number; out: number }>();
|
||||||
|
for (const item of networkStatsResponse.throughputByIP || []) {
|
||||||
|
throughputByIP.set(item.ip, { in: item.in, out: item.out });
|
||||||
|
}
|
||||||
|
|
||||||
// Build connectionsByIP from network stats if available
|
// Build connectionsByIP from network stats if available
|
||||||
if (networkStatsResponse.connectionsByIP && Array.isArray(networkStatsResponse.connectionsByIP)) {
|
if (networkStatsResponse.connectionsByIP && Array.isArray(networkStatsResponse.connectionsByIP)) {
|
||||||
networkStatsResponse.connectionsByIP.forEach((item: { ip: string; count: number }) => {
|
networkStatsResponse.connectionsByIP.forEach((item: { ip: string; count: number }) => {
|
||||||
connectionsByIP[item.ip] = item.count;
|
connectionsByIP[item.ip] = item.count;
|
||||||
});
|
});
|
||||||
} else {
|
|
||||||
// Fallback: calculate from connections
|
|
||||||
connectionsResponse.connections.forEach(conn => {
|
|
||||||
const ip = conn.remoteAddress;
|
|
||||||
connectionsByIP[ip] = (connectionsByIP[ip] || 0) + 1;
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const connections: interfaces.data.IConnectionInfo[] = Object.entries(connectionsByIP).map(([ip, count]) => {
|
||||||
|
const tp = throughputByIP.get(ip);
|
||||||
|
return {
|
||||||
|
id: `ip-${ip}`,
|
||||||
|
remoteAddress: ip,
|
||||||
|
localAddress: 'server',
|
||||||
|
startTime: 0,
|
||||||
|
protocol: 'https',
|
||||||
|
state: 'connected',
|
||||||
|
bytesReceived: tp?.in || 0,
|
||||||
|
bytesSent: tp?.out || 0,
|
||||||
|
connectionCount: count,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
return {
|
return {
|
||||||
connections: connectionsResponse.connections,
|
connections,
|
||||||
connectionsByIP,
|
connectionsByIP,
|
||||||
throughputRate: networkStatsResponse.throughputRate || { bytesInPerSecond: 0, bytesOutPerSecond: 0 },
|
throughputRate: networkStatsResponse.throughputRate || { bytesInPerSecond: 0, bytesOutPerSecond: 0 },
|
||||||
totalBytes: networkStatsResponse.totalDataTransferred
|
totalBytes: networkStatsResponse.totalDataTransferred
|
||||||
@@ -2589,7 +2593,7 @@ async function dispatchCombinedRefreshActionInner() {
|
|||||||
email: true,
|
email: true,
|
||||||
dns: true,
|
dns: true,
|
||||||
security: true,
|
security: true,
|
||||||
network: currentView === 'network', // Only fetch network if on network view
|
network: currentView === 'network' && currentSubview === 'activity',
|
||||||
radius: true,
|
radius: true,
|
||||||
vpn: true,
|
vpn: true,
|
||||||
},
|
},
|
||||||
@@ -2617,7 +2621,7 @@ async function dispatchCombinedRefreshActionInner() {
|
|||||||
|
|
||||||
// Build connectionsByIP from connectionDetails (now populated with real per-IP data)
|
// Build connectionsByIP from connectionDetails (now populated with real per-IP data)
|
||||||
network.connectionDetails.forEach(conn => {
|
network.connectionDetails.forEach(conn => {
|
||||||
connectionsByIP[conn.remoteAddress] = (connectionsByIP[conn.remoteAddress] || 0) + 1;
|
connectionsByIP[conn.remoteAddress] = (connectionsByIP[conn.remoteAddress] || 0) + (conn.connectionCount || 1);
|
||||||
});
|
});
|
||||||
|
|
||||||
// Build connections from connectionDetails (real per-IP aggregates)
|
// Build connections from connectionDetails (real per-IP aggregates)
|
||||||
@@ -2630,6 +2634,7 @@ async function dispatchCombinedRefreshActionInner() {
|
|||||||
state: conn.state as any,
|
state: conn.state as any,
|
||||||
bytesReceived: conn.bytesIn,
|
bytesReceived: conn.bytesIn,
|
||||||
bytesSent: conn.bytesOut,
|
bytesSent: conn.bytesOut,
|
||||||
|
connectionCount: conn.connectionCount,
|
||||||
}));
|
}));
|
||||||
|
|
||||||
networkStatePart.setState({
|
networkStatePart.setState({
|
||||||
|
|||||||
@@ -79,7 +79,6 @@ export class OpsViewNetworkActivity extends DeesElement {
|
|||||||
// Subscribe and track unsubscribe functions
|
// Subscribe and track unsubscribe functions
|
||||||
const statsUnsubscribe = appstate.statsStatePart.select().subscribe((state) => {
|
const statsUnsubscribe = appstate.statsStatePart.select().subscribe((state) => {
|
||||||
this.statsState = state;
|
this.statsState = state;
|
||||||
this.updateNetworkData();
|
|
||||||
});
|
});
|
||||||
this.rxSubscriptions.push(statsUnsubscribe);
|
this.rxSubscriptions.push(statsUnsubscribe);
|
||||||
|
|
||||||
@@ -560,6 +559,8 @@ export class OpsViewNetworkActivity extends DeesElement {
|
|||||||
'Throughput Out': this.formatBitsPerSecond(item.bytesOutPerSecond),
|
'Throughput Out': this.formatBitsPerSecond(item.bytesOutPerSecond),
|
||||||
'Transferred / min': this.formatBytes(totalBytesPerMin),
|
'Transferred / min': this.formatBytes(totalBytesPerMin),
|
||||||
'Connections': item.activeConnections,
|
'Connections': item.activeConnections,
|
||||||
|
'Req/s': item.requestsPerSecond != null ? item.requestsPerSecond.toFixed(1) : '-',
|
||||||
|
'Req/min': item.requestsLastMinute != null ? item.requestsLastMinute.toFixed(0) : '-',
|
||||||
'Requests': item.requestCount?.toLocaleString() ?? '0',
|
'Requests': item.requestCount?.toLocaleString() ?? '0',
|
||||||
'Routes': item.routeCount,
|
'Routes': item.routeCount,
|
||||||
};
|
};
|
||||||
@@ -583,7 +584,7 @@ export class OpsViewNetworkActivity extends DeesElement {
|
|||||||
return html`
|
return html`
|
||||||
<dees-table
|
<dees-table
|
||||||
.data=${backends}
|
.data=${backends}
|
||||||
.rowKey=${'backend'}
|
.rowKey=${'id'}
|
||||||
.highlightUpdates=${'flash'}
|
.highlightUpdates=${'flash'}
|
||||||
.displayFunction=${(item: interfaces.data.IBackendInfo) => {
|
.displayFunction=${(item: interfaces.data.IBackendInfo) => {
|
||||||
const totalErrors = item.connectErrors + item.handshakeErrors + item.requestErrors;
|
const totalErrors = item.connectErrors + item.handshakeErrors + item.requestErrors;
|
||||||
@@ -707,6 +708,9 @@ export class OpsViewNetworkActivity extends DeesElement {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const throughput = this.calculateThroughput();
|
const throughput = this.calculateThroughput();
|
||||||
|
if (this.networkState.lastUpdated && now - this.networkState.lastUpdated > 3000) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
// Convert to Mbps (bytes * 8 / 1,000,000)
|
// Convert to Mbps (bytes * 8 / 1,000,000)
|
||||||
const throughputInMbps = (throughput.in * 8) / 1000000;
|
const throughputInMbps = (throughput.in * 8) / 1000000;
|
||||||
|
|||||||
Reference in New Issue
Block a user