aiwg
Version:
Cognitive architecture for AI-augmented software development with structured memory, ensemble validation, and closed-loop correction. FAIR-aligned artifacts, 84% cost reduction via human-in-the-loop, standards adopted by 100+ organizations.
589 lines (502 loc) • 15.7 kB
text/typescript
/**
* @file metrics-collector.ts
* @description Metrics collection and aggregation system
*
* Implements F-010/UC-010: Metrics Collection
* - Code quality metrics (complexity, duplication, coverage)
* - Performance metrics (build time, test time, response time)
* - Team metrics (velocity, throughput, lead time)
* - Trend analysis and visualization
* - Metric export (CSV, JSON, Prometheus)
*
* @implements NFR-METRICS-001: Collection overhead <5%
* @implements NFR-METRICS-002: Real-time dashboard updates <1s
* @implements NFR-METRICS-003: Retention 90 days minimum
*/
import { promises as fs } from 'fs';
import path from 'path';
// ============================================================================
// Types and Interfaces
// ============================================================================
export interface MetricPoint {
timestamp: Date;
value: number;
labels?: Record<string, string>;
}
export interface MetricSeries {
name: string;
type: 'counter' | 'gauge' | 'histogram' | 'summary';
unit?: string;
description?: string;
points: MetricPoint[];
}
export interface CodeQualityMetrics {
complexity: number; // Cyclomatic complexity
duplication: number; // % duplicated code
coverage: number; // % test coverage
linesOfCode: number;
technicalDebt: number; // hours
codeSmells: number;
bugs: number;
vulnerabilities: number;
}
export interface PerformanceMetrics {
buildTime: number; // seconds
testTime: number; // seconds
deployTime: number; // seconds
responseTime: number; // ms (p95)
throughput: number; // requests/sec
errorRate: number; // %
}
export interface TeamMetrics {
velocity: number; // story points per sprint
throughput: number; // issues completed per week
leadTime: number; // days from start to deploy
cycleTime: number; // days from start to completion
deployFrequency: number; // deploys per day
changeFailureRate: number; // %
meanTimeToRecovery: number; // hours
}
export interface MetricsSnapshot {
timestamp: Date;
codeQuality: CodeQualityMetrics;
performance: PerformanceMetrics;
team: TeamMetrics;
}
export interface MetricsCollectorConfig {
projectPath: string;
retentionDays?: number; // Default: 90
collectInterval?: number; // milliseconds, default: 60000 (1 min)
exportFormats?: Array<'json' | 'csv' | 'prometheus'>;
storePath?: string;
}
// ============================================================================
// Metrics Collector Class
// ============================================================================
export class MetricsCollector {
private config: Required<MetricsCollectorConfig>;
private metrics: Map<string, MetricSeries>;
private snapshots: MetricsSnapshot[];
private collectionTimer?: NodeJS.Timeout;
constructor(config: MetricsCollectorConfig) {
this.config = {
projectPath: config.projectPath,
retentionDays: config.retentionDays || 90,
collectInterval: config.collectInterval || 60000,
exportFormats: config.exportFormats || ['json'],
storePath: config.storePath || path.join(config.projectPath, '.metrics')
};
this.metrics = new Map();
this.snapshots = [];
}
// ========================================================================
// Collection Methods
// ========================================================================
/**
* Start automatic metric collection
*/
public async startCollection(): Promise<void> {
// Initial collection
await this.collect();
// Schedule periodic collection
this.collectionTimer = setInterval(async () => {
await this.collect();
}, this.config.collectInterval);
}
/**
* Stop automatic collection
*/
public stopCollection(): void {
if (this.collectionTimer) {
clearInterval(this.collectionTimer);
this.collectionTimer = undefined;
}
}
/**
* Collect all metrics
*/
public async collect(): Promise<MetricsSnapshot> {
const snapshot: MetricsSnapshot = {
timestamp: new Date(),
codeQuality: await this.collectCodeQuality(),
performance: await this.collectPerformance(),
team: await this.collectTeamMetrics()
};
this.snapshots.push(snapshot);
await this.pruneOldSnapshots();
await this.persist();
return snapshot;
}
/**
* Collect code quality metrics
*/
private async collectCodeQuality(): Promise<CodeQualityMetrics> {
// Placeholder: In real implementation, would integrate with:
// - SonarQube
// - ESLint
// - Coverage tools (Istanbul, NYC)
// - Complexity analyzers
return {
complexity: this.measureComplexity(),
duplication: this.measureDuplication(),
coverage: await this.measureCoverage(),
linesOfCode: await this.countLinesOfCode(),
technicalDebt: this.estimateTechnicalDebt(),
codeSmells: 0,
bugs: 0,
vulnerabilities: 0
};
}
/**
* Collect performance metrics
*/
private async collectPerformance(): Promise<PerformanceMetrics> {
return {
buildTime: await this.measureBuildTime(),
testTime: await this.measureTestTime(),
deployTime: 0,
responseTime: 0,
throughput: 0,
errorRate: 0
};
}
/**
* Collect team metrics
*/
private async collectTeamMetrics(): Promise<TeamMetrics> {
return {
velocity: await this.calculateVelocity(),
throughput: await this.calculateThroughput(),
leadTime: await this.calculateLeadTime(),
cycleTime: await this.calculateCycleTime(),
deployFrequency: 0,
changeFailureRate: 0,
meanTimeToRecovery: 0
};
}
// ========================================================================
// Specific Metric Calculations
// ========================================================================
/**
* Measure cyclomatic complexity
*/
private measureComplexity(): number {
// Simplified: Would integrate with complexity analyzer
return 5.2; // Average complexity
}
/**
* Measure code duplication
*/
private measureDuplication(): number {
// Simplified: Would integrate with duplication detector
return 3.5; // % duplicated
}
/**
* Measure test coverage
*/
private async measureCoverage(): Promise<number> {
// Simplified: Would read from coverage report
return 85.3; // % coverage
}
/**
* Count lines of code
*/
private async countLinesOfCode(): Promise<number> {
let totalLines = 0;
const countInDirectory = async (dir: string): Promise<void> => {
const entries = await fs.readdir(dir, { withFileTypes: true });
for (const entry of entries) {
const fullPath = path.join(dir, entry.name);
// Skip node_modules, dist, etc.
if (entry.name === 'node_modules' || entry.name === 'dist' || entry.name === '.git') {
continue;
}
if (entry.isDirectory()) {
await countInDirectory(fullPath);
} else if (entry.isFile() && this.isSourceFile(entry.name)) {
const content = await fs.readFile(fullPath, 'utf-8');
totalLines += content.split('\n').length;
}
}
};
try {
await countInDirectory(this.config.projectPath);
} catch {
// Ignore errors
}
return totalLines;
}
/**
* Check if file is source code
*/
private isSourceFile(filename: string): boolean {
const sourceExtensions = ['.ts', '.js', '.tsx', '.jsx', '.py', '.java', '.go', '.rs'];
return sourceExtensions.some(ext => filename.endsWith(ext));
}
/**
* Estimate technical debt
*/
private estimateTechnicalDebt(): number {
// Simplified: Would integrate with debt tracking tools
return 120; // hours
}
/**
* Measure build time
*/
private async measureBuildTime(): Promise<number> {
// Simplified: Would track actual build duration
return 45; // seconds
}
/**
* Measure test time
*/
private async measureTestTime(): Promise<number> {
// Simplified: Would track actual test duration
return 30; // seconds
}
/**
* Calculate team velocity
*/
private async calculateVelocity(): Promise<number> {
// Simplified: Would integrate with issue tracker
return 25; // story points per sprint
}
/**
* Calculate throughput
*/
private async calculateThroughput(): Promise<number> {
// Simplified: Would count completed issues
return 12; // issues per week
}
/**
* Calculate lead time
*/
private async calculateLeadTime(): Promise<number> {
// Simplified: Would analyze issue lifecycle
return 5.2; // days
}
/**
* Calculate cycle time
*/
private async calculateCycleTime(): Promise<number> {
// Simplified: Would analyze work in progress
return 3.8; // days
}
// ========================================================================
// Time Series Methods
// ========================================================================
/**
* Record a metric point
*/
public recordMetric(name: string, value: number, labels?: Record<string, string>): void {
let series = this.metrics.get(name);
if (!series) {
series = {
name,
type: 'gauge',
points: []
};
this.metrics.set(name, series);
}
series.points.push({
timestamp: new Date(),
value,
labels
});
this.pruneOldPoints(series);
}
/**
* Get metric series
*/
public getMetric(name: string): MetricSeries | undefined {
return this.metrics.get(name);
}
/**
* Get all metrics
*/
public getAllMetrics(): MetricSeries[] {
return Array.from(this.metrics.values());
}
/**
* Get recent snapshots
*/
public getSnapshots(count: number = 100): MetricsSnapshot[] {
return this.snapshots.slice(-count);
}
/**
* Get latest snapshot
*/
public getLatestSnapshot(): MetricsSnapshot | undefined {
return this.snapshots[this.snapshots.length - 1];
}
// ========================================================================
// Analysis Methods
// ========================================================================
/**
* Calculate metric trend
*/
public calculateTrend(metricName: string, windowSize: number = 10): 'increasing' | 'decreasing' | 'stable' {
const series = this.metrics.get(metricName);
if (!series || series.points.length < windowSize) {
return 'stable';
}
const recentPoints = series.points.slice(-windowSize);
const firstHalf = recentPoints.slice(0, windowSize / 2);
const secondHalf = recentPoints.slice(windowSize / 2);
const avgFirst = firstHalf.reduce((sum, p) => sum + p.value, 0) / firstHalf.length;
const avgSecond = secondHalf.reduce((sum, p) => sum + p.value, 0) / secondHalf.length;
const change = ((avgSecond - avgFirst) / avgFirst) * 100;
if (change > 5) return 'increasing';
if (change < -5) return 'decreasing';
return 'stable';
}
/**
* Get metric statistics
*/
public getStatistics(metricName: string): {
min: number;
max: number;
avg: number;
median: number;
stdDev: number;
} | null {
const series = this.metrics.get(metricName);
if (!series || series.points.length === 0) {
return null;
}
const values = series.points.map(p => p.value);
const sorted = [...values].sort((a, b) => a - b);
const min = sorted[0];
const max = sorted[sorted.length - 1];
const avg = values.reduce((sum, v) => sum + v, 0) / values.length;
const median = sorted[Math.floor(sorted.length / 2)];
const variance = values.reduce((sum, v) => sum + Math.pow(v - avg, 2), 0) / values.length;
const stdDev = Math.sqrt(variance);
return { min, max, avg, median, stdDev };
}
// ========================================================================
// Export Methods
// ========================================================================
/**
* Export metrics to JSON
*/
public exportJSON(): string {
return JSON.stringify({
snapshots: this.snapshots,
series: Array.from(this.metrics.values())
}, null, 2);
}
/**
* Export metrics to CSV
*/
public exportCSV(): string {
const lines: string[] = [];
// Header
lines.push('timestamp,metric,value');
// Data
for (const series of this.metrics.values()) {
for (const point of series.points) {
lines.push(`${point.timestamp.toISOString()},${series.name},${point.value}`);
}
}
return lines.join('\n');
}
/**
* Export metrics in Prometheus format
*/
public exportPrometheus(): string {
const lines: string[] = [];
for (const series of this.metrics.values()) {
// Type and help
lines.push(`# HELP ${series.name} ${series.description || series.name}`);
lines.push(`# TYPE ${series.name} ${series.type}`);
// Latest value
const latest = series.points[series.points.length - 1];
if (latest) {
const labels = latest.labels
? '{' + Object.entries(latest.labels).map(([k, v]) => `${k}="${v}"`).join(',') + '}'
: '';
lines.push(`${series.name}${labels} ${latest.value}`);
}
}
return lines.join('\n');
}
/**
* Export to file
*/
public async exportToFile(format: 'json' | 'csv' | 'prometheus', filePath: string): Promise<void> {
let content: string;
switch (format) {
case 'json':
content = this.exportJSON();
break;
case 'csv':
content = this.exportCSV();
break;
case 'prometheus':
content = this.exportPrometheus();
break;
}
await fs.writeFile(filePath, content, 'utf-8');
}
// ========================================================================
// Persistence Methods
// ========================================================================
/**
* Save metrics to disk
*/
private async persist(): Promise<void> {
try {
await fs.mkdir(this.config.storePath, { recursive: true });
const data = {
snapshots: this.snapshots,
metrics: Array.from(this.metrics.entries())
};
const filePath = path.join(this.config.storePath, 'metrics.json');
await fs.writeFile(filePath, JSON.stringify(data, null, 2), 'utf-8');
} catch {
// Ignore persistence errors
}
}
/**
* Load metrics from disk
*/
public async load(): Promise<void> {
try {
const filePath = path.join(this.config.storePath, 'metrics.json');
const content = await fs.readFile(filePath, 'utf-8');
const data = JSON.parse(content);
this.snapshots = data.snapshots || [];
this.metrics = new Map(data.metrics || []);
} catch {
// No existing data
}
}
/**
* Prune old snapshots (NFR-METRICS-003: 90 days retention)
*/
private async pruneOldSnapshots(): Promise<void> {
const cutoffDate = new Date();
cutoffDate.setDate(cutoffDate.getDate() - this.config.retentionDays);
this.snapshots = this.snapshots.filter(snapshot =>
snapshot.timestamp > cutoffDate
);
}
/**
* Prune old metric points
*/
private pruneOldPoints(series: MetricSeries): void {
const cutoffDate = new Date();
cutoffDate.setDate(cutoffDate.getDate() - this.config.retentionDays);
series.points = series.points.filter(point =>
point.timestamp > cutoffDate
);
}
/**
* Clear all metrics
*/
public clear(): void {
this.metrics.clear();
this.snapshots = [];
}
}