UNPKG

@chinchillaenterprises/mcp-amplify

Version:

AWS Amplify MCP server with intelligent deployment automation, specialized logging suite, and recursive resource discovery

1,130 lines 50.6 kB
import { DescribeLogGroupsCommand, FilterLogEventsCommand } from "@aws-sdk/client-cloudwatch-logs"; import { getCurrentClients } from './account-handlers.js'; import { handleAmplifyDiscoverResources } from './resource-discovery-handlers.js'; // Helper function to parse time ranges function parseTimeRange(timeRange) { const now = new Date(); const endTime = new Date(now); let startTime; switch (timeRange) { case '5m': startTime = new Date(now.getTime() - 5 * 60 * 1000); break; case '15m': startTime = new Date(now.getTime() - 15 * 60 * 1000); break; case '1h': startTime = new Date(now.getTime() - 60 * 60 * 1000); break; case '6h': startTime = new Date(now.getTime() - 6 * 60 * 60 * 1000); break; case '24h': startTime = new Date(now.getTime() - 24 * 60 * 60 * 1000); break; default: startTime = new Date(now.getTime() - 15 * 60 * 1000); // Default to 15 minutes } return { startTime, endTime }; } // Helper function to build log level filter patterns function buildLogLevelFilter(logLevel) { switch (logLevel.toUpperCase()) { case 'ERROR': return '[ERROR]'; case 'WARN': return '[WARN]'; case 'INFO': return '[INFO]'; case 'DEBUG': return '[DEBUG]'; case 'ALL': default: return ''; } } export async function handleAmplifyGetLambdaLogs(args) { const { appId, branchName, functionName, logLevel = 'all', timeRange = '15m', customStartTime, customEndTime, maxEvents = 100, includeMetrics = false, filterPattern } = args; if (!appId) { throw new Error('appId is required'); } try { const clients = getCurrentClients(); // First, discover Lambda resources to get accurate function names and log groups console.error('🔍 Discovering Lambda resources...'); const discoveryResult = await handleAmplifyDiscoverResources({ appId, branchName, resourceType: 'lambda' }); if (!discoveryResult.success || !discoveryResult.lambdaFunctions || discoveryResult.lambdaFunctions.length === 0) { return { success: false, error: 'No Lambda functions found for this app/branch', appId, branchName }; } // Filter functions based on functionName parameter let targetFunctions = discoveryResult.lambdaFunctions; if (functionName && functionName !== 'all') { targetFunctions = discoveryResult.lambdaFunctions.filter((func) => func.functionName.includes(functionName) || func.logicalId.includes(functionName)); if (targetFunctions.length === 0) { return { success: false, error: `No Lambda functions found matching: ${functionName}`, appId, branchName, availableFunctions: discoveryResult.lambdaFunctions.map((f) => f.functionName) }; } } // Parse time range let startTime, endTime; if (customStartTime && customEndTime) { startTime = new Date(customStartTime); endTime = new Date(customEndTime); } else { ({ startTime, endTime } = parseTimeRange(timeRange)); } // Build filter pattern let combinedFilter = ''; const logLevelFilter = buildLogLevelFilter(logLevel); if (logLevelFilter) { combinedFilter = logLevelFilter; } if (filterPattern) { combinedFilter = combinedFilter ? `${combinedFilter} ${filterPattern}` : filterPattern; } console.error(`📋 Fetching logs for ${targetFunctions.length} Lambda function(s)...`); // Fetch logs for each function const functionLogs = []; for (const func of targetFunctions) { console.error(` 📄 Processing ${func.functionName}...`); try { const filterCommand = new FilterLogEventsCommand({ logGroupName: func.logGroupName, startTime: startTime.getTime(), endTime: endTime.getTime(), filterPattern: combinedFilter || undefined, limit: maxEvents }); const logResponse = await clients.cloudwatchLogs.send(filterCommand); const logs = logResponse.events?.map((event) => ({ timestamp: new Date(event.timestamp).toISOString(), message: event.message, logStream: event.logStreamName, requestId: extractRequestId(event.message || '') })) || []; functionLogs.push({ functionName: func.functionName, logicalId: func.logicalId, logGroupName: func.logGroupName, logsFound: logs.length, logs: logs.slice(0, maxEvents) }); } catch (error) { console.error(`❌ Error fetching logs for ${func.functionName}:`, error); functionLogs.push({ functionName: func.functionName, logicalId: func.logicalId, logGroupName: func.logGroupName, logsFound: 0, logs: [], error: error instanceof Error ? error.message : String(error) }); } } // Calculate summary statistics const totalLogs = functionLogs.reduce((sum, func) => sum + func.logsFound, 0); const functionsWithLogs = functionLogs.filter(func => func.logsFound > 0).length; const functionsWithErrors = functionLogs.filter(func => func.error).length; return { success: true, appId, branchName, timeRange: customStartTime ? 'custom' : timeRange, startTime: startTime.toISOString(), endTime: endTime.toISOString(), logLevel, filterPattern: combinedFilter || null, totalFunctions: targetFunctions.length, functionsWithLogs, functionsWithErrors, totalLogs, functionLogs, summary: { queryInfo: { timeRange: customStartTime ? 'custom' : timeRange, logLevel, filterPattern: combinedFilter || null, maxEvents }, results: { totalLogs, functionsWithLogs, functionsWithErrors } } }; } catch (error) { console.error('❌ Error in handleAmplifyGetLambdaLogs:', error); return { success: false, error: error instanceof Error ? error.message : String(error), appId, branchName }; } } // Helper function to extract request ID from log message function extractRequestId(message) { const requestIdMatch = message.match(/\[REQUEST ID: ([^\]]+)\]/); if (requestIdMatch) { return requestIdMatch[1]; } const awsRequestIdMatch = message.match(/RequestId: ([a-f0-9-]+)/); if (awsRequestIdMatch) { return awsRequestIdMatch[1]; } return null; } // Helper function to build AppSync-specific filter patterns function buildAppSyncFilter(resolverType, resolverName, logLevel) { let filters = []; // Add resolver type filter if (resolverType !== 'all') { filters.push(`[timestamp, requestId, level, message, resolver="${resolverType}"]`); } // Add resolver name filter if (resolverName) { filters.push(`[timestamp, requestId, level, message, resolver~"${resolverName}"]`); } // Add log level filter if (logLevel && logLevel !== 'all') { filters.push(`[timestamp, requestId, level="${logLevel.toUpperCase()}"]`); } return filters.length > 0 ? filters.join(' ') : ''; } // Helper function to extract GraphQL operation details from AppSync log messages function extractGraphQLDetails(message) { const details = {}; // Extract operation type (Query, Mutation, Subscription) const operationMatch = message.match(/\b(Query|Mutation|Subscription)\b/); if (operationMatch) { details.operationType = operationMatch[1]; } // Extract operation name const nameMatch = message.match(/operation\s+(\w+)/i); if (nameMatch) { details.operationName = nameMatch[1]; } // Extract field being resolved const fieldMatch = message.match(/field\s+(\w+)/i); if (fieldMatch) { details.field = fieldMatch[1]; } // Extract execution time const timeMatch = message.match(/(\d+(?:\.\d+)?)\s*ms/); if (timeMatch) { details.executionTime = parseFloat(timeMatch[1]); } return details; } export async function handleAmplifyGetAppSyncLogs(args) { const { appId, branchName, resolverType = 'all', resolverName, timeRange = '15m', customStartTime, customEndTime, includePerformance = false, includeRequestResponse = false, logLevel = 'all', maxEvents = 100 } = args; if (!appId) { throw new Error('appId is required'); } try { const clients = getCurrentClients(); // First, discover AppSync resources to get accurate API IDs and log groups console.error('🔍 Discovering AppSync resources...'); const discoveryResult = await handleAmplifyDiscoverResources({ appId, branchName, resourceType: 'all' // We need to discover all resources to find AppSync APIs }); if (!discoveryResult.success) { return { success: false, error: 'Failed to discover AppSync resources', appId, branchName }; } // Look for AppSync APIs in the discovered resources const appSyncApis = []; if (discoveryResult.apiGateways) { // Check if any API Gateways are actually AppSync APIs for (const api of discoveryResult.apiGateways) { if (api.apiType === 'GRAPHQL' || api.logicalId.toLowerCase().includes('appsync')) { let apiId = api.apiId; // Extract API ID from ARN if needed if (apiId && apiId.includes('arn:aws:appsync')) { apiId = apiId.split('/').pop(); } appSyncApis.push({ apiId, logicalId: api.logicalId, logGroupName: `/aws/appsync/apis/${apiId}`, name: api.name || api.logicalId }); } } } // Also try to find AppSync APIs directly by looking for known patterns try { const describeCommand = new DescribeLogGroupsCommand({ logGroupNamePrefix: '/aws/appsync/apis/', limit: 50 }); const logGroupsResponse = await clients.cloudwatchLogs.send(describeCommand); for (const logGroup of logGroupsResponse.logGroups || []) { if (logGroup.logGroupName && logGroup.logGroupName.includes(appId)) { const apiIdMatch = logGroup.logGroupName.match(/\/aws\/appsync\/apis\/([^\/]+)/); if (apiIdMatch) { const apiId = apiIdMatch[1]; const existing = appSyncApis.find(api => api.apiId === apiId); if (!existing) { appSyncApis.push({ apiId, logicalId: `AppSync-${apiId}`, logGroupName: logGroup.logGroupName, name: `AppSync API ${apiId}` }); } } } } } catch (error) { console.warn('⚠️ Could not scan for AppSync log groups:', error); } if (appSyncApis.length === 0) { return { success: false, error: 'No AppSync APIs found for this app/branch', appId, branchName }; } // Parse time range let startTime, endTime; if (customStartTime && customEndTime) { startTime = new Date(customStartTime); endTime = new Date(customEndTime); } else { ({ startTime, endTime } = parseTimeRange(timeRange)); } // Build filter pattern const filterPattern = buildAppSyncFilter(resolverType, resolverName, logLevel); console.error(`📋 Fetching AppSync logs for ${appSyncApis.length} API(s)...`); // Fetch logs for each AppSync API const apiLogs = []; for (const api of appSyncApis) { console.error(` 📄 Processing ${api.name}...`); try { const filterCommand = new FilterLogEventsCommand({ logGroupName: api.logGroupName, startTime: startTime.getTime(), endTime: endTime.getTime(), filterPattern: filterPattern || undefined, limit: maxEvents }); const logResponse = await clients.cloudwatchLogs.send(filterCommand); const logs = logResponse.events?.map((event) => { const graphqlDetails = extractGraphQLDetails(event.message || ''); const baseLog = { timestamp: new Date(event.timestamp).toISOString(), message: event.message, logStream: event.logStreamName, requestId: extractRequestId(event.message || ''), ...graphqlDetails }; // Add performance data if requested if (includePerformance && graphqlDetails.executionTime) { baseLog.performance = { executionTime: graphqlDetails.executionTime, isSlowQuery: graphqlDetails.executionTime > 1000 // Consider >1s as slow }; } // Add request/response data if requested (simplified for now) if (includeRequestResponse) { const hasRequest = event.message?.includes('request') || event.message?.includes('variables'); const hasResponse = event.message?.includes('response') || event.message?.includes('data'); baseLog.requestResponse = { hasRequestData: hasRequest, hasResponseData: hasResponse }; } return baseLog; }) || []; // Calculate performance statistics if requested let performanceStats = {}; if (includePerformance) { const timings = logs.filter((log) => log.executionTime).map((log) => log.executionTime); if (timings.length > 0) { performanceStats = { averageExecutionTime: timings.reduce((sum, time) => sum + time, 0) / timings.length, maxExecutionTime: Math.max(...timings), minExecutionTime: Math.min(...timings), slowQueries: timings.filter((time) => time > 1000).length, totalQueriesWithTiming: timings.length }; } } apiLogs.push({ apiId: api.apiId, logicalId: api.logicalId, name: api.name, logGroupName: api.logGroupName, logsFound: logs.length, logs: logs.slice(0, maxEvents), performanceStats: includePerformance ? performanceStats : undefined }); } catch (error) { console.error(`❌ Error fetching logs for ${api.name}:`, error); apiLogs.push({ apiId: api.apiId, logicalId: api.logicalId, name: api.name, logGroupName: api.logGroupName, logsFound: 0, logs: [], error: error instanceof Error ? error.message : String(error) }); } } // Calculate summary statistics const totalLogs = apiLogs.reduce((sum, api) => sum + api.logsFound, 0); const apisWithLogs = apiLogs.filter(api => api.logsFound > 0).length; const apisWithErrors = apiLogs.filter(api => api.error).length; // Extract operation type statistics const operationStats = {}; apiLogs.forEach(api => { api.logs.forEach((log) => { if (log.operationType) { operationStats[log.operationType] = (operationStats[log.operationType] || 0) + 1; } }); }); return { success: true, appId, branchName, timeRange: customStartTime ? 'custom' : timeRange, startTime: startTime.toISOString(), endTime: endTime.toISOString(), resolverType, resolverName: resolverName || null, logLevel, includePerformance, includeRequestResponse, filterPattern: filterPattern || null, totalApis: appSyncApis.length, apisWithLogs, apisWithErrors, totalLogs, operationStats, apiLogs, summary: { queryInfo: { timeRange: customStartTime ? 'custom' : timeRange, resolverType, resolverName: resolverName || null, logLevel, maxEvents }, results: { totalLogs, apisWithLogs, apisWithErrors, operationBreakdown: operationStats }, recommendations: [ totalLogs === 0 ? "No logs found. Check if AppSync API is receiving traffic." : null, includePerformance ? "Performance data included. Check for slow queries (>1s)." : "Consider enabling includePerformance for query performance insights.", includeRequestResponse ? "Request/response data included." : "Consider enabling includeRequestResponse for detailed request analysis." ].filter(Boolean) } }; } catch (error) { console.error('❌ Error in handleAmplifyGetAppSyncLogs:', error); return { success: false, error: error instanceof Error ? error.message : String(error), appId, branchName }; } } // Helper function to build API Gateway-specific filter patterns function buildApiGatewayFilter(httpMethod, endpoint, statusCode) { let filters = []; // Add HTTP method filter if (httpMethod && httpMethod !== 'all') { filters.push(`[timestamp, requestId, level, message, method="${httpMethod}"]`); } // Add endpoint filter if (endpoint) { filters.push(`[timestamp, requestId, level, message, path~"${endpoint}"]`); } // Add status code filter if (statusCode) { filters.push(`[timestamp, requestId, level, message, status="${statusCode}"]`); } return filters.length > 0 ? filters.join(' ') : ''; } // Helper function to extract API Gateway request details from log messages function extractApiGatewayDetails(message) { const details = {}; // Extract HTTP method const methodMatch = message.match(/\b(GET|POST|PUT|DELETE|PATCH|HEAD|OPTIONS)\b/); if (methodMatch) { details.httpMethod = methodMatch[1]; } // Extract endpoint/path const pathMatch = message.match(/\/[^\s]*\s/); if (pathMatch) { details.endpoint = pathMatch[0].trim(); } // Extract status code const statusMatch = message.match(/\s(\d{3})\s/); if (statusMatch) { details.statusCode = parseInt(statusMatch[1]); } // Extract response time/latency const timeMatch = message.match(/(\d+(?:\.\d+)?)\s*ms/); if (timeMatch) { details.responseTime = parseFloat(timeMatch[1]); } // Extract request ID const requestIdMatch = message.match(/\[([a-f0-9-]+)\]/); if (requestIdMatch) { details.requestId = requestIdMatch[1]; } // Extract user agent const userAgentMatch = message.match(/User-Agent:\s*([^"]+)/); if (userAgentMatch) { details.userAgent = userAgentMatch[1]; } // Extract IP address const ipMatch = message.match(/\b(?:\d{1,3}\.){3}\d{1,3}\b/); if (ipMatch) { details.ip = ipMatch[0]; } return details; } export async function handleAmplifyGetApiGatewayLogs(args) { const { appId, branchName, httpMethod = 'all', endpoint, includeRequestResponse = false, includeLatency = false, statusCode, timeRange = '15m', customStartTime, customEndTime, maxEvents = 100 } = args; if (!appId) { throw new Error('appId is required'); } try { const clients = getCurrentClients(); // First, discover API Gateway resources to get accurate API IDs and log groups console.error('🔍 Discovering API Gateway resources...'); const discoveryResult = await handleAmplifyDiscoverResources({ appId, branchName, resourceType: 'apigateway' }); if (!discoveryResult.success) { return { success: false, error: 'Failed to discover API Gateway resources', appId, branchName }; } // Look for API Gateway APIs in the discovered resources const apiGatewayApis = []; if (discoveryResult.apiGateways) { for (const api of discoveryResult.apiGateways) { // Include both REST and HTTP APIs if (api.apiType === 'REST' || api.apiType === 'HTTP' || !api.apiType) { apiGatewayApis.push({ apiId: api.apiId, logicalId: api.logicalId, name: api.name || api.logicalId, apiType: api.apiType || 'REST', // API Gateway log groups can have different patterns logGroupName: `/aws/apigateway/${api.apiId}/prod`, executionLogGroupName: `API-Gateway-Execution-Logs_${api.apiId}/prod` }); } } } // Also try to find API Gateway log groups directly by scanning for known patterns try { const patterns = [ '/aws/apigateway/', 'API-Gateway-Execution-Logs_' ]; for (const pattern of patterns) { const describeCommand = new DescribeLogGroupsCommand({ logGroupNamePrefix: pattern, limit: 50 }); const logGroupsResponse = await clients.cloudwatchLogs.send(describeCommand); for (const logGroup of logGroupsResponse.logGroups || []) { if (logGroup.logGroupName && logGroup.logGroupName.includes(appId)) { let apiIdMatch; let logGroupType = 'access'; if (logGroup.logGroupName.startsWith('/aws/apigateway/')) { apiIdMatch = logGroup.logGroupName.match(/\/aws\/apigateway\/([^\/]+)/); } else if (logGroup.logGroupName.startsWith('API-Gateway-Execution-Logs_')) { apiIdMatch = logGroup.logGroupName.match(/API-Gateway-Execution-Logs_([^\/]+)/); logGroupType = 'execution'; } if (apiIdMatch) { const apiId = apiIdMatch[1]; const existing = apiGatewayApis.find(api => api.apiId === apiId); if (existing) { if (logGroupType === 'execution') { existing.executionLogGroupName = logGroup.logGroupName; } } else { apiGatewayApis.push({ apiId, logicalId: `ApiGateway-${apiId}`, name: `API Gateway ${apiId}`, apiType: 'REST', logGroupName: logGroupType === 'access' ? logGroup.logGroupName : `/aws/apigateway/${apiId}/prod`, executionLogGroupName: logGroupType === 'execution' ? logGroup.logGroupName : `API-Gateway-Execution-Logs_${apiId}/prod` }); } } } } } } catch (error) { console.warn('⚠️ Could not scan for API Gateway log groups:', error); } if (apiGatewayApis.length === 0) { return { success: false, error: 'No API Gateway APIs found for this app/branch', appId, branchName }; } // Parse time range let startTime, endTime; if (customStartTime && customEndTime) { startTime = new Date(customStartTime); endTime = new Date(customEndTime); } else { ({ startTime, endTime } = parseTimeRange(timeRange)); } // Build filter pattern const filterPattern = buildApiGatewayFilter(httpMethod, endpoint, statusCode); console.error(`📋 Fetching API Gateway logs for ${apiGatewayApis.length} API(s)...`); // Fetch logs for each API Gateway API const apiLogs = []; for (const api of apiGatewayApis) { console.error(` 📄 Processing ${api.name}...`); // Try both access logs and execution logs const logGroupsToCheck = [ { name: 'access', logGroup: api.logGroupName }, { name: 'execution', logGroup: api.executionLogGroupName } ]; const combinedLogs = []; let totalLogsForApi = 0; for (const logGroupInfo of logGroupsToCheck) { try { const filterCommand = new FilterLogEventsCommand({ logGroupName: logGroupInfo.logGroup, startTime: startTime.getTime(), endTime: endTime.getTime(), filterPattern: filterPattern || undefined, limit: Math.floor(maxEvents / 2) // Split between access and execution logs }); const logResponse = await clients.cloudwatchLogs.send(filterCommand); const logs = logResponse.events?.map((event) => { const apiGatewayDetails = extractApiGatewayDetails(event.message || ''); const baseLog = { timestamp: new Date(event.timestamp).toISOString(), message: event.message, logStream: event.logStreamName, logType: logGroupInfo.name, requestId: apiGatewayDetails.requestId || extractRequestId(event.message || ''), ...apiGatewayDetails }; // Add latency data if requested if (includeLatency && apiGatewayDetails.responseTime) { baseLog.latency = { responseTime: apiGatewayDetails.responseTime, isSlowRequest: apiGatewayDetails.responseTime > 2000 // Consider >2s as slow }; } // Add request/response data if requested (simplified for now) if (includeRequestResponse) { const hasRequest = event.message?.includes('request') || event.message?.includes('body'); const hasResponse = event.message?.includes('response') || event.message?.includes('status'); baseLog.requestResponse = { hasRequestData: hasRequest, hasResponseData: hasResponse, method: apiGatewayDetails.httpMethod, endpoint: apiGatewayDetails.endpoint, statusCode: apiGatewayDetails.statusCode }; } return baseLog; }) || []; combinedLogs.push(...logs); totalLogsForApi += logs.length; } catch (error) { console.warn(`⚠️ Could not fetch ${logGroupInfo.name} logs for ${api.name}:`, error); } } // Sort combined logs by timestamp combinedLogs.sort((a, b) => new Date(b.timestamp).getTime() - new Date(a.timestamp).getTime()); // Calculate performance statistics if requested let performanceStats = {}; if (includeLatency) { const timings = combinedLogs.filter(log => log.responseTime).map(log => log.responseTime); if (timings.length > 0) { performanceStats = { averageResponseTime: timings.reduce((sum, time) => sum + time, 0) / timings.length, maxResponseTime: Math.max(...timings), minResponseTime: Math.min(...timings), slowRequests: timings.filter(time => time > 2000).length, totalRequestsWithTiming: timings.length }; } } // Calculate status code distribution const statusCodeStats = {}; combinedLogs.forEach(log => { if (log.statusCode) { statusCodeStats[log.statusCode] = (statusCodeStats[log.statusCode] || 0) + 1; } }); apiLogs.push({ apiId: api.apiId, logicalId: api.logicalId, name: api.name, apiType: api.apiType, logGroupName: api.logGroupName, executionLogGroupName: api.executionLogGroupName, logsFound: totalLogsForApi, logs: combinedLogs.slice(0, maxEvents), performanceStats: includeLatency ? performanceStats : undefined, statusCodeStats: Object.keys(statusCodeStats).length > 0 ? statusCodeStats : undefined, error: totalLogsForApi === 0 ? 'No logs found in either access or execution log groups' : undefined }); } // Calculate summary statistics const totalLogs = apiLogs.reduce((sum, api) => sum + api.logsFound, 0); const apisWithLogs = apiLogs.filter(api => api.logsFound > 0).length; const apisWithErrors = apiLogs.filter(api => api.error).length; // Extract HTTP method statistics const methodStats = {}; apiLogs.forEach(api => { api.logs.forEach((log) => { if (log.httpMethod) { methodStats[log.httpMethod] = (methodStats[log.httpMethod] || 0) + 1; } }); }); // Extract endpoint statistics const endpointStats = {}; apiLogs.forEach(api => { api.logs.forEach((log) => { if (log.endpoint) { endpointStats[log.endpoint] = (endpointStats[log.endpoint] || 0) + 1; } }); }); return { success: true, appId, branchName, timeRange: customStartTime ? 'custom' : timeRange, startTime: startTime.toISOString(), endTime: endTime.toISOString(), httpMethod, endpoint: endpoint || null, statusCode: statusCode || null, includeLatency, includeRequestResponse, filterPattern: filterPattern || null, totalApis: apiGatewayApis.length, apisWithLogs, apisWithErrors, totalLogs, methodStats, endpointStats, apiLogs, summary: { queryInfo: { timeRange: customStartTime ? 'custom' : timeRange, httpMethod, endpoint: endpoint || null, statusCode: statusCode || null, maxEvents }, results: { totalLogs, apisWithLogs, apisWithErrors, methodBreakdown: methodStats, endpointBreakdown: endpointStats }, recommendations: [ totalLogs === 0 ? "No logs found. Check if API Gateway is receiving traffic or if logging is enabled." : null, includeLatency ? "Performance data included. Check for slow requests (>2s)." : "Consider enabling includeLatency for request performance insights.", includeRequestResponse ? "Request/response data included." : "Consider enabling includeRequestResponse for detailed request analysis.", Object.keys(endpointStats).length > 5 ? "Many endpoints detected. Consider filtering by specific endpoint for focused analysis." : null ].filter(Boolean) } }; } catch (error) { console.error('❌ Error in handleAmplifyGetApiGatewayLogs:', error); return { success: false, error: error instanceof Error ? error.message : String(error), appId, branchName }; } } // Helper function to build Cognito-specific filter patterns function buildCognitoFilter(eventType, userId, includeFailedAttempts) { let filters = []; // Add event type filter if (eventType !== 'all') { switch (eventType) { case 'signin': filters.push('["signin", "sign_in", "authentication", "InitiateAuth", "RespondToAuthChallenge"]'); break; case 'signup': filters.push('["signup", "sign_up", "registration", "SignUp", "ConfirmSignUp"]'); break; case 'forgot_password': filters.push('["forgot_password", "reset_password", "ForgotPassword", "ConfirmForgotPassword"]'); break; case 'token_refresh': filters.push('["token_refresh", "RefreshToken", "refresh_token"]'); break; } } // Add user ID filter if specified if (userId) { filters.push(`[message~"${userId}"]`); } // Include failed attempts filter if (includeFailedAttempts === false) { filters.push('[level != "ERROR" && message != ~"[Ff]ailed" && message != ~"[Ee]rror"]'); } return filters.length > 0 ? filters.join(' ') : ''; } // Helper function to extract Cognito event details from log messages function extractCognitoEventDetails(message) { const details = {}; // Extract event type if (message.includes('signin') || message.includes('sign_in') || message.includes('InitiateAuth')) { details.eventType = 'signin'; } else if (message.includes('signup') || message.includes('sign_up') || message.includes('SignUp')) { details.eventType = 'signup'; } else if (message.includes('forgot_password') || message.includes('ForgotPassword')) { details.eventType = 'forgot_password'; } else if (message.includes('token_refresh') || message.includes('RefreshToken')) { details.eventType = 'token_refresh'; } // Extract user ID const userIdMatch = message.match(/user[_-]?id["\s]*:?\s*["']?([a-f0-9-]+)["']?/i); if (userIdMatch) { details.userId = userIdMatch[1]; } // Extract username const usernameMatch = message.match(/username["\s]*:?\s*["']?([^"'\s,}]+)["']?/i); if (usernameMatch) { details.username = usernameMatch[1]; } // Extract client ID const clientIdMatch = message.match(/client[_-]?id["\s]*:?\s*["']?([a-z0-9]+)["']?/i); if (clientIdMatch) { details.clientId = clientIdMatch[1]; } // Extract IP address const ipMatch = message.match(/ip[_-]?address["\s]*:?\s*["']?(\d+\.\d+\.\d+\.\d+)["']?/i); if (ipMatch) { details.ipAddress = ipMatch[1]; } // Extract user agent const userAgentMatch = message.match(/user[_-]?agent["\s]*:?\s*["']?([^"'\n\r]+)["']?/i); if (userAgentMatch) { details.userAgent = userAgentMatch[1]; } // Determine success/failure const isError = message.toLowerCase().includes('error') || message.toLowerCase().includes('failed') || message.toLowerCase().includes('denied'); const isSuccess = message.toLowerCase().includes('success') || message.toLowerCase().includes('confirmed') || message.toLowerCase().includes('authenticated'); if (isError) { details.success = false; // Extract error code const errorCodeMatch = message.match(/error[_-]?code["\s]*:?\s*["']?([A-Z_]+)["']?/i); if (errorCodeMatch) { details.errorCode = errorCodeMatch[1]; } } else if (isSuccess) { details.success = true; } return details; } export async function handleAmplifyGetCognitoLogs(args) { const { appId, branchName, eventType = 'all', userId, includeFailedAttempts = true, timeRange = '15m', customStartTime, customEndTime, logLevel = 'all', maxEvents = 100 } = args; if (!appId) { throw new Error('appId is required'); } try { const clients = getCurrentClients(); // First, discover Cognito resources to get accurate User Pool IDs and log groups console.error('🔍 Discovering Cognito resources...'); const discoveryResult = await handleAmplifyDiscoverResources({ appId, branchName, resourceType: 'cognito' }); if (!discoveryResult.success) { return { success: false, error: 'Failed to discover Cognito resources', appId, branchName }; } // Look for Cognito User Pools in the discovered resources const cognitoUserPools = []; if (discoveryResult.cognitoUserPools) { for (const userPool of discoveryResult.cognitoUserPools) { cognitoUserPools.push({ userPoolId: userPool.userPoolId, logicalId: userPool.logicalId, logGroupName: `/aws/cognito/userpools/${userPool.userPoolId}`, name: userPool.name || userPool.logicalId }); } } // Also try to find Cognito User Pools directly by looking for known patterns try { const describeCommand = new DescribeLogGroupsCommand({ logGroupNamePrefix: '/aws/cognito/userpools/', limit: 50 }); const logGroupsResponse = await clients.cloudwatchLogs.send(describeCommand); for (const logGroup of logGroupsResponse.logGroups || []) { if (logGroup.logGroupName && logGroup.logGroupName.includes(appId)) { const userPoolIdMatch = logGroup.logGroupName.match(/\/aws\/cognito\/userpools\/([^\/]+)/); if (userPoolIdMatch) { const userPoolId = userPoolIdMatch[1]; const existing = cognitoUserPools.find(pool => pool.userPoolId === userPoolId); if (!existing) { cognitoUserPools.push({ userPoolId, logicalId: `Cognito-${userPoolId}`, logGroupName: logGroup.logGroupName, name: `Cognito User Pool ${userPoolId}` }); } } } } } catch (error) { console.warn('⚠️ Could not scan for Cognito log groups:', error); } if (cognitoUserPools.length === 0) { return { success: false, error: 'No Cognito User Pools found for this app/branch', appId, branchName }; } // Parse time range let startTime, endTime; if (customStartTime && customEndTime) { startTime = new Date(customStartTime); endTime = new Date(customEndTime); } else { ({ startTime, endTime } = parseTimeRange(timeRange)); } // Build filter pattern let combinedFilter = ''; const cognitoFilter = buildCognitoFilter(eventType, userId, includeFailedAttempts); const logLevelFilter = buildLogLevelFilter(logLevel); if (logLevelFilter) { combinedFilter = logLevelFilter; } if (cognitoFilter) { combinedFilter = combinedFilter ? `${combinedFilter} ${cognitoFilter}` : cognitoFilter; } console.error(`📋 Fetching Cognito logs for ${cognitoUserPools.length} User Pool(s)...`); // Fetch logs for each Cognito User Pool const userPoolLogs = []; for (const userPool of cognitoUserPools) { console.error(` 📄 Processing ${userPool.name}...`); try { const filterCommand = new FilterLogEventsCommand({ logGroupName: userPool.logGroupName, startTime: startTime.getTime(), endTime: endTime.getTime(), filterPattern: combinedFilter || undefined, limit: maxEvents }); const logResponse = await clients.cloudwatchLogs.send(filterCommand); const logs = logResponse.events?.map((event) => { const cognitoDetails = extractCognitoEventDetails(event.message || ''); return { timestamp: new Date(event.timestamp).toISOString(), message: event.message, logStream: event.logStreamName, requestId: extractRequestId(event.message || ''), ...cognitoDetails }; }) || []; // Calculate event type statistics const eventStats = {}; logs.forEach((log) => { if (log.eventType) { eventStats[log.eventType] = (eventStats[log.eventType] || 0) + 1; } }); // Calculate success/failure statistics const successCount = logs.filter((log) => log.success === true).length; const failureCount = logs.filter((log) => log.success === false).length; const unknownCount = logs.filter((log) => log.success === undefined).length; userPoolLogs.push({ userPoolId: userPool.userPoolId, logicalId: userPool.logicalId, name: userPool.name, logGroupName: userPool.logGroupName, logsFound: logs.length, logs: logs.slice(0, maxEvents), eventStats, successFailureStats: { successful: successCount, failed: failureCount, unknown: unknownCount } }); } catch (error) { console.error(`❌ Error fetching logs for ${userPool.name}:`, error); userPoolLogs.push({ userPoolId: userPool.userPoolId, logicalId: userPool.logicalId, name: userPool.name, logGroupName: userPool.logGroupName, logsFound: 0, logs: [], error: error instanceof Error ? error.message : String(error) }); } } // Calculate summary statistics const totalLogs = userPoolLogs.reduce((sum, pool) => sum + pool.logsFound, 0); const userPoolsWithLogs = userPoolLogs.filter(pool => pool.logsFound > 0).length; const userPoolsWithErrors = userPoolLogs.filter(pool => pool.error).length; // Calculate overall event type statistics const overallEventStats = {}; const overallSuccessFailureStats = { successful: 0, failed: 0, unknown: 0 }; userPoolLogs.forEach(pool => { if (pool.eventStats) { Object.entries(pool.eventStats).forEach(([eventType, count]) => { overallEventStats[eventType] = (overallEventStats[eventType] || 0) + count; }); } if (pool.successFailureStats) { overallSuccessFailureStats.successful += pool.successFailureStats.successful; overallSuccessFailureStats.failed += pool.successFailureStats.failed; overallSuccessFailureStats.unknown += pool.successFailureStats.unknown; } }); // Extract unique users const uniqueUsers = new Set(); userPoolLogs.forEach(pool => { pool.logs.forEach((log) => { if (log.userId) uniqueUsers.add(log.userId); if (log.username) uniqueUsers.add(log.username); }); }); return { success: true, appId, branchName, timeRange: customStartTime ? 'custom' : timeRange, startTime: startTime.toISOString(), endTime: endTime.toISOString(), eventType, userId: userId || null, includeFailedAttempts, logLevel, filterPattern: combinedFilter || null, totalUserPools: cognitoUserPools.length, userPoolsWithLogs, userPoolsWithErrors, totalLogs, uniqueUsers: uniqueUsers.size, overallEventStats, overallSuccessFailureStats, userPoolLogs, summary: { queryInfo: { timeRange: customStartTime ? 'custom' : timeRange, eventType, userId: userId || null, includeFailedAttempts, logLevel, maxEvents }, results: { totalLogs, userPoolsWithLogs, userPoolsWithErrors, uniqueUsers: uniqueUsers.size, eventBreakdown: overallEventStats, successRate: overallSuccessFailureStats.successful + overallSuccessFailureStats.failed > 0 ? (overallSuccessFailureStats.successful / (overallSuccessFailureStats.successful + overallSuccessFailureStats.failed) * 100).toFixed(1) + '%' : 'N/A' }, securityInsights: { hasFailedAttempts: overallSuccessFailureStats.failed > 0, failedAttemptRate: overallSuccessFailureStats.failed + overallSuccessFailureStats.successful > 0 ? (overallSuccessFailureStats.failed / (overallSuccessFailureStats.failed + overallSuccessFailureStats.successful) * 100).toFixed(1) + '%' : 'N/A', uniqueUsersActive: uniqueUsers.size, mostCommonEvent: Object.entries(overallEventStats).sort((a, b) => b[1] - a[1])[0]?.[0] || 'none' }, recommendations: [ totalLogs === 0 ? "No Cognito logs found. Check if authentication is active." : null, overallSuccessFailureStats.failed > 0 ? "Failed authentication attempts detected. Monitor for potential security issues." : null, uniqueUsers.size > 0 ? `${uniqueUsers.size}