mcp-adr-analysis-server
Version:
MCP server for analyzing Architectural Decision Records and project architecture
513 lines • 16.4 kB
JavaScript
/**
* Monitoring and Analytics Framework
*
* Provides comprehensive monitoring and analytics for MCP server:
* - Request/response tracking
* - Performance metrics (latency, throughput)
* - Error tracking and reporting
* - Resource usage analytics
* - Cache performance metrics
* - Tool execution metrics
* - Health checks
* - Metrics aggregation and reporting
*/
/**
* Metric types for categorization
*/
export var MetricType;
(function (MetricType) {
MetricType["COUNTER"] = "counter";
MetricType["GAUGE"] = "gauge";
MetricType["HISTOGRAM"] = "histogram";
MetricType["TIMER"] = "timer";
})(MetricType || (MetricType = {}));
/**
* Metric category for organization
*/
export var MetricCategory;
(function (MetricCategory) {
MetricCategory["REQUEST"] = "request";
MetricCategory["RESOURCE"] = "resource";
MetricCategory["TOOL"] = "tool";
MetricCategory["CACHE"] = "cache";
MetricCategory["ERROR"] = "error";
MetricCategory["PERFORMANCE"] = "performance";
MetricCategory["SYSTEM"] = "system";
})(MetricCategory || (MetricCategory = {}));
/**
* Default monitoring configuration
*/
const DEFAULT_CONFIG = {
enabled: true,
metricsRetentionMs: 3600000, // 1 hour
aggregationIntervalMs: 60000, // 1 minute
maxMetricsInMemory: 10000,
enableHealthChecks: true,
healthCheckIntervalMs: 30000, // 30 seconds
};
/**
* Monitoring manager class
*/
export class MonitoringManager {
config;
metrics = [];
requests = new Map();
healthChecks = new Map();
aggregationTimer = null;
healthCheckTimer = null;
startTime;
constructor(config = {}) {
this.config = { ...DEFAULT_CONFIG, ...config };
this.startTime = new Date().toISOString();
if (this.config.enabled) {
this.startAggregation();
if (this.config.enableHealthChecks) {
this.startHealthChecks();
}
}
}
/**
* Record a counter metric (incrementing value)
*/
recordCounter(name, category, value = 1, tags, metadata) {
if (!this.config.enabled)
return;
const metric = {
name,
type: MetricType.COUNTER,
category,
value,
timestamp: new Date().toISOString(),
};
if (tags) {
metric.tags = tags;
}
if (metadata) {
metric.metadata = metadata;
}
this.addMetric(metric);
}
/**
* Record a gauge metric (point-in-time value)
*/
recordGauge(name, category, value, tags, metadata) {
if (!this.config.enabled)
return;
const metric = {
name,
type: MetricType.GAUGE,
category,
value,
timestamp: new Date().toISOString(),
};
if (tags) {
metric.tags = tags;
}
if (metadata) {
metric.metadata = metadata;
}
this.addMetric(metric);
}
/**
* Record a histogram value (for distribution analysis)
*/
recordHistogram(name, category, value, tags, metadata) {
if (!this.config.enabled)
return;
const metric = {
name,
type: MetricType.HISTOGRAM,
category,
value,
timestamp: new Date().toISOString(),
};
if (tags) {
metric.tags = tags;
}
if (metadata) {
metric.metadata = metadata;
}
this.addMetric(metric);
}
/**
* Start tracking a request
*/
startRequest(requestId, type, name, metadata) {
if (!this.config.enabled)
return;
const request = {
requestId,
type,
name,
startTime: new Date().toISOString(),
status: 'pending',
};
if (metadata) {
request.metadata = metadata;
}
this.requests.set(requestId, request);
this.recordCounter('request.started', MetricCategory.REQUEST, 1, { type, name });
}
/**
* Complete a request successfully
*/
completeRequest(requestId, metadata) {
if (!this.config.enabled)
return;
const request = this.requests.get(requestId);
if (!request)
return;
const endTime = new Date().toISOString();
const duration = new Date(endTime).getTime() - new Date(request.startTime).getTime();
request.endTime = endTime;
request.duration = duration;
request.status = 'success';
if (metadata) {
request.metadata = { ...request.metadata, ...metadata };
}
this.recordCounter('request.completed', MetricCategory.REQUEST, 1, {
type: request.type,
name: request.name,
});
this.recordHistogram('request.duration', MetricCategory.PERFORMANCE, duration, {
type: request.type,
name: request.name,
});
}
/**
* Mark a request as failed
*/
failRequest(requestId, error, metadata) {
if (!this.config.enabled)
return;
const request = this.requests.get(requestId);
if (!request)
return;
const endTime = new Date().toISOString();
const duration = new Date(endTime).getTime() - new Date(request.startTime).getTime();
request.endTime = endTime;
request.duration = duration;
request.status = 'error';
request.error = error;
if (metadata) {
request.metadata = { ...request.metadata, ...metadata };
}
this.recordCounter('request.failed', MetricCategory.REQUEST, 1, {
type: request.type,
name: request.name,
});
this.recordCounter('error.total', MetricCategory.ERROR, 1, {
type: request.type,
name: request.name,
error,
});
}
/**
* Record cache hit
*/
recordCacheHit(cacheKey) {
if (!this.config.enabled)
return;
this.recordCounter('cache.hit', MetricCategory.CACHE, 1, { key: cacheKey });
}
/**
* Record cache miss
*/
recordCacheMiss(cacheKey) {
if (!this.config.enabled)
return;
this.recordCounter('cache.miss', MetricCategory.CACHE, 1, { key: cacheKey });
}
/**
* Register a health check
*/
registerHealthCheck(_name, _check) {
// Store health check function for periodic execution
// Implementation would store the check function and run it periodically
// For now, we'll just track the results when manually called
}
/**
* Run a health check
*/
async runHealthCheck(name, check) {
const startTime = Date.now();
const timestamp = new Date().toISOString();
try {
const result = await check();
const duration = Date.now() - startTime;
const healthCheck = {
name,
status: result.status,
timestamp,
duration,
};
if (result.message) {
healthCheck.message = result.message;
}
this.healthChecks.set(name, healthCheck);
return healthCheck;
}
catch (error) {
const duration = Date.now() - startTime;
const healthCheck = {
name,
status: 'unhealthy',
message: error instanceof Error ? error.message : 'Unknown error',
timestamp,
duration,
};
this.healthChecks.set(name, healthCheck);
return healthCheck;
}
}
/**
* Get all health checks
*/
getHealthChecks() {
return Array.from(this.healthChecks.values());
}
/**
* Get overall health status
*/
getHealthStatus() {
const checks = this.getHealthChecks();
if (checks.length === 0)
return 'healthy';
const unhealthy = checks.filter(c => c.status === 'unhealthy');
const degraded = checks.filter(c => c.status === 'degraded');
if (unhealthy.length > 0)
return 'unhealthy';
if (degraded.length > 0)
return 'degraded';
return 'healthy';
}
/**
* Get performance snapshot
*/
getPerformanceSnapshot() {
const now = new Date().toISOString();
const requests = Array.from(this.requests.values());
const total = requests.length;
const success = requests.filter(r => r.status === 'success').length;
const error = requests.filter(r => r.status === 'error').length;
const pending = requests.filter(r => r.status === 'pending').length;
// Calculate latency percentiles
const durations = requests
.filter(r => r.duration !== undefined)
.map(r => r.duration)
.sort((a, b) => a - b);
const latency = this.calculatePercentiles(durations);
// Calculate throughput
const oneMinuteAgo = Date.now() - 60000;
const recentRequests = requests.filter(r => r.startTime && new Date(r.startTime).getTime() > oneMinuteAgo);
const throughput = {
requestsPerSecond: recentRequests.length / 60,
requestsPerMinute: recentRequests.length,
};
// Cache metrics
const cacheHits = this.getMetricSum('cache.hit', MetricCategory.CACHE);
const cacheMisses = this.getMetricSum('cache.miss', MetricCategory.CACHE);
const cacheTotal = cacheHits + cacheMisses;
const hitRate = cacheTotal > 0 ? cacheHits / cacheTotal : 0;
// Error metrics
const errorMetrics = this.metrics.filter(m => m.category === MetricCategory.ERROR);
const errorsByType = {};
for (const metric of errorMetrics) {
const errorType = metric.tags?.['error'] || 'unknown';
errorsByType[errorType] = (errorsByType[errorType] || 0) + metric.value;
}
return {
timestamp: now,
requests: { total, success, error, pending },
latency,
throughput,
cache: {
hitRate,
totalHits: cacheHits,
totalMisses: cacheMisses,
},
errors: {
total: error,
byType: errorsByType,
},
};
}
/**
* Get metric statistics by name and category
*/
getMetricStats(name, category) {
const metricValues = this.metrics
.filter(m => m.name === name && m.category === category)
.map(m => m.value)
.sort((a, b) => a - b);
if (metricValues.length === 0)
return null;
return this.calculateStats(metricValues);
}
/**
* Get all metrics
*/
getMetrics() {
return [...this.metrics];
}
/**
* Get metrics by category
*/
getMetricsByCategory(category) {
return this.metrics.filter(m => m.category === category);
}
/**
* Get recent requests
*/
getRecentRequests(limit = 100) {
return Array.from(this.requests.values())
.sort((a, b) => new Date(b.startTime).getTime() - new Date(a.startTime).getTime())
.slice(0, limit);
}
/**
* Clear old metrics based on retention policy
*/
cleanup() {
const cutoff = Date.now() - this.config.metricsRetentionMs;
const initialCount = this.metrics.length;
this.metrics = this.metrics.filter(m => new Date(m.timestamp).getTime() > cutoff);
// Also clean up old requests
const requestsToDelete = [];
for (const [id, request] of this.requests.entries()) {
if (request.endTime && new Date(request.endTime).getTime() < cutoff) {
requestsToDelete.push(id);
}
}
requestsToDelete.forEach(id => this.requests.delete(id));
return initialCount - this.metrics.length;
}
/**
* Reset all metrics
*/
reset() {
this.metrics = [];
this.requests.clear();
this.healthChecks.clear();
}
/**
* Stop monitoring
*/
stop() {
if (this.aggregationTimer) {
clearInterval(this.aggregationTimer);
this.aggregationTimer = null;
}
if (this.healthCheckTimer) {
clearInterval(this.healthCheckTimer);
this.healthCheckTimer = null;
}
}
/**
* Get uptime in milliseconds
*/
getUptime() {
return Date.now() - new Date(this.startTime).getTime();
}
/**
* Export metrics to JSON
*/
exportMetrics() {
return JSON.stringify({
metrics: this.metrics,
requests: Array.from(this.requests.values()),
healthChecks: Array.from(this.healthChecks.values()),
performance: this.getPerformanceSnapshot(),
uptime: this.getUptime(),
timestamp: new Date().toISOString(),
}, null, 2);
}
// Private methods
addMetric(metric) {
this.metrics.push(metric);
// Enforce max metrics limit
if (this.metrics.length > this.config.maxMetricsInMemory) {
const toRemove = this.metrics.length - this.config.maxMetricsInMemory;
this.metrics.splice(0, toRemove);
}
}
getMetricSum(name, category) {
return this.metrics
.filter(m => m.name === name && m.category === category)
.reduce((sum, m) => sum + m.value, 0);
}
calculatePercentiles(values) {
if (values.length === 0) {
return { p50: 0, p95: 0, p99: 0, avg: 0 };
}
const sorted = [...values].sort((a, b) => a - b);
const avg = sorted.reduce((sum, v) => sum + v, 0) / sorted.length;
return {
p50: this.percentile(sorted, 50),
p95: this.percentile(sorted, 95),
p99: this.percentile(sorted, 99),
avg,
};
}
calculateStats(values) {
if (values.length === 0) {
return { count: 0, sum: 0, min: 0, max: 0, avg: 0, p50: 0, p95: 0, p99: 0 };
}
const sorted = [...values].sort((a, b) => a - b);
const sum = sorted.reduce((s, v) => s + v, 0);
const avg = sum / sorted.length;
return {
count: sorted.length,
sum,
min: sorted[0],
max: sorted[sorted.length - 1],
avg,
p50: this.percentile(sorted, 50),
p95: this.percentile(sorted, 95),
p99: this.percentile(sorted, 99),
};
}
percentile(sorted, p) {
if (sorted.length === 0)
return 0;
const index = Math.ceil((p / 100) * sorted.length) - 1;
return sorted[Math.max(0, index)] || 0;
}
startAggregation() {
this.aggregationTimer = setInterval(() => {
this.cleanup();
}, this.config.aggregationIntervalMs);
}
startHealthChecks() {
// Placeholder for periodic health check execution
this.healthCheckTimer = setInterval(() => {
// Would run registered health checks here
}, this.config.healthCheckIntervalMs);
}
}
/**
* Singleton monitoring instance
*/
export const monitoring = new MonitoringManager();
/**
* Helper to track async operations
*/
export async function trackOperation(operation, name, category, tags) {
const startTime = Date.now();
try {
const result = await operation();
const duration = Date.now() - startTime;
monitoring.recordHistogram(`${name}.duration`, category, duration, tags);
monitoring.recordCounter(`${name}.success`, category, 1, tags);
return result;
}
catch (error) {
const duration = Date.now() - startTime;
monitoring.recordHistogram(`${name}.duration`, category, duration, tags);
monitoring.recordCounter(`${name}.error`, category, 1, {
...tags,
error: error instanceof Error ? error.message : 'Unknown error',
});
throw error;
}
}
//# sourceMappingURL=monitoring.js.map