gcp-billing-detect-anomalies
Identify unexpected spending patterns in Google Cloud billing data by analyzing cost anomalies within a specified timeframe and threshold for enhanced cost management.
Instructions
Detect unusual cost patterns and spending anomalies in Google Cloud billing data
Input Schema
TableJSON Schema
| Name | Required | Description | Default |
|---|---|---|---|
| billingAccountName | Yes | Billing account name (e.g., 'billingAccounts/123456-789ABC-DEF012') | |
| lookbackDays | No | Number of days to look back for comparison (7-90) | |
| projectId | No | Optional project ID to filter anomalies | |
| thresholdPercentage | No | Percentage threshold for anomaly detection (10-500%) |
Implementation Reference
- src/services/billing/tools.ts:814-923 (handler)Inline async handler function for the 'gcp-billing-detect-anomalies' tool. Generates mock current and historical cost data, calls detectCostAnomalies helper to identify anomalies exceeding the threshold, builds a formatted Markdown response with anomaly details, and handles errors.async ({ billingAccountName, lookbackDays, thresholdPercentage, projectId, }) => { try { // Use project hierarchy: provided -> state manager -> auth default const actualProjectId = projectId || stateManager.getCurrentProjectId() || (await getProjectId()); logger.debug( `Detecting cost anomalies for billing account: ${billingAccountName}, project: ${actualProjectId || "all"}`, ); // Mock current and historical cost data for demonstration const currentCosts: CostData[] = [ { billingAccountName, projectId: actualProjectId || "example-project-1", serviceId: "compute.googleapis.com", cost: { amount: 2500, currency: "USD" }, usage: { amount: 200, unit: "hours" }, period: { startTime: new Date( Date.now() - 24 * 60 * 60 * 1000, ).toISOString(), endTime: new Date().toISOString(), }, }, ]; const historicalCosts: CostData[] = [ { billingAccountName, projectId: actualProjectId || "example-project-1", serviceId: "compute.googleapis.com", cost: { amount: 1250, currency: "USD" }, usage: { amount: 100, unit: "hours" }, period: { startTime: new Date( Date.now() - lookbackDays * 24 * 60 * 60 * 1000, ).toISOString(), endTime: new Date( Date.now() - (lookbackDays - 1) * 24 * 60 * 60 * 1000, ).toISOString(), }, }, ]; const anomalies: CostAnomaly[] = detectCostAnomalies( currentCosts, historicalCosts, thresholdPercentage, ); let response = `# Cost Anomaly Detection\n\n`; response += `**Billing Account:** ${billingAccountName}\n`; response += `**Project:** ${actualProjectId || "All projects"}\n`; response += `**Lookback Period:** ${lookbackDays} days\n`; response += `**Threshold:** ${thresholdPercentage}%\n`; response += `**Anomalies Found:** ${anomalies.length}\n\n`; response += `⚠️ **Note:** This is a demonstration with mock data. `; response += `For actual anomaly detection, you would need access to historical billing data.\n\n`; if (anomalies.length > 0) { response += `## Detected Anomalies\n\n`; anomalies.forEach((anomaly: CostAnomaly, index: number) => { response += `### ${index + 1}. ${anomaly.anomalyType.toUpperCase()} - ${anomaly.severity.toUpperCase()}\n\n`; response += `**Project:** ${anomaly.projectId}\n`; response += `**Service:** ${anomaly.serviceId}\n`; response += `**Description:** ${anomaly.description}\n`; response += `**Current Cost:** ${formatCurrency(anomaly.currentCost)}\n`; response += `**Expected Cost:** ${formatCurrency(anomaly.expectedCost)}\n`; response += `**Change:** ${anomaly.percentageChange > 0 ? "+" : ""}${anomaly.percentageChange.toFixed(1)}%\n`; response += `**Detected:** ${new Date(anomaly.detectedAt).toLocaleString("en-AU")}\n`; if (anomaly.recommendations && anomaly.recommendations.length > 0) { response += `**Recommendations:**\n`; anomaly.recommendations.forEach((rec) => { response += `- ${rec}\n`; }); } response += "\n"; }); } else { response += formatCostAnomalies(anomalies); } return { content: [ { type: "text", text: response, }, ], }; } catch (error: any) { logger.error(`Error detecting cost anomalies: ${error.message}`); throw new GcpMcpError( `Failed to detect cost anomalies: ${error.message}`, error.code || "UNKNOWN", error.status || 500, ); } },
- Zod input schema definition for the tool, specifying parameters for billing account, lookback period, anomaly threshold, and optional project filter.{ title: "Detect Cost Anomalies", description: "Detect unusual cost patterns and spending anomalies in Google Cloud billing data", inputSchema: { billingAccountName: z .string() .describe( "Billing account name (e.g., 'billingAccounts/123456-789ABC-DEF012')", ), lookbackDays: z .number() .min(7) .max(90) .default(30) .describe("Number of days to look back for comparison (7-90)"), thresholdPercentage: z .number() .min(10) .max(500) .default(50) .describe("Percentage threshold for anomaly detection (10-500%)"), projectId: z .string() .optional() .describe("Optional project ID to filter anomalies"), },
- src/services/billing/tools.ts:784-924 (registration)MCP server registration of the 'gcp-billing-detect-anomalies' tool within the registerBillingTools function.server.registerTool( "gcp-billing-detect-anomalies", { title: "Detect Cost Anomalies", description: "Detect unusual cost patterns and spending anomalies in Google Cloud billing data", inputSchema: { billingAccountName: z .string() .describe( "Billing account name (e.g., 'billingAccounts/123456-789ABC-DEF012')", ), lookbackDays: z .number() .min(7) .max(90) .default(30) .describe("Number of days to look back for comparison (7-90)"), thresholdPercentage: z .number() .min(10) .max(500) .default(50) .describe("Percentage threshold for anomaly detection (10-500%)"), projectId: z .string() .optional() .describe("Optional project ID to filter anomalies"), }, }, async ({ billingAccountName, lookbackDays, thresholdPercentage, projectId, }) => { try { // Use project hierarchy: provided -> state manager -> auth default const actualProjectId = projectId || stateManager.getCurrentProjectId() || (await getProjectId()); logger.debug( `Detecting cost anomalies for billing account: ${billingAccountName}, project: ${actualProjectId || "all"}`, ); // Mock current and historical cost data for demonstration const currentCosts: CostData[] = [ { billingAccountName, projectId: actualProjectId || "example-project-1", serviceId: "compute.googleapis.com", cost: { amount: 2500, currency: "USD" }, usage: { amount: 200, unit: "hours" }, period: { startTime: new Date( Date.now() - 24 * 60 * 60 * 1000, ).toISOString(), endTime: new Date().toISOString(), }, }, ]; const historicalCosts: CostData[] = [ { billingAccountName, projectId: actualProjectId || "example-project-1", serviceId: "compute.googleapis.com", cost: { amount: 1250, currency: "USD" }, usage: { amount: 100, unit: "hours" }, period: { startTime: new Date( Date.now() - lookbackDays * 24 * 60 * 60 * 1000, ).toISOString(), endTime: new Date( Date.now() - (lookbackDays - 1) * 24 * 60 * 60 * 1000, ).toISOString(), }, }, ]; const anomalies: CostAnomaly[] = detectCostAnomalies( currentCosts, historicalCosts, thresholdPercentage, ); let response = `# Cost Anomaly Detection\n\n`; response += `**Billing Account:** ${billingAccountName}\n`; response += `**Project:** ${actualProjectId || "All projects"}\n`; response += `**Lookback Period:** ${lookbackDays} days\n`; response += `**Threshold:** ${thresholdPercentage}%\n`; response += `**Anomalies Found:** ${anomalies.length}\n\n`; response += `⚠️ **Note:** This is a demonstration with mock data. `; response += `For actual anomaly detection, you would need access to historical billing data.\n\n`; if (anomalies.length > 0) { response += `## Detected Anomalies\n\n`; anomalies.forEach((anomaly: CostAnomaly, index: number) => { response += `### ${index + 1}. ${anomaly.anomalyType.toUpperCase()} - ${anomaly.severity.toUpperCase()}\n\n`; response += `**Project:** ${anomaly.projectId}\n`; response += `**Service:** ${anomaly.serviceId}\n`; response += `**Description:** ${anomaly.description}\n`; response += `**Current Cost:** ${formatCurrency(anomaly.currentCost)}\n`; response += `**Expected Cost:** ${formatCurrency(anomaly.expectedCost)}\n`; response += `**Change:** ${anomaly.percentageChange > 0 ? "+" : ""}${anomaly.percentageChange.toFixed(1)}%\n`; response += `**Detected:** ${new Date(anomaly.detectedAt).toLocaleString("en-AU")}\n`; if (anomaly.recommendations && anomaly.recommendations.length > 0) { response += `**Recommendations:**\n`; anomaly.recommendations.forEach((rec) => { response += `- ${rec}\n`; }); } response += "\n"; }); } else { response += formatCostAnomalies(anomalies); } return { content: [ { type: "text", text: response, }, ], }; } catch (error: any) { logger.error(`Error detecting cost anomalies: ${error.message}`); throw new GcpMcpError( `Failed to detect cost anomalies: ${error.message}`, error.code || "UNKNOWN", error.status || 500, ); } }, );
- Key helper function that performs the actual anomaly detection by comparing current costs against historical data using percentage change threshold, classifying anomalies by type and severity, and generating recommendations.export function detectCostAnomalies( currentCosts: CostData[], historicalCosts: CostData[], thresholdPercentage: number = 50, ): CostAnomaly[] { const anomalies: CostAnomaly[] = []; for (const current of currentCosts) { if (!current.projectId || !current.serviceId) continue; // Find corresponding historical cost const historical = historicalCosts.find( (h) => h.projectId === current.projectId && h.serviceId === current.serviceId, ); if (!historical) continue; const percentageChange = calculatePercentageChange( current.cost.amount, historical.cost.amount, ); if (Math.abs(percentageChange) >= thresholdPercentage) { const anomalyType = percentageChange > 0 ? "spike" : "drop"; const severity = Math.abs(percentageChange) >= 100 ? "critical" : Math.abs(percentageChange) >= 75 ? "high" : Math.abs(percentageChange) >= 50 ? "medium" : "low"; anomalies.push({ projectId: current.projectId, serviceId: current.serviceId, anomalyType, severity, description: `${percentageChange > 0 ? "Significant increase" : "Significant decrease"} in costs for ${current.serviceId}`, currentCost: current.cost.amount, expectedCost: historical.cost.amount, percentageChange, detectedAt: new Date().toISOString(), period: current.period, recommendations: generateAnomalyRecommendations( anomalyType, current.serviceId, Math.abs(percentageChange), ), }); } } return anomalies; }
- Helper function used by the handler to format detected anomalies into a user-friendly Markdown response.export function formatCostAnomalies(anomalies: CostAnomaly[]): string { if (anomalies.length === 0) { return "✅ No cost anomalies detected for the specified period."; } let result = "## 🚨 Cost Anomalies Detected\n\n"; for (const anomaly of anomalies) { const severityIcon = { low: "🟡", medium: "🟠", high: "🔴", critical: "💥", }[anomaly.severity]; const changeDirection = anomaly.percentageChange > 0 ? "📈" : "📉"; result += `### ${severityIcon} ${anomaly.anomalyType.replace("_", " ").toUpperCase()}\n\n`; result += `**Project:** ${anomaly.projectId}\n`; result += `**Service:** ${anomaly.serviceId}\n`; result += `**Description:** ${anomaly.description}\n`; result += `**Change:** ${changeDirection} ${Math.abs(anomaly.percentageChange).toFixed(1)}%\n`; result += `**Current Cost:** ${formatCurrency(anomaly.currentCost)}\n`; result += `**Expected Cost:** ${formatCurrency(anomaly.expectedCost)}\n`; result += `**Detected:** ${new Date(anomaly.detectedAt).toLocaleString("en-AU")}\n`; if (anomaly.recommendations && anomaly.recommendations.length > 0) { result += "\n**Recommendations:**\n"; for (const rec of anomaly.recommendations) { result += `- ${rec}\n`; } } result += "\n---\n\n"; } return result;