Files
smartproxy/ts/proxies/smart-proxy/rust-metrics-adapter.ts
T

320 lines
11 KiB
TypeScript
Raw Normal View History

import type { IMetrics, IBackendMetrics, IProtocolCacheEntry, IProtocolDistribution, IRequestRateMetrics, IThroughputData, IThroughputHistoryPoint } from './models/metrics-types.js';
import type { RustProxyBridge } from './rust-proxy-bridge.js';
import type { IRustBackendMetrics, IRustHttpDomainRequestMetrics, IRustIpMetrics, IRustMetricsSnapshot, IRustRouteMetrics } from './models/rust-types.js';
/**
* Adapts Rust JSON metrics to the IMetrics interface.
*
* Polls the Rust binary periodically via the bridge and caches the result.
* All IMetrics getters read from the cache synchronously.
*
* Rust Metrics JSON fields (camelCase via serde):
* activeConnections, totalConnections, bytesIn, bytesOut,
* throughputInBytesPerSec, throughputOutBytesPerSec,
* routes: { [routeName]: { activeConnections, totalConnections, bytesIn, bytesOut, ... } }
*/
export class RustMetricsAdapter implements IMetrics {
private bridge: RustProxyBridge;
private cache: IRustMetricsSnapshot | null = null;
private pollTimer: ReturnType<typeof setInterval> | null = null;
private pollIntervalMs: number;
constructor(bridge: RustProxyBridge, pollIntervalMs = 1000) {
this.bridge = bridge;
this.pollIntervalMs = pollIntervalMs;
}
/**
* Poll Rust for metrics once. Can be awaited to ensure cache is fresh.
*/
public async poll(): Promise<void> {
try {
this.cache = await this.bridge.getMetrics();
} catch {
// Ignore poll errors (bridge may be shutting down)
}
}
public startPolling(): void {
if (this.pollTimer) return;
// Immediate first poll so cache is populated ASAP
this.poll();
this.pollTimer = setInterval(() => {
this.poll();
}, this.pollIntervalMs);
if (this.pollTimer.unref) {
this.pollTimer.unref();
}
}
public stopPolling(): void {
if (this.pollTimer) {
clearInterval(this.pollTimer);
this.pollTimer = null;
}
}
// --- IMetrics implementation ---
public connections = {
active: (): number => {
return this.cache?.activeConnections ?? 0;
},
total: (): number => {
return this.cache?.totalConnections ?? 0;
},
byRoute: (): Map<string, number> => {
const result = new Map<string, number>();
if (this.cache?.routes) {
for (const [name, rm] of Object.entries(this.cache.routes) as Array<[string, IRustRouteMetrics]>) {
result.set(name, rm.activeConnections ?? 0);
}
}
return result;
},
byIP: (): Map<string, number> => {
const result = new Map<string, number>();
if (this.cache?.ips) {
for (const [ip, im] of Object.entries(this.cache.ips) as Array<[string, IRustIpMetrics]>) {
result.set(ip, im.activeConnections ?? 0);
}
}
return result;
},
topIPs: (limit: number = 10): Array<{ ip: string; count: number }> => {
const result: Array<{ ip: string; count: number }> = [];
if (this.cache?.ips) {
for (const [ip, im] of Object.entries(this.cache.ips) as Array<[string, IRustIpMetrics]>) {
result.push({ ip, count: im.activeConnections ?? 0 });
}
}
result.sort((a, b) => b.count - a.count);
return result.slice(0, limit);
},
domainRequestsByIP: (): Map<string, Map<string, number>> => {
const result = new Map<string, Map<string, number>>();
if (this.cache?.ips) {
for (const [ip, im] of Object.entries(this.cache.ips) as Array<[string, IRustIpMetrics]>) {
const dr = im.domainRequests;
if (dr && typeof dr === 'object') {
const domainMap = new Map<string, number>();
for (const [domain, count] of Object.entries(dr)) {
domainMap.set(domain, count as number);
}
if (domainMap.size > 0) {
result.set(ip, domainMap);
}
}
}
}
return result;
},
topDomainRequests: (limit: number = 20): Array<{ ip: string; domain: string; count: number }> => {
const result: Array<{ ip: string; domain: string; count: number }> = [];
if (this.cache?.ips) {
for (const [ip, im] of Object.entries(this.cache.ips) as Array<[string, IRustIpMetrics]>) {
const dr = im.domainRequests;
if (dr && typeof dr === 'object') {
for (const [domain, count] of Object.entries(dr)) {
result.push({ ip, domain, count: count as number });
}
}
}
}
result.sort((a, b) => b.count - a.count);
return result.slice(0, limit);
},
frontendProtocols: (): IProtocolDistribution => {
const fp = this.cache?.frontendProtocols;
return {
h1Active: fp?.h1Active ?? 0,
h1Total: fp?.h1Total ?? 0,
h2Active: fp?.h2Active ?? 0,
h2Total: fp?.h2Total ?? 0,
h3Active: fp?.h3Active ?? 0,
h3Total: fp?.h3Total ?? 0,
wsActive: fp?.wsActive ?? 0,
wsTotal: fp?.wsTotal ?? 0,
otherActive: fp?.otherActive ?? 0,
otherTotal: fp?.otherTotal ?? 0,
};
},
backendProtocols: (): IProtocolDistribution => {
const bp = this.cache?.backendProtocols;
return {
h1Active: bp?.h1Active ?? 0,
h1Total: bp?.h1Total ?? 0,
h2Active: bp?.h2Active ?? 0,
h2Total: bp?.h2Total ?? 0,
h3Active: bp?.h3Active ?? 0,
h3Total: bp?.h3Total ?? 0,
wsActive: bp?.wsActive ?? 0,
wsTotal: bp?.wsTotal ?? 0,
otherActive: bp?.otherActive ?? 0,
otherTotal: bp?.otherTotal ?? 0,
};
},
};
public throughput = {
instant: (): IThroughputData => {
return {
in: this.cache?.throughputInBytesPerSec ?? 0,
out: this.cache?.throughputOutBytesPerSec ?? 0,
};
},
recent: (): IThroughputData => {
return {
in: this.cache?.throughputRecentInBytesPerSec ?? 0,
out: this.cache?.throughputRecentOutBytesPerSec ?? 0,
};
},
average: (): IThroughputData => {
return this.throughput.instant();
},
custom: (_seconds: number): IThroughputData => {
return this.throughput.instant();
},
history: (seconds: number): Array<IThroughputHistoryPoint> => {
if (!this.cache?.throughputHistory) return [];
return this.cache.throughputHistory.slice(-seconds).map((p) => ({
timestamp: p.timestampMs,
in: p.bytesIn,
out: p.bytesOut,
}));
},
byRoute: (_windowSeconds?: number): Map<string, IThroughputData> => {
const result = new Map<string, IThroughputData>();
if (this.cache?.routes) {
for (const [name, rm] of Object.entries(this.cache.routes) as Array<[string, IRustRouteMetrics]>) {
result.set(name, {
in: rm.throughputInBytesPerSec ?? 0,
out: rm.throughputOutBytesPerSec ?? 0,
});
}
}
return result;
},
byIP: (_windowSeconds?: number): Map<string, IThroughputData> => {
const result = new Map<string, IThroughputData>();
if (this.cache?.ips) {
for (const [ip, im] of Object.entries(this.cache.ips) as Array<[string, IRustIpMetrics]>) {
result.set(ip, {
in: im.throughputInBytesPerSec ?? 0,
out: im.throughputOutBytesPerSec ?? 0,
});
}
}
return result;
},
};
public requests = {
perSecond: (): number => {
return this.cache?.httpRequestsPerSec ?? 0;
},
perMinute: (): number => {
return (this.cache?.httpRequestsPerSecRecent ?? 0) * 60;
},
total: (): number => {
return this.cache?.totalHttpRequests ?? this.cache?.totalConnections ?? 0;
},
byDomain: (): Map<string, IRequestRateMetrics> => {
const result = new Map<string, IRequestRateMetrics>();
if (this.cache?.httpDomainRequests) {
for (const [domain, metrics] of Object.entries(this.cache.httpDomainRequests) as Array<[string, IRustHttpDomainRequestMetrics]>) {
result.set(domain, {
perSecond: metrics.requestsPerSecond ?? 0,
lastMinute: metrics.requestsLastMinute ?? 0,
});
}
}
return result;
},
};
public totals = {
bytesIn: (): number => {
return this.cache?.bytesIn ?? 0;
},
bytesOut: (): number => {
return this.cache?.bytesOut ?? 0;
},
connections: (): number => {
return this.cache?.totalConnections ?? 0;
},
};
public backends = {
byBackend: (): Map<string, IBackendMetrics> => {
const result = new Map<string, IBackendMetrics>();
if (this.cache?.backends) {
for (const [key, bm] of Object.entries(this.cache.backends) as Array<[string, IRustBackendMetrics]>) {
const totalTimeUs = bm.totalConnectTimeUs ?? 0;
const count = bm.connectCount ?? 0;
const poolHits = bm.poolHits ?? 0;
const poolMisses = bm.poolMisses ?? 0;
const poolTotal = poolHits + poolMisses;
result.set(key, {
protocol: bm.protocol ?? 'unknown',
activeConnections: bm.activeConnections ?? 0,
totalConnections: bm.totalConnections ?? 0,
connectErrors: bm.connectErrors ?? 0,
handshakeErrors: bm.handshakeErrors ?? 0,
requestErrors: bm.requestErrors ?? 0,
avgConnectTimeMs: count > 0 ? (totalTimeUs / count) / 1000 : 0,
poolHitRate: poolTotal > 0 ? poolHits / poolTotal : 0,
h2Failures: bm.h2Failures ?? 0,
});
}
}
return result;
},
protocols: (): Map<string, string> => {
const result = new Map<string, string>();
if (this.cache?.backends) {
for (const [key, bm] of Object.entries(this.cache.backends) as Array<[string, IRustBackendMetrics]>) {
result.set(key, bm.protocol ?? 'unknown');
}
}
return result;
},
topByErrors: (limit: number = 10): Array<{ backend: string; errors: number }> => {
const result: Array<{ backend: string; errors: number }> = [];
if (this.cache?.backends) {
for (const [key, bm] of Object.entries(this.cache.backends) as Array<[string, IRustBackendMetrics]>) {
const errors = (bm.connectErrors ?? 0) + (bm.handshakeErrors ?? 0) + (bm.requestErrors ?? 0);
if (errors > 0) result.push({ backend: key, errors });
}
}
result.sort((a, b) => b.errors - a.errors);
return result.slice(0, limit);
},
detectedProtocols: (): IProtocolCacheEntry[] => {
return this.cache?.detectedProtocols ?? [];
},
};
public udp = {
activeSessions: (): number => this.cache?.activeUdpSessions ?? 0,
totalSessions: (): number => this.cache?.totalUdpSessions ?? 0,
datagramsIn: (): number => this.cache?.totalDatagramsIn ?? 0,
datagramsOut: (): number => this.cache?.totalDatagramsOut ?? 0,
};
public percentiles = {
connectionDuration: (): { p50: number; p95: number; p99: number } => {
return { p50: 0, p95: 0, p99: 0 };
},
bytesTransferred: (): {
in: { p50: number; p95: number; p99: number };
out: { p50: number; p95: number; p99: number };
} => {
return {
in: { p50: 0, p95: 0, p99: 0 },
out: { p50: 0, p95: 0, p99: 0 },
};
},
};
}