379 lines
12 KiB
TypeScript
379 lines
12 KiB
TypeScript
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 createActiveConnectionSnapshots(entries: Array<{
|
|
count: number;
|
|
sourceIp?: string;
|
|
routeId?: string;
|
|
domain?: string;
|
|
localPort?: number;
|
|
}>) {
|
|
const snapshots: any[] = [];
|
|
let index = 0;
|
|
for (const entry of entries) {
|
|
for (let i = 0; i < entry.count; i++) {
|
|
snapshots.push({
|
|
id: `test-connection-${index++}`,
|
|
sourceIp: entry.sourceIp || '192.0.2.10',
|
|
sourcePort: 40000 + index,
|
|
localPort: entry.localPort || 443,
|
|
domain: entry.domain,
|
|
routeId: entry.routeId,
|
|
targetHost: '127.0.0.1',
|
|
targetPort: 8443,
|
|
protocol: 'https',
|
|
state: 'active',
|
|
startedAtMs: Date.now(),
|
|
ageMs: 0,
|
|
bytesIn: 0,
|
|
bytesOut: 0,
|
|
});
|
|
}
|
|
}
|
|
return snapshots;
|
|
}
|
|
|
|
function createProxyMetrics(args: {
|
|
connectionsByRoute: Map<string, number>;
|
|
throughputByRoute: Map<string, { in: number; out: number }>;
|
|
domainRequestsByIP: Map<string, Map<string, number>>;
|
|
domainRequestRates?: Map<string, { perSecond: number; lastMinute: number }>;
|
|
backendMetrics?: Map<string, any>;
|
|
protocolCache?: any[];
|
|
requestsTotal?: number;
|
|
connectionsByIP?: Map<string, number>;
|
|
throughputByIP?: Map<string, { in: number; out: number }>;
|
|
}) {
|
|
const connectionsByIP = args.connectionsByIP || new Map<string, number>();
|
|
const throughputByIP = args.throughputByIP || new Map<string, { in: number; out: number }>();
|
|
return {
|
|
connections: {
|
|
active: () => 0,
|
|
total: () => 0,
|
|
byRoute: () => args.connectionsByRoute,
|
|
byIP: () => connectionsByIP,
|
|
topIPs: (limit = 10) => Array.from(connectionsByIP.entries())
|
|
.sort((a, b) => b[1] - a[1])
|
|
.slice(0, limit)
|
|
.map(([ip, count]) => ({ ip, count })),
|
|
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: () => throughputByIP,
|
|
},
|
|
requests: {
|
|
perSecond: () => 0,
|
|
perMinute: () => 0,
|
|
total: () => args.requestsTotal || 0,
|
|
byDomain: () => args.domainRequestRates || new Map<string, { perSecond: number; lastMinute: number }>(),
|
|
},
|
|
totals: {
|
|
bytesIn: () => 0,
|
|
bytesOut: () => 0,
|
|
connections: () => 0,
|
|
},
|
|
backends: {
|
|
byBackend: () => args.backendMetrics || new Map<string, any>(),
|
|
protocols: () => new Map<string, string>(),
|
|
topByErrors: () => [],
|
|
detectedProtocols: () => args.protocolCache || [],
|
|
},
|
|
};
|
|
}
|
|
|
|
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,
|
|
getActiveConnectionSnapshots: () => createActiveConnectionSnapshots([
|
|
{ count: 3, routeId: 'route-id-only', domain: 'alpha.example.com' },
|
|
{ count: 1, routeId: 'route-id-only', domain: 'beta.example.com' },
|
|
]),
|
|
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);
|
|
});
|
|
|
|
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,
|
|
getActiveConnectionSnapshots: () => createActiveConnectionSnapshots([
|
|
{ count: 10, routeId: 'route-id-only', domain: 'beta.example.com' },
|
|
]),
|
|
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,
|
|
getActiveConnectionSnapshots: () => [],
|
|
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();
|
|
});
|
|
|
|
tap.test('MetricsManager queues IP intelligence without awaiting enrichment', async () => {
|
|
const proxyMetrics = createProxyMetrics({
|
|
connectionsByRoute: new Map(),
|
|
throughputByRoute: new Map(),
|
|
domainRequestsByIP: new Map(),
|
|
connectionsByIP: new Map([
|
|
['8.8.8.8', 4],
|
|
['1.1.1.1', 2],
|
|
]),
|
|
throughputByIP: new Map([
|
|
['8.8.8.8', { in: 500, out: 250 }],
|
|
['1.1.1.1', { in: 1500, out: 1000 }],
|
|
]),
|
|
});
|
|
|
|
const queuedIps: string[][] = [];
|
|
const manager = new MetricsManager({
|
|
smartProxy: {
|
|
getMetrics: () => proxyMetrics,
|
|
getActiveConnectionSnapshots: () => createActiveConnectionSnapshots([
|
|
{ count: 4, sourceIp: '8.8.8.8' },
|
|
{ count: 2, sourceIp: '1.1.1.1' },
|
|
]),
|
|
routeManager: { getRoutes: () => [] },
|
|
},
|
|
securityPolicyManager: {
|
|
queueObservedIps: (ips: string[]) => queuedIps.push(ips),
|
|
listIpIntelligence: async () => [],
|
|
},
|
|
} as any);
|
|
|
|
await manager.getNetworkStats();
|
|
|
|
expect(queuedIps).toHaveLength(1);
|
|
expect(queuedIps[0]).toContain('8.8.8.8');
|
|
expect(queuedIps[0]).toContain('1.1.1.1');
|
|
});
|
|
|
|
tap.test('MetricsManager aggregates top ASNs from IP intelligence', async () => {
|
|
const proxyMetrics = createProxyMetrics({
|
|
connectionsByRoute: new Map(),
|
|
throughputByRoute: new Map(),
|
|
domainRequestsByIP: new Map(),
|
|
connectionsByIP: new Map([
|
|
['8.8.8.8', 4],
|
|
['8.8.4.4', 3],
|
|
['1.1.1.1', 5],
|
|
]),
|
|
throughputByIP: new Map([
|
|
['8.8.8.8', { in: 500, out: 250 }],
|
|
['8.8.4.4', { in: 700, out: 350 }],
|
|
['1.1.1.1', { in: 2000, out: 1000 }],
|
|
]),
|
|
});
|
|
|
|
const manager = new MetricsManager({
|
|
smartProxy: {
|
|
getMetrics: () => proxyMetrics,
|
|
getActiveConnectionSnapshots: () => createActiveConnectionSnapshots([
|
|
{ count: 4, sourceIp: '8.8.8.8' },
|
|
{ count: 3, sourceIp: '8.8.4.4' },
|
|
{ count: 5, sourceIp: '1.1.1.1' },
|
|
]),
|
|
routeManager: { getRoutes: () => [] },
|
|
},
|
|
securityPolicyManager: {
|
|
queueObservedIps: () => undefined,
|
|
listIpIntelligence: async ({ ipAddresses }: { ipAddresses?: string[] }) => [
|
|
{ ipAddress: '8.8.8.8', asn: 15169, asnOrg: 'Google LLC', countryCode: 'US' },
|
|
{ ipAddress: '8.8.4.4', asn: 15169, asnOrg: 'Google LLC', countryCode: 'US' },
|
|
{ ipAddress: '1.1.1.1', asn: 13335, asnOrg: 'Cloudflare, Inc.', countryCode: 'US' },
|
|
].filter((record) => !ipAddresses || ipAddresses.includes(record.ipAddress)),
|
|
},
|
|
} as any);
|
|
|
|
const stats = await manager.getNetworkStats();
|
|
|
|
expect(stats.topASNs).toHaveLength(2);
|
|
expect(stats.topASNs[0].asn).toEqual(15169);
|
|
expect(stats.topASNs[0].organization).toEqual('Google LLC');
|
|
expect(stats.topASNs[0].activeConnections).toEqual(7);
|
|
expect(stats.topASNs[0].ipCount).toEqual(2);
|
|
expect(stats.topASNs[0].bytesInPerSecond).toEqual(1200);
|
|
expect(stats.topASNs[0].bytesOutPerSecond).toEqual(600);
|
|
expect(stats.topASNs[0].sampleIps).toContain('8.8.8.8');
|
|
expect(stats.topASNs[1].asn).toEqual(13335);
|
|
expect(stats.topASNs[1].activeConnections).toEqual(5);
|
|
});
|
|
|
|
export default tap.start();
|