@gork-labs/secondbrain-mcp
Version:
Second Brain MCP Server - Agent team orchestration with dynamic tool discovery
364 lines (363 loc) • 13.9 kB
JavaScript
import { logger } from '../utils/logger.js';
/**
* Metrics Collector
* Collects and analyzes system performance and usage metrics
*/
export class MetricsCollector {
storage;
operationStartTimes = new Map();
systemStartTime;
constructor(storage) {
this.storage = storage;
this.systemStartTime = Date.now();
}
/**
* Start timing an operation
*/
startOperation(operationId, operation) {
this.operationStartTimes.set(operationId, Date.now());
logger.debug('Started timing operation', {
operationId,
operation,
timestamp: new Date().toISOString()
});
}
/**
* End timing an operation and record performance metric
*/
endOperation(operationId, operation, success, errorType, metadata) {
const startTime = this.operationStartTimes.get(operationId);
if (!startTime) {
logger.warn('Operation end called without start', { operationId, operation });
return 0;
}
const duration = Date.now() - startTime;
this.operationStartTimes.delete(operationId);
const metric = {
timestamp: new Date().toISOString(),
operation,
duration,
success,
errorType,
resourceUsage: {
memory: process.memoryUsage().heapUsed
},
requestSize: metadata?.requestSize,
responseSize: metadata?.responseSize
};
this.storage.recordPerformanceMetric(metric);
logger.debug('Completed operation timing', {
operationId,
operation,
duration,
success,
errorType
});
return duration;
}
/**
* Record a usage event
*/
recordUsage(subagent, sessionId, operation, success, userContext) {
const metric = {
timestamp: new Date().toISOString(),
subagent,
sessionId,
operation,
success,
userContext
};
this.storage.recordUsageMetric(metric);
logger.debug('Recorded usage metric', {
subagent,
sessionId,
operation,
success,
complexity: userContext?.complexity
});
}
/**
* Analyze performance trends for specific operations
*/
analyzeOperationPerformance(operation, days = 7) {
const endDate = new Date();
const startDate = new Date();
startDate.setDate(endDate.getDate() - days);
const metrics = this.storage.getPerformanceMetrics(operation)
.filter(m => new Date(m.timestamp) >= startDate);
if (metrics.length === 0) {
return this.createEmptyPerformanceInsight(operation);
}
const durations = metrics.map(m => m.duration);
const avgDuration = durations.reduce((sum, d) => sum + d, 0) / durations.length;
// Calculate 95th percentile
const sortedDurations = durations.sort((a, b) => a - b);
const p95Index = Math.floor(sortedDurations.length * 0.95);
const p95Duration = sortedDurations[p95Index] || avgDuration;
const successRate = metrics.filter(m => m.success).length / metrics.length;
// Analyze error patterns
const errorPatterns = this.analyzeErrorPatterns(metrics);
// Generate optimization suggestions
const optimizationSuggestions = this.generateOptimizationSuggestions(avgDuration, p95Duration, successRate, errorPatterns);
// Calculate trend
const trend = this.calculatePerformanceTrend(metrics);
return {
operation,
avgDuration,
p95Duration,
successRate,
errorPatterns,
optimizationSuggestions,
trend,
timestamp: new Date().toISOString()
};
}
createEmptyPerformanceInsight(operation) {
return {
operation,
avgDuration: 0,
p95Duration: 0,
successRate: 0,
errorPatterns: [],
optimizationSuggestions: ['No data available for analysis'],
trend: 'stable',
timestamp: new Date().toISOString()
};
}
analyzeErrorPatterns(metrics) {
const errorTypes = {};
for (const metric of metrics) {
if (!metric.success && metric.errorType) {
errorTypes[metric.errorType] = (errorTypes[metric.errorType] || 0) + 1;
}
}
// Return most common error patterns
return Object.entries(errorTypes)
.sort(([, a], [, b]) => b - a)
.slice(0, 5)
.map(([errorType, count]) => `${errorType} (${count} occurrences)`);
}
generateOptimizationSuggestions(avgDuration, p95Duration, successRate, errorPatterns) {
const suggestions = [];
// Performance suggestions
if (avgDuration > 2000) {
suggestions.push('Average response time is high - consider caching or optimization');
}
if (p95Duration > avgDuration * 3) {
suggestions.push('High variability in response times - investigate outliers');
}
// Reliability suggestions
if (successRate < 0.95) {
suggestions.push('Success rate below 95% - review error handling and validation');
}
// Error-specific suggestions
if (errorPatterns.length > 0) {
suggestions.push(`Common errors detected: ${errorPatterns[0]}`);
suggestions.push('Consider implementing specific error prevention measures');
}
// Memory usage suggestions
const currentMemory = process.memoryUsage().heapUsed;
if (currentMemory > 500 * 1024 * 1024) { // 500MB
suggestions.push('High memory usage detected - consider memory optimization');
}
return suggestions.length > 0 ? suggestions : ['Performance appears optimal'];
}
calculatePerformanceTrend(metrics) {
if (metrics.length < 10)
return 'stable';
// Compare first half with second half
const midpoint = Math.floor(metrics.length / 2);
const firstHalf = metrics.slice(0, midpoint);
const secondHalf = metrics.slice(midpoint);
const firstAvgDuration = firstHalf.reduce((sum, m) => sum + m.duration, 0) / firstHalf.length;
const secondAvgDuration = secondHalf.reduce((sum, m) => sum + m.duration, 0) / secondHalf.length;
const improvement = (firstAvgDuration - secondAvgDuration) / firstAvgDuration;
if (improvement > 0.1)
return 'improving'; // 10% improvement
if (improvement < -0.1)
return 'declining'; // 10% degradation
return 'stable';
}
/**
* Analyze usage patterns for chatmodes
*/
analyzeUsagePatterns(subagent, days = 7) {
const endDate = new Date();
const startDate = new Date();
startDate.setDate(endDate.getDate() - days);
const metrics = this.storage.getUsageMetrics(subagent)
.filter(m => new Date(m.timestamp) >= startDate);
if (subagent) {
return [this.analyzeSpecificSubagentUsage(subagent, metrics)];
}
// Analyze all subagents
const subagentGroups = {};
for (const metric of metrics) {
if (!subagentGroups[metric.subagent]) {
subagentGroups[metric.subagent] = [];
}
subagentGroups[metric.subagent].push(metric);
}
return Object.entries(subagentGroups).map(([cm, cms]) => this.analyzeSpecificSubagentUsage(cm, cms));
}
analyzeSpecificSubagentUsage(subagent, metrics) {
if (metrics.length === 0) {
return this.createEmptyUsagePattern(subagent);
}
// Calculate peak hours
const hourCounts = {};
for (const metric of metrics) {
const hour = new Date(metric.timestamp).getHours();
hourCounts[hour] = (hourCounts[hour] || 0) + 1;
}
const peakHours = Object.entries(hourCounts)
.sort(([, a], [, b]) => b - a)
.slice(0, 3)
.map(([hour]) => parseInt(hour));
// Calculate session duration (simplified)
const sessionDurations = {};
for (const metric of metrics) {
if (!sessionDurations[metric.sessionId]) {
sessionDurations[metric.sessionId] = 0;
}
sessionDurations[metric.sessionId]++;
}
const avgSessionDuration = Object.values(sessionDurations)
.reduce((sum, duration) => sum + duration, 0) / Object.values(sessionDurations).length;
// Calculate success rate
const successRate = metrics.filter(m => m.success).length / metrics.length;
// Analyze common tasks
const taskTypes = metrics
.map(m => m.userContext?.taskType)
.filter(Boolean);
const taskCounts = {};
for (const task of taskTypes) {
taskCounts[task] = (taskCounts[task] || 0) + 1;
}
const commonTasks = Object.entries(taskCounts)
.sort(([, a], [, b]) => b - a)
.slice(0, 3)
.map(([task]) => task);
// Analyze user behaviors
const complexities = metrics
.map(m => m.userContext?.complexity)
.filter(Boolean);
const complexityMode = this.findMode(complexities) || 'medium';
return {
subagent,
peakHours,
avgSessionDuration,
successRate,
commonTasks,
userBehaviors: {
refinementFrequency: 0, // Will be populated by quality analyzer
taskComplexity: complexityMode,
preferredFeatures: commonTasks
}
};
}
createEmptyUsagePattern(subagent) {
return {
subagent,
peakHours: [],
avgSessionDuration: 0,
successRate: 0,
commonTasks: [],
userBehaviors: {
refinementFrequency: 0,
taskComplexity: 'medium',
preferredFeatures: []
}
};
}
findMode(array) {
const counts = {};
for (const item of array) {
counts[item] = (counts[item] || 0) + 1;
}
let maxCount = 0;
let mode = null;
for (const [item, count] of Object.entries(counts)) {
if (count > maxCount) {
maxCount = count;
mode = item;
}
}
return mode;
}
/**
* Get current system health metrics
*/
getSystemHealth() {
const memoryUsage = process.memoryUsage();
const uptime = Date.now() - this.systemStartTime;
// Calculate error rate from recent performance metrics
const recentMetrics = this.storage.getPerformanceMetrics(undefined, 100);
const errorRate = recentMetrics.length > 0
? recentMetrics.filter(m => !m.success).length / recentMetrics.length
: 0;
// Calculate average response time
const avgResponseTime = recentMetrics.length > 0
? recentMetrics.reduce((sum, m) => sum + m.duration, 0) / recentMetrics.length
: 0;
const storageHealth = this.storage.getStorageHealth();
return {
status: this.determineHealthStatus(errorRate, avgResponseTime, memoryUsage.heapUsed),
totalRecords: storageHealth.totalRecords,
memoryUsage: memoryUsage.heapUsed,
lastCleanup: storageHealth.lastCleanup,
storageLocation: storageHealth.storageLocation,
uptime,
errorRate,
avgResponseTime
};
}
determineHealthStatus(errorRate, avgResponseTime, memoryUsage) {
if (errorRate > 0.1 || avgResponseTime > 5000 || memoryUsage > 1024 * 1024 * 1024) {
return 'critical';
}
if (errorRate > 0.05 || avgResponseTime > 2000 || memoryUsage > 512 * 1024 * 1024) {
return 'warning';
}
return 'healthy';
}
/**
* Get performance summary for all operations
*/
getPerformanceSummary(days = 7) {
const metrics = this.storage.getPerformanceMetrics();
const operations = [...new Set(metrics.map(m => m.operation))];
const summary = {};
for (const operation of operations) {
summary[operation] = this.analyzeOperationPerformance(operation, days);
}
return summary;
}
/**
* Export metrics for external analysis
*/
exportMetrics(operation, days = 30) {
const endDate = new Date();
const startDate = new Date();
startDate.setDate(endDate.getDate() - days);
const performance = this.storage.getPerformanceMetrics(operation)
.filter(m => new Date(m.timestamp) >= startDate);
const usage = this.storage.getUsageMetrics()
.filter(m => new Date(m.timestamp) >= startDate);
const summary = {
timeRange: `${days} days`,
totalOperations: performance.length,
uniqueSessions: new Set(usage.map(u => u.sessionId)).size,
avgResponseTime: performance.reduce((sum, p) => sum + p.duration, 0) / performance.length,
successRate: performance.filter(p => p.success).length / performance.length,
exportedAt: new Date().toISOString()
};
logger.info('Exported metrics for analysis', {
operation,
days,
performanceRecords: performance.length,
usageRecords: usage.length
});
return { performance, usage, summary };
}
}