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