gcp-error-reporting-analyse-trends
Analyze error trends in Google Cloud Error Reporting to identify patterns, prioritize issues, and improve system reliability using actionable insights derived from error logs.
Input Schema
TableJSON Schema
| Name | Required | Description | Default |
|---|---|---|---|
No arguments | |||
Implementation Reference
- Executes GCP Error Reporting API call for group stats with timed counts to analyze trends, aggregates data over time buckets, identifies spikes, summarizes top error groups, and returns a comprehensive Markdown analysis report.async ({ timeRange, serviceFilter, resolution }) => { try { const projectId = await getProjectId(); // Initialize Google Auth client (same pattern as trace service) const auth = await initGoogleAuth(true); if (!auth) { throw new GcpMcpError( "Google Cloud authentication not available. Please configure authentication to access error reporting data.", "UNAUTHENTICATED", 401, ); } const client = await auth.getClient(); const token = await client.getAccessToken(); // Parse time range - ensure we have a valid timeRange value const actualTimeRange = timeRange || "24h"; const actualResolution = resolution || "1h"; // Map time range to Google Cloud Error Reporting periods let period: string; switch (actualTimeRange) { case "1h": period = "PERIOD_1_HOUR"; break; case "6h": period = "PERIOD_6_HOURS"; break; case "24h": case "1d": period = "PERIOD_1_DAY"; break; case "7d": period = "PERIOD_1_WEEK"; break; case "30d": period = "PERIOD_30_DAYS"; break; default: // Default to 1 day for trend analysis period = "PERIOD_1_DAY"; break; } // Calculate timed count duration based on resolution let timedCountDuration: string; switch (actualResolution) { case "1m": timedCountDuration = "60s"; break; case "5m": timedCountDuration = "300s"; break; case "1h": timedCountDuration = "3600s"; break; case "1d": timedCountDuration = "86400s"; break; default: timedCountDuration = "3600s"; // Default to 1 hour break; } // Build query parameters for trends analysis const params = new URLSearchParams({ "timeRange.period": period, timedCountDuration: timedCountDuration, order: "COUNT_DESC", pageSize: "50", }); // Add service filter if provided if (serviceFilter) { params.set("serviceFilter.service", serviceFilter); } // Make REST API call for trends const apiUrl = `https://clouderrorreporting.googleapis.com/v1beta1/projects/${projectId}/groupStats?${params}`; const response = await fetch(apiUrl, { method: "GET", headers: { Authorization: `Bearer ${token.token}`, Accept: "application/json", }, }); if (!response.ok) { const errorText = await response.text(); throw new GcpMcpError( `Failed to fetch error trends: ${errorText}`, "FAILED_PRECONDITION", response.status, ); } const data = await response.json(); const errorGroupStats = data.errorGroupStats || []; if (!errorGroupStats || errorGroupStats.length === 0) { return { content: [ { type: "text", text: `# Error Trends Analysis\n\nProject: ${projectId}\nTime Range: ${actualTimeRange}\n${serviceFilter ? `Service Filter: ${serviceFilter}\n` : ""}\nResolution: ${actualResolution}\n\nNo error data found for trend analysis.`, }, ], }; } let content = `# Error Trends Analysis\n\nProject: ${projectId}\nTime Range: ${actualTimeRange}\n${serviceFilter ? `Service Filter: ${serviceFilter}\n` : ""}\nResolution: ${actualResolution}\n\n`; // Aggregate trends across all error groups const timeSlots = new Map<string, number>(); let totalErrors = 0; const totalGroups = errorGroupStats.length; errorGroupStats.forEach((stat: any) => { const count = parseInt(stat.count || "0"); totalErrors += count; if (stat.timedCounts) { stat.timedCounts.forEach((timedCount: any) => { const timeKey = timedCount.startTime; const currentCount = timeSlots.get(timeKey) || 0; timeSlots.set( timeKey, currentCount + parseInt(timedCount.count || "0"), ); }); } }); content += `## Summary\n\n`; content += `- **Total Error Groups:** ${totalGroups}\n`; content += `- **Total Errors:** ${totalErrors.toLocaleString()}\n`; content += `- **Average per Group:** ${Math.round(totalErrors / totalGroups).toLocaleString()}\n\n`; // Sort time slots chronologically const sortedTimeSlots = Array.from(timeSlots.entries()).sort( ([a], [b]) => new Date(a).getTime() - new Date(b).getTime(), ); // Initialize variables for recommendations let averageErrors = 0; let spikes: Array<[string, number]> = []; if (sortedTimeSlots.length > 0) { content += `## Error Count Over Time\n\n`; content += `| Time Period | Error Count |\n`; content += `|-------------|-------------|\n`; sortedTimeSlots.forEach(([time, count]) => { const timeStr = new Date(time).toLocaleString(); content += `| ${timeStr} | ${count.toLocaleString()} |\n`; }); content += `\n`; // Identify spikes (errors significantly above average) averageErrors = totalErrors / sortedTimeSlots.length; spikes = sortedTimeSlots.filter( ([, count]) => count > averageErrors * 2, ); if (spikes.length > 0) { content += `## Error Spikes Detected\n\n`; content += `*Time periods with error counts > 2x average (${Math.round(averageErrors)})*\n\n`; spikes.forEach(([time, count]) => { const timeStr = new Date(time).toLocaleString(); const multiplier = Math.round((count / averageErrors) * 10) / 10; content += `- **${timeStr}:** ${count.toLocaleString()} errors (${multiplier}x average)\n`; }); content += `\n`; } } // Top error groups contributing to trends content += `## Top Contributing Error Groups\n\n`; const topErrors = errorGroupStats.slice(0, 5).map((stat: any) => ({ service: stat.representative?.serviceContext?.service || "Unknown", message: stat.representative?.message || "No message", count: parseInt(stat.count || "0"), groupId: stat.group?.groupId || "unknown", })); topErrors.forEach( ( error: { service: string; message: string; count: number; groupId: string; }, index: number, ) => { const percentage = Math.round((error.count / totalErrors) * 100); content += `${index + 1}. **${error.service}** (${percentage}% of total)\n`; content += ` - ${error.message}\n`; content += ` - ${error.count.toLocaleString()} occurrences\n`; content += ` - Group ID: ${error.groupId}\n\n`; }, ); // Recommendations based on trends content += `## Recommendations\n\n`; if (spikes.length > 0) { content += `- **Investigate Error Spikes:** Focus on the ${spikes.length} time periods with significantly elevated error rates\n`; content += `- **Correlate with Deployments:** Check if error spikes align with recent deployments or configuration changes\n`; } content += `- **Monitor Top Contributors:** The top ${Math.min(3, topErrors.length)} error groups account for the majority of errors\n`; content += `- **Set Up Alerting:** Configure alerts for error rates exceeding ${Math.round(averageErrors * 1.5)} errors per ${resolution}\n`; content += `- **Review Patterns:** Look for recurring patterns in error timing to identify systemic issues\n`; return { content: [ { type: "text", text: content, }, ], }; } catch (error: unknown) { const errorMessage = error instanceof Error ? error.message : "Unknown error"; throw new GcpMcpError( `Failed to analyse error trends: ${errorMessage}`, "INTERNAL_ERROR", 500, ); } },
- Zod schema validating input parameters: timeRange (optional, default '24h'), serviceFilter (optional string), resolution (enum '1m'|'5m'|'1h'|'1d', default '1h').inputSchema: { timeRange: z .string() .optional() .default("24h") .describe('Time range to analyse (e.g., "1h", "24h", "7d")'), serviceFilter: z.string().optional().describe("Filter by service name"), resolution: z .enum(["1m", "5m", "1h", "1d"]) .optional() .default("1h") .describe("Time resolution for trend analysis"), },
- src/services/error-reporting/tools.ts:408-661 (registration)Registers the 'gcp-error-reporting-analyse-trends' tool on the MCP server with title, description, input schema, and handler function.// Tool to analyse error trends over time server.tool( "gcp-error-reporting-analyse-trends", { title: "Analyse Error Trends", description: "Analyse error trends over time to identify patterns and spikes", inputSchema: { timeRange: z .string() .optional() .default("24h") .describe('Time range to analyse (e.g., "1h", "24h", "7d")'), serviceFilter: z.string().optional().describe("Filter by service name"), resolution: z .enum(["1m", "5m", "1h", "1d"]) .optional() .default("1h") .describe("Time resolution for trend analysis"), }, }, async ({ timeRange, serviceFilter, resolution }) => { try { const projectId = await getProjectId(); // Initialize Google Auth client (same pattern as trace service) const auth = await initGoogleAuth(true); if (!auth) { throw new GcpMcpError( "Google Cloud authentication not available. Please configure authentication to access error reporting data.", "UNAUTHENTICATED", 401, ); } const client = await auth.getClient(); const token = await client.getAccessToken(); // Parse time range - ensure we have a valid timeRange value const actualTimeRange = timeRange || "24h"; const actualResolution = resolution || "1h"; // Map time range to Google Cloud Error Reporting periods let period: string; switch (actualTimeRange) { case "1h": period = "PERIOD_1_HOUR"; break; case "6h": period = "PERIOD_6_HOURS"; break; case "24h": case "1d": period = "PERIOD_1_DAY"; break; case "7d": period = "PERIOD_1_WEEK"; break; case "30d": period = "PERIOD_30_DAYS"; break; default: // Default to 1 day for trend analysis period = "PERIOD_1_DAY"; break; } // Calculate timed count duration based on resolution let timedCountDuration: string; switch (actualResolution) { case "1m": timedCountDuration = "60s"; break; case "5m": timedCountDuration = "300s"; break; case "1h": timedCountDuration = "3600s"; break; case "1d": timedCountDuration = "86400s"; break; default: timedCountDuration = "3600s"; // Default to 1 hour break; } // Build query parameters for trends analysis const params = new URLSearchParams({ "timeRange.period": period, timedCountDuration: timedCountDuration, order: "COUNT_DESC", pageSize: "50", }); // Add service filter if provided if (serviceFilter) { params.set("serviceFilter.service", serviceFilter); } // Make REST API call for trends const apiUrl = `https://clouderrorreporting.googleapis.com/v1beta1/projects/${projectId}/groupStats?${params}`; const response = await fetch(apiUrl, { method: "GET", headers: { Authorization: `Bearer ${token.token}`, Accept: "application/json", }, }); if (!response.ok) { const errorText = await response.text(); throw new GcpMcpError( `Failed to fetch error trends: ${errorText}`, "FAILED_PRECONDITION", response.status, ); } const data = await response.json(); const errorGroupStats = data.errorGroupStats || []; if (!errorGroupStats || errorGroupStats.length === 0) { return { content: [ { type: "text", text: `# Error Trends Analysis\n\nProject: ${projectId}\nTime Range: ${actualTimeRange}\n${serviceFilter ? `Service Filter: ${serviceFilter}\n` : ""}\nResolution: ${actualResolution}\n\nNo error data found for trend analysis.`, }, ], }; } let content = `# Error Trends Analysis\n\nProject: ${projectId}\nTime Range: ${actualTimeRange}\n${serviceFilter ? `Service Filter: ${serviceFilter}\n` : ""}\nResolution: ${actualResolution}\n\n`; // Aggregate trends across all error groups const timeSlots = new Map<string, number>(); let totalErrors = 0; const totalGroups = errorGroupStats.length; errorGroupStats.forEach((stat: any) => { const count = parseInt(stat.count || "0"); totalErrors += count; if (stat.timedCounts) { stat.timedCounts.forEach((timedCount: any) => { const timeKey = timedCount.startTime; const currentCount = timeSlots.get(timeKey) || 0; timeSlots.set( timeKey, currentCount + parseInt(timedCount.count || "0"), ); }); } }); content += `## Summary\n\n`; content += `- **Total Error Groups:** ${totalGroups}\n`; content += `- **Total Errors:** ${totalErrors.toLocaleString()}\n`; content += `- **Average per Group:** ${Math.round(totalErrors / totalGroups).toLocaleString()}\n\n`; // Sort time slots chronologically const sortedTimeSlots = Array.from(timeSlots.entries()).sort( ([a], [b]) => new Date(a).getTime() - new Date(b).getTime(), ); // Initialize variables for recommendations let averageErrors = 0; let spikes: Array<[string, number]> = []; if (sortedTimeSlots.length > 0) { content += `## Error Count Over Time\n\n`; content += `| Time Period | Error Count |\n`; content += `|-------------|-------------|\n`; sortedTimeSlots.forEach(([time, count]) => { const timeStr = new Date(time).toLocaleString(); content += `| ${timeStr} | ${count.toLocaleString()} |\n`; }); content += `\n`; // Identify spikes (errors significantly above average) averageErrors = totalErrors / sortedTimeSlots.length; spikes = sortedTimeSlots.filter( ([, count]) => count > averageErrors * 2, ); if (spikes.length > 0) { content += `## Error Spikes Detected\n\n`; content += `*Time periods with error counts > 2x average (${Math.round(averageErrors)})*\n\n`; spikes.forEach(([time, count]) => { const timeStr = new Date(time).toLocaleString(); const multiplier = Math.round((count / averageErrors) * 10) / 10; content += `- **${timeStr}:** ${count.toLocaleString()} errors (${multiplier}x average)\n`; }); content += `\n`; } } // Top error groups contributing to trends content += `## Top Contributing Error Groups\n\n`; const topErrors = errorGroupStats.slice(0, 5).map((stat: any) => ({ service: stat.representative?.serviceContext?.service || "Unknown", message: stat.representative?.message || "No message", count: parseInt(stat.count || "0"), groupId: stat.group?.groupId || "unknown", })); topErrors.forEach( ( error: { service: string; message: string; count: number; groupId: string; }, index: number, ) => { const percentage = Math.round((error.count / totalErrors) * 100); content += `${index + 1}. **${error.service}** (${percentage}% of total)\n`; content += ` - ${error.message}\n`; content += ` - ${error.count.toLocaleString()} occurrences\n`; content += ` - Group ID: ${error.groupId}\n\n`; }, ); // Recommendations based on trends content += `## Recommendations\n\n`; if (spikes.length > 0) { content += `- **Investigate Error Spikes:** Focus on the ${spikes.length} time periods with significantly elevated error rates\n`; content += `- **Correlate with Deployments:** Check if error spikes align with recent deployments or configuration changes\n`; } content += `- **Monitor Top Contributors:** The top ${Math.min(3, topErrors.length)} error groups account for the majority of errors\n`; content += `- **Set Up Alerting:** Configure alerts for error rates exceeding ${Math.round(averageErrors * 1.5)} errors per ${resolution}\n`; content += `- **Review Patterns:** Look for recurring patterns in error timing to identify systemic issues\n`; return { content: [ { type: "text", text: content, }, ], }; } catch (error: unknown) { const errorMessage = error instanceof Error ? error.message : "Unknown error"; throw new GcpMcpError( `Failed to analyse error trends: ${errorMessage}`, "INTERNAL_ERROR", 500, ); } }, );