Skip to main content
Glama
index.ts6.46 kB
/** * Prometheus-style metrics collection */ interface Metric { name: string; type: 'counter' | 'gauge' | 'histogram'; help: string; value: number; labels?: Record<string, string>; } class MetricsCollector { private metrics: Map<string, Metric> = new Map(); private counters: Map<string, number> = new Map(); private gauges: Map<string, number> = new Map(); private histograms: Map<string, number[]> = new Map(); /** * Increment a counter metric */ incrementCounter(name: string, labels?: Record<string, string>): void { const key = this.getKey(name, labels); const current = this.counters.get(key) || 0; this.counters.set(key, current + 1); if (!this.metrics.has(name)) { this.metrics.set(name, { name, type: 'counter', help: `Counter for ${name}`, value: 0, labels, }); } } /** * Set a gauge metric value */ setGauge(name: string, value: number, labels?: Record<string, string>): void { const key = this.getKey(name, labels); this.gauges.set(key, value); if (!this.metrics.has(name)) { this.metrics.set(name, { name, type: 'gauge', help: `Gauge for ${name}`, value, labels, }); } } /** * Record a histogram observation */ observeHistogram(name: string, value: number, labels?: Record<string, string>): void { const key = this.getKey(name, labels); const observations = this.histograms.get(key) || []; observations.push(value); this.histograms.set(key, observations); if (!this.metrics.has(name)) { this.metrics.set(name, { name, type: 'histogram', help: `Histogram for ${name}`, value: 0, labels, }); } } /** * Get metric key with labels */ private getKey(name: string, labels?: Record<string, string>): string { if (!labels || Object.keys(labels).length === 0) { return name; } const labelStr = Object.entries(labels) .sort(([a], [b]) => a.localeCompare(b)) .map(([k, v]) => `${k}="${v}"`) .join(','); return `${name}{${labelStr}}`; } /** * Format labels for Prometheus */ private formatLabels(labels?: Record<string, string>): string { if (!labels || Object.keys(labels).length === 0) { return ''; } const labelStr = Object.entries(labels) .sort(([a], [b]) => a.localeCompare(b)) .map(([k, v]) => `${k}="${v}"`) .join(','); return `{${labelStr}}`; } /** * Export metrics in Prometheus text format */ export(): string { const lines: string[] = []; // Export counters const counterMetrics = Array.from(this.metrics.values()) .filter(m => m.type === 'counter'); for (const metric of counterMetrics) { lines.push(`# HELP ${metric.name} ${metric.help}`); lines.push(`# TYPE ${metric.name} counter`); for (const [key, value] of this.counters.entries()) { if (key.startsWith(metric.name)) { const labelMatch = key.match(/\{(.+)\}/); const labels = labelMatch ? `{${labelMatch[1]}}` : ''; lines.push(`${metric.name}${labels} ${value}`); } } } // Export gauges const gaugeMetrics = Array.from(this.metrics.values()) .filter(m => m.type === 'gauge'); for (const metric of gaugeMetrics) { lines.push(`# HELP ${metric.name} ${metric.help}`); lines.push(`# TYPE ${metric.name} gauge`); for (const [key, value] of this.gauges.entries()) { if (key.startsWith(metric.name)) { const labelMatch = key.match(/\{(.+)\}/); const labels = labelMatch ? `{${labelMatch[1]}}` : ''; lines.push(`${metric.name}${labels} ${value}`); } } } // Export histograms (simplified - just count, sum, and buckets) const histogramMetrics = Array.from(this.metrics.values()) .filter(m => m.type === 'histogram'); for (const metric of histogramMetrics) { lines.push(`# HELP ${metric.name} ${metric.help}`); lines.push(`# TYPE ${metric.name} histogram`); for (const [key, observations] of this.histograms.entries()) { if (key.startsWith(metric.name)) { const labelMatch = key.match(/\{(.+)\}/); const labelBase = labelMatch ? labelMatch[1] : ''; const count = observations.length; const sum = observations.reduce((a, b) => a + b, 0); // Buckets: 0.01, 0.05, 0.1, 0.5, 1, 2.5, 5, 10 const buckets = [0.01, 0.05, 0.1, 0.5, 1, 2.5, 5, 10]; for (const bucket of buckets) { const bucketCount = observations.filter(v => v <= bucket).length; const bucketLabels = labelBase ? `{${labelBase},le="${bucket}"}` : `{le="${bucket}"}`; lines.push(`${metric.name}_bucket${bucketLabels} ${bucketCount}`); } // +Inf bucket const infLabels = labelBase ? `{${labelBase},le="+Inf"}` : `{le="+Inf"}`; lines.push(`${metric.name}_bucket${infLabels} ${count}`); // Count and sum const countLabels = labelBase ? `{${labelBase}}` : ''; lines.push(`${metric.name}_count${countLabels} ${count}`); lines.push(`${metric.name}_sum${countLabels} ${sum}`); } } } return lines.join('\n') + '\n'; } /** * Reset all metrics */ reset(): void { this.counters.clear(); this.gauges.clear(); this.histograms.clear(); } } // Global metrics instance export const metrics = new MetricsCollector(); // Standard metrics export function recordRequest(method: string, status: number, duration: number): void { metrics.incrementCounter('mcp_requests_total', { method, status: status.toString() }); metrics.observeHistogram('mcp_request_duration_seconds', duration / 1000, { method }); } export function recordToolCall(toolName: string, success: boolean, duration: number): void { metrics.incrementCounter('mcp_tool_calls_total', { tool: toolName, success: success.toString() }); metrics.observeHistogram('mcp_tool_duration_seconds', duration / 1000, { tool: toolName }); } export function setActiveSessions(count: number): void { metrics.setGauge('mcp_active_sessions', count); } export function recordError(errorType: string): void { metrics.incrementCounter('mcp_errors_total', { type: errorType }); }

Latest Blog Posts

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/flight505/MCP_DinCoder'

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