feat: add test monitoring and alerting system
This commit is contained in:
@@ -0,0 +1,154 @@
|
||||
import * as fs from 'fs';
|
||||
import * as path from 'path';
|
||||
|
||||
interface Alert {
|
||||
id: string;
|
||||
timestamp: number;
|
||||
severity: 'low' | 'medium' | 'high' | 'critical';
|
||||
message: string;
|
||||
tier: string;
|
||||
metrics: any;
|
||||
resolved: boolean;
|
||||
}
|
||||
|
||||
export class TestAlertManager {
|
||||
private alerts: Alert[] = [];
|
||||
private alertFile: string;
|
||||
|
||||
constructor(alertFile: string = 'test-results/alerts.json') {
|
||||
this.alertFile = alertFile;
|
||||
this.loadAlerts();
|
||||
}
|
||||
|
||||
private loadAlerts(): void {
|
||||
if (fs.existsSync(this.alertFile)) {
|
||||
const data = fs.readFileSync(this.alertFile, 'utf-8');
|
||||
this.alerts = JSON.parse(data);
|
||||
}
|
||||
}
|
||||
|
||||
private saveAlerts(): void {
|
||||
const dir = path.dirname(this.alertFile);
|
||||
if (!fs.existsSync(dir)) {
|
||||
fs.mkdirSync(dir, { recursive: true });
|
||||
}
|
||||
fs.writeFileSync(this.alertFile, JSON.stringify(this.alerts, null, 2));
|
||||
}
|
||||
|
||||
createAlert(
|
||||
severity: 'low' | 'medium' | 'high' | 'critical',
|
||||
message: string,
|
||||
tier: string,
|
||||
metrics: any
|
||||
): Alert {
|
||||
const alert: Alert = {
|
||||
id: `alert-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`,
|
||||
timestamp: Date.now(),
|
||||
severity,
|
||||
message,
|
||||
tier,
|
||||
metrics,
|
||||
resolved: false,
|
||||
};
|
||||
|
||||
this.alerts.push(alert);
|
||||
this.saveAlerts();
|
||||
|
||||
return alert;
|
||||
}
|
||||
|
||||
resolveAlert(alertId: string): void {
|
||||
const alert = this.alerts.find(a => a.id === alertId);
|
||||
if (alert) {
|
||||
alert.resolved = true;
|
||||
this.saveAlerts();
|
||||
}
|
||||
}
|
||||
|
||||
getActiveAlerts(): Alert[] {
|
||||
return this.alerts.filter(a => !a.resolved);
|
||||
}
|
||||
|
||||
getAlertsBySeverity(severity: 'low' | 'medium' | 'high' | 'critical'): Alert[] {
|
||||
return this.alerts.filter(a => a.severity === severity && !a.resolved);
|
||||
}
|
||||
|
||||
getAlertsByTier(tier: string): Alert[] {
|
||||
return this.alerts.filter(a => a.tier === tier && !a.resolved);
|
||||
}
|
||||
|
||||
getRecentAlerts(hours: number = 24): Alert[] {
|
||||
const cutoff = Date.now() - hours * 60 * 60 * 1000;
|
||||
return this.alerts.filter(a => a.timestamp >= cutoff && !a.resolved);
|
||||
}
|
||||
|
||||
getAlertSummary(): {
|
||||
total: number;
|
||||
active: number;
|
||||
bySeverity: Record<string, number>;
|
||||
byTier: Record<string, number>;
|
||||
} {
|
||||
const activeAlerts = this.getActiveAlerts();
|
||||
|
||||
const bySeverity: Record<string, number> = {
|
||||
low: 0,
|
||||
medium: 0,
|
||||
high: 0,
|
||||
critical: 0,
|
||||
};
|
||||
|
||||
const byTier: Record<string, number> = {};
|
||||
|
||||
activeAlerts.forEach(alert => {
|
||||
bySeverity[alert.severity]++;
|
||||
byTier[alert.tier] = (byTier[alert.tier] || 0) + 1;
|
||||
});
|
||||
|
||||
return {
|
||||
total: this.alerts.length,
|
||||
active: activeAlerts.length,
|
||||
bySeverity,
|
||||
byTier,
|
||||
};
|
||||
}
|
||||
|
||||
clearOldAlerts(days: number = 30): void {
|
||||
const cutoff = Date.now() - days * 24 * 60 * 60 * 1000;
|
||||
this.alerts = this.alerts.filter(a => a.timestamp >= cutoff);
|
||||
this.saveAlerts();
|
||||
}
|
||||
|
||||
exportReport(): string {
|
||||
const summary = this.getAlertSummary();
|
||||
const activeAlerts = this.getActiveAlerts();
|
||||
|
||||
let report = '🚨 测试告警报告\n';
|
||||
report += '='.repeat(50) + '\n\n';
|
||||
report += `总告警数: ${summary.total}\n`;
|
||||
report += `活跃告警: ${summary.active}\n\n`;
|
||||
|
||||
report += '按严重程度统计:\n';
|
||||
report += ` Critical: ${summary.bySeverity.critical}\n`;
|
||||
report += ` High: ${summary.bySeverity.high}\n`;
|
||||
report += ` Medium: ${summary.bySeverity.medium}\n`;
|
||||
report += ` Low: ${summary.bySeverity.low}\n\n`;
|
||||
|
||||
if (activeAlerts.length > 0) {
|
||||
report += '活跃告警详情:\n';
|
||||
activeAlerts.forEach((alert, index) => {
|
||||
report += `\n${index + 1}. [${alert.severity.toUpperCase()}] ${alert.message}\n`;
|
||||
report += ` Tier: ${alert.tier}\n`;
|
||||
report += ` Time: ${new Date(alert.timestamp).toLocaleString('zh-CN')}\n`;
|
||||
});
|
||||
} else {
|
||||
report += '✅ 当前无活跃告警\n';
|
||||
}
|
||||
|
||||
return report;
|
||||
}
|
||||
|
||||
clearAllAlerts(): void {
|
||||
this.alerts = [];
|
||||
this.saveAlerts();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,147 @@
|
||||
interface TestMetric {
|
||||
timestamp: number;
|
||||
tier: string;
|
||||
totalTests: number;
|
||||
passedTests: number;
|
||||
failedTests: number;
|
||||
skippedTests: number;
|
||||
duration: number;
|
||||
successRate: number;
|
||||
}
|
||||
|
||||
interface AlertRule {
|
||||
name: string;
|
||||
condition: (metrics: TestMetric) => boolean;
|
||||
severity: 'low' | 'medium' | 'high' | 'critical';
|
||||
message: string;
|
||||
}
|
||||
|
||||
export class TestMonitor {
|
||||
private metrics: TestMetric[] = [];
|
||||
private alertRules: AlertRule[] = [];
|
||||
|
||||
constructor() {
|
||||
this.initializeDefaultRules();
|
||||
}
|
||||
|
||||
private initializeDefaultRules(): void {
|
||||
this.alertRules = [
|
||||
{
|
||||
name: 'success-rate-low',
|
||||
condition: (m) => m.successRate < 0.8,
|
||||
severity: 'critical',
|
||||
message: '测试通过率低于80%',
|
||||
},
|
||||
{
|
||||
name: 'success-rate-medium',
|
||||
condition: (m) => m.successRate < 0.9 && m.successRate >= 0.8,
|
||||
severity: 'high',
|
||||
message: '测试通过率低于90%',
|
||||
},
|
||||
{
|
||||
name: 'duration-exceeded',
|
||||
condition: (m) => m.duration > 30 * 60 * 1000,
|
||||
severity: 'medium',
|
||||
message: '测试执行时间超过30分钟',
|
||||
},
|
||||
{
|
||||
name: 'failed-tests-high',
|
||||
condition: (m) => m.failedTests > 10,
|
||||
severity: 'high',
|
||||
message: '失败测试数量超过10个',
|
||||
},
|
||||
{
|
||||
name: 'tier-deep-failed',
|
||||
condition: (m) => m.tier === 'deep' && m.failedTests > 0,
|
||||
severity: 'critical',
|
||||
message: '深度层测试存在失败',
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
recordMetric(metric: TestMetric): void {
|
||||
this.metrics.push(metric);
|
||||
this.checkAlerts(metric);
|
||||
}
|
||||
|
||||
private checkAlerts(metric: TestMetric): void {
|
||||
const triggeredAlerts = this.alertRules.filter(rule => rule.condition(metric));
|
||||
|
||||
if (triggeredAlerts.length > 0) {
|
||||
triggeredAlerts.forEach(alert => {
|
||||
this.triggerAlert(alert, metric);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
private triggerAlert(alert: AlertRule, metric: TestMetric): void {
|
||||
const alertMessage = {
|
||||
timestamp: new Date().toISOString(),
|
||||
alert: alert.name,
|
||||
severity: alert.severity,
|
||||
message: alert.message,
|
||||
tier: metric.tier,
|
||||
metrics: {
|
||||
total: metric.totalTests,
|
||||
passed: metric.passedTests,
|
||||
failed: metric.failedTests,
|
||||
skipped: metric.skippedTests,
|
||||
duration: metric.duration,
|
||||
successRate: metric.successRate,
|
||||
},
|
||||
};
|
||||
|
||||
console.log(`🚨 [${alert.severity.toUpperCase()}] ${alert.message}`);
|
||||
console.log(JSON.stringify(alertMessage, null, 2));
|
||||
}
|
||||
|
||||
getMetricsByTier(tier: string): TestMetric[] {
|
||||
return this.metrics.filter(m => m.tier === tier);
|
||||
}
|
||||
|
||||
getAverageSuccessRate(tier?: string): number {
|
||||
const filteredMetrics = tier
|
||||
? this.getMetricsByTier(tier)
|
||||
: this.metrics;
|
||||
|
||||
if (filteredMetrics.length === 0) return 0;
|
||||
|
||||
const sum = filteredMetrics.reduce((acc, m) => acc + m.successRate, 0);
|
||||
return sum / filteredMetrics.length;
|
||||
}
|
||||
|
||||
getTrend(tier: string, window: number = 5): 'improving' | 'declining' | 'stable' {
|
||||
const tierMetrics = this.getMetricsByTier(tier);
|
||||
const recentMetrics = tierMetrics.slice(-window);
|
||||
|
||||
if (recentMetrics.length < 2) return 'stable';
|
||||
|
||||
const firstHalf = recentMetrics.slice(0, Math.floor(recentMetrics.length / 2));
|
||||
const secondHalf = recentMetrics.slice(Math.floor(recentMetrics.length / 2));
|
||||
|
||||
const firstAvg = firstHalf.reduce((acc, m) => acc + m.successRate, 0) / firstHalf.length;
|
||||
const secondAvg = secondHalf.reduce((acc, m) => acc + m.successRate, 0) / secondHalf.length;
|
||||
|
||||
const diff = secondAvg - firstAvg;
|
||||
|
||||
if (diff > 0.05) return 'improving';
|
||||
if (diff < -0.05) return 'declining';
|
||||
return 'stable';
|
||||
}
|
||||
|
||||
addAlertRule(rule: AlertRule): void {
|
||||
this.alertRules.push(rule);
|
||||
}
|
||||
|
||||
removeAlertRule(ruleName: string): void {
|
||||
this.alertRules = this.alertRules.filter(r => r.name !== ruleName);
|
||||
}
|
||||
|
||||
getMetrics(): TestMetric[] {
|
||||
return [...this.metrics];
|
||||
}
|
||||
|
||||
clearMetrics(): void {
|
||||
this.metrics = [];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,103 @@
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
|
||||
console.log('🔍 测试监控和告警系统...');
|
||||
|
||||
const alertFile = 'test-results/alerts.json';
|
||||
|
||||
const mockMetrics = [
|
||||
{
|
||||
timestamp: Date.now() - 3600000,
|
||||
tier: 'fast',
|
||||
totalTests: 100,
|
||||
passedTests: 95,
|
||||
failedTests: 5,
|
||||
skippedTests: 0,
|
||||
duration: 60000,
|
||||
successRate: 0.95,
|
||||
},
|
||||
{
|
||||
timestamp: Date.now() - 1800000,
|
||||
tier: 'standard',
|
||||
totalTests: 200,
|
||||
passedTests: 180,
|
||||
failedTests: 15,
|
||||
skippedTests: 5,
|
||||
duration: 180000,
|
||||
successRate: 0.9,
|
||||
},
|
||||
{
|
||||
timestamp: Date.now(),
|
||||
tier: 'deep',
|
||||
totalTests: 50,
|
||||
passedTests: 40,
|
||||
failedTests: 10,
|
||||
skippedTests: 0,
|
||||
duration: 600000,
|
||||
successRate: 0.8,
|
||||
},
|
||||
];
|
||||
|
||||
console.log('📊 模拟测试指标...');
|
||||
|
||||
const alertManager = {
|
||||
alerts: [],
|
||||
alertFile,
|
||||
|
||||
createAlert(severity, message, tier, metrics) {
|
||||
const alert = {
|
||||
id: `alert-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`,
|
||||
timestamp: Date.now(),
|
||||
severity,
|
||||
message,
|
||||
tier,
|
||||
metrics,
|
||||
resolved: false,
|
||||
};
|
||||
this.alerts.push(alert);
|
||||
console.log(`🚨 [${severity.toUpperCase()}] ${message}`);
|
||||
return alert;
|
||||
},
|
||||
|
||||
getAlertSummary() {
|
||||
const activeAlerts = this.alerts.filter(a => !a.resolved);
|
||||
const bySeverity = { low: 0, medium: 0, high: 0, critical: 0 };
|
||||
activeAlerts.forEach(a => bySeverity[a.severity]++);
|
||||
return { total: this.alerts.length, active: activeAlerts.length, bySeverity };
|
||||
},
|
||||
};
|
||||
|
||||
console.log('🔍 检查告警规则...');
|
||||
|
||||
for (const metric of mockMetrics) {
|
||||
if (metric.successRate < 0.8) {
|
||||
alertManager.createAlert('critical', '测试通过率低于80%', metric.tier, metric);
|
||||
} else if (metric.successRate < 0.9) {
|
||||
alertManager.createAlert('high', '测试通过率低于90%', metric.tier, metric);
|
||||
}
|
||||
|
||||
if (metric.failedTests > 10) {
|
||||
alertManager.createAlert('high', '失败测试数量超过10个', metric.tier, metric);
|
||||
}
|
||||
|
||||
if (metric.tier === 'deep' && metric.failedTests > 0) {
|
||||
alertManager.createAlert('critical', '深度层测试存在失败', metric.tier, metric);
|
||||
}
|
||||
}
|
||||
|
||||
const summary = alertManager.getAlertSummary();
|
||||
console.log('\n📊 告警统计:');
|
||||
console.log(` 总告警数: ${summary.total}`);
|
||||
console.log(` 活跃告警: ${summary.active}`);
|
||||
console.log(` Critical: ${summary.bySeverity.critical}`);
|
||||
console.log(` High: ${summary.bySeverity.high}`);
|
||||
console.log(` Medium: ${summary.bySeverity.medium}`);
|
||||
console.log(` Low: ${summary.bySeverity.low}`);
|
||||
|
||||
if (summary.active > 0) {
|
||||
console.log('\n✅ 监控系统工作正常,已生成告警');
|
||||
process.exit(0);
|
||||
} else {
|
||||
console.log('\n⚠️ 未生成告警');
|
||||
process.exit(1);
|
||||
}
|
||||
Reference in New Issue
Block a user