Skip to main content
Glama
http-client.ts11.9 kB
import axios, { AxiosInstance, AxiosRequestConfig, AxiosError } from 'axios'; import https from 'https'; import http from 'http'; import fs from 'fs'; import { Config, HttpClientConfig, TLSConfig, GrafanaError } from './types.js'; import { sanitizeHeaders, categorizeError, formatUserError, formatInternalError, safeStringify, } from './security-utils.js'; import { ResilientErrorHandler } from './retry-client.js'; /** * HTTP client for Grafana API with authentication, TLS support, and error handling */ export class GrafanaHttpClient { private client: AxiosInstance; private config: Config; private static tlsFileCache = new Map<string, Buffer>(); private responseCache = new Map<string, { data: any; expires: number }>(); private readonly CACHE_TTL = 60000; // 1 minute cache TTL private resilientHandler: ResilientErrorHandler; constructor(config: Config) { this.config = config; // Initialize resilient error handler this.resilientHandler = new ResilientErrorHandler( { maxRetries: 3, baseDelayMs: 1000, maxDelayMs: 10000, exponentialBase: 2, retryableStatuses: [408, 429, 500, 502, 503, 504], }, { failureThreshold: 5, timeoutMs: 60000, // 1 minute circuit breaker timeout }, ); const httpConfig: HttpClientConfig = { baseURL: config.GRAFANA_URL, timeout: config.GRAFANA_TIMEOUT, headers: { Authorization: `Bearer ${config.GRAFANA_TOKEN}`, 'Content-Type': 'application/json', Accept: 'application/json', }, debug: config.GRAFANA_DEBUG, tlsConfig: this.createTLSConfig(config), }; this.client = this.createAxiosInstance(httpConfig); } /** * Create TLS configuration from environment variables */ private createTLSConfig(config: Config): TLSConfig | undefined { if ( !config.GRAFANA_TLS_CERT_FILE && !config.GRAFANA_TLS_KEY_FILE && !config.GRAFANA_TLS_CA_FILE && !config.GRAFANA_TLS_SKIP_VERIFY ) { return undefined; } return { certFile: config.GRAFANA_TLS_CERT_FILE, keyFile: config.GRAFANA_TLS_KEY_FILE, caFile: config.GRAFANA_TLS_CA_FILE, skipVerify: config.GRAFANA_TLS_SKIP_VERIFY, }; } /** * Create Axios instance with TLS and debugging configuration */ private createAxiosInstance(config: HttpClientConfig): AxiosInstance { const axiosConfig: AxiosRequestConfig = { baseURL: config.baseURL, timeout: config.timeout, headers: config.headers, // Add connection pooling for better performance httpAgent: new http.Agent({ keepAlive: true, maxSockets: 10, maxFreeSockets: 5, timeout: config.timeout, }), }; // Configure TLS if needed if (config.tlsConfig) { const httpsAgent = this.createHttpsAgent(config.tlsConfig); axiosConfig.httpsAgent = httpsAgent; } const client = axios.create(axiosConfig); // Add debug logging if enabled if (config.debug) { this.addDebugInterceptors(client); } // Add error handling this.addErrorInterceptors(client); return client; } /** * Load TLS file with caching to avoid repeated file reads */ private loadTLSFile(filepath: string): Buffer { if (!GrafanaHttpClient.tlsFileCache.has(filepath)) { if (!fs.existsSync(filepath)) { throw new Error(`TLS file not found: ${filepath}`); } GrafanaHttpClient.tlsFileCache.set(filepath, fs.readFileSync(filepath)); } return GrafanaHttpClient.tlsFileCache.get(filepath)!; } /** * Create HTTPS agent with custom TLS configuration */ private createHttpsAgent(tlsConfig: TLSConfig): https.Agent { const agentOptions: https.AgentOptions = { rejectUnauthorized: !tlsConfig.skipVerify, keepAlive: true, maxSockets: 10, maxFreeSockets: 5, }; try { // Load client certificate if provided (with caching) if (tlsConfig.certFile && tlsConfig.keyFile) { agentOptions.cert = this.loadTLSFile(tlsConfig.certFile); agentOptions.key = this.loadTLSFile(tlsConfig.keyFile); } // Load CA certificate if provided (with caching) 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); } /** * Add debug logging interceptors */ private addDebugInterceptors(client: AxiosInstance): void { // Request interceptor client.interceptors.request.use( (config) => { console.log( `[Grafana HTTP] ${config.method?.toUpperCase()} ${config.url}`, ); if (config.data) { console.log( '[Grafana HTTP] Request body:', safeStringify(config.data), ); } if (config.params) { console.log( '[Grafana HTTP] Query params:', safeStringify(config.params), ); } if (config.headers) { console.log( '[Grafana HTTP] Headers:', safeStringify( sanitizeHeaders(config.headers as Record<string, any>), ), ); } return config; }, (error) => { console.error('[Grafana HTTP] Request error:', error); return Promise.reject(error); }, ); // Response interceptor client.interceptors.response.use( (response) => { console.log(`[Grafana HTTP] ${response.status} ${response.statusText}`); if (this.config.GRAFANA_DEBUG && response.data) { // Only log response data if it's not too large and sanitize it const responseStr = safeStringify(response.data); if (responseStr.length < 5000) { console.log('[Grafana HTTP] Response:', responseStr); } else { console.log( '[Grafana HTTP] Response: [Large response body - truncated for security]', ); } } return response; }, (error) => { if (axios.isAxiosError(error)) { console.error( `[Grafana HTTP] ${error.response?.status} ${error.response?.statusText}`, ); if (error.response?.data) { console.error( '[Grafana HTTP] Error response:', safeStringify(error.response.data), ); } } return Promise.reject(error); }, ); } /** * Add error handling interceptors */ private addErrorInterceptors(client: AxiosInstance): void { client.interceptors.response.use( (response) => response, (error: AxiosError) => { // Categorize the error for safe handling const categorizedError = categorizeError(error, 'HTTP Client'); // Log the internal error details for debugging console.error( '[Grafana HTTP] Error:', formatInternalError(categorizedError), ); // Create a Grafana error with safe user message const grafanaError: GrafanaError = { message: formatUserError(categorizedError), status: error.response?.status, }; // Only include safe error details grafanaError.error = categorizedError.publicMessage; return Promise.reject(grafanaError); }, ); } /** * Get cached response if available and not expired */ private getCachedResponse<T>(cacheKey: string): T | null { const cached = this.responseCache.get(cacheKey); if (cached && cached.expires > Date.now()) { return cached.data as T; } if (cached) { this.responseCache.delete(cacheKey); } return null; } /** * Cache response with TTL */ private setCachedResponse(cacheKey: string, data: any): void { this.responseCache.set(cacheKey, { data, expires: Date.now() + this.CACHE_TTL, }); } /** * Generate cache key from URL and parameters */ private generateCacheKey(url: string, params?: Record<string, any>): string { const paramString = params ? JSON.stringify(params) : ''; return `${url}:${paramString}`; } /** * Make a GET request with optional caching and resilience */ async get<T = any>(url: string, params?: Record<string, any>, useCache = false): Promise<T> { if (useCache) { const cacheKey = this.generateCacheKey(url, params); const cached = this.getCachedResponse<T>(cacheKey); if (cached) { return cached; } const result = await this.resilientHandler.executeWithResilience( async () => { const response = await this.client.get<T>(url, { params }); return response.data; }, `GET ${url}`, ); this.setCachedResponse(cacheKey, result); return result; } return this.resilientHandler.executeWithResilience( async () => { const response = await this.client.get<T>(url, { params }); return response.data; }, `GET ${url}`, ); } /** * Make a POST request */ async post<T = any>( url: string, data?: any, config?: AxiosRequestConfig, ): Promise<T> { const response = await this.client.post<T>(url, data, config); return response.data; } /** * Make a PUT request */ async put<T = any>( url: string, data?: any, config?: AxiosRequestConfig, ): Promise<T> { const response = await this.client.put<T>(url, data, config); return response.data; } /** * Make a DELETE request */ async delete<T = any>(url: string, config?: AxiosRequestConfig): Promise<T> { const response = await this.client.delete<T>(url, config); return response.data; } /** * Make a PATCH request */ async patch<T = any>( url: string, data?: any, config?: AxiosRequestConfig, ): Promise<T> { const response = await this.client.patch<T>(url, data, config); return response.data; } /** * Clear response cache */ clearCache(): void { this.responseCache.clear(); } /** * Clean up resources and connections */ cleanup(): void { this.clearCache(); // Clear static TLS file cache when appropriate GrafanaHttpClient.tlsFileCache.clear(); } /** * Get cache statistics */ getCacheStats(): { size: number; keys: string[] } { return { size: this.responseCache.size, keys: Array.from(this.responseCache.keys()), }; } /** * Get circuit breaker status */ getCircuitBreakerStatus() { return this.resilientHandler.getCircuitBreakerState(); } /** * Reset circuit breaker manually */ resetCircuitBreaker(): void { this.resilientHandler.resetCircuitBreaker(); } /** * Get comprehensive health status */ getHealthStatus(): { cache: { size: number; keys: string[] }; circuitBreaker: ReturnType<ResilientErrorHandler['getCircuitBreakerState']>; config: { baseURL: string; timeout: number; debug: boolean }; } { return { cache: this.getCacheStats(), circuitBreaker: this.getCircuitBreakerStatus(), config: { baseURL: this.config.GRAFANA_URL, timeout: this.config.GRAFANA_TIMEOUT, debug: this.config.GRAFANA_DEBUG, }, }; } /** * Test connection to Grafana API */ async testConnection(): Promise<boolean> { try { await this.get('/api/health'); return true; } catch (_error) { return false; } } /** * Get Grafana instance information */ async getInstanceInfo(): Promise<any> { return this.get('/api/frontend/settings'); } }

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