feat(network): Add traffic stats endpoint and dashboard UI; enhance platform services and certificate health reporting

This commit is contained in:
2025-11-27 09:26:04 +00:00
parent ff5b51072f
commit 3d7727c304
11 changed files with 659 additions and 33 deletions

View File

@@ -79,8 +79,84 @@ export class CaddyLogReceiver {
private recentLogs: ICaddyAccessLog[] = [];
private maxRecentLogs = 100;
// Traffic stats aggregation (hourly rolling window)
private trafficStats: {
timestamp: number;
requestCount: number;
errorCount: number; // 4xx + 5xx
totalDuration: number; // microseconds
totalSize: number; // bytes
statusCounts: Record<string, number>; // "2xx", "3xx", "4xx", "5xx"
}[] = [];
private maxStatsAge = 3600 * 1000; // 1 hour in ms
private statsInterval = 60 * 1000; // 1 minute buckets
constructor(port = 9999) {
this.port = port;
// Initialize first stats bucket
this.trafficStats.push(this.createStatsBucket());
}
/**
* Create a new stats bucket
*/
private createStatsBucket(): typeof this.trafficStats[0] {
return {
timestamp: Math.floor(Date.now() / this.statsInterval) * this.statsInterval,
requestCount: 0,
errorCount: 0,
totalDuration: 0,
totalSize: 0,
statusCounts: { '2xx': 0, '3xx': 0, '4xx': 0, '5xx': 0 },
};
}
/**
* Get current stats bucket, creating new one if needed
*/
private getCurrentStatsBucket(): typeof this.trafficStats[0] {
const now = Date.now();
const currentBucketTime = Math.floor(now / this.statsInterval) * this.statsInterval;
// Get or create current bucket
let bucket = this.trafficStats[this.trafficStats.length - 1];
if (!bucket || bucket.timestamp !== currentBucketTime) {
bucket = this.createStatsBucket();
this.trafficStats.push(bucket);
// Clean up old buckets
const cutoff = now - this.maxStatsAge;
while (this.trafficStats.length > 0 && this.trafficStats[0].timestamp < cutoff) {
this.trafficStats.shift();
}
}
return bucket;
}
/**
* Record a request in traffic stats
*/
private recordTrafficStats(log: ICaddyAccessLog): void {
const bucket = this.getCurrentStatsBucket();
bucket.requestCount++;
bucket.totalDuration += log.duration;
bucket.totalSize += log.size || 0;
// Categorize status code
const statusCategory = Math.floor(log.status / 100);
if (statusCategory === 2) {
bucket.statusCounts['2xx']++;
} else if (statusCategory === 3) {
bucket.statusCounts['3xx']++;
} else if (statusCategory === 4) {
bucket.statusCounts['4xx']++;
bucket.errorCount++;
} else if (statusCategory === 5) {
bucket.statusCounts['5xx']++;
bucket.errorCount++;
}
}
/**
@@ -181,6 +257,9 @@ export class CaddyLogReceiver {
return;
}
// Always record traffic stats (before sampling) for accurate aggregation
this.recordTrafficStats(log);
// Update adaptive sampling
this.updateSampling();
@@ -414,4 +493,57 @@ export class CaddyLogReceiver {
recentLogsCount: this.recentLogs.length,
};
}
/**
* Get aggregated traffic stats for the specified time range
* @param minutes Number of minutes to aggregate (default: 60)
*/
getTrafficStats(minutes = 60): {
requestCount: number;
errorCount: number;
avgResponseTime: number; // in milliseconds
totalBytes: number;
statusCounts: Record<string, number>;
requestsPerMinute: number;
errorRate: number; // percentage
} {
const now = Date.now();
const cutoff = now - (minutes * 60 * 1000);
// Aggregate all buckets within the time range
let requestCount = 0;
let errorCount = 0;
let totalDuration = 0;
let totalBytes = 0;
const statusCounts: Record<string, number> = { '2xx': 0, '3xx': 0, '4xx': 0, '5xx': 0 };
for (const bucket of this.trafficStats) {
if (bucket.timestamp >= cutoff) {
requestCount += bucket.requestCount;
errorCount += bucket.errorCount;
totalDuration += bucket.totalDuration;
totalBytes += bucket.totalSize;
for (const [status, count] of Object.entries(bucket.statusCounts)) {
statusCounts[status] = (statusCounts[status] || 0) + count;
}
}
}
// Calculate averages
const avgResponseTime = requestCount > 0
? (totalDuration / requestCount) / 1000 // Convert from microseconds to milliseconds
: 0;
const requestsPerMinute = requestCount / Math.max(minutes, 1);
const errorRate = requestCount > 0 ? (errorCount / requestCount) * 100 : 0;
return {
requestCount,
errorCount,
avgResponseTime: Math.round(avgResponseTime * 100) / 100, // Round to 2 decimal places
totalBytes,
statusCounts,
requestsPerMinute: Math.round(requestsPerMinute * 100) / 100,
errorRate: Math.round(errorRate * 100) / 100,
};
}
}