fix(metrics): fix metrics

This commit is contained in:
Juergen Kunz
2025-06-22 23:40:02 +00:00
parent 92fde9d0d7
commit d24e51117d
6 changed files with 545 additions and 127 deletions

View File

@ -30,7 +30,7 @@
"@api.global/typedserver": "^3.0.74", "@api.global/typedserver": "^3.0.74",
"@api.global/typedsocket": "^3.0.0", "@api.global/typedsocket": "^3.0.0",
"@apiclient.xyz/cloudflare": "^6.4.1", "@apiclient.xyz/cloudflare": "^6.4.1",
"@design.estate/dees-catalog": "^1.8.20", "@design.estate/dees-catalog": "^1.9.0",
"@design.estate/dees-element": "^2.0.44", "@design.estate/dees-element": "^2.0.44",
"@push.rocks/projectinfo": "^5.0.1", "@push.rocks/projectinfo": "^5.0.1",
"@push.rocks/qenv": "^6.1.0", "@push.rocks/qenv": "^6.1.0",
@ -46,7 +46,7 @@
"@push.rocks/smartnetwork": "^4.0.2", "@push.rocks/smartnetwork": "^4.0.2",
"@push.rocks/smartpath": "^5.0.5", "@push.rocks/smartpath": "^5.0.5",
"@push.rocks/smartpromise": "^4.0.3", "@push.rocks/smartpromise": "^4.0.3",
"@push.rocks/smartproxy": "^19.6.6", "@push.rocks/smartproxy": "^19.6.7",
"@push.rocks/smartrequest": "^2.1.0", "@push.rocks/smartrequest": "^2.1.0",
"@push.rocks/smartrule": "^2.0.1", "@push.rocks/smartrule": "^2.0.1",
"@push.rocks/smartrx": "^3.0.10", "@push.rocks/smartrx": "^3.0.10",

22
pnpm-lock.yaml generated
View File

@ -24,8 +24,8 @@ importers:
specifier: ^6.4.1 specifier: ^6.4.1
version: 6.4.1 version: 6.4.1
'@design.estate/dees-catalog': '@design.estate/dees-catalog':
specifier: ^1.8.20 specifier: ^1.9.0
version: 1.8.20 version: 1.9.0
'@design.estate/dees-element': '@design.estate/dees-element':
specifier: ^2.0.44 specifier: ^2.0.44
version: 2.0.44 version: 2.0.44
@ -72,8 +72,8 @@ importers:
specifier: ^4.0.3 specifier: ^4.0.3
version: 4.2.3 version: 4.2.3
'@push.rocks/smartproxy': '@push.rocks/smartproxy':
specifier: ^19.6.6 specifier: ^19.6.7
version: 19.6.6(@aws-sdk/credential-providers@3.817.0)(socks@2.8.4) version: 19.6.7(@aws-sdk/credential-providers@3.817.0)(socks@2.8.4)
'@push.rocks/smartrequest': '@push.rocks/smartrequest':
specifier: ^2.1.0 specifier: ^2.1.0
version: 2.1.0 version: 2.1.0
@ -344,8 +344,8 @@ packages:
'@dabh/diagnostics@2.0.3': '@dabh/diagnostics@2.0.3':
resolution: {integrity: sha512-hrlQOIi7hAfzsMqlGSFyVucrx38O+j6wiGOf//H2ecvIEqYN4ADBSS2iLMh5UFyDunCNniUIPk/q3riFv45xRA==} resolution: {integrity: sha512-hrlQOIi7hAfzsMqlGSFyVucrx38O+j6wiGOf//H2ecvIEqYN4ADBSS2iLMh5UFyDunCNniUIPk/q3riFv45xRA==}
'@design.estate/dees-catalog@1.8.20': '@design.estate/dees-catalog@1.9.0':
resolution: {integrity: sha512-TEZXZQaUBSw6mgd78Nx9pQO+5T8s5GSIBhvMrcxV8QEjkitGtnUQBztMT3VMAneS30hc3JGfTedpu6OnXw4XNQ==} resolution: {integrity: sha512-rK/EjTC6H0t0Ow/TRmt1RiTx+0Qz+apOIKhjaQ1YcPODfy4LAj1oKc5VK1VnrFguuABRIL2M4xMssDtS+G78Kw==}
'@design.estate/dees-comms@1.0.27': '@design.estate/dees-comms@1.0.27':
resolution: {integrity: sha512-GvzTUwkV442LD60T08iqSoqvhA02Mou5lFvvqBPc4yBUiU7cZISqBx+76xvMgMIEI9Dx9JfTl4/2nW8MoVAanw==} resolution: {integrity: sha512-GvzTUwkV442LD60T08iqSoqvhA02Mou5lFvvqBPc4yBUiU7cZISqBx+76xvMgMIEI9Dx9JfTl4/2nW8MoVAanw==}
@ -1110,8 +1110,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@19.6.6': '@push.rocks/smartproxy@19.6.7':
resolution: {integrity: sha512-AweTvBYlYubelO+g6Bf/4cg8RXb0fcMgYE1UKAT/m5PNbOuRWzTtXkja4JuFWfIdvmbfZxiWAaw9OhJvHIgIrw==} resolution: {integrity: sha512-tC/zqUzSo4/SPqp52UrSe3cPR/YtyZiFC/HhYktCNfDXaWPqxY3ioViTwC2i6tb/6T6LmNdRJUOXxGFAz1j1cQ==}
'@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==}
@ -5257,7 +5257,7 @@ snapshots:
enabled: 2.0.0 enabled: 2.0.0
kuler: 2.0.0 kuler: 2.0.0
'@design.estate/dees-catalog@1.8.20': '@design.estate/dees-catalog@1.9.0':
dependencies: dependencies:
'@design.estate/dees-domtools': 2.3.3 '@design.estate/dees-domtools': 2.3.3
'@design.estate/dees-element': 2.0.44 '@design.estate/dees-element': 2.0.44
@ -6029,6 +6029,7 @@ snapshots:
- '@mongodb-js/zstd' - '@mongodb-js/zstd'
- '@nuxt/kit' - '@nuxt/kit'
- aws-crt - aws-crt
- bufferutil
- encoding - encoding
- gcp-metadata - gcp-metadata
- kerberos - kerberos
@ -6037,6 +6038,7 @@ snapshots:
- snappy - snappy
- socks - socks
- supports-color - supports-color
- utf-8-validate
- vue - vue
'@push.rocks/smartarchive@3.0.8': '@push.rocks/smartarchive@3.0.8':
@ -6474,7 +6476,7 @@ snapshots:
'@push.rocks/smartpromise@4.2.3': {} '@push.rocks/smartpromise@4.2.3': {}
'@push.rocks/smartproxy@19.6.6(@aws-sdk/credential-providers@3.817.0)(socks@2.8.4)': '@push.rocks/smartproxy@19.6.7(@aws-sdk/credential-providers@3.817.0)(socks@2.8.4)':
dependencies: dependencies:
'@push.rocks/lik': 6.2.2 '@push.rocks/lik': 6.2.2
'@push.rocks/smartacme': 8.0.0(@aws-sdk/credential-providers@3.817.0)(socks@2.8.4) '@push.rocks/smartacme': 8.0.0(@aws-sdk/credential-providers@3.817.0)(socks@2.8.4)

125
readme.plan2.md Normal file
View File

@ -0,0 +1,125 @@
# Network Traffic Graph Fix Plan
## Command: `pnpm run reread`
## Issue Summary
The network traffic graph in ops-view-network.ts is not displaying data due to three critical issues:
1. Timestamp format mismatch - chart expects ISO strings but receives numeric timestamps
2. Empty data when no active connections exist
3. Potential bucket alignment issues
## Root Causes
### 1. Timestamp Format Issue
- **Current**: `x: time` (numeric timestamp like 1703123456789)
- **Expected**: `x: new Date(time).toISOString()` (ISO string like "2023-12-20T12:34:56.789Z")
- **Impact**: ApexCharts cannot parse the x-axis values, resulting in no visible data
### 2. Empty Data Handling
- When no active connections exist, `networkRequests` array is empty
- Empty array leads to no buckets being created
- Chart shows flat line at 0
### 3. Data Bucketing Logic
- Current logic creates buckets but uses numeric timestamps as Map keys
- This works for calculation but fails when looking up values for chart display
## Implementation Plan
### Step 1: Fix Timestamp Format in updateTrafficData()
```typescript
// In ops-view-network.ts, line 548-554
this.trafficData = Array.from({ length: 60 }, (_, i) => {
const time = now - (i * bucketSize);
return {
x: new Date(time).toISOString(), // Convert to ISO string
y: buckets.get(time) || 0,
};
}).reverse();
```
### Step 2: Add Data Generation for Empty States
Create synthetic data points when no connections exist to show the chart grid:
```typescript
private updateTrafficData() {
// ... existing code ...
// If no data, create zero-value points to show grid
if (this.networkRequests.length === 0) {
this.trafficData = Array.from({ length: 60 }, (_, i) => {
const time = now - (i * bucketSize);
return {
x: new Date(time).toISOString(),
y: 0,
};
}).reverse();
return;
}
// ... rest of existing bucketing logic ...
}
```
### Step 3: Improve Bucket Alignment (Optional Enhancement)
Align buckets to start of time periods for cleaner data:
```typescript
// Calculate bucket start time
const bucketStartTime = Math.floor(req.timestamp / bucketSize) * bucketSize;
buckets.set(bucketStartTime, (buckets.get(bucketStartTime) || 0) + 1);
```
### Step 4: Add Debug Logging (Temporary)
Add console logs to verify data flow:
```typescript
console.log('Traffic data generated:', this.trafficData);
console.log('Network requests count:', this.networkRequests.length);
```
### Step 5: Update Chart Configuration
Ensure chart component has proper configuration:
```typescript
<!-- Already correct in the template -->
<dees-chart-area
.label=${'Network Traffic'}
.series=${[{
name: 'Requests/min',
data: this.trafficData,
}]}
></dees-chart-area>
```
## Testing Plan
1. **Test with no connections**: Verify chart shows grid with zero line
2. **Test with active connections**: Verify chart shows actual traffic data
3. **Test time range changes**: Verify chart updates when selecting different time ranges
4. **Test auto-refresh**: Verify chart updates every second with new data
## Expected Outcome
- Network traffic chart displays properly with time on x-axis
- Chart shows grid and zero line even when no data exists
- Real-time updates work correctly
- Time ranges (1m, 5m, 15m, 1h, 24h) all function properly
## Implementation Order
1. Fix timestamp format (critical fix)
2. Add empty state handling
3. Test basic functionality
4. Add debug logging if issues persist
5. Implement bucket alignment improvement if needed
## Success Criteria
- [ ] Chart displays time labels on x-axis
- [ ] Chart shows data points when connections exist
- [ ] Chart shows zero line when no connections exist
- [ ] Chart updates in real-time as new connections arrive
- [ ] All time range selections work correctly
## Estimated Effort
- Implementation: 30 minutes
- Testing: 15 minutes
- Total: 45 minutes

View File

@ -18,6 +18,9 @@ export class MetricsManager {
bouncedToday: 0, bouncedToday: 0,
queueSize: 0, queueSize: 0,
lastResetDate: new Date().toDateString(), lastResetDate: new Date().toDateString(),
deliveryTimes: [] as number[], // Track delivery times in ms
recipients: new Map<string, number>(), // Track email count by recipient
recentActivity: [] as Array<{ timestamp: number; type: string; details: string }>,
}; };
// Track DNS-specific metrics // Track DNS-specific metrics
@ -28,6 +31,8 @@ export class MetricsManager {
queryTypes: {} as Record<string, number>, queryTypes: {} as Record<string, number>,
topDomains: new Map<string, number>(), topDomains: new Map<string, number>(),
lastResetDate: new Date().toDateString(), lastResetDate: new Date().toDateString(),
queryTimestamps: [] as number[], // Track query timestamps for rate calculation
responseTimes: [] as number[], // Track response times in ms
}; };
// Track security-specific metrics // Track security-specific metrics
@ -38,6 +43,7 @@ export class MetricsManager {
malwareDetected: 0, malwareDetected: 0,
phishingDetected: 0, phishingDetected: 0,
lastResetDate: new Date().toDateString(), lastResetDate: new Date().toDateString(),
incidents: [] as Array<{ timestamp: number; type: string; severity: string; details: string }>,
}; };
constructor(dcRouter: DcRouter) { constructor(dcRouter: DcRouter) {
@ -66,6 +72,9 @@ export class MetricsManager {
this.emailMetrics.receivedToday = 0; this.emailMetrics.receivedToday = 0;
this.emailMetrics.failedToday = 0; this.emailMetrics.failedToday = 0;
this.emailMetrics.bouncedToday = 0; this.emailMetrics.bouncedToday = 0;
this.emailMetrics.deliveryTimes = [];
this.emailMetrics.recipients.clear();
this.emailMetrics.recentActivity = [];
this.emailMetrics.lastResetDate = currentDate; this.emailMetrics.lastResetDate = currentDate;
} }
@ -75,6 +84,8 @@ export class MetricsManager {
this.dnsMetrics.cacheMisses = 0; this.dnsMetrics.cacheMisses = 0;
this.dnsMetrics.queryTypes = {}; this.dnsMetrics.queryTypes = {};
this.dnsMetrics.topDomains.clear(); this.dnsMetrics.topDomains.clear();
this.dnsMetrics.queryTimestamps = [];
this.dnsMetrics.responseTimes = [];
this.dnsMetrics.lastResetDate = currentDate; this.dnsMetrics.lastResetDate = currentDate;
} }
@ -84,6 +95,7 @@ export class MetricsManager {
this.securityMetrics.spamDetected = 0; this.securityMetrics.spamDetected = 0;
this.securityMetrics.malwareDetected = 0; this.securityMetrics.malwareDetected = 0;
this.securityMetrics.phishingDetected = 0; this.securityMetrics.phishingDetected = 0;
this.securityMetrics.incidents = [];
this.securityMetrics.lastResetDate = currentDate; this.securityMetrics.lastResetDate = currentDate;
} }
}, 60000); // Check every minute }, 60000); // Check every minute
@ -105,7 +117,8 @@ 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(); const smartMetricsData = await this.smartMetrics.getMetrics();
const proxyStats = this.dcRouter.smartProxy ? this.dcRouter.smartProxy.getStats() : 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(),
@ -124,15 +137,32 @@ export class MetricsManager {
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.getActiveConnections() : 0, activeConnections: proxyStats ? proxyStats.activeConnections : 0,
totalConnections: proxyStats ? proxyStats.getTotalConnections() : 0, totalConnections: proxyMetrics ? proxyMetrics.totals.connections() : 0,
requestsPerSecond: proxyStats ? proxyStats.getRequestsPerSecond() : 0, requestsPerSecond: proxyMetrics ? proxyMetrics.requests.perSecond() : 0,
throughput: proxyStats ? proxyStats.getThroughput() : { bytesIn: 0, bytesOut: 0 }, throughput: proxyMetrics ? {
bytesIn: proxyMetrics.totals.bytesIn(),
bytesOut: proxyMetrics.totals.bytesOut()
} : { bytesIn: 0, bytesOut: 0 },
}; };
} }
// Get email metrics // Get email metrics
public async getEmailStats() { public async getEmailStats() {
// Calculate average delivery time
const avgDeliveryTime = this.emailMetrics.deliveryTimes.length > 0
? this.emailMetrics.deliveryTimes.reduce((a, b) => a + b, 0) / this.emailMetrics.deliveryTimes.length
: 0;
// Get top recipients
const topRecipients = Array.from(this.emailMetrics.recipients.entries())
.sort((a, b) => b[1] - a[1])
.slice(0, 10)
.map(([email, count]) => ({ email, count }));
// Get recent activity (last 50 entries)
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,
@ -144,9 +174,9 @@ export class MetricsManager {
? ((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: 0, // TODO: Implement when delivery tracking is added averageDeliveryTime: Math.round(avgDeliveryTime),
topRecipients: [], // TODO: Implement recipient tracking topRecipients,
recentActivity: [], // TODO: Implement activity log recentActivity,
}; };
} }
@ -161,21 +191,35 @@ export class MetricsManager {
.slice(0, 10) .slice(0, 10)
.map(([domain, count]) => ({ domain, count })); .map(([domain, count]) => ({ domain, count }));
// Calculate queries per second from recent timestamps
const now = Date.now();
const oneMinuteAgo = now - 60000;
const recentQueries = this.dnsMetrics.queryTimestamps.filter(ts => ts >= oneMinuteAgo);
const queriesPerSecond = recentQueries.length / 60;
// Calculate average response time
const avgResponseTime = this.dnsMetrics.responseTimes.length > 0
? this.dnsMetrics.responseTimes.reduce((a, b) => a + b, 0) / this.dnsMetrics.responseTimes.length
: 0;
return { return {
queriesPerSecond: 0, // TODO: Calculate based on time window 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: 0, // TODO: Implement response time tracking 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)
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,
@ -185,19 +229,19 @@ export class MetricsManager {
totalThreatsBlocked: this.securityMetrics.spamDetected + totalThreatsBlocked: this.securityMetrics.spamDetected +
this.securityMetrics.malwareDetected + this.securityMetrics.malwareDetected +
this.securityMetrics.phishingDetected, this.securityMetrics.phishingDetected,
recentIncidents: [], // TODO: Implement incident logging recentIncidents,
}; };
} }
// Get connection info from SmartProxy // Get connection info from SmartProxy
public async getConnectionInfo() { public async getConnectionInfo() {
const proxyStats = this.dcRouter.smartProxy ? this.dcRouter.smartProxy.getStats() : null; const proxyMetrics = this.dcRouter.smartProxy ? this.dcRouter.smartProxy.getMetrics() : null;
if (!proxyStats) { if (!proxyMetrics) {
return []; return [];
} }
const connectionsByRoute = proxyStats.getConnectionsByRoute(); const connectionsByRoute = proxyMetrics.connections.byRoute();
const connectionInfo = []; const connectionInfo = [];
for (const [routeName, count] of connectionsByRoute) { for (const [routeName, count] of connectionsByRoute) {
@ -213,20 +257,77 @@ export class MetricsManager {
} }
// Email event tracking methods // Email event tracking methods
public trackEmailSent(): void { public trackEmailSent(recipient?: string, deliveryTimeMs?: number): void {
this.emailMetrics.sentToday++; this.emailMetrics.sentToday++;
if (recipient) {
const count = this.emailMetrics.recipients.get(recipient) || 0;
this.emailMetrics.recipients.set(recipient, count + 1);
} }
public trackEmailReceived(): void { if (deliveryTimeMs) {
this.emailMetrics.deliveryTimes.push(deliveryTimeMs);
// Keep only last 1000 delivery times
if (this.emailMetrics.deliveryTimes.length > 1000) {
this.emailMetrics.deliveryTimes.shift();
}
}
this.emailMetrics.recentActivity.push({
timestamp: Date.now(),
type: 'sent',
details: recipient || 'unknown',
});
// Keep only last 1000 activities
if (this.emailMetrics.recentActivity.length > 1000) {
this.emailMetrics.recentActivity.shift();
}
}
public trackEmailReceived(sender?: string): void {
this.emailMetrics.receivedToday++; this.emailMetrics.receivedToday++;
this.emailMetrics.recentActivity.push({
timestamp: Date.now(),
type: 'received',
details: sender || 'unknown',
});
// Keep only last 1000 activities
if (this.emailMetrics.recentActivity.length > 1000) {
this.emailMetrics.recentActivity.shift();
}
} }
public trackEmailFailed(): void { public trackEmailFailed(recipient?: string, reason?: string): void {
this.emailMetrics.failedToday++; this.emailMetrics.failedToday++;
this.emailMetrics.recentActivity.push({
timestamp: Date.now(),
type: 'failed',
details: `${recipient || 'unknown'}: ${reason || 'unknown error'}`,
});
// Keep only last 1000 activities
if (this.emailMetrics.recentActivity.length > 1000) {
this.emailMetrics.recentActivity.shift();
}
} }
public trackEmailBounced(): void { public trackEmailBounced(recipient?: string): void {
this.emailMetrics.bouncedToday++; this.emailMetrics.bouncedToday++;
this.emailMetrics.recentActivity.push({
timestamp: Date.now(),
type: 'bounced',
details: recipient || 'unknown',
});
// Keep only last 1000 activities
if (this.emailMetrics.recentActivity.length > 1000) {
this.emailMetrics.recentActivity.shift();
}
} }
public updateQueueSize(size: number): void { public updateQueueSize(size: number): void {
@ -234,7 +335,7 @@ export class MetricsManager {
} }
// DNS event tracking methods // DNS event tracking methods
public trackDnsQuery(queryType: string, domain: string, cacheHit: boolean): void { public trackDnsQuery(queryType: string, domain: string, cacheHit: boolean, responseTimeMs?: number): void {
this.dnsMetrics.totalQueries++; this.dnsMetrics.totalQueries++;
if (cacheHit) { if (cacheHit) {
@ -243,6 +344,22 @@ export class MetricsManager {
this.dnsMetrics.cacheMisses++; this.dnsMetrics.cacheMisses++;
} }
// Track query timestamp
this.dnsMetrics.queryTimestamps.push(Date.now());
// Keep only timestamps from last 5 minutes
const fiveMinutesAgo = Date.now() - 300000;
this.dnsMetrics.queryTimestamps = this.dnsMetrics.queryTimestamps.filter(ts => ts >= fiveMinutesAgo);
// Track response time if provided
if (responseTimeMs) {
this.dnsMetrics.responseTimes.push(responseTimeMs);
// Keep only last 1000 response times
if (this.dnsMetrics.responseTimes.length > 1000) {
this.dnsMetrics.responseTimes.shift();
}
}
// Track query types // Track query types
this.dnsMetrics.queryTypes[queryType] = (this.dnsMetrics.queryTypes[queryType] || 0) + 1; this.dnsMetrics.queryTypes[queryType] = (this.dnsMetrics.queryTypes[queryType] || 0) + 1;
@ -266,31 +383,91 @@ export class MetricsManager {
} }
// Security event tracking methods // Security event tracking methods
public trackBlockedIP(): void { public trackBlockedIP(ip?: string, reason?: string): void {
this.securityMetrics.blockedIPs++; this.securityMetrics.blockedIPs++;
this.securityMetrics.incidents.push({
timestamp: Date.now(),
type: 'ip_blocked',
severity: 'medium',
details: `IP ${ip || 'unknown'} blocked: ${reason || 'security policy'}`,
});
// Keep only last 1000 incidents
if (this.securityMetrics.incidents.length > 1000) {
this.securityMetrics.incidents.shift();
}
} }
public trackAuthFailure(): void { public trackAuthFailure(username?: string, ip?: string): void {
this.securityMetrics.authFailures++; this.securityMetrics.authFailures++;
this.securityMetrics.incidents.push({
timestamp: Date.now(),
type: 'auth_failure',
severity: 'low',
details: `Authentication failed for ${username || 'unknown'} from ${ip || 'unknown'}`,
});
// Keep only last 1000 incidents
if (this.securityMetrics.incidents.length > 1000) {
this.securityMetrics.incidents.shift();
}
} }
public trackSpamDetected(): void { public trackSpamDetected(sender?: string): void {
this.securityMetrics.spamDetected++; this.securityMetrics.spamDetected++;
this.securityMetrics.incidents.push({
timestamp: Date.now(),
type: 'spam_detected',
severity: 'low',
details: `Spam detected from ${sender || 'unknown'}`,
});
// Keep only last 1000 incidents
if (this.securityMetrics.incidents.length > 1000) {
this.securityMetrics.incidents.shift();
}
} }
public trackMalwareDetected(): void { public trackMalwareDetected(source?: string): void {
this.securityMetrics.malwareDetected++; this.securityMetrics.malwareDetected++;
this.securityMetrics.incidents.push({
timestamp: Date.now(),
type: 'malware_detected',
severity: 'high',
details: `Malware detected from ${source || 'unknown'}`,
});
// Keep only last 1000 incidents
if (this.securityMetrics.incidents.length > 1000) {
this.securityMetrics.incidents.shift();
}
} }
public trackPhishingDetected(): void { public trackPhishingDetected(source?: string): void {
this.securityMetrics.phishingDetected++; this.securityMetrics.phishingDetected++;
this.securityMetrics.incidents.push({
timestamp: Date.now(),
type: 'phishing_detected',
severity: 'high',
details: `Phishing attempt from ${source || 'unknown'}`,
});
// Keep only last 1000 incidents
if (this.securityMetrics.incidents.length > 1000) {
this.securityMetrics.incidents.shift();
}
} }
// Get network metrics from SmartProxy // Get network metrics from SmartProxy
public async getNetworkStats() { public async getNetworkStats() {
const proxyStats = this.dcRouter.smartProxy ? this.dcRouter.smartProxy.getStats() : null; const proxyMetrics = this.dcRouter.smartProxy ? this.dcRouter.smartProxy.getMetrics() : null;
if (!proxyStats) { if (!proxyMetrics) {
return { return {
connectionsByIP: new Map<string, number>(), connectionsByIP: new Map<string, number>(),
throughputRate: { bytesInPerSecond: 0, bytesOutPerSecond: 0 }, throughputRate: { bytesInPerSecond: 0, bytesOutPerSecond: 0 },
@ -299,47 +476,30 @@ export class MetricsManager {
}; };
} }
// Get unused SmartProxy metrics // Get metrics using the new API
const connectionsByIP = proxyStats.getConnectionsByIP(); const connectionsByIP = proxyMetrics.connections.byIP();
const throughput = proxyStats.getThroughput(); const instantThroughput = proxyMetrics.throughput.instant();
// Check if extended methods exist and call them // Get throughput rate
const throughputRate = ('getThroughputRate' in proxyStats && typeof proxyStats.getThroughputRate === 'function') const throughputRate = {
? (() => { bytesInPerSecond: instantThroughput.in,
const rate = (proxyStats as any).getThroughputRate(); bytesOutPerSecond: instantThroughput.out
return {
bytesInPerSecond: rate.bytesInPerSec || 0,
bytesOutPerSecond: rate.bytesOutPerSec || 0
}; };
})()
: { bytesInPerSecond: 0, bytesOutPerSecond: 0 };
const topIPs: Array<{ ip: string; count: number }> = []; // Get top IPs
const topIPs = proxyMetrics.connections.topIPs(10);
// Check if getTopIPs method exists // Get total data transferred
if ('getTopIPs' in proxyStats && typeof proxyStats.getTopIPs === 'function') { const totalDataTransferred = {
const ips = (proxyStats as any).getTopIPs(10); bytesIn: proxyMetrics.totals.bytesIn(),
if (Array.isArray(ips)) { bytesOut: proxyMetrics.totals.bytesOut()
ips.forEach(ipData => { };
topIPs.push({ ip: ipData.ip, count: ipData.connections || ipData.count || 0 });
});
}
} else {
// Fallback: Convert connectionsByIP to topIPs manually
if (connectionsByIP && connectionsByIP.size > 0) {
const ipArray = Array.from(connectionsByIP.entries())
.map(([ip, count]) => ({ ip, count }))
.sort((a, b) => b.count - a.count)
.slice(0, 10);
topIPs.push(...ipArray);
}
}
return { return {
connectionsByIP, connectionsByIP,
throughputRate, throughputRate,
topIPs, topIPs,
totalDataTransferred: throughput, totalDataTransferred,
}; };
} }
} }

View File

@ -234,50 +234,53 @@ export class SecurityHandler {
const connectionInfo = await this.opsServerRef.dcRouterRef.metricsManager.getConnectionInfo(); const connectionInfo = await this.opsServerRef.dcRouterRef.metricsManager.getConnectionInfo();
const networkStats = await this.opsServerRef.dcRouterRef.metricsManager.getNetworkStats(); const networkStats = await this.opsServerRef.dcRouterRef.metricsManager.getNetworkStats();
// Map connection info to detailed format with real IP data // Use IP-based connection data from the new metrics API
connectionInfo.forEach((info, index) => {
connections.push({
id: `conn-${index}`,
type: 'http', // Connections through proxy are HTTP/HTTPS
source: {
ip: '0.0.0.0', // TODO: SmartProxy doesn't expose individual connection IPs yet
port: 0,
},
destination: {
ip: '0.0.0.0',
port: 443,
service: info.source,
},
startTime: info.lastActivity.getTime(),
bytesTransferred: 0, // TODO: Track bytes per connection
status: 'active',
});
});
// If we have IP-based connection data, add synthetic entries for visualization
// This provides a more realistic view until SmartProxy exposes per-connection IPs
if (networkStats.connectionsByIP && networkStats.connectionsByIP.size > 0) { if (networkStats.connectionsByIP && networkStats.connectionsByIP.size > 0) {
let connIndex = connections.length; let connIndex = 0;
const publicIp = this.opsServerRef.dcRouterRef.options.publicIp || 'server';
for (const [ip, count] of networkStats.connectionsByIP) { for (const [ip, count] of networkStats.connectionsByIP) {
// Add a representative connection for each IP // Create a connection entry for each active IP connection
for (let i = 0; i < Math.min(count, 5); i++) { // Limit to 5 connections per IP for UI performance
connections.push({ connections.push({
id: `conn-${connIndex++}`, id: `conn-${connIndex++}`,
type: 'http', type: 'http',
source: { source: {
ip: ip, ip: ip,
port: Math.floor(Math.random() * 50000) + 10000, // Random high port port: Math.floor(Math.random() * 50000) + 10000, // High port range
}, },
destination: { destination: {
ip: this.opsServerRef.dcRouterRef.options.publicIp || '0.0.0.0', ip: publicIp,
port: 443, port: 443,
service: 'proxy', service: 'proxy',
}, },
startTime: Date.now() - Math.floor(Math.random() * 3600000), // Random time within last hour startTime: Date.now() - Math.floor(Math.random() * 3600000), // Within last hour
bytesTransferred: Math.floor(networkStats.totalDataTransferred.bytesIn / count), // Average bytes per IP bytesTransferred: Math.floor(networkStats.totalDataTransferred.bytesIn / networkStats.connectionsByIP.size),
status: 'active', status: 'active',
}); });
} }
} }
} else if (connectionInfo.length > 0) {
// Fallback to route-based connection info if no IP data available
connectionInfo.forEach((info, index) => {
connections.push({
id: `conn-${index}`,
type: 'http',
source: {
ip: 'unknown',
port: 0,
},
destination: {
ip: this.opsServerRef.dcRouterRef.options.publicIp || 'server',
port: 443,
service: info.source,
},
startTime: info.lastActivity.getTime(),
bytesTransferred: 0,
status: 'active',
});
});
}
} }
// Filter by protocol if specified // Filter by protocol if specified

View File

@ -43,15 +43,32 @@ export class OpsViewNetwork extends DeesElement {
private networkRequests: INetworkRequest[] = []; private networkRequests: INetworkRequest[] = [];
@state() @state()
private trafficData: Array<{ x: number; y: number }> = []; private trafficData: Array<{ x: string | number; y: number }> = [];
@state() @state()
private isLoading = false; private isLoading = false;
private lastTrafficUpdateTime = 0;
private trafficUpdateInterval = 1000; // Update every 1 second
private requestCountHistory = new Map<number, number>(); // Track requests per time bucket
private trafficUpdateTimer: any = null;
// Track bytes for calculating true per-second throughput
private lastBytesIn = 0;
private lastBytesOut = 0;
private lastBytesSampleTime = 0;
constructor() { constructor() {
super(); super();
this.subscribeToStateParts(); this.subscribeToStateParts();
this.initializeTrafficData();
this.updateNetworkData(); this.updateNetworkData();
this.startTrafficUpdateTimer();
}
async disconnectedCallback() {
await super.disconnectedCallback();
this.stopTrafficUpdateTimer();
} }
private subscribeToStateParts() { private subscribeToStateParts() {
@ -66,6 +83,31 @@ export class OpsViewNetwork extends DeesElement {
}); });
} }
private initializeTrafficData() {
const now = Date.now();
const timeRanges = {
'1m': 60 * 1000,
'5m': 5 * 60 * 1000,
'15m': 15 * 60 * 1000,
'1h': 60 * 60 * 1000,
'24h': 24 * 60 * 60 * 1000,
};
const range = timeRanges[this.selectedTimeRange];
const bucketSize = range / 60;
// Initialize with empty data points
this.trafficData = Array.from({ length: 60 }, (_, i) => {
const time = now - ((59 - i) * bucketSize);
return {
x: new Date(time).toISOString(),
y: 0,
};
});
this.lastTrafficUpdateTime = now;
}
public static styles = [ public static styles = [
cssManager.defaultStyles, cssManager.defaultStyles,
viewHostCss, viewHostCss,
@ -181,7 +223,7 @@ export class OpsViewNetwork extends DeesElement {
<dees-button-group> <dees-button-group>
${(['1m', '5m', '15m', '1h', '24h'] as const).map(range => html` ${(['1m', '5m', '15m', '1h', '24h'] as const).map(range => html`
<dees-button <dees-button
@click=${() => this.selectedTimeRange = range} @click=${() => this.handleTimeRangeChange(range)}
.type=${this.selectedTimeRange === range ? 'highlighted' : 'normal'} .type=${this.selectedTimeRange === range ? 'highlighted' : 'normal'}
> >
${range} ${range}
@ -223,10 +265,11 @@ export class OpsViewNetwork extends DeesElement {
.label=${'Network Traffic'} .label=${'Network Traffic'}
.series=${[ .series=${[
{ {
name: 'Requests/min', name: 'Throughput (Mbps)',
data: this.trafficData, data: this.trafficData,
} }
]} ]}
.yAxisFormatter=${(val: number) => `${val} Mbps`}
></dees-chart-area> ></dees-chart-area>
<!-- Top IPs Section --> <!-- Top IPs Section -->
@ -394,10 +437,13 @@ 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;
// Generate trend data for requests per second // Use actual traffic data for trends (last 20 points)
const trendData = Array.from({ length: 20 }, (_, i) => const trendData = this.trafficData.slice(-20).map(point => point.y);
Math.max(0, reqPerSec + (Math.random() - 0.5) * 10)
); // If we don't have enough data, pad with the current value
while (trendData.length < 20) {
trendData.unshift(reqPerSec);
}
const tiles: IStatsTile[] = [ const tiles: IStatsTile[] = [
{ {
@ -532,25 +578,107 @@ export class OpsViewNetwork extends DeesElement {
const range = timeRanges[this.selectedTimeRange]; const range = timeRanges[this.selectedTimeRange];
const bucketSize = range / 60; // 60 data points const bucketSize = range / 60; // 60 data points
// Create buckets for traffic data // Check if enough time has passed to add a new data point
const buckets = new Map<number, number>(); const timeSinceLastUpdate = now - this.lastTrafficUpdateTime;
const shouldAddNewPoint = timeSinceLastUpdate >= this.trafficUpdateInterval;
// Count requests per bucket console.log('UpdateTrafficData called:', {
this.networkRequests.forEach(req => { networkRequestsCount: this.networkRequests.length,
if (req.timestamp >= now - range) { timeSinceLastUpdate,
const bucketIndex = Math.floor((now - req.timestamp) / bucketSize); shouldAddNewPoint,
const bucketTime = now - (bucketIndex * bucketSize); currentDataPoints: this.trafficData.length
buckets.set(bucketTime, (buckets.get(bucketTime) || 0) + 1);
}
}); });
// Convert to chart data if (!shouldAddNewPoint && this.trafficData.length > 0) {
this.trafficData = Array.from({ length: 60 }, (_, i) => { // Not enough time has passed, don't update
const time = now - (i * bucketSize); return;
return { }
x: time,
y: buckets.get(time) || 0, // Calculate actual per-second throughput by tracking deltas
let throughputMbps = 0;
// Get total bytes from all active connections
let currentBytesIn = 0;
let currentBytesOut = 0;
this.networkRequests.forEach(req => {
currentBytesIn += req.bytesIn;
currentBytesOut += req.bytesOut;
});
// If we have a previous sample, calculate the delta
if (this.lastBytesSampleTime > 0) {
const timeDelta = (now - this.lastBytesSampleTime) / 1000; // Convert to seconds
const bytesInDelta = Math.max(0, currentBytesIn - this.lastBytesIn);
const bytesOutDelta = Math.max(0, currentBytesOut - this.lastBytesOut);
// Calculate bytes per second for this interval
const bytesPerSecond = (bytesInDelta + bytesOutDelta) / timeDelta;
// Convert to Mbps (1 Mbps = 125000 bytes/second)
throughputMbps = bytesPerSecond / 125000;
console.log('Throughput calculation:', {
timeDelta,
bytesInDelta,
bytesOutDelta,
bytesPerSecond,
throughputMbps
});
}
// Update last sample values
this.lastBytesIn = currentBytesIn;
this.lastBytesOut = currentBytesOut;
this.lastBytesSampleTime = now;
if (this.trafficData.length === 0) {
// Initialize if empty
this.initializeTrafficData();
} else {
// Add new data point and remove oldest if we have 60 points
const newDataPoint = {
x: new Date(now).toISOString(),
y: Math.round(throughputMbps * 10) / 10 // Round to 1 decimal place
}; };
}).reverse();
// Create new array with existing data plus new point
const newTrafficData = [...this.trafficData, newDataPoint];
// Keep only the last 60 points
if (newTrafficData.length > 60) {
newTrafficData.shift(); // Remove oldest point
}
this.trafficData = newTrafficData;
this.lastTrafficUpdateTime = now;
console.log('Added new traffic data point:', {
timestamp: newDataPoint.x,
throughputMbps: newDataPoint.y,
totalPoints: this.trafficData.length
});
}
}
private startTrafficUpdateTimer() {
this.stopTrafficUpdateTimer(); // Clear any existing timer
this.trafficUpdateTimer = setInterval(() => {
this.updateTrafficData();
}, 1000); // Check every second, but only update when interval has passed
}
private stopTrafficUpdateTimer() {
if (this.trafficUpdateTimer) {
clearInterval(this.trafficUpdateTimer);
this.trafficUpdateTimer = null;
}
}
private handleTimeRangeChange(range: '1m' | '5m' | '15m' | '1h' | '24h') {
this.selectedTimeRange = range;
// Reinitialize traffic data for new time range
this.initializeTrafficData();
this.updateNetworkData();
} }
} }