find-traces-from-logs
Extract and analyze traces from Google Cloud logs using specified filters to identify and troubleshoot errors, ensuring efficient monitoring and issue resolution.
Input Schema
TableJSON Schema
| Name | Required | Description | Default |
|---|---|---|---|
| filter | Yes | Filter for logs (e.g., "severity>=ERROR AND timestamp>"-1d"") | |
| limit | No | Maximum number of logs to check | |
| projectId | No | Optional Google Cloud project ID |
Implementation Reference
- src/services/trace/tools.ts:426-618 (handler)Main handler function that fetches logs using Google Cloud Logging client, extracts trace IDs using extractTraceIdFromLog helper, builds a map of unique traces with log metadata, and returns formatted Markdown table.async ({ projectId, filter, limit }, context) => { try { // Use provided project ID or get the default one from state manager first const actualProjectId = projectId || stateManager.getCurrentProjectId() || await getProjectId(); // Process the filter to handle relative time formats let processedFilter = filter; // Check for relative time patterns in the filter const relativeTimeRegex = /(timestamp[><]=?\s*["'])(-?\d+[hmd])(["'])/g; processedFilter = processedFilter.replace(relativeTimeRegex, (match, prefix, timeValue, suffix) => { logger.debug(`Found relative time in filter: ${match}, timeValue: ${timeValue}`); // Parse the relative time const value = parseInt(timeValue.slice(1, -1)); const unit = timeValue.slice(-1); const isNegative = timeValue.startsWith('-'); // Calculate the absolute time const now = new Date(); let targetDate = new Date(now); if (unit === 'h') { targetDate.setHours(targetDate.getHours() + (isNegative ? -value : value)); } else if (unit === 'd') { targetDate.setDate(targetDate.getDate() + (isNegative ? -value : value)); } else if (unit === 'm') { targetDate.setMinutes(targetDate.getMinutes() + (isNegative ? -value : value)); } // Format as RFC3339 const formattedTime = targetDate.toISOString(); logger.debug(`Converted relative time ${timeValue} to absolute time: ${formattedTime}`); // Return the updated filter part return `${prefix}${formattedTime}${suffix}`; }); logger.debug(`Original filter: ${filter}`); logger.debug(`Processed filter: ${processedFilter}`); // Initialize the logging client const logging = new Logging({ projectId: actualProjectId }); // Fetch logs with the processed filter const [entries] = await logging.getEntries({ filter: processedFilter, pageSize: limit }); if (!entries || entries.length === 0) { return { content: [{ type: 'text', text: `No logs found matching the filter: "${filter}" in project: ${actualProjectId}` }] }; } // Extract trace IDs from logs const traceMap = new Map<string, { traceId: string; timestamp: string; severity: string; logName: string; message: string; }>(); for (const entry of entries) { const metadata = entry.metadata; const traceId = extractTraceIdFromLog(metadata); if (traceId) { // Convert timestamp to string let timestampStr = 'Unknown'; if (metadata.timestamp) { if (typeof metadata.timestamp === 'string') { timestampStr = metadata.timestamp; } else { try { // Handle different timestamp formats if (typeof metadata.timestamp === 'object' && metadata.timestamp !== null) { if ('seconds' in metadata.timestamp && 'nanos' in metadata.timestamp) { // Handle Timestamp proto format const seconds = Number(metadata.timestamp.seconds); const nanos = Number(metadata.timestamp.nanos || 0); const milliseconds = seconds * 1000 + nanos / 1000000; timestampStr = new Date(milliseconds).toISOString(); } else { // Try to convert using JSON timestampStr = JSON.stringify(metadata.timestamp); } } else { timestampStr = String(metadata.timestamp); } } catch (e) { timestampStr = 'Invalid timestamp'; } } } // Convert severity to string let severityStr = 'DEFAULT'; if (metadata.severity) { severityStr = String(metadata.severity); } // Convert logName to string let logNameStr = 'Unknown'; if (metadata.logName) { logNameStr = String(metadata.logName); } // Extract message let messageStr = 'No message'; if (metadata.textPayload) { messageStr = String(metadata.textPayload); } else if (metadata.jsonPayload) { try { messageStr = JSON.stringify(metadata.jsonPayload); } catch (e) { messageStr = 'Invalid JSON payload'; } } traceMap.set(traceId, { traceId, timestamp: timestampStr, severity: severityStr, logName: logNameStr, message: messageStr }); } } if (traceMap.size === 0) { return { content: [{ type: 'text', text: `No traces found in the logs matching the filter: "${filter}" in project: ${actualProjectId}` }] }; } // Format the traces for display let markdown = `# Traces Found in Logs\n\n`; markdown += `Project: ${actualProjectId}\n`; markdown += `Log Filter: ${filter}\n`; markdown += `Found ${traceMap.size} unique traces in ${entries.length} log entries:\n\n`; // Table header markdown += '| Trace ID | Timestamp | Severity | Log Name | Message |\n'; markdown += '|----------|-----------|----------|----------|--------|\n'; // Table rows for (const trace of traceMap.values()) { const traceId = trace.traceId; // Handle timestamp formatting safely let timestamp = trace.timestamp; try { if (timestamp !== 'Unknown' && timestamp !== 'Invalid timestamp') { timestamp = new Date(trace.timestamp).toISOString(); } } catch (e) { // Keep the original timestamp if conversion fails } const severity = trace.severity; const logName = trace.logName.split('/').pop() || trace.logName; const message = trace.message.length > 100 ? trace.message.substring(0, 100) + '...' : trace.message; markdown += `| \`${traceId}\` | ${timestamp} | ${severity} | ${logName} | ${message} |\n`; } markdown += '\n\nTo view a specific trace, use the `get-trace` tool with the trace ID.'; return { content: [{ type: 'text', text: markdown }] }; } catch (error: any) { // Error handling for find-traces-from-logs tool throw new GcpMcpError( `Failed to find traces from logs: ${error.message}`, error.code || 'UNKNOWN', error.statusCode || 500 ); } }
- src/services/trace/tools.ts:421-425 (schema)Zod schema defining input parameters: optional projectId, required filter for log query, optional limit (default 10, max 100).{ projectId: z.string().optional().describe('Optional Google Cloud project ID'), filter: z.string().describe('Filter for logs (e.g., "severity>=ERROR AND timestamp>\"-1d\"")'), limit: z.number().min(1).max(100).default(10).describe('Maximum number of logs to check') },
- src/services/trace/tools.ts:419-619 (registration)Registration of the 'find-traces-from-logs' tool on the MCP server within registerTraceTools function, including inline schema and handler.server.tool( 'find-traces-from-logs', { projectId: z.string().optional().describe('Optional Google Cloud project ID'), filter: z.string().describe('Filter for logs (e.g., "severity>=ERROR AND timestamp>\"-1d\"")'), limit: z.number().min(1).max(100).default(10).describe('Maximum number of logs to check') }, async ({ projectId, filter, limit }, context) => { try { // Use provided project ID or get the default one from state manager first const actualProjectId = projectId || stateManager.getCurrentProjectId() || await getProjectId(); // Process the filter to handle relative time formats let processedFilter = filter; // Check for relative time patterns in the filter const relativeTimeRegex = /(timestamp[><]=?\s*["'])(-?\d+[hmd])(["'])/g; processedFilter = processedFilter.replace(relativeTimeRegex, (match, prefix, timeValue, suffix) => { logger.debug(`Found relative time in filter: ${match}, timeValue: ${timeValue}`); // Parse the relative time const value = parseInt(timeValue.slice(1, -1)); const unit = timeValue.slice(-1); const isNegative = timeValue.startsWith('-'); // Calculate the absolute time const now = new Date(); let targetDate = new Date(now); if (unit === 'h') { targetDate.setHours(targetDate.getHours() + (isNegative ? -value : value)); } else if (unit === 'd') { targetDate.setDate(targetDate.getDate() + (isNegative ? -value : value)); } else if (unit === 'm') { targetDate.setMinutes(targetDate.getMinutes() + (isNegative ? -value : value)); } // Format as RFC3339 const formattedTime = targetDate.toISOString(); logger.debug(`Converted relative time ${timeValue} to absolute time: ${formattedTime}`); // Return the updated filter part return `${prefix}${formattedTime}${suffix}`; }); logger.debug(`Original filter: ${filter}`); logger.debug(`Processed filter: ${processedFilter}`); // Initialize the logging client const logging = new Logging({ projectId: actualProjectId }); // Fetch logs with the processed filter const [entries] = await logging.getEntries({ filter: processedFilter, pageSize: limit }); if (!entries || entries.length === 0) { return { content: [{ type: 'text', text: `No logs found matching the filter: "${filter}" in project: ${actualProjectId}` }] }; } // Extract trace IDs from logs const traceMap = new Map<string, { traceId: string; timestamp: string; severity: string; logName: string; message: string; }>(); for (const entry of entries) { const metadata = entry.metadata; const traceId = extractTraceIdFromLog(metadata); if (traceId) { // Convert timestamp to string let timestampStr = 'Unknown'; if (metadata.timestamp) { if (typeof metadata.timestamp === 'string') { timestampStr = metadata.timestamp; } else { try { // Handle different timestamp formats if (typeof metadata.timestamp === 'object' && metadata.timestamp !== null) { if ('seconds' in metadata.timestamp && 'nanos' in metadata.timestamp) { // Handle Timestamp proto format const seconds = Number(metadata.timestamp.seconds); const nanos = Number(metadata.timestamp.nanos || 0); const milliseconds = seconds * 1000 + nanos / 1000000; timestampStr = new Date(milliseconds).toISOString(); } else { // Try to convert using JSON timestampStr = JSON.stringify(metadata.timestamp); } } else { timestampStr = String(metadata.timestamp); } } catch (e) { timestampStr = 'Invalid timestamp'; } } } // Convert severity to string let severityStr = 'DEFAULT'; if (metadata.severity) { severityStr = String(metadata.severity); } // Convert logName to string let logNameStr = 'Unknown'; if (metadata.logName) { logNameStr = String(metadata.logName); } // Extract message let messageStr = 'No message'; if (metadata.textPayload) { messageStr = String(metadata.textPayload); } else if (metadata.jsonPayload) { try { messageStr = JSON.stringify(metadata.jsonPayload); } catch (e) { messageStr = 'Invalid JSON payload'; } } traceMap.set(traceId, { traceId, timestamp: timestampStr, severity: severityStr, logName: logNameStr, message: messageStr }); } } if (traceMap.size === 0) { return { content: [{ type: 'text', text: `No traces found in the logs matching the filter: "${filter}" in project: ${actualProjectId}` }] }; } // Format the traces for display let markdown = `# Traces Found in Logs\n\n`; markdown += `Project: ${actualProjectId}\n`; markdown += `Log Filter: ${filter}\n`; markdown += `Found ${traceMap.size} unique traces in ${entries.length} log entries:\n\n`; // Table header markdown += '| Trace ID | Timestamp | Severity | Log Name | Message |\n'; markdown += '|----------|-----------|----------|----------|--------|\n'; // Table rows for (const trace of traceMap.values()) { const traceId = trace.traceId; // Handle timestamp formatting safely let timestamp = trace.timestamp; try { if (timestamp !== 'Unknown' && timestamp !== 'Invalid timestamp') { timestamp = new Date(trace.timestamp).toISOString(); } } catch (e) { // Keep the original timestamp if conversion fails } const severity = trace.severity; const logName = trace.logName.split('/').pop() || trace.logName; const message = trace.message.length > 100 ? trace.message.substring(0, 100) + '...' : trace.message; markdown += `| \`${traceId}\` | ${timestamp} | ${severity} | ${logName} | ${message} |\n`; } markdown += '\n\nTo view a specific trace, use the `get-trace` tool with the trace ID.'; return { content: [{ type: 'text', text: markdown }] }; } catch (error: any) { // Error handling for find-traces-from-logs tool throw new GcpMcpError( `Failed to find traces from logs: ${error.message}`, error.code || 'UNKNOWN', error.statusCode || 500 ); } } );
- src/services/trace/types.ts:681-716 (helper)Helper function to extract trace ID from Google Cloud log entry metadata, checking trace field, labels, and jsonPayload in standard formats.export function extractTraceIdFromLog(logEntry: any): string | undefined { // Check for trace in the standard logging.googleapis.com/trace field if (logEntry.trace) { // The trace field is typically in the format "projects/PROJECT_ID/traces/TRACE_ID" const match = logEntry.trace.match(/traces\/([a-f0-9]+)$/i); if (match && match[1]) { return match[1]; } } // Check for trace in labels if (logEntry.labels && logEntry.labels['logging.googleapis.com/trace']) { const traceLabel = logEntry.labels['logging.googleapis.com/trace']; const match = traceLabel.match(/traces\/([a-f0-9]+)$/i); if (match && match[1]) { return match[1]; } } // Check for trace in jsonPayload if (logEntry.jsonPayload) { if (logEntry.jsonPayload.traceId) { return logEntry.jsonPayload.traceId; } if (logEntry.jsonPayload['logging.googleapis.com/trace']) { const tracePayload = logEntry.jsonPayload['logging.googleapis.com/trace']; const match = tracePayload.match(/traces\/([a-f0-9]+)$/i); if (match && match[1]) { return match[1]; } } } return undefined; }