UNPKG

mcp-server-logzio

Version:

Model Context Protocol server for Logz.io log management platform

307 lines 10.7 kB
import axios from 'axios'; import { getLogger } from '../utils/logger.js'; import { ApiError, AuthenticationError, RateLimitError, isRetryableError, getRetryDelay, } from '../utils/errors.js'; /** * Logz.io API client with retry logic and error handling */ export class LogzioApiClient { axios; config; logger = getLogger('LogzioApiClient'); constructor(config) { this.config = config; this.axios = this.createAxiosInstance(); } /** * Create configured axios instance */ createAxiosInstance() { const instance = axios.create({ baseURL: this.config.logzioUrl || 'https://api.logz.io', timeout: this.config.timeout, headers: { 'Content-Type': 'application/json', 'Accept': 'application/json', 'User-Agent': 'mcp-server-logzio/0.1.0', 'X-API-TOKEN': this.config.apiKey, }, }); // Request interceptor for logging instance.interceptors.request.use((config) => { this.logger.debug('Making API request', { method: config.method, url: config.url, }); return config; }, (error) => { this.logger.error('Request interceptor error', error); return Promise.reject(error); }); // Response interceptor for logging and error handling instance.interceptors.response.use((response) => { this.logger.debug('Received API response', { status: response.status, url: response.config.url, }); return response; }, (error) => { return this.handleResponseError(error); }); return instance; } /** * Handle response errors and convert to custom error types */ handleResponseError(error) { if (axios.isAxiosError(error) && error.response) { const { status, data } = error.response; switch (status) { case 401: throw new AuthenticationError('Authentication failed. This could be due to:\n' + '• Invalid or expired API key\n' + '• Wrong region - we default to US region (api.logz.io)\n' + '• If you\'re not in the US region, specify your region:\n' + ' - EU: add "region eu" to your command\n' + ' - CA: add "region ca" to your command\n' + ' - AU: add "region au" to your command\n' + ' - UK: add "region uk" to your command\n' + ' - US-West: add "region us-west" to your command\n' + '• Check your Logz.io account URL to determine your region:\n' + ' - app.logz.io → use "region us"\n' + ' - app-eu.logz.io → use "region eu"\n' + ' - app-ca.logz.io → use "region ca"'); case 429: const retryAfter = this.parseRetryAfter(error.response.headers); throw new RateLimitError('Rate limit exceeded', retryAfter, { status, data }); default: throw ApiError.fromResponse({ status, data }); } } throw new ApiError(error instanceof Error ? error.message : 'Unknown API error'); } /** * Parse retry-after header */ parseRetryAfter(headers) { const retryAfter = headers['retry-after'] || headers['Retry-After']; if (typeof retryAfter === 'string') { const seconds = parseInt(retryAfter, 10); return isNaN(seconds) ? undefined : seconds * 1000; } return undefined; } /** * Make HTTP request with retry logic */ async makeRequest(config, attempt = 1) { try { const response = await this.axios(config); return response.data; } catch (error) { if (attempt <= this.config.retryAttempts && isRetryableError(error)) { const delay = getRetryDelay(error) || this.calculateBackoffDelay(attempt); this.logger.warn(`Request failed, retrying in ${delay}ms`, { attempt, maxAttempts: this.config.retryAttempts, error: error instanceof Error ? error.message : 'Unknown error', }); await this.sleep(delay); return this.makeRequest(config, attempt + 1); } throw error; } } /** * Calculate exponential backoff delay */ calculateBackoffDelay(attempt) { return Math.min(this.config.retryDelay * Math.pow(2, attempt - 1), 30000 // Max 30 seconds ); } /** * Sleep for specified milliseconds */ sleep(ms) { return new Promise((resolve) => setTimeout(resolve, ms)); } /** * Search logs using simple query */ async searchLogs(params) { // Build Elasticsearch query body for Logz.io API const queryBody = { query: { query_string: { query: params.query } }, size: params.size || 50, }; // Add time range filter if specified if (params.from || params.to) { queryBody.query = { bool: { must: [queryBody.query], filter: { range: { '@timestamp': { ...(params.from && { gte: params.from }), ...(params.to && { lte: params.to }) } } } } }; } // Add sorting if (params.sort) { if (params.sort.includes('desc')) { queryBody.sort = [{ '@timestamp': { order: 'desc' } }]; } else { queryBody.sort = [{ '@timestamp': { order: 'asc' } }]; } } else { queryBody.sort = [{ '@timestamp': { order: 'desc' } }]; } return this.makeRequest({ method: 'POST', url: '/v1/search', data: queryBody, }); } /** * Execute Lucene query */ async queryLogs(payload) { return this.makeRequest({ method: 'POST', url: '/v1/search', data: payload, }); } /** * Get log statistics using search aggregations */ async getLogStats(params) { // Build aggregation query to get statistics const queryBody = { query: { match_all: {} }, size: 0, // We only want aggregations, not individual logs aggs: { time_histogram: { date_histogram: { field: '@timestamp', interval: this.getTimeInterval(params.from, params.to), order: { _key: 'desc' } } } } }; // Add time range filter if specified if (params.from || params.to) { queryBody.query = { bool: { filter: { range: { '@timestamp': { ...(params.from && { gte: params.from }), ...(params.to && { lte: params.to }) } } } } }; } // Add groupBy aggregations if specified if (params.groupBy && params.groupBy.length > 0) { params.groupBy.forEach(field => { queryBody.aggs[`by_${field}`] = { terms: { field: `${field}.keyword`, size: 20, order: { _count: 'desc' } } }; }); } // Add common useful aggregations queryBody.aggs.by_level = { terms: { field: 'level.keyword', size: 10, order: { _count: 'desc' } } }; const response = await this.makeRequest({ method: 'POST', url: '/v1/search', data: queryBody, }); // Transform the response to match LogStatsResponse format const total = typeof response.hits?.total === 'number' ? response.hits.total : response.hits?.total?.value || 0; return { total, timeRange: { from: params.from, to: params.to, }, took: response.took || 0, aggregations: response.aggregations || {}, buckets: response.aggregations?.time_histogram?.buckets?.map((bucket) => ({ key: bucket.key_as_string || bucket.key, count: bucket.doc_count, timestamp: bucket.key_as_string, })) || [], }; } /** * Calculate appropriate time interval for histograms */ getTimeInterval(from, to) { if (!from || !to) return '1h'; const start = new Date(from); const end = new Date(to); const diffHours = (end.getTime() - start.getTime()) / (1000 * 60 * 60); if (diffHours <= 6) return '30m'; if (diffHours <= 24) return '1h'; if (diffHours <= 72) return '3h'; if (diffHours <= 168) return '6h'; return '1d'; } /** * Test API connectivity */ async healthCheck() { // Use search endpoint as health check since no dedicated health endpoint exists try { await this.makeRequest({ method: 'POST', url: '/v1/search', data: { query: { match_all: {} }, size: 0 // Don't return any results, just test connectivity }, }); return { status: 'ok', timestamp: new Date().toISOString() }; } catch (error) { throw new Error(`Health check failed: ${error instanceof Error ? error.message : 'Unknown error'}`); } } } //# sourceMappingURL=client.js.map