Skip to main content
Glama
optimized-http-client.ts13.7 kB
import axios, { AxiosInstance, AxiosRequestConfig, AxiosError } from 'axios'; import https from 'https'; import http from 'http'; import fs from 'fs'; import { Config, TLSConfig } from './types.js'; import { ResilientErrorHandler } from './retry-client.js'; import { performanceMonitor } from './performance-monitor.js'; /** * Memory-optimized cache with LRU eviction and size limits */ class OptimizedCache<T> { private cache = new Map<string, { data: T; expires: number; lastAccessed: number }>(); private readonly maxSize: number; private readonly maxMemoryMB: number; constructor(maxSize = 1000, maxMemoryMB = 10) { this.maxSize = maxSize; this.maxMemoryMB = maxMemoryMB * 1024 * 1024; // Convert to bytes } get(key: string): T | null { const entry = this.cache.get(key); if (!entry) return null; if (entry.expires < Date.now()) { this.cache.delete(key); return null; } entry.lastAccessed = Date.now(); return entry.data; } set(key: string, data: T, ttl: number): void { const expires = Date.now() + ttl; const lastAccessed = Date.now(); this.cache.set(key, { data, expires, lastAccessed }); this.evictIfNeeded(); } private evictIfNeeded(): void { // Evict expired entries first const now = Date.now(); for (const [key, entry] of this.cache.entries()) { if (entry.expires < now) { this.cache.delete(key); } } // If still over size limit, evict LRU entries while (this.cache.size > this.maxSize) { let oldestKey = ''; let oldestTime = Infinity; for (const [key, entry] of this.cache.entries()) { if (entry.lastAccessed < oldestTime) { oldestTime = entry.lastAccessed; oldestKey = key; } } if (oldestKey) { this.cache.delete(oldestKey); } else { break; } } // Estimate memory usage and evict if needed const estimatedMemory = this.estimateMemoryUsage(); if (estimatedMemory > this.maxMemoryMB) { this.evictLargestEntries(); } } private estimateMemoryUsage(): number { // Rough estimation - in production, use more sophisticated memory profiling return this.cache.size * 2048; // Assume 2KB per entry average } private evictLargestEntries(): void { // Simple heuristic: remove 25% of entries const targetSize = Math.floor(this.cache.size * 0.75); const entries = Array.from(this.cache.entries()).sort((a, b) => a[1].lastAccessed - b[1].lastAccessed, ); for (let i = 0; i < this.cache.size - targetSize; i++) { this.cache.delete(entries[i][0]); } } clear(): void { this.cache.clear(); } getStats(): { size: number; keys: string[]; hitRate?: number } { return { size: this.cache.size, keys: Array.from(this.cache.keys()), }; } } /** * Optimized HTTP client with advanced performance features */ export class OptimizedGrafanaHttpClient { private client: AxiosInstance; private config: Config; private static tlsFileCache = new Map<string, Buffer>(); private responseCache = new OptimizedCache<any>(1000, 50); // 1000 entries, 50MB max private resilientHandler: ResilientErrorHandler; private requestPool = new Set<string>(); // Track in-flight requests for deduplication // Performance optimizations private readonly CACHE_TTL = 60000; // 1 minute private readonly MAX_CONCURRENT_REQUESTS = 50; private activeRequests = 0; private requestQueue: Array<() => Promise<any>> = []; constructor(config: Config) { this.config = config; // Initialize resilient error handler with optimized settings this.resilientHandler = new ResilientErrorHandler( { maxRetries: 3, baseDelayMs: 500, // Reduced from 1000ms maxDelayMs: 5000, // Reduced from 10000ms exponentialBase: 1.5, // Gentler backoff retryableStatuses: [408, 429, 500, 502, 503, 504], }, { failureThreshold: 3, // More sensitive circuit breaker timeoutMs: 30000, // 30 second timeout }, ); this.client = this.createOptimizedAxiosInstance(); this.setupPerformanceMonitoring(); } private createOptimizedAxiosInstance(): AxiosInstance { const axiosConfig: AxiosRequestConfig = { baseURL: this.config.GRAFANA_URL, timeout: Math.min(this.config.GRAFANA_TIMEOUT, 10000), // Cap timeout at 10s headers: { Authorization: `Bearer ${this.config.GRAFANA_TOKEN}`, 'Content-Type': 'application/json', Accept: 'application/json', 'Accept-Encoding': 'gzip, deflate', // Enable compression 'Connection': 'keep-alive', }, // Optimized agents with connection pooling httpAgent: new http.Agent({ keepAlive: true, maxSockets: 10, maxFreeSockets: 5, timeout: 5000, keepAliveMsecs: 30000, }), httpsAgent: new https.Agent({ keepAlive: true, maxSockets: 10, maxFreeSockets: 5, timeout: 5000, keepAliveMsecs: 30000, rejectUnauthorized: true, }), // Response compression decompress: true, }; // Configure TLS if needed if (this.config.GRAFANA_TLS_CERT_FILE || this.config.GRAFANA_TLS_KEY_FILE) { const tlsConfig = this.createTLSConfig(); if (tlsConfig) { axiosConfig.httpsAgent = this.createOptimizedHttpsAgent(tlsConfig); } } const client = axios.create(axiosConfig); // Add optimized interceptors this.addOptimizedInterceptors(client); return client; } private createTLSConfig(): TLSConfig | undefined { if ( !this.config.GRAFANA_TLS_CERT_FILE && !this.config.GRAFANA_TLS_KEY_FILE && !this.config.GRAFANA_TLS_CA_FILE && !this.config.GRAFANA_TLS_SKIP_VERIFY ) { return undefined; } return { certFile: this.config.GRAFANA_TLS_CERT_FILE, keyFile: this.config.GRAFANA_TLS_KEY_FILE, caFile: this.config.GRAFANA_TLS_CA_FILE, skipVerify: this.config.GRAFANA_TLS_SKIP_VERIFY, }; } private createOptimizedHttpsAgent(tlsConfig: TLSConfig): https.Agent { const agentOptions: https.AgentOptions = { rejectUnauthorized: !tlsConfig.skipVerify, keepAlive: true, maxSockets: 10, maxFreeSockets: 5, timeout: 5000, keepAliveMsecs: 30000, }; try { // Use cached TLS files if (tlsConfig.certFile && tlsConfig.keyFile) { agentOptions.cert = this.loadTLSFile(tlsConfig.certFile); agentOptions.key = this.loadTLSFile(tlsConfig.keyFile); } if (tlsConfig.caFile) { agentOptions.ca = this.loadTLSFile(tlsConfig.caFile); } } catch (error) { throw new Error( `Failed to load TLS configuration: ${error instanceof Error ? error.message : 'Unknown error'}`, ); } return new https.Agent(agentOptions); } private loadTLSFile(filepath: string): Buffer { if (!OptimizedGrafanaHttpClient.tlsFileCache.has(filepath)) { if (!fs.existsSync(filepath)) { throw new Error(`TLS file not found: ${filepath}`); } OptimizedGrafanaHttpClient.tlsFileCache.set(filepath, fs.readFileSync(filepath)); } return OptimizedGrafanaHttpClient.tlsFileCache.get(filepath)!; } private addOptimizedInterceptors(client: AxiosInstance): void { // Request interceptor with performance monitoring client.interceptors.request.use( (config) => { performanceMonitor.startTimer(`http-${config.method}-${config.url}`); // Track concurrent requests this.activeRequests++; return config; }, (error) => Promise.reject(error), ); // Response interceptor with metrics and caching client.interceptors.response.use( (response) => { const timerName = `http-${response.config.method}-${response.config.url}`; const duration = performanceMonitor.endTimer(timerName); // Record request metrics performanceMonitor.recordRequest({ method: response.config.method?.toUpperCase() || 'GET', url: response.config.url || '', duration, status: response.status, cacheHit: false, timestamp: Date.now(), }); this.activeRequests--; this.processRequestQueue(); return response; }, (error: AxiosError) => { const timerName = `http-${error.config?.method}-${error.config?.url}`; const duration = performanceMonitor.endTimer(timerName); // Record error metrics performanceMonitor.recordRequest({ method: error.config?.method?.toUpperCase() || 'GET', url: error.config?.url || '', duration, status: error.response?.status || 0, cacheHit: false, timestamp: Date.now(), }); this.activeRequests--; this.processRequestQueue(); return Promise.reject(error); }, ); } private setupPerformanceMonitoring(): void { performanceMonitor.on('performance-alert', (alerts: string[]) => { console.warn('[Performance Alert]', alerts); }); } private processRequestQueue(): void { if (this.activeRequests < this.MAX_CONCURRENT_REQUESTS && this.requestQueue.length > 0) { const nextRequest = this.requestQueue.shift(); if (nextRequest) { nextRequest(); } } } private async executeRequest<T>(operation: () => Promise<T>): Promise<T> { if (this.activeRequests >= this.MAX_CONCURRENT_REQUESTS) { // Queue the request return new Promise((resolve, reject) => { this.requestQueue.push(async () => { try { const result = await operation(); resolve(result); } catch (error) { reject(error); } }); }); } return operation(); } /** * Optimized GET request with deduplication and caching */ async get<T = any>(url: string, params?: Record<string, any>, useCache = true): Promise<T> { const cacheKey = this.generateCacheKey(url, params); // Check cache first if (useCache) { const cached = this.responseCache.get(cacheKey); if (cached) { performanceMonitor.recordRequest({ method: 'GET', url, duration: 0, status: 200, cacheHit: true, timestamp: Date.now(), }); return cached; } } // Deduplicate in-flight requests if (this.requestPool.has(cacheKey)) { // Wait for ongoing request to complete return new Promise((resolve, reject) => { const checkInterval = setInterval(() => { if (!this.requestPool.has(cacheKey)) { clearInterval(checkInterval); // Try cache again const cached = this.responseCache.get(cacheKey); if (cached) { resolve(cached); } else { // If not in cache, make new request this.get(url, params, useCache).then(resolve).catch(reject); } } }, 10); // Timeout after 30 seconds setTimeout(() => { clearInterval(checkInterval); reject(new Error('Request deduplication timeout')); }, 30000); }); } this.requestPool.add(cacheKey); try { const result = await this.executeRequest(async () => { return this.resilientHandler.executeWithResilience( async () => { const response = await this.client.get<T>(url, { params }); return response.data; }, `GET ${url}`, ); }); if (useCache) { this.responseCache.set(cacheKey, result, this.CACHE_TTL); } return result; } finally { this.requestPool.delete(cacheKey); } } /** * Optimized POST request */ async post<T = any>(url: string, data?: any, config?: AxiosRequestConfig): Promise<T> { return this.executeRequest(async () => { return this.resilientHandler.executeWithResilience( async () => { const response = await this.client.post<T>(url, data, config); return response.data; }, `POST ${url}`, ); }); } private generateCacheKey(url: string, params?: Record<string, any>): string { const paramString = params ? JSON.stringify(params) : ''; return `${url}:${paramString}`; } /** * Get comprehensive performance metrics */ getPerformanceMetrics(): { cache: ReturnType<OptimizedCache<any>['getStats']>; activeRequests: number; queueLength: number; performance: ReturnType<typeof performanceMonitor.getCurrentMetrics>; } { return { cache: this.responseCache.getStats(), activeRequests: this.activeRequests, queueLength: this.requestQueue.length, performance: performanceMonitor.getCurrentMetrics(), }; } /** * Optimize cache and connections */ optimize(): void { // Clear expired cache entries this.responseCache.clear(); // Clear TLS cache if not used recently if (OptimizedGrafanaHttpClient.tlsFileCache.size > 10) { OptimizedGrafanaHttpClient.tlsFileCache.clear(); } // Force garbage collection if available if (global.gc) { global.gc(); } } /** * Cleanup resources */ cleanup(): void { this.responseCache.clear(); OptimizedGrafanaHttpClient.tlsFileCache.clear(); this.requestQueue.length = 0; this.requestPool.clear(); performanceMonitor.cleanup(); } }

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/quanticsoul4772/grafana-mcp'

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