fix(metrics): fix metrics
This commit is contained in:
@ -30,7 +30,7 @@
|
||||
"@api.global/typedserver": "^3.0.74",
|
||||
"@api.global/typedsocket": "^3.0.0",
|
||||
"@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",
|
||||
"@push.rocks/projectinfo": "^5.0.1",
|
||||
"@push.rocks/qenv": "^6.1.0",
|
||||
@ -46,7 +46,7 @@
|
||||
"@push.rocks/smartnetwork": "^4.0.2",
|
||||
"@push.rocks/smartpath": "^5.0.5",
|
||||
"@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/smartrule": "^2.0.1",
|
||||
"@push.rocks/smartrx": "^3.0.10",
|
||||
|
22
pnpm-lock.yaml
generated
22
pnpm-lock.yaml
generated
@ -24,8 +24,8 @@ importers:
|
||||
specifier: ^6.4.1
|
||||
version: 6.4.1
|
||||
'@design.estate/dees-catalog':
|
||||
specifier: ^1.8.20
|
||||
version: 1.8.20
|
||||
specifier: ^1.9.0
|
||||
version: 1.9.0
|
||||
'@design.estate/dees-element':
|
||||
specifier: ^2.0.44
|
||||
version: 2.0.44
|
||||
@ -72,8 +72,8 @@ importers:
|
||||
specifier: ^4.0.3
|
||||
version: 4.2.3
|
||||
'@push.rocks/smartproxy':
|
||||
specifier: ^19.6.6
|
||||
version: 19.6.6(@aws-sdk/credential-providers@3.817.0)(socks@2.8.4)
|
||||
specifier: ^19.6.7
|
||||
version: 19.6.7(@aws-sdk/credential-providers@3.817.0)(socks@2.8.4)
|
||||
'@push.rocks/smartrequest':
|
||||
specifier: ^2.1.0
|
||||
version: 2.1.0
|
||||
@ -344,8 +344,8 @@ packages:
|
||||
'@dabh/diagnostics@2.0.3':
|
||||
resolution: {integrity: sha512-hrlQOIi7hAfzsMqlGSFyVucrx38O+j6wiGOf//H2ecvIEqYN4ADBSS2iLMh5UFyDunCNniUIPk/q3riFv45xRA==}
|
||||
|
||||
'@design.estate/dees-catalog@1.8.20':
|
||||
resolution: {integrity: sha512-TEZXZQaUBSw6mgd78Nx9pQO+5T8s5GSIBhvMrcxV8QEjkitGtnUQBztMT3VMAneS30hc3JGfTedpu6OnXw4XNQ==}
|
||||
'@design.estate/dees-catalog@1.9.0':
|
||||
resolution: {integrity: sha512-rK/EjTC6H0t0Ow/TRmt1RiTx+0Qz+apOIKhjaQ1YcPODfy4LAj1oKc5VK1VnrFguuABRIL2M4xMssDtS+G78Kw==}
|
||||
|
||||
'@design.estate/dees-comms@1.0.27':
|
||||
resolution: {integrity: sha512-GvzTUwkV442LD60T08iqSoqvhA02Mou5lFvvqBPc4yBUiU7cZISqBx+76xvMgMIEI9Dx9JfTl4/2nW8MoVAanw==}
|
||||
@ -1110,8 +1110,8 @@ packages:
|
||||
'@push.rocks/smartpromise@4.2.3':
|
||||
resolution: {integrity: sha512-Ycg/TJR+tMt+S3wSFurOpEoW6nXv12QBtKXgBcjMZ4RsdO28geN46U09osPn9N9WuwQy1PkmTV5J/V4F9U8qEw==}
|
||||
|
||||
'@push.rocks/smartproxy@19.6.6':
|
||||
resolution: {integrity: sha512-AweTvBYlYubelO+g6Bf/4cg8RXb0fcMgYE1UKAT/m5PNbOuRWzTtXkja4JuFWfIdvmbfZxiWAaw9OhJvHIgIrw==}
|
||||
'@push.rocks/smartproxy@19.6.7':
|
||||
resolution: {integrity: sha512-tC/zqUzSo4/SPqp52UrSe3cPR/YtyZiFC/HhYktCNfDXaWPqxY3ioViTwC2i6tb/6T6LmNdRJUOXxGFAz1j1cQ==}
|
||||
|
||||
'@push.rocks/smartpuppeteer@2.0.5':
|
||||
resolution: {integrity: sha512-yK/qSeWVHIGWRp3c8S5tfdGP6WCKllZC4DR8d8CQlEjszOSBmHtlTdyyqOMBZ/BA4kd+eU5f3A1r4K2tGYty1g==}
|
||||
@ -5257,7 +5257,7 @@ snapshots:
|
||||
enabled: 2.0.0
|
||||
kuler: 2.0.0
|
||||
|
||||
'@design.estate/dees-catalog@1.8.20':
|
||||
'@design.estate/dees-catalog@1.9.0':
|
||||
dependencies:
|
||||
'@design.estate/dees-domtools': 2.3.3
|
||||
'@design.estate/dees-element': 2.0.44
|
||||
@ -6029,6 +6029,7 @@ snapshots:
|
||||
- '@mongodb-js/zstd'
|
||||
- '@nuxt/kit'
|
||||
- aws-crt
|
||||
- bufferutil
|
||||
- encoding
|
||||
- gcp-metadata
|
||||
- kerberos
|
||||
@ -6037,6 +6038,7 @@ snapshots:
|
||||
- snappy
|
||||
- socks
|
||||
- supports-color
|
||||
- utf-8-validate
|
||||
- vue
|
||||
|
||||
'@push.rocks/smartarchive@3.0.8':
|
||||
@ -6474,7 +6476,7 @@ snapshots:
|
||||
|
||||
'@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:
|
||||
'@push.rocks/lik': 6.2.2
|
||||
'@push.rocks/smartacme': 8.0.0(@aws-sdk/credential-providers@3.817.0)(socks@2.8.4)
|
||||
|
125
readme.plan2.md
Normal file
125
readme.plan2.md
Normal 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
|
@ -18,6 +18,9 @@ export class MetricsManager {
|
||||
bouncedToday: 0,
|
||||
queueSize: 0,
|
||||
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
|
||||
@ -28,6 +31,8 @@ export class MetricsManager {
|
||||
queryTypes: {} as Record<string, number>,
|
||||
topDomains: new Map<string, number>(),
|
||||
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
|
||||
@ -38,6 +43,7 @@ export class MetricsManager {
|
||||
malwareDetected: 0,
|
||||
phishingDetected: 0,
|
||||
lastResetDate: new Date().toDateString(),
|
||||
incidents: [] as Array<{ timestamp: number; type: string; severity: string; details: string }>,
|
||||
};
|
||||
|
||||
constructor(dcRouter: DcRouter) {
|
||||
@ -66,6 +72,9 @@ export class MetricsManager {
|
||||
this.emailMetrics.receivedToday = 0;
|
||||
this.emailMetrics.failedToday = 0;
|
||||
this.emailMetrics.bouncedToday = 0;
|
||||
this.emailMetrics.deliveryTimes = [];
|
||||
this.emailMetrics.recipients.clear();
|
||||
this.emailMetrics.recentActivity = [];
|
||||
this.emailMetrics.lastResetDate = currentDate;
|
||||
}
|
||||
|
||||
@ -75,6 +84,8 @@ export class MetricsManager {
|
||||
this.dnsMetrics.cacheMisses = 0;
|
||||
this.dnsMetrics.queryTypes = {};
|
||||
this.dnsMetrics.topDomains.clear();
|
||||
this.dnsMetrics.queryTimestamps = [];
|
||||
this.dnsMetrics.responseTimes = [];
|
||||
this.dnsMetrics.lastResetDate = currentDate;
|
||||
}
|
||||
|
||||
@ -84,6 +95,7 @@ export class MetricsManager {
|
||||
this.securityMetrics.spamDetected = 0;
|
||||
this.securityMetrics.malwareDetected = 0;
|
||||
this.securityMetrics.phishingDetected = 0;
|
||||
this.securityMetrics.incidents = [];
|
||||
this.securityMetrics.lastResetDate = currentDate;
|
||||
}
|
||||
}, 60000); // Check every minute
|
||||
@ -105,7 +117,8 @@ export class MetricsManager {
|
||||
// Get server metrics from SmartMetrics and SmartProxy
|
||||
public async getServerStats() {
|
||||
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 {
|
||||
uptime: process.uptime(),
|
||||
@ -124,15 +137,32 @@ export class MetricsManager {
|
||||
user: parseFloat(smartMetricsData.cpuUsageText || '0'),
|
||||
system: 0, // SmartMetrics doesn't separate user/system
|
||||
},
|
||||
activeConnections: proxyStats ? proxyStats.getActiveConnections() : 0,
|
||||
totalConnections: proxyStats ? proxyStats.getTotalConnections() : 0,
|
||||
requestsPerSecond: proxyStats ? proxyStats.getRequestsPerSecond() : 0,
|
||||
throughput: proxyStats ? proxyStats.getThroughput() : { bytesIn: 0, bytesOut: 0 },
|
||||
activeConnections: proxyStats ? proxyStats.activeConnections : 0,
|
||||
totalConnections: proxyMetrics ? proxyMetrics.totals.connections() : 0,
|
||||
requestsPerSecond: proxyMetrics ? proxyMetrics.requests.perSecond() : 0,
|
||||
throughput: proxyMetrics ? {
|
||||
bytesIn: proxyMetrics.totals.bytesIn(),
|
||||
bytesOut: proxyMetrics.totals.bytesOut()
|
||||
} : { bytesIn: 0, bytesOut: 0 },
|
||||
};
|
||||
}
|
||||
|
||||
// Get email metrics
|
||||
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 {
|
||||
sentToday: this.emailMetrics.sentToday,
|
||||
receivedToday: this.emailMetrics.receivedToday,
|
||||
@ -144,9 +174,9 @@ export class MetricsManager {
|
||||
? ((this.emailMetrics.sentToday - this.emailMetrics.failedToday) / this.emailMetrics.sentToday) * 100
|
||||
: 100,
|
||||
queueSize: this.emailMetrics.queueSize,
|
||||
averageDeliveryTime: 0, // TODO: Implement when delivery tracking is added
|
||||
topRecipients: [], // TODO: Implement recipient tracking
|
||||
recentActivity: [], // TODO: Implement activity log
|
||||
averageDeliveryTime: Math.round(avgDeliveryTime),
|
||||
topRecipients,
|
||||
recentActivity,
|
||||
};
|
||||
}
|
||||
|
||||
@ -161,21 +191,35 @@ export class MetricsManager {
|
||||
.slice(0, 10)
|
||||
.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 {
|
||||
queriesPerSecond: 0, // TODO: Calculate based on time window
|
||||
queriesPerSecond: Math.round(queriesPerSecond * 10) / 10,
|
||||
totalQueries: this.dnsMetrics.totalQueries,
|
||||
cacheHits: this.dnsMetrics.cacheHits,
|
||||
cacheMisses: this.dnsMetrics.cacheMisses,
|
||||
cacheHitRate: cacheHitRate,
|
||||
topDomains: topDomains,
|
||||
queryTypes: this.dnsMetrics.queryTypes,
|
||||
averageResponseTime: 0, // TODO: Implement response time tracking
|
||||
averageResponseTime: Math.round(avgResponseTime),
|
||||
activeDomains: this.dnsMetrics.topDomains.size,
|
||||
};
|
||||
}
|
||||
|
||||
// Get security metrics
|
||||
public async getSecurityStats() {
|
||||
// Get recent incidents (last 20)
|
||||
const recentIncidents = this.securityMetrics.incidents.slice(-20);
|
||||
|
||||
return {
|
||||
blockedIPs: this.securityMetrics.blockedIPs,
|
||||
authFailures: this.securityMetrics.authFailures,
|
||||
@ -185,19 +229,19 @@ export class MetricsManager {
|
||||
totalThreatsBlocked: this.securityMetrics.spamDetected +
|
||||
this.securityMetrics.malwareDetected +
|
||||
this.securityMetrics.phishingDetected,
|
||||
recentIncidents: [], // TODO: Implement incident logging
|
||||
recentIncidents,
|
||||
};
|
||||
}
|
||||
|
||||
// Get connection info from SmartProxy
|
||||
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 [];
|
||||
}
|
||||
|
||||
const connectionsByRoute = proxyStats.getConnectionsByRoute();
|
||||
const connectionsByRoute = proxyMetrics.connections.byRoute();
|
||||
const connectionInfo = [];
|
||||
|
||||
for (const [routeName, count] of connectionsByRoute) {
|
||||
@ -213,20 +257,77 @@ export class MetricsManager {
|
||||
}
|
||||
|
||||
// Email event tracking methods
|
||||
public trackEmailSent(): void {
|
||||
public trackEmailSent(recipient?: string, deliveryTimeMs?: number): void {
|
||||
this.emailMetrics.sentToday++;
|
||||
|
||||
if (recipient) {
|
||||
const count = this.emailMetrics.recipients.get(recipient) || 0;
|
||||
this.emailMetrics.recipients.set(recipient, count + 1);
|
||||
}
|
||||
|
||||
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(): void {
|
||||
public trackEmailReceived(sender?: string): void {
|
||||
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.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.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 {
|
||||
@ -234,7 +335,7 @@ export class MetricsManager {
|
||||
}
|
||||
|
||||
// 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++;
|
||||
|
||||
if (cacheHit) {
|
||||
@ -243,6 +344,22 @@ export class MetricsManager {
|
||||
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
|
||||
this.dnsMetrics.queryTypes[queryType] = (this.dnsMetrics.queryTypes[queryType] || 0) + 1;
|
||||
|
||||
@ -266,31 +383,91 @@ export class MetricsManager {
|
||||
}
|
||||
|
||||
// Security event tracking methods
|
||||
public trackBlockedIP(): void {
|
||||
public trackBlockedIP(ip?: string, reason?: string): void {
|
||||
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.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.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.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.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
|
||||
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 {
|
||||
connectionsByIP: new Map<string, number>(),
|
||||
throughputRate: { bytesInPerSecond: 0, bytesOutPerSecond: 0 },
|
||||
@ -299,47 +476,30 @@ export class MetricsManager {
|
||||
};
|
||||
}
|
||||
|
||||
// Get unused SmartProxy metrics
|
||||
const connectionsByIP = proxyStats.getConnectionsByIP();
|
||||
const throughput = proxyStats.getThroughput();
|
||||
// Get metrics using the new API
|
||||
const connectionsByIP = proxyMetrics.connections.byIP();
|
||||
const instantThroughput = proxyMetrics.throughput.instant();
|
||||
|
||||
// Check if extended methods exist and call them
|
||||
const throughputRate = ('getThroughputRate' in proxyStats && typeof proxyStats.getThroughputRate === 'function')
|
||||
? (() => {
|
||||
const rate = (proxyStats as any).getThroughputRate();
|
||||
return {
|
||||
bytesInPerSecond: rate.bytesInPerSec || 0,
|
||||
bytesOutPerSecond: rate.bytesOutPerSec || 0
|
||||
};
|
||||
})()
|
||||
: { bytesInPerSecond: 0, bytesOutPerSecond: 0 };
|
||||
// Get throughput rate
|
||||
const throughputRate = {
|
||||
bytesInPerSecond: instantThroughput.in,
|
||||
bytesOutPerSecond: instantThroughput.out
|
||||
};
|
||||
|
||||
const topIPs: Array<{ ip: string; count: number }> = [];
|
||||
// Get top IPs
|
||||
const topIPs = proxyMetrics.connections.topIPs(10);
|
||||
|
||||
// Check if getTopIPs method exists
|
||||
if ('getTopIPs' in proxyStats && typeof proxyStats.getTopIPs === 'function') {
|
||||
const ips = (proxyStats as any).getTopIPs(10);
|
||||
if (Array.isArray(ips)) {
|
||||
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);
|
||||
}
|
||||
}
|
||||
// Get total data transferred
|
||||
const totalDataTransferred = {
|
||||
bytesIn: proxyMetrics.totals.bytesIn(),
|
||||
bytesOut: proxyMetrics.totals.bytesOut()
|
||||
};
|
||||
|
||||
return {
|
||||
connectionsByIP,
|
||||
throughputRate,
|
||||
topIPs,
|
||||
totalDataTransferred: throughput,
|
||||
totalDataTransferred,
|
||||
};
|
||||
}
|
||||
}
|
@ -234,49 +234,52 @@ export class SecurityHandler {
|
||||
const connectionInfo = await this.opsServerRef.dcRouterRef.metricsManager.getConnectionInfo();
|
||||
const networkStats = await this.opsServerRef.dcRouterRef.metricsManager.getNetworkStats();
|
||||
|
||||
// Map connection info to detailed format with real IP data
|
||||
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
|
||||
// Use IP-based connection data from the new metrics API
|
||||
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) {
|
||||
// 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({
|
||||
id: `conn-${connIndex++}`,
|
||||
type: 'http',
|
||||
source: {
|
||||
ip: ip,
|
||||
port: Math.floor(Math.random() * 50000) + 10000, // High port range
|
||||
},
|
||||
destination: {
|
||||
ip: publicIp,
|
||||
port: 443,
|
||||
service: 'proxy',
|
||||
},
|
||||
startTime: Date.now() - Math.floor(Math.random() * 3600000), // Within last hour
|
||||
bytesTransferred: Math.floor(networkStats.totalDataTransferred.bytesIn / networkStats.connectionsByIP.size),
|
||||
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-${connIndex++}`,
|
||||
id: `conn-${index}`,
|
||||
type: 'http',
|
||||
source: {
|
||||
ip: ip,
|
||||
port: Math.floor(Math.random() * 50000) + 10000, // Random high port
|
||||
ip: 'unknown',
|
||||
port: 0,
|
||||
},
|
||||
destination: {
|
||||
ip: this.opsServerRef.dcRouterRef.options.publicIp || '0.0.0.0',
|
||||
ip: this.opsServerRef.dcRouterRef.options.publicIp || 'server',
|
||||
port: 443,
|
||||
service: 'proxy',
|
||||
service: info.source,
|
||||
},
|
||||
startTime: Date.now() - Math.floor(Math.random() * 3600000), // Random time within last hour
|
||||
bytesTransferred: Math.floor(networkStats.totalDataTransferred.bytesIn / count), // Average bytes per IP
|
||||
startTime: info.lastActivity.getTime(),
|
||||
bytesTransferred: 0,
|
||||
status: 'active',
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -43,15 +43,32 @@ export class OpsViewNetwork extends DeesElement {
|
||||
private networkRequests: INetworkRequest[] = [];
|
||||
|
||||
@state()
|
||||
private trafficData: Array<{ x: number; y: number }> = [];
|
||||
private trafficData: Array<{ x: string | number; y: number }> = [];
|
||||
|
||||
@state()
|
||||
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() {
|
||||
super();
|
||||
this.subscribeToStateParts();
|
||||
this.initializeTrafficData();
|
||||
this.updateNetworkData();
|
||||
this.startTrafficUpdateTimer();
|
||||
}
|
||||
|
||||
async disconnectedCallback() {
|
||||
await super.disconnectedCallback();
|
||||
this.stopTrafficUpdateTimer();
|
||||
}
|
||||
|
||||
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 = [
|
||||
cssManager.defaultStyles,
|
||||
viewHostCss,
|
||||
@ -181,7 +223,7 @@ export class OpsViewNetwork extends DeesElement {
|
||||
<dees-button-group>
|
||||
${(['1m', '5m', '15m', '1h', '24h'] as const).map(range => html`
|
||||
<dees-button
|
||||
@click=${() => this.selectedTimeRange = range}
|
||||
@click=${() => this.handleTimeRangeChange(range)}
|
||||
.type=${this.selectedTimeRange === range ? 'highlighted' : 'normal'}
|
||||
>
|
||||
${range}
|
||||
@ -223,10 +265,11 @@ export class OpsViewNetwork extends DeesElement {
|
||||
.label=${'Network Traffic'}
|
||||
.series=${[
|
||||
{
|
||||
name: 'Requests/min',
|
||||
name: 'Throughput (Mbps)',
|
||||
data: this.trafficData,
|
||||
}
|
||||
]}
|
||||
.yAxisFormatter=${(val: number) => `${val} Mbps`}
|
||||
></dees-chart-area>
|
||||
|
||||
<!-- Top IPs Section -->
|
||||
@ -394,10 +437,13 @@ export class OpsViewNetwork extends DeesElement {
|
||||
const throughput = this.calculateThroughput();
|
||||
const activeConnections = this.statsState.serverStats?.activeConnections || 0;
|
||||
|
||||
// Generate trend data for requests per second
|
||||
const trendData = Array.from({ length: 20 }, (_, i) =>
|
||||
Math.max(0, reqPerSec + (Math.random() - 0.5) * 10)
|
||||
);
|
||||
// Use actual traffic data for trends (last 20 points)
|
||||
const trendData = this.trafficData.slice(-20).map(point => point.y);
|
||||
|
||||
// If we don't have enough data, pad with the current value
|
||||
while (trendData.length < 20) {
|
||||
trendData.unshift(reqPerSec);
|
||||
}
|
||||
|
||||
const tiles: IStatsTile[] = [
|
||||
{
|
||||
@ -532,25 +578,107 @@ export class OpsViewNetwork extends DeesElement {
|
||||
const range = timeRanges[this.selectedTimeRange];
|
||||
const bucketSize = range / 60; // 60 data points
|
||||
|
||||
// Create buckets for traffic data
|
||||
const buckets = new Map<number, number>();
|
||||
// Check if enough time has passed to add a new data point
|
||||
const timeSinceLastUpdate = now - this.lastTrafficUpdateTime;
|
||||
const shouldAddNewPoint = timeSinceLastUpdate >= this.trafficUpdateInterval;
|
||||
|
||||
// Count requests per bucket
|
||||
this.networkRequests.forEach(req => {
|
||||
if (req.timestamp >= now - range) {
|
||||
const bucketIndex = Math.floor((now - req.timestamp) / bucketSize);
|
||||
const bucketTime = now - (bucketIndex * bucketSize);
|
||||
buckets.set(bucketTime, (buckets.get(bucketTime) || 0) + 1);
|
||||
}
|
||||
console.log('UpdateTrafficData called:', {
|
||||
networkRequestsCount: this.networkRequests.length,
|
||||
timeSinceLastUpdate,
|
||||
shouldAddNewPoint,
|
||||
currentDataPoints: this.trafficData.length
|
||||
});
|
||||
|
||||
// Convert to chart data
|
||||
this.trafficData = Array.from({ length: 60 }, (_, i) => {
|
||||
const time = now - (i * bucketSize);
|
||||
return {
|
||||
x: time,
|
||||
y: buckets.get(time) || 0,
|
||||
if (!shouldAddNewPoint && this.trafficData.length > 0) {
|
||||
// Not enough time has passed, don't update
|
||||
return;
|
||||
}
|
||||
|
||||
// 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();
|
||||
}
|
||||
}
|
Reference in New Issue
Block a user