UNPKG

mcp-server-logzio

Version:

Model Context Protocol server for Logz.io log management platform

307 lines (306 loc) • 14.2 kB
import { z } from 'zod'; import { getLogger } from '../utils/logger.js'; import { ToolError, ValidationError } from '../utils/errors.js'; import { LogSeveritySchema } from '../api/types.js'; import { parseTimeRange } from '../api/endpoints.js'; /** * Search logs tool parameter schema */ export const SearchLogsParamsSchema = z.object({ query: z.string().min(1, 'Query cannot be empty').describe('Search query string. Can be simple text or use Lucene syntax for advanced queries. ' + 'EXAMPLES: ' + '• Simple text: "error database connection" ' + '• App-specific: "myapp" (automatically excludes system logs) ' + '• Error focus: "payment failed" ' + 'COMMON FIELDS: k8s_namespace_name, container_name, level, host, service'), timeRange: z.string().optional().describe('Time range for the search. Options: 1h, 6h, 12h, 24h, 3d, 7d, 30d. ' + 'TIP: Start with 24h for broad searches, use 1h for recent issues'), from: z.string().datetime().optional().describe('Start time for search (ISO 8601 format). Overrides timeRange if provided.'), to: z.string().datetime().optional().describe('End time for search (ISO 8601 format). Overrides timeRange if provided.'), logType: z.string().optional().describe('Filter by log type. COMMON VALUES: application, ingress, system, database'), severity: LogSeveritySchema.optional().describe('Filter by log severity level. Use "error" for critical issues, "warn" for problems'), limit: z.number().min(1).max(1000).default(50).describe('Maximum number of log entries to return (1-1000). Use 20 for quick scans, 100+ for analysis'), sort: z.enum(['asc', 'desc']).default('desc').describe('Sort order by timestamp. Use "desc" for recent-first (recommended), "asc" for chronological'), }); /** * Extract key information from log entry for summary */ function extractLogSummary(log) { const timestamp = log['@timestamp'] || log.timestamp || 'N/A'; const level = log.level || log.severity || 'INFO'; const message = log.message || log.msg || ''; // Smart source detection const source = log.k8s_pod_name || log.container_name || log.host || log.source || log.service || ''; // Extract key metadata (excluding noise) const excludeFields = [ '@timestamp', 'timestamp', 'level', 'severity', 'message', 'msg', 'time', 'log', 'stream', '_id', '_index', '_type', '_score' ]; const key_metadata = {}; const importantFields = [ 'k8s_namespace_name', 'k8s_pod_name', 'container_name', 'env_id', 'status_code', 'method', 'path', 'duration', 'error_type', 'user_id' ]; // Add important fields first importantFields.forEach(field => { if (log[field] !== undefined && log[field] !== null && log[field] !== '') { key_metadata[field] = log[field]; } }); // Add other non-excluded fields (limit to prevent overwhelming output) let otherFieldCount = 0; Object.keys(log).forEach(key => { if (!excludeFields.includes(key) && !importantFields.includes(key) && otherFieldCount < 5 && log[key] !== undefined && log[key] !== null && log[key] !== '') { key_metadata[key] = log[key]; otherFieldCount++; } }); return { timestamp, level, message, source, key_metadata }; } /** * Format log entry for display with improved readability */ function formatLogEntry(log, index) { const summary = extractLogSummary(log); // Format timestamp nicely const timeStr = summary.timestamp !== 'N/A' ? new Date(summary.timestamp).toISOString().replace('T', ' ').replace('Z', ' UTC') : 'N/A'; // Truncate very long messages (increased from 200 to 1000 for better log analysis) const message = summary.message.length > 1000 ? summary.message.substring(0, 1000) + '...' : summary.message; let formatted = `${index + 1}. [${timeStr}] ${(summary.level || 'INFO').toString().toUpperCase()}`; if (summary.source) { formatted += ` (${summary.source})`; } formatted += `\n šŸ“ ${message || 'No message'}`; // Add key metadata if present if (Object.keys(summary.key_metadata).length > 0) { formatted += '\n šŸ·ļø Metadata:'; Object.entries(summary.key_metadata).forEach(([key, value]) => { const displayValue = typeof value === 'string' && value.length > 50 ? value.substring(0, 50) + '...' : value; formatted += `\n • ${key}: ${displayValue}`; }); } return formatted; } /** * Detect if a query should be treated as an exact phrase and format it accordingly */ function smartPhraseDetection(query) { // If already has quotes, field syntax, or boolean operators, leave as-is if (query.includes('"') || query.includes(':') || query.includes(' AND ') || query.includes(' OR ') || query.includes(' NOT ') || query.includes('*') || query.includes('?')) { return query; } // If it's multiple words that look like a phrase, wrap in quotes for exact matching const words = query.trim().split(/\s+/); if (words.length > 1) { // Check if it looks like a phrase (not individual field values) const hasSpecialChars = query.includes('-') || query.includes('_') || query.includes('.'); const isLikelyPhrase = words.length <= 6 && (hasSpecialChars || words.some(word => word.length > 3)); if (isLikelyPhrase) { return `"${query}"`; } } return query; } /** * Generate smart query suggestions based on input */ function generateQuerySuggestions(query, params) { const suggestions = []; // If no time range specified, suggest appropriate ranges if (!params.timeRange && !params.from && !params.to) { suggestions.push('šŸ’” Tip: Add timeRange="1h" for recent issues or "24h" for broader analysis'); } // If no severity filter, suggest focusing on errors for debugging if (!params.severity && query.toLowerCase().includes('error')) { suggestions.push('šŸ’” Tip: Add severity="error" to focus on critical issues'); } // Provide tips about search precision if (query && !query.includes('"') && query.split(/\s+/).length > 1) { suggestions.push('šŸ’” Search precision: Multi-word queries are automatically treated as exact phrases. Use individual words for broader matching.'); } return suggestions; } /** * Search logs tool implementation */ export async function searchLogs(client, params) { const logger = getLogger('search-logs'); try { // Validate parameters const validatedParams = SearchLogsParamsSchema.parse(params); logger.info('Searching logs', { query: validatedParams.query, timeRange: validatedParams.timeRange, limit: validatedParams.limit, }); // Apply smart phrase detection const enhancedQuery = smartPhraseDetection(validatedParams.query); const wasQuoted = enhancedQuery !== validatedParams.query && enhancedQuery.includes('"'); if (wasQuoted) { logger.info('Applied smart phrase detection', { originalQuery: validatedParams.query, enhancedQuery }); } // Determine time range let from = validatedParams.from; let to = validatedParams.to; if (!from || !to) { const timeRange = parseTimeRange(validatedParams.timeRange || '24h'); from = from || timeRange.from; to = to || timeRange.to; } // Build search parameters const searchParams = { query: enhancedQuery, size: validatedParams.limit, sort: validatedParams.sort === 'desc' ? '@timestamp:desc' : '@timestamp:asc', }; if (from) searchParams.from = from; if (to) searchParams.to = to; if (validatedParams.logType) searchParams.type = validatedParams.logType; // Add severity filter to query if specified if (validatedParams.severity) { searchParams.query += ` AND level:${validatedParams.severity}`; } // Record search start time const searchStartTime = Date.now(); // Execute search const response = await client.searchLogs(searchParams); // Calculate actual search time const searchDuration = Date.now() - searchStartTime; if (!response.hits || !response.hits.hits) { const suggestions = generateQuerySuggestions(validatedParams.query, validatedParams); const suggestionText = suggestions.length > 0 ? '\n\n' + suggestions.join('\n') : ''; return { content: [{ type: 'text', text: `No logs found matching the search criteria.${suggestionText}`, }], }; } const logs = response.hits.hits; const total = typeof response.hits.total === 'number' ? response.hits.total : response.hits.total?.value || 0; logger.info('Search completed', { total, returned: logs.length, took: searchDuration, }); // Generate suggestions for query improvement const suggestions = generateQuerySuggestions(validatedParams.query, validatedParams); // Format results with improved structure const formattedLogs = logs.map((hit, index) => formatLogEntry(hit._source, index)); // Create comprehensive summary const summary = `šŸ” **Search Results** šŸ“Š Found ${total.toLocaleString()} total logs (showing top ${logs.length}) ā±ļø Search completed in ${searchDuration}ms šŸ”Ž Query: "${validatedParams.query}"${wasQuoted ? ' [exact phrase]' : ''} šŸ“… Time range: ${from || 'N/A'} to ${to || 'N/A'} ${validatedParams.severity ? `šŸ“ˆ Severity: ${validatedParams.severity}` : ''} ${validatedParams.logType ? `šŸ·ļø Log type: ${validatedParams.logType}` : ''} ${suggestions.length > 0 ? suggestions.join('\n') + '\n' : ''}`; const logEntries = formattedLogs.join('\n\n---\n\n'); return { content: [{ type: 'text', text: summary + '\n\nšŸ“ **Log Entries**\n\n' + logEntries, }], }; } catch (error) { logger.error('Search logs failed', error); if (error instanceof z.ZodError) { throw new ValidationError(`Invalid parameters: ${error.issues.map(e => e.message).join(', ')}`, undefined, { zodError: error.issues }); } throw new ToolError(`Failed to search logs: ${error instanceof Error ? error.message : 'Unknown error'}`, 'search_logs', { originalError: error }); } } /** * MCP tool definition for search logs */ export const searchLogsTool = { name: 'search_logs', description: 'Search through Logz.io logs with filters and time ranges. Use this tool to find specific log entries, debug issues, or analyze application behavior. ' + '\n\nšŸŽÆ **EXAMPLES:**\n' + '• Simple text: query="error database connection"\n' + '• App-specific: query="myapp"\n' + '• Time-sensitive: query="payment failed" + timeRange="1h"\n' + '• Error focus: query="timeout" + severity="error"\n' + '\nšŸ” **SEARCH PRECISION:**\n' + '• Multi-word queries are automatically treated as exact phrases\n' + '• "this-should-not-return-results" will match exactly, not individual words\n' + '• Use quotes explicitly for complex phrases: "error AND warning"\n' + '• Use individual words for broader matching\n' + '\nšŸ·ļø **COMMON FIELDS:** k8s_namespace_name, k8s_pod_name, container_name, level, host, service, env_id\n' + '\nšŸ’” **TIPS:**\n' + '• Use timeRange="1h" for recent issues, "24h" for broader analysis\n' + '• Add severity="error" to focus on critical problems\n' + '• For complex filtering, use mcp_logzio_query_logs instead', inputSchema: { type: 'object', properties: { query: { type: 'string', description: 'Search query string. Examples: "error database", "myapp", "payment failed"', }, timeRange: { type: 'string', enum: ['1h', '6h', '12h', '24h', '3d', '7d', '30d'], description: 'Time range for the search. Start with "24h" for analysis, "1h" for recent issues', }, from: { type: 'string', format: 'date-time', description: 'Start time for search (ISO 8601 format). Overrides timeRange if provided.', }, to: { type: 'string', format: 'date-time', description: 'End time for search (ISO 8601 format). Overrides timeRange if provided.', }, logType: { type: 'string', description: 'Filter by log type. Common values: application, ingress, system, database', }, severity: { type: 'string', enum: ['trace', 'debug', 'info', 'warn', 'error', 'fatal'], description: 'Filter by log severity level. Use "error" for critical issues', }, limit: { type: 'number', minimum: 1, maximum: 1000, default: 50, description: 'Maximum number of log entries to return. Use 20 for quick scans, 100+ for analysis', }, sort: { type: 'string', enum: ['asc', 'desc'], default: 'desc', description: 'Sort order by timestamp. "desc" shows recent logs first (recommended)', }, }, required: ['query'], }, }; //# sourceMappingURL=search.js.map