UNPKG

azureai-optimizer

Version:

AI-Powered Azure Infrastructure Optimization via Model Context Protocol

299 lines (295 loc) 12.5 kB
/** * Cost Analysis Tool * AI-powered Azure cost optimization analysis */ // import { CostManagementClient } from '@azure/arm-costmanagement'; // import { AdvisorManagementClient } from '@azure/arm-advisor'; import { Logger } from '../utils/logger.js'; import { AIAnalyzer } from '../ai/analyzer.js'; import { MCPError, ErrorCode } from '../utils/errors.js'; export class CostAnalysisTool { name = 'analyze_cost_optimization'; description = 'AI-powered Azure cost analysis with optimization recommendations'; inputSchema = { type: 'object', properties: { subscription_id: { type: 'string', description: 'Azure subscription ID to analyze' }, resource_group: { type: 'string', description: 'Optional: Specific resource group to analyze' }, days_back: { type: 'number', description: 'Number of days to analyze (default: 30)', default: 30, minimum: 1, maximum: 365 }, include_recommendations: { type: 'boolean', description: 'Include Azure Advisor recommendations (default: true)', default: true }, granularity: { type: 'string', enum: ['daily', 'monthly'], description: 'Cost data granularity (default: daily)', default: 'daily' }, group_by: { type: 'array', items: { type: 'string', enum: ['ServiceName', 'ResourceGroupName', 'ResourceLocation', 'ResourceType'] }, description: 'Group cost data by specified dimensions' } }, required: ['subscription_id'] }; aiAnalyzer; logger; constructor(config) { this.logger = new Logger('CostAnalysisTool'); this.aiAnalyzer = new AIAnalyzer(config.aiProviders); } async execute(args, _context) { const startTime = Date.now(); let apiCalls = 0; try { this.logger.info(`🔍 Starting cost analysis for subscription: ${args.subscription_id}`); // Initialize Azure clients // this.costClient = new CostManagementClient(context.credential); // this.advisorClient = new AdvisorManagementClient(context.credential, args.subscription_id); // Set default values const daysBack = args.days_back || 30; const granularity = args.granularity || 'daily'; const includeRecommendations = args.include_recommendations !== false; // Build cost query // const costQuery = this._buildCostQuery(args, daysBack, granularity); // Fetch cost data this.logger.info('📊 Fetching cost data from Azure Cost Management API...'); // Stub implementation - actual implementation would use proper Azure Cost Management API const costData = { properties: { rows: [], columns: [] } }; // const costData = await this.costClient.query(`/subscriptions/${args.subscription_id}`, costQuery); apiCalls++; // Process cost data const processedCostData = this.processCostData(costData, daysBack); // Fetch Azure Advisor recommendations if requested let advisorRecommendations = []; if (includeRecommendations) { this.logger.info('💡 Fetching Azure Advisor recommendations...'); try { // const recommendations = await this.advisorClient.recommendations.list(); // advisorRecommendations = this._processAdvisorRecommendations(recommendations); advisorRecommendations = []; apiCalls++; } catch (error) { this.logger.warn('⚠️ Failed to fetch Advisor recommendations:', error); } } // AI-powered analysis this.logger.info('🤖 Performing AI analysis of cost data...'); const aiInsights = await this.aiAnalyzer.analyzeCostData({ costData: processedCostData, advisorRecommendations, subscriptionId: args.subscription_id, analysisContext: { daysBack, ...(args.resource_group ? { resourceGroup: args.resource_group } : {}), granularity } }); // Build result const result = { success: true, data: { cost_summary: processedCostData.summary, cost_breakdown: processedCostData.breakdown, trends: processedCostData.trends, ai_insights: aiInsights, ...(includeRecommendations && { advisor_recommendations: advisorRecommendations }) }, metadata: { execution_time: Date.now() - startTime, api_calls: apiCalls, data_freshness: new Date().toISOString() } }; this.logger.info(`✅ Cost analysis completed successfully`); this.logger.info(`💰 Total cost analyzed: ${processedCostData.summary.total_cost} ${processedCostData.summary.currency}`); this.logger.info(`🎯 Found ${aiInsights.top_optimization_opportunities.length} optimization opportunities`); return result; } catch (error) { this.logger.error('❌ Cost analysis failed:', error); if (error instanceof MCPError) { throw error; } throw new MCPError(ErrorCode.INTERNAL_ERROR, `Cost analysis failed: ${error instanceof Error ? error.message : 'Unknown error'}`); } } /* Commented out - stub implementation private _buildCostQuery(args: CostAnalysisArgs, daysBack: number, granularity: string): any { const endDate = new Date(); const startDate = new Date(); startDate.setDate(endDate.getDate() - daysBack); const query: any = { type: 'ActualCost', timeframe: 'Custom', timePeriod: { from: startDate.toISOString().split('T')[0], to: endDate.toISOString().split('T')[0] }, dataset: { granularity: granularity === 'daily' ? 'Daily' : 'Monthly', aggregation: { totalCost: { name: 'PreTaxCost', function: 'Sum' } }, grouping: [] as any[] } }; // Add grouping dimensions if (args.group_by) { query.dataset.grouping = args.group_by.map((dimension: string) => ({ type: 'Dimension', name: dimension })); } else { // Default grouping by service query.dataset.grouping.push({ type: 'Dimension', name: 'ServiceName' }); } // Add resource group filter if specified if (args.resource_group) { (query.dataset as any).filter = { dimensions: { name: 'ResourceGroupName', operator: 'In', values: [args.resource_group] } }; } return query; } */ processCostData(costData, daysBack) { // Process the raw cost data from Azure Cost Management API // This is a simplified implementation - production would handle more complex scenarios const rows = costData.properties?.rows || []; // const _columns = costData.properties?.columns || []; let totalCost = 0; const serviceBreakdown = {}; const dailyCosts = []; // Process each row of cost data rows.forEach((row) => { const cost = row[0] || 0; // Assuming cost is in first column const service = row[1] || 'Unknown'; // Assuming service name is in second column const date = row[2] || new Date().toISOString(); // Assuming date is in third column totalCost += cost; serviceBreakdown[service] = (serviceBreakdown[service] || 0) + cost; // Group daily costs const dateKey = new Date(date).toISOString().split('T')[0]; const existingDay = dailyCosts.find(d => d.date === dateKey); if (existingDay) { existingDay.cost += cost; } else { dailyCosts.push({ date: dateKey, cost }); } }); // Convert service breakdown to array and calculate percentages const byService = Object.entries(serviceBreakdown) .map(([service, cost]) => ({ service, cost, percentage: totalCost > 0 ? (cost / totalCost) * 100 : 0 })) .sort((a, b) => b.cost - a.cost); // Analyze trends const costTrend = this.analyzeCostTrend(dailyCosts); const anomalies = this.detectCostAnomalies(dailyCosts); return { summary: { total_cost: totalCost, currency: 'USD', // This should come from the API response period: `${daysBack} days`, cost_change_percentage: this.calculateCostChange(dailyCosts) }, breakdown: { by_service: byService }, trends: { daily_costs: dailyCosts.sort((a, b) => a.date.localeCompare(b.date)), cost_trend: costTrend, anomalies } }; } /* Commented out - stub implementation private _processAdvisorRecommendations(recommendations: any): any[] { // Process Azure Advisor recommendations return recommendations.map((rec: any) => ({ title: rec.shortDescription?.problem || 'Optimization Opportunity', description: rec.shortDescription?.solution || 'No description available', category: rec.category || 'Cost', impact: rec.impact || 'Medium', potential_savings: rec.extendedProperties?.savingsAmount || 0 })); } */ analyzeCostTrend(dailyCosts) { if (dailyCosts.length < 2) return 'stable'; const firstHalf = dailyCosts.slice(0, Math.floor(dailyCosts.length / 2)); const secondHalf = dailyCosts.slice(Math.floor(dailyCosts.length / 2)); const firstHalfAvg = firstHalf.reduce((sum, day) => sum + day.cost, 0) / firstHalf.length; const secondHalfAvg = secondHalf.reduce((sum, day) => sum + day.cost, 0) / secondHalf.length; const changePercentage = ((secondHalfAvg - firstHalfAvg) / firstHalfAvg) * 100; if (changePercentage > 10) return 'increasing'; if (changePercentage < -10) return 'decreasing'; return 'stable'; } detectCostAnomalies(dailyCosts) { // Simple anomaly detection using standard deviation const costs = dailyCosts.map(d => d.cost); const mean = costs.reduce((sum, cost) => sum + cost, 0) / costs.length; const stdDev = Math.sqrt(costs.reduce((sum, cost) => sum + Math.pow(cost - mean, 2), 0) / costs.length); const threshold = 2; // 2 standard deviations return dailyCosts .filter(day => Math.abs(day.cost - mean) > threshold * stdDev) .map(day => ({ date: day.date, cost: day.cost, expected_cost: mean, deviation: ((day.cost - mean) / mean) * 100 })); } calculateCostChange(dailyCosts) { if (dailyCosts.length < 2) return 0; const sortedCosts = dailyCosts.sort((a, b) => a.date.localeCompare(b.date)); const firstWeek = sortedCosts.slice(0, 7); const lastWeek = sortedCosts.slice(-7); const firstWeekAvg = firstWeek.reduce((sum, day) => sum + day.cost, 0) / firstWeek.length; const lastWeekAvg = lastWeek.reduce((sum, day) => sum + day.cost, 0) / lastWeek.length; return firstWeekAvg > 0 ? ((lastWeekAvg - firstWeekAvg) / firstWeekAvg) * 100 : 0; } } //# sourceMappingURL=cost-analysis.js.map