UNPKG

auto-publishing-mcp-server

Version:

Enterprise-grade MCP Server for Auto-Publishing with pre-publish validation, multi-cloud deployment, and monitoring

440 lines (387 loc) 11 kB
/** * Loki Log Monitoring Tool * Query and analyze logs through Loki API */ import https from 'https'; import { URL } from 'url'; export class LokiMonitorTool { constructor(config = {}) { this.lokiUrl = config.lokiUrl || process.env.LOKI_URL || 'http://192.168.101.1:3100'; this.defaultLimit = config.defaultLimit || 100; this.timeout = config.timeout || 30000; } /** * Execute Loki monitoring queries */ async execute(args, context) { const { action, ...params } = args; switch (action) { case 'query': return await this.queryLogs(params); case 'query_range': return await this.queryLogsRange(params); case 'labels': return await this.getLabels(params); case 'label_values': return await this.getLabelValues(params); case 'tail': return await this.tailLogs(params); case 'series': return await this.getSeries(params); default: throw new Error(`Unknown Loki action: ${action}`); } } /** * Query logs for a specific time point */ async queryLogs(params) { const { query = '{job=~".+"}', time, direction = 'backward', limit = this.defaultLimit } = params; const url = new URL(`${this.lokiUrl}/loki/api/v1/query`); url.searchParams.set('query', query); url.searchParams.set('direction', direction); url.searchParams.set('limit', limit.toString()); if (time) { url.searchParams.set('time', time); } try { const response = await this.makeRequest(url); const logs = this.formatLogResponse(response.data); return { output: `Found ${logs.length} log entries`, data: { query, logs, resultType: response.resultType, stats: response.stats || {} } }; } catch (error) { throw new Error(`Loki query failed: ${error.message}`); } } /** * Query logs over a time range */ async queryLogsRange(params) { const { query = '{job=~".+"}', start, end, direction = 'backward', limit = this.defaultLimit, step = '1m' } = params; const url = new URL(`${this.lokiUrl}/loki/api/v1/query_range`); url.searchParams.set('query', query); url.searchParams.set('direction', direction); url.searchParams.set('limit', limit.toString()); url.searchParams.set('step', step); if (start) url.searchParams.set('start', start); if (end) url.searchParams.set('end', end); try { const response = await this.makeRequest(url); const logs = this.formatLogResponse(response.data); return { output: `Found ${logs.length} log entries in range`, data: { query, logs, resultType: response.resultType, stats: response.stats || {}, timeRange: { start, end } } }; } catch (error) { throw new Error(`Loki range query failed: ${error.message}`); } } /** * Get available labels */ async getLabels(params) { const { start, end } = params; const url = new URL(`${this.lokiUrl}/loki/api/v1/labels`); if (start) url.searchParams.set('start', start); if (end) url.searchParams.set('end', end); try { const response = await this.makeRequest(url); return { output: `Found ${response.data.length} labels`, data: { labels: response.data, timeRange: { start, end } } }; } catch (error) { throw new Error(`Failed to get labels: ${error.message}`); } } /** * Get values for a specific label */ async getLabelValues(params) { const { label, start, end } = params; if (!label) { throw new Error('Label parameter is required'); } const url = new URL(`${this.lokiUrl}/loki/api/v1/label/${label}/values`); if (start) url.searchParams.set('start', start); if (end) url.searchParams.set('end', end); try { const response = await this.makeRequest(url); return { output: `Found ${response.data.length} values for label '${label}'`, data: { label, values: response.data, timeRange: { start, end } } }; } catch (error) { throw new Error(`Failed to get label values: ${error.message}`); } } /** * Tail logs (simulate real-time streaming) */ async tailLogs(params) { const { query = '{job=~".+"}', delayFor = 0, limit = this.defaultLimit, start } = params; const url = new URL(`${this.lokiUrl}/loki/api/v1/tail`); url.searchParams.set('query', query); url.searchParams.set('delay_for', delayFor.toString()); url.searchParams.set('limit', limit.toString()); if (start) url.searchParams.set('start', start); try { // Note: This is a simplified version. Real tailing would use WebSocket const response = await this.makeRequest(url); const logs = this.formatLogResponse(response.streams || []); return { output: `Tailing logs with query: ${query}`, data: { query, logs, isStreaming: true } }; } catch (error) { throw new Error(`Failed to tail logs: ${error.message}`); } } /** * Get series information */ async getSeries(params) { const { match, start, end } = params; const url = new URL(`${this.lokiUrl}/loki/api/v1/series`); if (match) { if (Array.isArray(match)) { match.forEach(m => url.searchParams.append('match[]', m)); } else { url.searchParams.set('match[]', match); } } if (start) url.searchParams.set('start', start); if (end) url.searchParams.set('end', end); try { const response = await this.makeRequest(url); return { output: `Found ${response.data.length} series`, data: { series: response.data, timeRange: { start, end } } }; } catch (error) { throw new Error(`Failed to get series: ${error.message}`); } } /** * Make HTTP request to Loki API */ async makeRequest(url) { return new Promise((resolve, reject) => { const options = { hostname: url.hostname, port: url.port, path: url.pathname + url.search, method: 'GET', headers: { 'Content-Type': 'application/json', 'Accept': 'application/json' }, timeout: this.timeout }; const req = https.request(options, (res) => { let data = ''; res.on('data', (chunk) => { data += chunk; }); res.on('end', () => { try { if (res.statusCode >= 200 && res.statusCode < 300) { const response = JSON.parse(data); resolve(response); } else { reject(new Error(`HTTP ${res.statusCode}: ${data}`)); } } catch (error) { reject(new Error(`Failed to parse response: ${error.message}`)); } }); }); req.on('error', (error) => { reject(error); }); req.on('timeout', () => { req.destroy(); reject(new Error('Request timeout')); }); req.end(); }); } /** * Format log response for consistent output */ formatLogResponse(data) { if (!data) return []; if (Array.isArray(data)) { // Handle streams format return data.flatMap(stream => stream.values?.map(([timestamp, line]) => ({ timestamp: new Date(parseInt(timestamp) / 1000000).toISOString(), line: line, labels: stream.stream || {} })) || [] ); } return []; } /** * Get input schema for MCP tool registration */ getInputSchema() { return { type: "object", properties: { action: { type: "string", enum: ["query", "query_range", "labels", "label_values", "tail", "series"], description: "Loki API action to perform" }, query: { type: "string", description: "LogQL query string (e.g., '{job=\"webapp\"}')", default: '{job=~".+"}' }, time: { type: "string", description: "Query time point (RFC3339 or Unix timestamp)" }, start: { type: "string", description: "Start time for range queries (RFC3339 or Unix timestamp)" }, end: { type: "string", description: "End time for range queries (RFC3339 or Unix timestamp)" }, direction: { type: "string", enum: ["forward", "backward"], description: "Query direction", default: "backward" }, limit: { type: "number", description: "Maximum number of entries to return", default: 100 }, step: { type: "string", description: "Query resolution step (e.g., '1m', '5s')", default: "1m" }, label: { type: "string", description: "Label name for label_values action" }, match: { type: ["string", "array"], description: "Series selector for series action" }, delayFor: { type: "number", description: "Delay for tail operation (seconds)", default: 0 } }, required: ["action"] }; } /** * Helper methods for common queries */ async getErrorLogs(params = {}) { const { timeRange = '1h', jobs = [], limit = 50 } = params; let query = '{job=~".+"}'; if (jobs.length > 0) { query = `{job=~"${jobs.join('|')}"}`; } query += ' |~ "(?i)error|exception|fail|panic"'; return await this.queryLogsRange({ query, start: `${Date.now() - (this.parseTimeRange(timeRange) * 1000)}000000`, end: `${Date.now()}000000`, limit }); } async getDeploymentLogs(params = {}) { const { deploymentId, timeRange = '1h', limit = 100 } = params; let query = '{job="deployments"}'; if (deploymentId) { query += ` |= "${deploymentId}"`; } return await this.queryLogsRange({ query, start: `${Date.now() - (this.parseTimeRange(timeRange) * 1000)}000000`, end: `${Date.now()}000000`, limit }); } /** * Parse time range string to seconds */ parseTimeRange(timeRange) { const match = timeRange.match(/^(\d+)([smhd])$/); if (!match) return 3600; // Default 1 hour const value = parseInt(match[1]); const unit = match[2]; switch (unit) { case 's': return value; case 'm': return value * 60; case 'h': return value * 3600; case 'd': return value * 86400; default: return 3600; } } } export default LokiMonitorTool;