azureai-optimizer
Version:
AI-Powered Azure Infrastructure Optimization via Model Context Protocol
299 lines (295 loc) • 12.5 kB
JavaScript
/**
* 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