update
This commit is contained in:
75
ts/monitoring/classes.metricscache.ts
Normal file
75
ts/monitoring/classes.metricscache.ts
Normal file
@@ -0,0 +1,75 @@
|
|||||||
|
export interface ICacheEntry<T> {
|
||||||
|
data: T;
|
||||||
|
timestamp: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export class MetricsCache {
|
||||||
|
private cache = new Map<string, ICacheEntry<any>>();
|
||||||
|
private readonly defaultTTL: number;
|
||||||
|
|
||||||
|
constructor(defaultTTL: number = 500) {
|
||||||
|
this.defaultTTL = defaultTTL;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get cached data or compute and cache it
|
||||||
|
*/
|
||||||
|
public get<T>(key: string, computeFn: () => T | Promise<T>, ttl?: number): T | Promise<T> {
|
||||||
|
const cached = this.cache.get(key);
|
||||||
|
const now = Date.now();
|
||||||
|
const actualTTL = ttl ?? this.defaultTTL;
|
||||||
|
|
||||||
|
if (cached && (now - cached.timestamp) < actualTTL) {
|
||||||
|
return cached.data;
|
||||||
|
}
|
||||||
|
|
||||||
|
const result = computeFn();
|
||||||
|
|
||||||
|
// Handle both sync and async compute functions
|
||||||
|
if (result instanceof Promise) {
|
||||||
|
return result.then(data => {
|
||||||
|
this.cache.set(key, { data, timestamp: now });
|
||||||
|
return data;
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
this.cache.set(key, { data: result, timestamp: now });
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Invalidate a specific cache entry
|
||||||
|
*/
|
||||||
|
public invalidate(key: string): void {
|
||||||
|
this.cache.delete(key);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Clear all cache entries
|
||||||
|
*/
|
||||||
|
public clear(): void {
|
||||||
|
this.cache.clear();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get cache statistics
|
||||||
|
*/
|
||||||
|
public getStats(): { size: number; keys: string[] } {
|
||||||
|
return {
|
||||||
|
size: this.cache.size,
|
||||||
|
keys: Array.from(this.cache.keys())
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Clean up expired entries
|
||||||
|
*/
|
||||||
|
public cleanup(): void {
|
||||||
|
const now = Date.now();
|
||||||
|
for (const [key, entry] of this.cache.entries()) {
|
||||||
|
if (now - entry.timestamp > this.defaultTTL) {
|
||||||
|
this.cache.delete(key);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@@ -1,11 +1,13 @@
|
|||||||
import * as plugins from '../plugins.js';
|
import * as plugins from '../plugins.js';
|
||||||
import { DcRouter } from '../classes.dcrouter.js';
|
import { DcRouter } from '../classes.dcrouter.js';
|
||||||
|
import { MetricsCache } from './classes.metricscache.js';
|
||||||
|
|
||||||
export class MetricsManager {
|
export class MetricsManager {
|
||||||
private logger: plugins.smartlog.Smartlog;
|
private logger: plugins.smartlog.Smartlog;
|
||||||
private smartMetrics: plugins.smartmetrics.SmartMetrics;
|
private smartMetrics: plugins.smartmetrics.SmartMetrics;
|
||||||
private dcRouter: DcRouter;
|
private dcRouter: DcRouter;
|
||||||
private resetInterval?: NodeJS.Timeout;
|
private resetInterval?: NodeJS.Timeout;
|
||||||
|
private metricsCache: MetricsCache;
|
||||||
|
|
||||||
// Constants
|
// Constants
|
||||||
private readonly MAX_TOP_DOMAINS = 1000; // Limit topDomains Map size
|
private readonly MAX_TOP_DOMAINS = 1000; // Limit topDomains Map size
|
||||||
@@ -57,6 +59,8 @@ export class MetricsManager {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
this.smartMetrics = new plugins.smartmetrics.SmartMetrics(this.logger, 'dcrouter');
|
this.smartMetrics = new plugins.smartmetrics.SmartMetrics(this.logger, 'dcrouter');
|
||||||
|
// Initialize metrics cache with 500ms TTL
|
||||||
|
this.metricsCache = new MetricsCache(500);
|
||||||
}
|
}
|
||||||
|
|
||||||
public async start(): Promise<void> {
|
public async start(): Promise<void> {
|
||||||
@@ -116,144 +120,154 @@ export class MetricsManager {
|
|||||||
|
|
||||||
// Get server metrics from SmartMetrics and SmartProxy
|
// Get server metrics from SmartMetrics and SmartProxy
|
||||||
public async getServerStats() {
|
public async getServerStats() {
|
||||||
const smartMetricsData = await this.smartMetrics.getMetrics();
|
return this.metricsCache.get('serverStats', async () => {
|
||||||
const proxyMetrics = this.dcRouter.smartProxy ? this.dcRouter.smartProxy.getMetrics() : null;
|
const smartMetricsData = await this.smartMetrics.getMetrics();
|
||||||
const proxyStats = this.dcRouter.smartProxy ? this.dcRouter.smartProxy.getStatistics() : null;
|
const proxyMetrics = this.dcRouter.smartProxy ? this.dcRouter.smartProxy.getMetrics() : null;
|
||||||
|
const proxyStats = this.dcRouter.smartProxy ? this.dcRouter.smartProxy.getStatistics() : null;
|
||||||
|
|
||||||
return {
|
return {
|
||||||
uptime: process.uptime(),
|
uptime: process.uptime(),
|
||||||
startTime: Date.now() - (process.uptime() * 1000),
|
startTime: Date.now() - (process.uptime() * 1000),
|
||||||
memoryUsage: {
|
memoryUsage: {
|
||||||
heapUsed: process.memoryUsage().heapUsed,
|
heapUsed: process.memoryUsage().heapUsed,
|
||||||
heapTotal: process.memoryUsage().heapTotal,
|
heapTotal: process.memoryUsage().heapTotal,
|
||||||
external: process.memoryUsage().external,
|
external: process.memoryUsage().external,
|
||||||
rss: process.memoryUsage().rss,
|
rss: process.memoryUsage().rss,
|
||||||
// Add SmartMetrics memory data
|
// Add SmartMetrics memory data
|
||||||
maxMemoryMB: this.smartMetrics.maxMemoryMB,
|
maxMemoryMB: this.smartMetrics.maxMemoryMB,
|
||||||
actualUsageBytes: smartMetricsData.memoryUsageBytes,
|
actualUsageBytes: smartMetricsData.memoryUsageBytes,
|
||||||
actualUsagePercentage: smartMetricsData.memoryPercentage,
|
actualUsagePercentage: smartMetricsData.memoryPercentage,
|
||||||
},
|
},
|
||||||
cpuUsage: {
|
cpuUsage: {
|
||||||
user: parseFloat(smartMetricsData.cpuUsageText || '0'),
|
user: parseFloat(smartMetricsData.cpuUsageText || '0'),
|
||||||
system: 0, // SmartMetrics doesn't separate user/system
|
system: 0, // SmartMetrics doesn't separate user/system
|
||||||
},
|
},
|
||||||
activeConnections: proxyStats ? proxyStats.activeConnections : 0,
|
activeConnections: proxyStats ? proxyStats.activeConnections : 0,
|
||||||
totalConnections: proxyMetrics ? proxyMetrics.totals.connections() : 0,
|
totalConnections: proxyMetrics ? proxyMetrics.totals.connections() : 0,
|
||||||
requestsPerSecond: proxyMetrics ? proxyMetrics.requests.perSecond() : 0,
|
requestsPerSecond: proxyMetrics ? proxyMetrics.requests.perSecond() : 0,
|
||||||
throughput: proxyMetrics ? {
|
throughput: proxyMetrics ? {
|
||||||
bytesIn: proxyMetrics.totals.bytesIn(),
|
bytesIn: proxyMetrics.totals.bytesIn(),
|
||||||
bytesOut: proxyMetrics.totals.bytesOut()
|
bytesOut: proxyMetrics.totals.bytesOut()
|
||||||
} : { bytesIn: 0, bytesOut: 0 },
|
} : { bytesIn: 0, bytesOut: 0 },
|
||||||
};
|
};
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// Get email metrics
|
// Get email metrics
|
||||||
public async getEmailStats() {
|
public async getEmailStats() {
|
||||||
// Calculate average delivery time
|
return this.metricsCache.get('emailStats', () => {
|
||||||
const avgDeliveryTime = this.emailMetrics.deliveryTimes.length > 0
|
// Calculate average delivery time
|
||||||
? this.emailMetrics.deliveryTimes.reduce((a, b) => a + b, 0) / this.emailMetrics.deliveryTimes.length
|
const avgDeliveryTime = this.emailMetrics.deliveryTimes.length > 0
|
||||||
: 0;
|
? this.emailMetrics.deliveryTimes.reduce((a, b) => a + b, 0) / this.emailMetrics.deliveryTimes.length
|
||||||
|
: 0;
|
||||||
|
|
||||||
// Get top recipients
|
// Get top recipients
|
||||||
const topRecipients = Array.from(this.emailMetrics.recipients.entries())
|
const topRecipients = Array.from(this.emailMetrics.recipients.entries())
|
||||||
.sort((a, b) => b[1] - a[1])
|
.sort((a, b) => b[1] - a[1])
|
||||||
.slice(0, 10)
|
.slice(0, 10)
|
||||||
.map(([email, count]) => ({ email, count }));
|
.map(([email, count]) => ({ email, count }));
|
||||||
|
|
||||||
// Get recent activity (last 50 entries)
|
// Get recent activity (last 50 entries)
|
||||||
const recentActivity = this.emailMetrics.recentActivity.slice(-50);
|
const recentActivity = this.emailMetrics.recentActivity.slice(-50);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
sentToday: this.emailMetrics.sentToday,
|
sentToday: this.emailMetrics.sentToday,
|
||||||
receivedToday: this.emailMetrics.receivedToday,
|
receivedToday: this.emailMetrics.receivedToday,
|
||||||
failedToday: this.emailMetrics.failedToday,
|
failedToday: this.emailMetrics.failedToday,
|
||||||
bounceRate: this.emailMetrics.bouncedToday > 0
|
bounceRate: this.emailMetrics.bouncedToday > 0
|
||||||
? (this.emailMetrics.bouncedToday / this.emailMetrics.sentToday) * 100
|
? (this.emailMetrics.bouncedToday / this.emailMetrics.sentToday) * 100
|
||||||
: 0,
|
: 0,
|
||||||
deliveryRate: this.emailMetrics.sentToday > 0
|
deliveryRate: this.emailMetrics.sentToday > 0
|
||||||
? ((this.emailMetrics.sentToday - this.emailMetrics.failedToday) / this.emailMetrics.sentToday) * 100
|
? ((this.emailMetrics.sentToday - this.emailMetrics.failedToday) / this.emailMetrics.sentToday) * 100
|
||||||
: 100,
|
: 100,
|
||||||
queueSize: this.emailMetrics.queueSize,
|
queueSize: this.emailMetrics.queueSize,
|
||||||
averageDeliveryTime: Math.round(avgDeliveryTime),
|
averageDeliveryTime: Math.round(avgDeliveryTime),
|
||||||
topRecipients,
|
topRecipients,
|
||||||
recentActivity,
|
recentActivity,
|
||||||
};
|
};
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// Get DNS metrics
|
// Get DNS metrics
|
||||||
public async getDnsStats() {
|
public async getDnsStats() {
|
||||||
const cacheHitRate = this.dnsMetrics.totalQueries > 0
|
return this.metricsCache.get('dnsStats', () => {
|
||||||
? (this.dnsMetrics.cacheHits / this.dnsMetrics.totalQueries) * 100
|
const cacheHitRate = this.dnsMetrics.totalQueries > 0
|
||||||
: 0;
|
? (this.dnsMetrics.cacheHits / this.dnsMetrics.totalQueries) * 100
|
||||||
|
: 0;
|
||||||
|
|
||||||
const topDomains = Array.from(this.dnsMetrics.topDomains.entries())
|
const topDomains = Array.from(this.dnsMetrics.topDomains.entries())
|
||||||
.sort((a, b) => b[1] - a[1])
|
.sort((a, b) => b[1] - a[1])
|
||||||
.slice(0, 10)
|
.slice(0, 10)
|
||||||
.map(([domain, count]) => ({ domain, count }));
|
.map(([domain, count]) => ({ domain, count }));
|
||||||
|
|
||||||
// Calculate queries per second from recent timestamps
|
// Calculate queries per second from recent timestamps
|
||||||
const now = Date.now();
|
const now = Date.now();
|
||||||
const oneMinuteAgo = now - 60000;
|
const oneMinuteAgo = now - 60000;
|
||||||
const recentQueries = this.dnsMetrics.queryTimestamps.filter(ts => ts >= oneMinuteAgo);
|
const recentQueries = this.dnsMetrics.queryTimestamps.filter(ts => ts >= oneMinuteAgo);
|
||||||
const queriesPerSecond = recentQueries.length / 60;
|
const queriesPerSecond = recentQueries.length / 60;
|
||||||
|
|
||||||
// Calculate average response time
|
// Calculate average response time
|
||||||
const avgResponseTime = this.dnsMetrics.responseTimes.length > 0
|
const avgResponseTime = this.dnsMetrics.responseTimes.length > 0
|
||||||
? this.dnsMetrics.responseTimes.reduce((a, b) => a + b, 0) / this.dnsMetrics.responseTimes.length
|
? this.dnsMetrics.responseTimes.reduce((a, b) => a + b, 0) / this.dnsMetrics.responseTimes.length
|
||||||
: 0;
|
: 0;
|
||||||
|
|
||||||
return {
|
return {
|
||||||
queriesPerSecond: Math.round(queriesPerSecond * 10) / 10,
|
queriesPerSecond: Math.round(queriesPerSecond * 10) / 10,
|
||||||
totalQueries: this.dnsMetrics.totalQueries,
|
totalQueries: this.dnsMetrics.totalQueries,
|
||||||
cacheHits: this.dnsMetrics.cacheHits,
|
cacheHits: this.dnsMetrics.cacheHits,
|
||||||
cacheMisses: this.dnsMetrics.cacheMisses,
|
cacheMisses: this.dnsMetrics.cacheMisses,
|
||||||
cacheHitRate: cacheHitRate,
|
cacheHitRate: cacheHitRate,
|
||||||
topDomains: topDomains,
|
topDomains: topDomains,
|
||||||
queryTypes: this.dnsMetrics.queryTypes,
|
queryTypes: this.dnsMetrics.queryTypes,
|
||||||
averageResponseTime: Math.round(avgResponseTime),
|
averageResponseTime: Math.round(avgResponseTime),
|
||||||
activeDomains: this.dnsMetrics.topDomains.size,
|
activeDomains: this.dnsMetrics.topDomains.size,
|
||||||
};
|
};
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// Get security metrics
|
// Get security metrics
|
||||||
public async getSecurityStats() {
|
public async getSecurityStats() {
|
||||||
// Get recent incidents (last 20)
|
return this.metricsCache.get('securityStats', () => {
|
||||||
const recentIncidents = this.securityMetrics.incidents.slice(-20);
|
// Get recent incidents (last 20)
|
||||||
|
const recentIncidents = this.securityMetrics.incidents.slice(-20);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
blockedIPs: this.securityMetrics.blockedIPs,
|
blockedIPs: this.securityMetrics.blockedIPs,
|
||||||
authFailures: this.securityMetrics.authFailures,
|
authFailures: this.securityMetrics.authFailures,
|
||||||
spamDetected: this.securityMetrics.spamDetected,
|
spamDetected: this.securityMetrics.spamDetected,
|
||||||
malwareDetected: this.securityMetrics.malwareDetected,
|
malwareDetected: this.securityMetrics.malwareDetected,
|
||||||
phishingDetected: this.securityMetrics.phishingDetected,
|
phishingDetected: this.securityMetrics.phishingDetected,
|
||||||
totalThreatsBlocked: this.securityMetrics.spamDetected +
|
totalThreatsBlocked: this.securityMetrics.spamDetected +
|
||||||
this.securityMetrics.malwareDetected +
|
this.securityMetrics.malwareDetected +
|
||||||
this.securityMetrics.phishingDetected,
|
this.securityMetrics.phishingDetected,
|
||||||
recentIncidents,
|
recentIncidents,
|
||||||
};
|
};
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// Get connection info from SmartProxy
|
// Get connection info from SmartProxy
|
||||||
public async getConnectionInfo() {
|
public async getConnectionInfo() {
|
||||||
const proxyMetrics = this.dcRouter.smartProxy ? this.dcRouter.smartProxy.getMetrics() : null;
|
return this.metricsCache.get('connectionInfo', () => {
|
||||||
|
const proxyMetrics = this.dcRouter.smartProxy ? this.dcRouter.smartProxy.getMetrics() : null;
|
||||||
|
|
||||||
if (!proxyMetrics) {
|
if (!proxyMetrics) {
|
||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
|
|
||||||
const connectionsByRoute = proxyMetrics.connections.byRoute();
|
const connectionsByRoute = proxyMetrics.connections.byRoute();
|
||||||
const connectionInfo = [];
|
const connectionInfo = [];
|
||||||
|
|
||||||
for (const [routeName, count] of connectionsByRoute) {
|
for (const [routeName, count] of connectionsByRoute) {
|
||||||
connectionInfo.push({
|
connectionInfo.push({
|
||||||
type: 'https',
|
type: 'https',
|
||||||
count,
|
count,
|
||||||
source: routeName,
|
source: routeName,
|
||||||
lastActivity: new Date(),
|
lastActivity: new Date(),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
return connectionInfo;
|
return connectionInfo;
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// Email event tracking methods
|
// Email event tracking methods
|
||||||
@@ -465,41 +479,44 @@ export class MetricsManager {
|
|||||||
|
|
||||||
// Get network metrics from SmartProxy
|
// Get network metrics from SmartProxy
|
||||||
public async getNetworkStats() {
|
public async getNetworkStats() {
|
||||||
const proxyMetrics = this.dcRouter.smartProxy ? this.dcRouter.smartProxy.getMetrics() : null;
|
// Use shorter cache TTL for network stats to ensure real-time updates
|
||||||
|
return this.metricsCache.get('networkStats', () => {
|
||||||
|
const proxyMetrics = this.dcRouter.smartProxy ? this.dcRouter.smartProxy.getMetrics() : null;
|
||||||
|
|
||||||
if (!proxyMetrics) {
|
if (!proxyMetrics) {
|
||||||
return {
|
return {
|
||||||
connectionsByIP: new Map<string, number>(),
|
connectionsByIP: new Map<string, number>(),
|
||||||
throughputRate: { bytesInPerSecond: 0, bytesOutPerSecond: 0 },
|
throughputRate: { bytesInPerSecond: 0, bytesOutPerSecond: 0 },
|
||||||
topIPs: [],
|
topIPs: [],
|
||||||
totalDataTransferred: { bytesIn: 0, bytesOut: 0 },
|
totalDataTransferred: { bytesIn: 0, bytesOut: 0 },
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get metrics using the new API
|
||||||
|
const connectionsByIP = proxyMetrics.connections.byIP();
|
||||||
|
const instantThroughput = proxyMetrics.throughput.instant();
|
||||||
|
|
||||||
|
// Get throughput rate
|
||||||
|
const throughputRate = {
|
||||||
|
bytesInPerSecond: instantThroughput.in,
|
||||||
|
bytesOutPerSecond: instantThroughput.out
|
||||||
};
|
};
|
||||||
}
|
|
||||||
|
|
||||||
// Get metrics using the new API
|
// Get top IPs
|
||||||
const connectionsByIP = proxyMetrics.connections.byIP();
|
const topIPs = proxyMetrics.connections.topIPs(10);
|
||||||
const instantThroughput = proxyMetrics.throughput.instant();
|
|
||||||
|
|
||||||
// Get throughput rate
|
// Get total data transferred
|
||||||
const throughputRate = {
|
const totalDataTransferred = {
|
||||||
bytesInPerSecond: instantThroughput.in,
|
bytesIn: proxyMetrics.totals.bytesIn(),
|
||||||
bytesOutPerSecond: instantThroughput.out
|
bytesOut: proxyMetrics.totals.bytesOut()
|
||||||
};
|
};
|
||||||
|
|
||||||
// Get top IPs
|
return {
|
||||||
const topIPs = proxyMetrics.connections.topIPs(10);
|
connectionsByIP,
|
||||||
|
throughputRate,
|
||||||
// Get total data transferred
|
topIPs,
|
||||||
const totalDataTransferred = {
|
totalDataTransferred,
|
||||||
bytesIn: proxyMetrics.totals.bytesIn(),
|
};
|
||||||
bytesOut: proxyMetrics.totals.bytesOut()
|
}, 1000); // Use 200ms cache for more frequent updates
|
||||||
};
|
|
||||||
|
|
||||||
return {
|
|
||||||
connectionsByIP,
|
|
||||||
throughputRate,
|
|
||||||
topIPs,
|
|
||||||
totalDataTransferred,
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
}
|
}
|
@@ -162,6 +162,133 @@ export class StatsHandler {
|
|||||||
}
|
}
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// Combined Metrics Handler - More efficient for frontend polling
|
||||||
|
this.typedrouter.addTypedHandler(
|
||||||
|
new plugins.typedrequest.TypedHandler<interfaces.requests.IReq_GetCombinedMetrics>(
|
||||||
|
'getCombinedMetrics',
|
||||||
|
async (dataArg, toolsArg) => {
|
||||||
|
const sections = dataArg.sections || {
|
||||||
|
server: true,
|
||||||
|
email: true,
|
||||||
|
dns: true,
|
||||||
|
security: true,
|
||||||
|
network: true,
|
||||||
|
};
|
||||||
|
|
||||||
|
const metrics: any = {};
|
||||||
|
|
||||||
|
// Run all metrics collection in parallel
|
||||||
|
const promises: Promise<void>[] = [];
|
||||||
|
|
||||||
|
if (sections.server) {
|
||||||
|
promises.push(
|
||||||
|
this.collectServerStats().then(stats => {
|
||||||
|
metrics.server = {
|
||||||
|
uptime: stats.uptime,
|
||||||
|
startTime: Date.now() - (stats.uptime * 1000),
|
||||||
|
memoryUsage: stats.memoryUsage,
|
||||||
|
cpuUsage: stats.cpuUsage,
|
||||||
|
activeConnections: stats.activeConnections,
|
||||||
|
totalConnections: stats.totalConnections,
|
||||||
|
};
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (sections.email) {
|
||||||
|
promises.push(
|
||||||
|
this.collectEmailStats().then(stats => {
|
||||||
|
metrics.email = {
|
||||||
|
sent: stats.sentToday,
|
||||||
|
received: stats.receivedToday,
|
||||||
|
bounced: Math.floor(stats.sentToday * stats.bounceRate / 100),
|
||||||
|
queued: stats.queueSize,
|
||||||
|
failed: 0,
|
||||||
|
averageDeliveryTime: 0,
|
||||||
|
deliveryRate: stats.deliveryRate,
|
||||||
|
bounceRate: stats.bounceRate,
|
||||||
|
};
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (sections.dns) {
|
||||||
|
promises.push(
|
||||||
|
this.collectDnsStats().then(stats => {
|
||||||
|
metrics.dns = {
|
||||||
|
totalQueries: stats.totalQueries,
|
||||||
|
cacheHits: stats.cacheHits,
|
||||||
|
cacheMisses: stats.cacheMisses,
|
||||||
|
cacheHitRate: stats.cacheHitRate,
|
||||||
|
activeDomains: stats.topDomains.length,
|
||||||
|
averageResponseTime: 0,
|
||||||
|
queryTypes: stats.queryTypes,
|
||||||
|
};
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (sections.security && this.opsServerRef.dcRouterRef.metricsManager) {
|
||||||
|
promises.push(
|
||||||
|
this.opsServerRef.dcRouterRef.metricsManager.getSecurityStats().then(stats => {
|
||||||
|
metrics.security = {
|
||||||
|
blockedIPs: stats.blockedIPs,
|
||||||
|
reputationScores: {},
|
||||||
|
spamDetected: stats.spamDetected,
|
||||||
|
malwareDetected: stats.malwareDetected,
|
||||||
|
phishingDetected: stats.phishingDetected,
|
||||||
|
authenticationFailures: stats.authFailures,
|
||||||
|
suspiciousActivities: stats.totalThreatsBlocked,
|
||||||
|
};
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (sections.network && this.opsServerRef.dcRouterRef.metricsManager) {
|
||||||
|
promises.push(
|
||||||
|
this.opsServerRef.dcRouterRef.metricsManager.getNetworkStats().then(stats => {
|
||||||
|
const connectionDetails: interfaces.data.IConnectionDetails[] = [];
|
||||||
|
stats.connectionsByIP.forEach((count, ip) => {
|
||||||
|
connectionDetails.push({
|
||||||
|
remoteAddress: ip,
|
||||||
|
protocol: 'https' as any,
|
||||||
|
state: 'established' as any,
|
||||||
|
startTime: Date.now(),
|
||||||
|
bytesIn: 0,
|
||||||
|
bytesOut: 0,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
metrics.network = {
|
||||||
|
totalBandwidth: {
|
||||||
|
in: stats.throughputRate.bytesInPerSecond,
|
||||||
|
out: stats.throughputRate.bytesOutPerSecond,
|
||||||
|
},
|
||||||
|
activeConnections: stats.connectionsByIP.size,
|
||||||
|
connectionDetails: connectionDetails.slice(0, 50), // Limit to 50 connections
|
||||||
|
topEndpoints: stats.topIPs.map(ip => ({
|
||||||
|
endpoint: ip.ip,
|
||||||
|
requests: ip.count,
|
||||||
|
bandwidth: {
|
||||||
|
in: 0,
|
||||||
|
out: 0,
|
||||||
|
},
|
||||||
|
})),
|
||||||
|
};
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
await Promise.all(promises);
|
||||||
|
|
||||||
|
return {
|
||||||
|
metrics,
|
||||||
|
timestamp: Date.now(),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
)
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
private async collectServerStats(): Promise<{
|
private async collectServerStats(): Promise<{
|
||||||
|
@@ -103,3 +103,29 @@ export interface IHealthStatus {
|
|||||||
};
|
};
|
||||||
version?: string;
|
version?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface INetworkMetrics {
|
||||||
|
totalBandwidth: {
|
||||||
|
in: number;
|
||||||
|
out: number;
|
||||||
|
};
|
||||||
|
activeConnections: number;
|
||||||
|
connectionDetails: IConnectionDetails[];
|
||||||
|
topEndpoints: Array<{
|
||||||
|
endpoint: string;
|
||||||
|
requests: number;
|
||||||
|
bandwidth: {
|
||||||
|
in: number;
|
||||||
|
out: number;
|
||||||
|
};
|
||||||
|
}>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface IConnectionDetails {
|
||||||
|
remoteAddress: string;
|
||||||
|
protocol: 'http' | 'https' | 'smtp' | 'smtps';
|
||||||
|
state: 'connecting' | 'connected' | 'established' | 'closing';
|
||||||
|
startTime: number;
|
||||||
|
bytesIn: number;
|
||||||
|
bytesOut: number;
|
||||||
|
}
|
25
ts_interfaces/requests/combined.stats.ts
Normal file
25
ts_interfaces/requests/combined.stats.ts
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
import type * as data from '../data/index.js';
|
||||||
|
|
||||||
|
export interface IReq_GetCombinedMetrics {
|
||||||
|
method: 'getCombinedMetrics';
|
||||||
|
request: {
|
||||||
|
identity: data.IIdentity;
|
||||||
|
sections?: {
|
||||||
|
server?: boolean;
|
||||||
|
email?: boolean;
|
||||||
|
dns?: boolean;
|
||||||
|
security?: boolean;
|
||||||
|
network?: boolean;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
response: {
|
||||||
|
metrics: {
|
||||||
|
server?: data.IServerStats;
|
||||||
|
email?: data.IEmailStats;
|
||||||
|
dns?: data.IDnsStats;
|
||||||
|
security?: data.ISecurityMetrics;
|
||||||
|
network?: data.INetworkMetrics;
|
||||||
|
};
|
||||||
|
timestamp: number;
|
||||||
|
};
|
||||||
|
}
|
@@ -2,3 +2,4 @@ export * from './admin.js';
|
|||||||
export * from './config.js';
|
export * from './config.js';
|
||||||
export * from './logs.js';
|
export * from './logs.js';
|
||||||
export * from './stats.js';
|
export * from './stats.js';
|
||||||
|
export * from './combined.stats.js';
|
@@ -89,7 +89,7 @@ export const configStatePart = await appState.getStatePart<IConfigState>(
|
|||||||
export const uiStatePart = await appState.getStatePart<IUiState>(
|
export const uiStatePart = await appState.getStatePart<IUiState>(
|
||||||
'ui',
|
'ui',
|
||||||
{
|
{
|
||||||
activeView: 'dashboard',
|
activeView: 'overview',
|
||||||
sidebarCollapsed: false,
|
sidebarCollapsed: false,
|
||||||
autoRefresh: true,
|
autoRefresh: true,
|
||||||
refreshInterval: 1000, // 1 second
|
refreshInterval: 1000, // 1 second
|
||||||
@@ -184,56 +184,35 @@ export const logoutAction = loginStatePart.createAction(async (statePartArg) =>
|
|||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
// Fetch All Stats Action
|
// Fetch All Stats Action - Using combined endpoint for efficiency
|
||||||
export const fetchAllStatsAction = statsStatePart.createAction(async (statePartArg) => {
|
export const fetchAllStatsAction = statsStatePart.createAction(async (statePartArg) => {
|
||||||
const context = getActionContext();
|
const context = getActionContext();
|
||||||
|
|
||||||
const currentState = statePartArg.getState();
|
const currentState = statePartArg.getState();
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// Fetch server stats
|
// Use combined metrics endpoint - single request instead of 4
|
||||||
const serverStatsRequest = new plugins.domtools.plugins.typedrequest.TypedRequest<
|
const combinedRequest = new plugins.domtools.plugins.typedrequest.TypedRequest<
|
||||||
interfaces.requests.IReq_GetServerStatistics
|
interfaces.requests.IReq_GetCombinedMetrics
|
||||||
>('/typedrequest', 'getServerStatistics');
|
>('/typedrequest', 'getCombinedMetrics');
|
||||||
|
|
||||||
const serverStatsResponse = await serverStatsRequest.fire({
|
const combinedResponse = await combinedRequest.fire({
|
||||||
identity: context.identity,
|
identity: context.identity,
|
||||||
includeHistory: false,
|
sections: {
|
||||||
|
server: true,
|
||||||
|
email: true,
|
||||||
|
dns: true,
|
||||||
|
security: true,
|
||||||
|
network: false, // Network is fetched separately for the network view
|
||||||
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
// Fetch email stats
|
// Update state with all stats from combined response
|
||||||
const emailStatsRequest = new plugins.domtools.plugins.typedrequest.TypedRequest<
|
|
||||||
interfaces.requests.IReq_GetEmailStatistics
|
|
||||||
>('/typedrequest', 'getEmailStatistics');
|
|
||||||
|
|
||||||
const emailStatsResponse = await emailStatsRequest.fire({
|
|
||||||
identity: context.identity,
|
|
||||||
});
|
|
||||||
|
|
||||||
// Fetch DNS stats
|
|
||||||
const dnsStatsRequest = new plugins.domtools.plugins.typedrequest.TypedRequest<
|
|
||||||
interfaces.requests.IReq_GetDnsStatistics
|
|
||||||
>('/typedrequest', 'getDnsStatistics');
|
|
||||||
|
|
||||||
const dnsStatsResponse = await dnsStatsRequest.fire({
|
|
||||||
identity: context.identity,
|
|
||||||
});
|
|
||||||
|
|
||||||
// Fetch security metrics
|
|
||||||
const securityRequest = new plugins.domtools.plugins.typedrequest.TypedRequest<
|
|
||||||
interfaces.requests.IReq_GetSecurityMetrics
|
|
||||||
>('/typedrequest', 'getSecurityMetrics');
|
|
||||||
|
|
||||||
const securityResponse = await securityRequest.fire({
|
|
||||||
identity: context.identity,
|
|
||||||
});
|
|
||||||
|
|
||||||
// Update state with all stats
|
|
||||||
return {
|
return {
|
||||||
serverStats: serverStatsResponse.stats,
|
serverStats: combinedResponse.metrics.server || currentState.serverStats,
|
||||||
emailStats: emailStatsResponse.stats,
|
emailStats: combinedResponse.metrics.email || currentState.emailStats,
|
||||||
dnsStats: dnsStatsResponse.stats,
|
dnsStats: combinedResponse.metrics.dns || currentState.dnsStats,
|
||||||
securityMetrics: securityResponse.metrics,
|
securityMetrics: combinedResponse.metrics.security || currentState.securityMetrics,
|
||||||
lastUpdated: Date.now(),
|
lastUpdated: Date.now(),
|
||||||
isLoading: false,
|
isLoading: false,
|
||||||
error: null,
|
error: null,
|
||||||
@@ -342,6 +321,14 @@ export const toggleAutoRefreshAction = uiStatePart.createAction(async (statePart
|
|||||||
// Set Active View Action
|
// Set Active View Action
|
||||||
export const setActiveViewAction = uiStatePart.createAction<string>(async (statePartArg, viewName) => {
|
export const setActiveViewAction = uiStatePart.createAction<string>(async (statePartArg, viewName) => {
|
||||||
const currentState = statePartArg.getState();
|
const currentState = statePartArg.getState();
|
||||||
|
|
||||||
|
// If switching to network view, ensure we fetch network data
|
||||||
|
if (viewName === 'network' && currentState.activeView !== 'network') {
|
||||||
|
setTimeout(() => {
|
||||||
|
networkStatePart.dispatchAction(fetchNetworkStatsAction, null);
|
||||||
|
}, 100);
|
||||||
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
...currentState,
|
...currentState,
|
||||||
activeView: viewName,
|
activeView: viewName,
|
||||||
@@ -410,18 +397,118 @@ export const fetchNetworkStatsAction = networkStatePart.createAction(async (stat
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Combined refresh action for efficient polling
|
||||||
|
async function dispatchCombinedRefreshAction() {
|
||||||
|
const context = getActionContext();
|
||||||
|
const currentView = uiStatePart.getState().activeView;
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Always fetch basic stats for dashboard widgets
|
||||||
|
const combinedRequest = new plugins.domtools.plugins.typedrequest.TypedRequest<
|
||||||
|
interfaces.requests.IReq_GetCombinedMetrics
|
||||||
|
>('/typedrequest', 'getCombinedMetrics');
|
||||||
|
|
||||||
|
const combinedResponse = await combinedRequest.fire({
|
||||||
|
identity: context.identity,
|
||||||
|
sections: {
|
||||||
|
server: true,
|
||||||
|
email: true,
|
||||||
|
dns: true,
|
||||||
|
security: true,
|
||||||
|
network: currentView === 'network' || currentView === 'Network', // Only fetch network if on network view
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
// Update all stats from combined response
|
||||||
|
statsStatePart.setState({
|
||||||
|
...statsStatePart.getState(),
|
||||||
|
serverStats: combinedResponse.metrics.server || statsStatePart.getState().serverStats,
|
||||||
|
emailStats: combinedResponse.metrics.email || statsStatePart.getState().emailStats,
|
||||||
|
dnsStats: combinedResponse.metrics.dns || statsStatePart.getState().dnsStats,
|
||||||
|
securityMetrics: combinedResponse.metrics.security || statsStatePart.getState().securityMetrics,
|
||||||
|
lastUpdated: Date.now(),
|
||||||
|
isLoading: false,
|
||||||
|
error: null,
|
||||||
|
});
|
||||||
|
|
||||||
|
// Update network stats if included
|
||||||
|
if (combinedResponse.metrics.network && (currentView === 'network' || currentView === 'Network')) {
|
||||||
|
const network = combinedResponse.metrics.network;
|
||||||
|
const connectionsByIP: { [ip: string]: number } = {};
|
||||||
|
|
||||||
|
// Convert connection details to IP counts
|
||||||
|
network.connectionDetails.forEach(conn => {
|
||||||
|
connectionsByIP[conn.remoteAddress] = (connectionsByIP[conn.remoteAddress] || 0) + 1;
|
||||||
|
});
|
||||||
|
|
||||||
|
// Fetch detailed connections for the network view
|
||||||
|
try {
|
||||||
|
const connectionsRequest = new plugins.domtools.plugins.typedrequest.TypedRequest<
|
||||||
|
interfaces.requests.IReq_GetActiveConnections
|
||||||
|
>('/typedrequest', 'getActiveConnections');
|
||||||
|
|
||||||
|
const connectionsResponse = await connectionsRequest.fire({
|
||||||
|
identity: context.identity,
|
||||||
|
});
|
||||||
|
|
||||||
|
networkStatePart.setState({
|
||||||
|
...networkStatePart.getState(),
|
||||||
|
connections: connectionsResponse.connections,
|
||||||
|
connectionsByIP,
|
||||||
|
throughputRate: {
|
||||||
|
bytesInPerSecond: network.totalBandwidth.in,
|
||||||
|
bytesOutPerSecond: network.totalBandwidth.out
|
||||||
|
},
|
||||||
|
topIPs: network.topEndpoints.map(e => ({ ip: e.endpoint, count: e.requests })),
|
||||||
|
lastUpdated: Date.now(),
|
||||||
|
isLoading: false,
|
||||||
|
error: null,
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Failed to fetch connections:', error);
|
||||||
|
networkStatePart.setState({
|
||||||
|
...networkStatePart.getState(),
|
||||||
|
connections: [],
|
||||||
|
connectionsByIP,
|
||||||
|
throughputRate: {
|
||||||
|
bytesInPerSecond: network.totalBandwidth.in,
|
||||||
|
bytesOutPerSecond: network.totalBandwidth.out
|
||||||
|
},
|
||||||
|
topIPs: network.topEndpoints.map(e => ({ ip: e.endpoint, count: e.requests })),
|
||||||
|
lastUpdated: Date.now(),
|
||||||
|
isLoading: false,
|
||||||
|
error: null,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Combined refresh failed:', error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Initialize auto-refresh
|
// Initialize auto-refresh
|
||||||
let refreshInterval: NodeJS.Timeout | null = null;
|
let refreshInterval: NodeJS.Timeout | null = null;
|
||||||
|
let currentRefreshRate = 1000; // Track current refresh rate to avoid unnecessary restarts
|
||||||
|
|
||||||
// Initialize auto-refresh when UI state is ready
|
// Initialize auto-refresh when UI state is ready
|
||||||
(() => {
|
(() => {
|
||||||
const startAutoRefresh = () => {
|
const startAutoRefresh = () => {
|
||||||
const uiState = uiStatePart.getState();
|
const uiState = uiStatePart.getState();
|
||||||
if (uiState.autoRefresh && loginStatePart.getState().isLoggedIn) {
|
const loginState = loginStatePart.getState();
|
||||||
refreshInterval = setInterval(() => {
|
|
||||||
statsStatePart.dispatchAction(fetchAllStatsAction, null);
|
// Only start if conditions are met and not already running at the same rate
|
||||||
networkStatePart.dispatchAction(fetchNetworkStatsAction, null);
|
if (uiState.autoRefresh && loginState.isLoggedIn) {
|
||||||
}, uiState.refreshInterval);
|
// Check if we need to restart the interval (rate changed or not running)
|
||||||
|
if (!refreshInterval || currentRefreshRate !== uiState.refreshInterval) {
|
||||||
|
stopAutoRefresh();
|
||||||
|
currentRefreshRate = uiState.refreshInterval;
|
||||||
|
refreshInterval = setInterval(() => {
|
||||||
|
// Use combined refresh action for efficiency
|
||||||
|
dispatchCombinedRefreshAction();
|
||||||
|
}, uiState.refreshInterval);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
stopAutoRefresh();
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -429,18 +516,31 @@ let refreshInterval: NodeJS.Timeout | null = null;
|
|||||||
if (refreshInterval) {
|
if (refreshInterval) {
|
||||||
clearInterval(refreshInterval);
|
clearInterval(refreshInterval);
|
||||||
refreshInterval = null;
|
refreshInterval = null;
|
||||||
|
currentRefreshRate = 0;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// Watch for changes
|
// Watch for relevant changes only
|
||||||
uiStatePart.state.subscribe(() => {
|
let previousAutoRefresh = uiStatePart.getState().autoRefresh;
|
||||||
stopAutoRefresh();
|
let previousRefreshInterval = uiStatePart.getState().refreshInterval;
|
||||||
startAutoRefresh();
|
let previousIsLoggedIn = loginStatePart.getState().isLoggedIn;
|
||||||
|
|
||||||
|
uiStatePart.state.subscribe((state) => {
|
||||||
|
// Only restart if relevant values changed
|
||||||
|
if (state.autoRefresh !== previousAutoRefresh ||
|
||||||
|
state.refreshInterval !== previousRefreshInterval) {
|
||||||
|
previousAutoRefresh = state.autoRefresh;
|
||||||
|
previousRefreshInterval = state.refreshInterval;
|
||||||
|
startAutoRefresh();
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
loginStatePart.state.subscribe(() => {
|
loginStatePart.state.subscribe((state) => {
|
||||||
stopAutoRefresh();
|
// Only restart if login state changed
|
||||||
startAutoRefresh();
|
if (state.isLoggedIn !== previousIsLoggedIn) {
|
||||||
|
previousIsLoggedIn = state.isLoggedIn;
|
||||||
|
startAutoRefresh();
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// Initial start
|
// Initial start
|
||||||
|
@@ -127,6 +127,16 @@ export class OpsDashboard extends DeesElement {
|
|||||||
this.login(e.detail.data.username, e.detail.data.password);
|
this.login(e.detail.data.username, e.detail.data.password);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Handle view changes
|
||||||
|
const appDash = this.shadowRoot.querySelector('dees-simple-appdash');
|
||||||
|
if (appDash) {
|
||||||
|
appDash.addEventListener('viewSwitch', (e: CustomEvent) => {
|
||||||
|
const viewName = e.detail.tabName;
|
||||||
|
console.log('View switched to:', viewName);
|
||||||
|
appstate.uiStatePart.dispatchAction(appstate.setActiveViewAction, viewName.toLowerCase());
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
// Handle initial state
|
// Handle initial state
|
||||||
const loginState = appstate.loginStatePart.getState();
|
const loginState = appstate.loginStatePart.getState();
|
||||||
console.log('Initial login state:', loginState);
|
console.log('Initial login state:', loginState);
|
||||||
|
@@ -43,6 +43,10 @@ export class OpsViewNetwork extends DeesElement {
|
|||||||
@state()
|
@state()
|
||||||
private trafficDataOut: Array<{ x: string | number; y: number }> = [];
|
private trafficDataOut: Array<{ x: string | number; y: number }> = [];
|
||||||
|
|
||||||
|
// Track if we need to update the chart to avoid unnecessary re-renders
|
||||||
|
private lastChartUpdate = 0;
|
||||||
|
private chartUpdateThreshold = 1000; // Minimum ms between chart updates
|
||||||
|
|
||||||
private lastTrafficUpdateTime = 0;
|
private lastTrafficUpdateTime = 0;
|
||||||
private trafficUpdateInterval = 1000; // Update every 1 second
|
private trafficUpdateInterval = 1000; // Update every 1 second
|
||||||
private requestCountHistory = new Map<number, number>(); // Track requests per time bucket
|
private requestCountHistory = new Map<number, number>(); // Track requests per time bucket
|
||||||
@@ -59,21 +63,35 @@ export class OpsViewNetwork extends DeesElement {
|
|||||||
this.startTrafficUpdateTimer();
|
this.startTrafficUpdateTimer();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async connectedCallback() {
|
||||||
|
await super.connectedCallback();
|
||||||
|
|
||||||
|
// When network view becomes visible, ensure we fetch network data
|
||||||
|
console.log('Network view connected - fetching initial data');
|
||||||
|
await appstate.networkStatePart.dispatchAction(appstate.fetchNetworkStatsAction, null);
|
||||||
|
|
||||||
|
// Also update the active view state
|
||||||
|
appstate.uiStatePart.dispatchAction(appstate.setActiveViewAction, 'network');
|
||||||
|
}
|
||||||
|
|
||||||
async disconnectedCallback() {
|
async disconnectedCallback() {
|
||||||
await super.disconnectedCallback();
|
await super.disconnectedCallback();
|
||||||
this.stopTrafficUpdateTimer();
|
this.stopTrafficUpdateTimer();
|
||||||
}
|
}
|
||||||
|
|
||||||
private subscribeToStateParts() {
|
private subscribeToStateParts() {
|
||||||
appstate.statsStatePart.state.subscribe((state) => {
|
// Subscribe and track unsubscribe functions
|
||||||
|
const statsUnsubscribe = appstate.statsStatePart.state.subscribe((state) => {
|
||||||
this.statsState = state;
|
this.statsState = state;
|
||||||
this.updateNetworkData();
|
this.updateNetworkData();
|
||||||
});
|
});
|
||||||
|
this.rxSubscriptions.push(statsUnsubscribe);
|
||||||
|
|
||||||
appstate.networkStatePart.state.subscribe((state) => {
|
const networkUnsubscribe = appstate.networkStatePart.state.subscribe((state) => {
|
||||||
this.networkState = state;
|
this.networkState = state;
|
||||||
this.updateNetworkData();
|
this.updateNetworkData();
|
||||||
});
|
});
|
||||||
|
this.rxSubscriptions.push(networkUnsubscribe);
|
||||||
}
|
}
|
||||||
|
|
||||||
private initializeTrafficData() {
|
private initializeTrafficData() {
|
||||||
@@ -169,6 +187,13 @@ export class OpsViewNetwork extends DeesElement {
|
|||||||
];
|
];
|
||||||
|
|
||||||
public render() {
|
public render() {
|
||||||
|
console.log('Network view render - chart data points:', {
|
||||||
|
inPoints: this.trafficDataIn.length,
|
||||||
|
outPoints: this.trafficDataOut.length,
|
||||||
|
lastInValue: this.trafficDataIn[this.trafficDataIn.length - 1]?.y,
|
||||||
|
lastOutValue: this.trafficDataOut[this.trafficDataOut.length - 1]?.y
|
||||||
|
});
|
||||||
|
|
||||||
return html`
|
return html`
|
||||||
<ops-sectionheading>Network Activity</ops-sectionheading>
|
<ops-sectionheading>Network Activity</ops-sectionheading>
|
||||||
|
|
||||||
@@ -278,7 +303,6 @@ export class OpsViewNetwork extends DeesElement {
|
|||||||
iconName: 'copy',
|
iconName: 'copy',
|
||||||
action: async () => {
|
action: async () => {
|
||||||
await navigator.clipboard.writeText(request.id);
|
await navigator.clipboard.writeText(request.id);
|
||||||
console.log('Request ID copied to clipboard');
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
@@ -367,6 +391,8 @@ export class OpsViewNetwork extends DeesElement {
|
|||||||
const throughput = this.calculateThroughput();
|
const throughput = this.calculateThroughput();
|
||||||
const activeConnections = this.statsState.serverStats?.activeConnections || 0;
|
const activeConnections = this.statsState.serverStats?.activeConnections || 0;
|
||||||
|
|
||||||
|
// Throughput data is now available in the stats tiles
|
||||||
|
|
||||||
// Use request count history for the requests/sec trend
|
// Use request count history for the requests/sec trend
|
||||||
const trendData = [...this.requestsPerSecHistory];
|
const trendData = [...this.requestsPerSecHistory];
|
||||||
|
|
||||||
@@ -466,25 +492,36 @@ export class OpsViewNetwork extends DeesElement {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private async updateNetworkData() {
|
private async updateNetworkData() {
|
||||||
// Convert connection data to network requests format
|
// Only update if connections changed significantly
|
||||||
if (this.networkState.connections.length > 0) {
|
const newConnectionCount = this.networkState.connections.length;
|
||||||
this.networkRequests = this.networkState.connections.map((conn, index) => ({
|
const oldConnectionCount = this.networkRequests.length;
|
||||||
id: conn.id,
|
|
||||||
timestamp: conn.startTime,
|
// Check if we need to update the network requests array
|
||||||
method: 'GET', // Default method for proxy connections
|
const shouldUpdate = newConnectionCount !== oldConnectionCount ||
|
||||||
url: '/',
|
newConnectionCount === 0 ||
|
||||||
hostname: conn.remoteAddress,
|
(newConnectionCount > 0 && this.networkRequests.length === 0);
|
||||||
port: conn.protocol === 'https' ? 443 : 80,
|
|
||||||
protocol: conn.protocol === 'https' || conn.protocol === 'http' ? conn.protocol : 'tcp',
|
if (shouldUpdate) {
|
||||||
statusCode: conn.state === 'connected' ? 200 : undefined,
|
// Convert connection data to network requests format
|
||||||
duration: Date.now() - conn.startTime,
|
if (newConnectionCount > 0) {
|
||||||
bytesIn: conn.bytesReceived,
|
this.networkRequests = this.networkState.connections.map((conn, index) => ({
|
||||||
bytesOut: conn.bytesSent,
|
id: conn.id,
|
||||||
remoteIp: conn.remoteAddress,
|
timestamp: conn.startTime,
|
||||||
route: 'proxy',
|
method: 'GET', // Default method for proxy connections
|
||||||
}));
|
url: '/',
|
||||||
} else {
|
hostname: conn.remoteAddress,
|
||||||
this.networkRequests = [];
|
port: conn.protocol === 'https' ? 443 : 80,
|
||||||
|
protocol: conn.protocol === 'https' || conn.protocol === 'http' ? conn.protocol : 'tcp',
|
||||||
|
statusCode: conn.state === 'connected' ? 200 : undefined,
|
||||||
|
duration: Date.now() - conn.startTime,
|
||||||
|
bytesIn: conn.bytesReceived,
|
||||||
|
bytesOut: conn.bytesSent,
|
||||||
|
remoteIp: conn.remoteAddress,
|
||||||
|
route: 'proxy',
|
||||||
|
}));
|
||||||
|
} else {
|
||||||
|
this.networkRequests = [];
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Generate traffic data based on request history
|
// Generate traffic data based on request history
|
||||||
@@ -492,87 +529,58 @@ export class OpsViewNetwork extends DeesElement {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private updateTrafficData() {
|
private updateTrafficData() {
|
||||||
|
// This method is called when network data updates
|
||||||
|
// The actual chart updates are handled by the timer calling addTrafficDataPoint()
|
||||||
|
console.log('UpdateTrafficData called - network data updated');
|
||||||
|
}
|
||||||
|
|
||||||
|
private startTrafficUpdateTimer() {
|
||||||
|
this.stopTrafficUpdateTimer(); // Clear any existing timer
|
||||||
|
this.trafficUpdateTimer = setInterval(() => {
|
||||||
|
// Add a new data point every second
|
||||||
|
this.addTrafficDataPoint();
|
||||||
|
}, 1000); // Update every second
|
||||||
|
}
|
||||||
|
|
||||||
|
private addTrafficDataPoint() {
|
||||||
const now = Date.now();
|
const now = Date.now();
|
||||||
// Fixed 5 minute time range
|
|
||||||
const range = 5 * 60 * 1000; // 5 minutes
|
|
||||||
const bucketSize = range / 60; // 60 data points // 60 data points
|
|
||||||
|
|
||||||
// Check if enough time has passed to add a new data point
|
// Throttle chart updates to avoid excessive re-renders
|
||||||
const timeSinceLastUpdate = now - this.lastTrafficUpdateTime;
|
if (now - this.lastChartUpdate < this.chartUpdateThreshold) {
|
||||||
const shouldAddNewPoint = timeSinceLastUpdate >= this.trafficUpdateInterval;
|
|
||||||
|
|
||||||
console.log('UpdateTrafficData called:', {
|
|
||||||
networkRequestsCount: this.networkRequests.length,
|
|
||||||
timeSinceLastUpdate,
|
|
||||||
shouldAddNewPoint,
|
|
||||||
currentDataPoints: this.trafficDataIn.length
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!shouldAddNewPoint && this.trafficDataIn.length > 0) {
|
|
||||||
// Not enough time has passed, don't update
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Use real-time throughput data from SmartProxy (same as throughput tiles)
|
|
||||||
const throughput = this.calculateThroughput();
|
const throughput = this.calculateThroughput();
|
||||||
|
|
||||||
// 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;
|
||||||
const throughputOutMbps = (throughput.out * 8) / 1000000;
|
const throughputOutMbps = (throughput.out * 8) / 1000000;
|
||||||
|
|
||||||
console.log('Throughput calculation:', {
|
// Add new data points
|
||||||
bytesInPerSecond: throughput.in,
|
const timestamp = new Date(now).toISOString();
|
||||||
bytesOutPerSecond: throughput.out,
|
|
||||||
throughputInMbps,
|
|
||||||
throughputOutMbps,
|
|
||||||
throughputTileValue: `${this.formatBitsPerSecond(throughput.in)} IN, ${this.formatBitsPerSecond(throughput.out)} OUT`
|
|
||||||
});
|
|
||||||
|
|
||||||
if (this.trafficDataIn.length === 0) {
|
const newDataPointIn = {
|
||||||
// Initialize if empty
|
x: timestamp,
|
||||||
this.initializeTrafficData();
|
y: Math.round(throughputInMbps * 10) / 10
|
||||||
|
};
|
||||||
|
|
||||||
|
const newDataPointOut = {
|
||||||
|
x: timestamp,
|
||||||
|
y: Math.round(throughputOutMbps * 10) / 10
|
||||||
|
};
|
||||||
|
|
||||||
|
// Efficient array updates - modify in place when possible
|
||||||
|
if (this.trafficDataIn.length >= 60) {
|
||||||
|
// Remove oldest and add newest
|
||||||
|
this.trafficDataIn = [...this.trafficDataIn.slice(1), newDataPointIn];
|
||||||
|
this.trafficDataOut = [...this.trafficDataOut.slice(1), newDataPointOut];
|
||||||
} else {
|
} else {
|
||||||
// Add new data points for both in and out
|
// Still filling up the initial data
|
||||||
const timestamp = new Date(now).toISOString();
|
this.trafficDataIn = [...this.trafficDataIn, newDataPointIn];
|
||||||
|
this.trafficDataOut = [...this.trafficDataOut, newDataPointOut];
|
||||||
const newDataPointIn = {
|
|
||||||
x: timestamp,
|
|
||||||
y: Math.round(throughputInMbps * 10) / 10 // Round to 1 decimal place
|
|
||||||
};
|
|
||||||
|
|
||||||
const newDataPointOut = {
|
|
||||||
x: timestamp,
|
|
||||||
y: Math.round(throughputOutMbps * 10) / 10 // Round to 1 decimal place
|
|
||||||
};
|
|
||||||
|
|
||||||
// Create new arrays with existing data plus new points
|
|
||||||
const newTrafficDataIn = [...this.trafficDataIn, newDataPointIn];
|
|
||||||
const newTrafficDataOut = [...this.trafficDataOut, newDataPointOut];
|
|
||||||
|
|
||||||
// Keep only the last 60 points
|
|
||||||
if (newTrafficDataIn.length > 60) {
|
|
||||||
newTrafficDataIn.shift(); // Remove oldest point
|
|
||||||
newTrafficDataOut.shift();
|
|
||||||
}
|
|
||||||
|
|
||||||
this.trafficDataIn = newTrafficDataIn;
|
|
||||||
this.trafficDataOut = newTrafficDataOut;
|
|
||||||
this.lastTrafficUpdateTime = now;
|
|
||||||
|
|
||||||
console.log('Added new traffic data points:', {
|
|
||||||
timestamp: timestamp,
|
|
||||||
throughputInMbps: newDataPointIn.y,
|
|
||||||
throughputOutMbps: newDataPointOut.y,
|
|
||||||
totalPoints: this.trafficDataIn.length
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
private startTrafficUpdateTimer() {
|
this.lastChartUpdate = now;
|
||||||
this.stopTrafficUpdateTimer(); // Clear any existing timer
|
|
||||||
this.trafficUpdateTimer = setInterval(() => {
|
|
||||||
this.updateTrafficData();
|
|
||||||
}, 1000); // Check every second, but only update when interval has passed
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private stopTrafficUpdateTimer() {
|
private stopTrafficUpdateTimer() {
|
||||||
@@ -581,5 +589,4 @@ export class OpsViewNetwork extends DeesElement {
|
|||||||
this.trafficUpdateTimer = null;
|
this.trafficUpdateTimer = null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
Reference in New Issue
Block a user