Skip to main content
Glama

Watchtower DAP Windows Debugging

by rlaksana
metrics.ts7.52 kB
import { Metrics } from '../schemas/index'; /** * OpenTelemetry Metrics Collection * * Provides metrics collection for debugging performance and system health * following the DAP performance targets: TFFB ≤ 1000ms, Step p95 ≤ 200ms */ export class MetricsCollector { private static instance: MetricsCollector; private metrics: Map<string, Metrics[]> = new Map(); private timers: Map<string, number> = new Map(); private constructor() { // Initialize default metrics this.initializeMetrics(); } static getInstance(): MetricsCollector { if (!MetricsCollector.instance) { MetricsCollector.instance = new MetricsCollector(); } return MetricsCollector.instance; } /** * Initialize default metrics */ private initializeMetrics(): void { const defaultMetrics = [ { metric: 'dap.start.count', value: 0, tags: { type: 'request' } }, { metric: 'dap.start.time', value: 0, tags: { type: 'duration' } }, { metric: 'dap.breakpoints.set.count', value: 0, tags: { type: 'request' } }, { metric: 'dap.continue.count', value: 0, tags: { type: 'request' } }, { metric: 'dap.pause.count', value: 0, tags: { type: 'request' } }, { metric: 'dap.step.count', value: 0, tags: { type: 'request' } }, { metric: 'dap.step.time', value: 0, tags: { type: 'duration' } }, { metric: 'dap.threads.count', value: 0, tags: { type: 'request' } }, { metric: 'dap.stackTrace.count', value: 0, tags: { type: 'request' } }, { metric: 'dap.scopes.count', value: 0, tags: { type: 'request' } }, { metric: 'dap.variables.count', value: 0, tags: { type: 'request' } }, { metric: 'dap.evaluate.count', value: 0, tags: { type: 'request' } }, { metric: 'dap.events.poll.count', value: 0, tags: { type: 'request' } }, { metric: 'dap.terminate.count', value: 0, tags: { type: 'request' } }, { metric: 'dap.disconnect.count', value: 0, tags: { type: 'request' } }, { metric: 'sessions.active', value: 0, tags: { type: 'gauge' } }, { metric: 'sessions.total', value: 0, tags: { type: 'counter' } }, { metric: 'events.buffer.size', value: 0, tags: { type: 'gauge' } }, { metric: 'events.buffer.capacity', value: 0, tags: { type: 'gauge' } }, ]; defaultMetrics.forEach(metric => { const metricWithTimestamp = { ...metric, timestamp: Date.now() }; this.metrics.set(metric.metric, [metricWithTimestamp]); }); } /** * Start timing an operation */ startTimer(name: string): void { this.timers.set(name, Date.now()); } /** * Stop timing and record duration */ stopTimer(name: string, tags?: Record<string, string>): number { const startTime = this.timers.get(name); if (!startTime) { return 0; } const duration = Date.now() - startTime; this.timers.delete(name); this.record(`${name}.time`, duration, tags); return duration; } /** * Record a metric value */ record(metric: string, value: number, tags?: Record<string, string>): void { if (!this.hasMetric(metric)) { this.metrics.set(metric, []); } const metricData: Metrics = { timestamp: Date.now(), metric, value, tags: tags || {}, }; const metricList = this.metrics.get(metric)!; metricList.push(metricData); // Keep only the last 1000 data points per metric if (metricList.length > 1000) { metricList.splice(0, metricList.length - 1000); } } /** * Increment a counter metric */ increment(metric: string, delta: number = 1, tags?: Record<string, string>): void { const currentValue = this.get(metric, 0, tags); this.record(metric, currentValue + delta, tags); } /** * Get metric value(s) */ get(metric: string, defaultValue: number = 0, tags?: Record<string, string>): number { const metricList = this.metrics.get(metric) || []; if (tags) { // Filter by tags if provided const matching = metricList.filter( m => m.tags && Object.entries(tags).every( ([key, value]) => m.tags && m.tags[key] !== undefined && m.tags[key] === value ) ); return matching.length > 0 ? matching[matching.length - 1]?.value || defaultValue : defaultValue; } return metricList.length > 0 ? metricList[metricList.length - 1]?.value || defaultValue : defaultValue; } /** * Get multiple metric values */ getMultiple(metric: string, limit: number = 100): Metrics[] { const metricList = this.metrics.get(metric) || []; return metricList.slice(-limit); } /** * Calculate performance targets */ calculatePerformanceTargets(): { tffb: { current: number; target: number; status: 'pass' | 'fail' | 'unknown' }; stepP95: { current: number; target: number; status: 'pass' | 'fail' | 'unknown' }; } { // Time to First Break (TFFB) calculation const tffbValues = this.getMultiple('dap.start.time', 100) .map(m => m.value) .filter(v => v > 0); const tffbCurrent = tffbValues.length > 0 ? this.calculatePercentile(tffbValues, 95) : 0; const tffbTarget = 1000; // Target: ≤ 1000ms const tffbStatus = tffbCurrent > 0 ? (tffbCurrent <= tffbTarget ? 'pass' : 'fail') : 'unknown'; // Step operation p95 calculation const stepValues = this.getMultiple('dap.step.time', 100) .map(m => m.value) .filter(v => v > 0); const stepP95Current = stepValues.length > 0 ? this.calculatePercentile(stepValues, 95) : 0; const stepP95Target = 200; // Target: ≤ 200ms const stepP95Status = stepP95Current > 0 ? (stepP95Current <= stepP95Target ? 'pass' : 'fail') : 'unknown'; return { tffb: { current: tffbCurrent, target: tffbTarget, status: tffbStatus, }, stepP95: { current: stepP95Current, target: stepP95Target, status: stepP95Status, }, }; } /** * Calculate percentile from array of values */ private calculatePercentile(values: number[], percentile: number): number { if (values.length === 0) return 0; const sorted = [...values].sort((a, b) => a - b); const index = Math.ceil((percentile / 100) * sorted.length) - 1; return sorted[Math.max(0, Math.min(index, sorted.length - 1))] || 0; } /** * Check if metric exists */ private hasMetric(metric: string): boolean { return this.metrics.has(metric); } /** * Export metrics for OpenTelemetry */ export(): Record<string, any> { const exported: Record<string, any> = {}; const performanceTargets = this.calculatePerformanceTargets(); // Export all metrics for (const [metric, values] of Array.from(this.metrics.entries())) { if (values.length > 0) { exported[metric] = { values: values.map(v => ({ timestamp: v.timestamp, value: v.value, tags: v.tags, })), current: values[values.length - 1]?.value || 0, count: values.length, }; } } // Export performance targets exported['performance.targets'] = performanceTargets; return exported; } /** * Reset all metrics */ reset(): void { this.metrics.clear(); this.timers.clear(); this.initializeMetrics(); } } /** * Setup OpenTelemetry metrics */ export function setupMetrics(): MetricsCollector { return MetricsCollector.getInstance(); }

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/rlaksana/mcp-watchtower'

If you have feedback or need assistance with the MCP directory API, please join our Discord server