UNPKG

@cloud-carbon-footprint/gcp

Version:

The core logic to get cloud usage data and estimate energy and carbon emissions from Google Cloud Platform.

295 lines 16.6 kB
"use strict"; var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); const ramda_1 = __importDefault(require("ramda")); const core_1 = require("@cloud-carbon-footprint/core"); const common_1 = require("@cloud-carbon-footprint/common"); const domain_1 = require("../domain"); const MachineTypes_1 = require("./MachineTypes"); const RecommendationsTypes_1 = require("./RecommendationsTypes"); const GCPRegions_1 = require("./GCPRegions"); class Recommendations { computeEstimator; hddStorageEstimator; ssdStorageEstimator; googleServiceWrapper; RECOMMENDER_IDS = [ 'google.compute.image.IdleResourceRecommender', 'google.compute.address.IdleResourceRecommender', 'google.compute.disk.IdleResourceRecommender', 'google.compute.instance.IdleResourceRecommender', 'google.compute.instance.MachineTypeRecommender', 'google.compute.instanceGroupManager.MachineTypeRecommender', 'google.logging.productSuggestion.ContainerRecommender', 'google.monitoring.productSuggestion.ComputeRecommender', ]; primaryImpactPerformance = 'PERFORMANCE'; recommendationsLogger; costAndCo2eTotals; constructor(computeEstimator, hddStorageEstimator, ssdStorageEstimator, googleServiceWrapper) { this.computeEstimator = computeEstimator; this.hddStorageEstimator = hddStorageEstimator; this.ssdStorageEstimator = ssdStorageEstimator; this.googleServiceWrapper = googleServiceWrapper; this.recommendationsLogger = new common_1.Logger('GCPRecommendations'); this.costAndCo2eTotals = { cost: 0, kilowattHours: 0, }; } async getRecommendations() { const activeProjectsAndZones = await this.googleServiceWrapper.getActiveProjectsAndZones(); return await this.getRecommendationsForProjects(activeProjectsAndZones); } async getRecommendationsForProjects(projects) { return ramda_1.default.flatten(await Promise.all(projects.map(async (project) => { return await this.getFilteredRecommendations(project); }))); } async getFilteredRecommendations(project) { const recommendationsByIds = await Promise.all(project.zones.map(async (zone) => { return await this.googleServiceWrapper.getRecommendationsForRecommenderIds(project.id, zone, this.RECOMMENDER_IDS); })); const nonEmptyRecommendations = recommendationsByIds .flat() .filter((recommendationId) => recommendationId.recommendations.length > 0); const unknownRecommendations = []; if (nonEmptyRecommendations.length > 0) { const recommendationsResult = []; await Promise.all(ramda_1.default.flatten(nonEmptyRecommendations.map(({ zone, recommendations }) => recommendations.map(async (rec) => { const [estimatedCO2eSavings, resourceDetails] = await this.getEstimatedCO2eSavings(project.id, zone, rec); const cost = this.getEstimatedCostSavings(rec); if (rec.recommenderSubtype === RecommendationsTypes_1.RECOMMENDATION_TYPES.DELETE_ADDRESS) { unknownRecommendations.push({ rec, zone, cost, resourceDetails, }); return; } else { this.accumulateCostAndCo2e(cost, estimatedCO2eSavings.kilowattHours); recommendationsResult.push({ cloudProvider: 'GCP', accountId: project.id, accountName: project.name, region: this.parseRegionFromZone(zone), recommendationType: rec.recommenderSubtype, recommendationDetail: rec.description, costSavings: cost, co2eSavings: estimatedCO2eSavings.co2e, kilowattHourSavings: estimatedCO2eSavings.kilowattHours, resourceId: resourceDetails.resourceId, instanceName: resourceDetails.resourceName, }); } })))); for (let i = 0; i < unknownRecommendations.length; i++) { const { rec, zone, cost, resourceDetails } = unknownRecommendations[i]; const { co2e, kilowattHours } = await this.getCo2eEstimationsForUnknowns(zone, cost); recommendationsResult.push({ cloudProvider: 'GCP', accountId: project.id, accountName: project.name, region: this.parseRegionFromZone(zone), recommendationType: rec.recommenderSubtype, recommendationDetail: rec.description, costSavings: cost, co2eSavings: co2e, kilowattHourSavings: kilowattHours, resourceId: resourceDetails.resourceId, instanceName: resourceDetails.resourceName, }); } return recommendationsResult; } return []; } async getCo2eEstimationsForUnknowns(zone, cost) { if (this.costAndCo2eTotals.cost === 0) return { co2e: 0, kilowattHours: 0, }; const region = this.parseRegionFromZone(zone); const emissionsFactors = await this.getEmissionsFactors(region, this.recommendationsLogger); const kilowattHoursPerCost = this.costAndCo2eTotals.kilowattHours / this.costAndCo2eTotals.cost; const kilowattHours = cost * kilowattHoursPerCost; const co2e = kilowattHours * emissionsFactors[this.parseRegionFromZone(zone)]; return { co2e, kilowattHours }; } accumulateCostAndCo2e(cost, kilowattHours) { this.costAndCo2eTotals.cost += cost; this.costAndCo2eTotals.kilowattHours += kilowattHours; } async getEstimatedCO2eSavings(projectId, zone, recommendation) { let footprintEstimate = { timestamp: undefined, kilowattHours: 0, co2e: 0, }; let resourceDetails = { resourceId: '', resourceName: recommendation.content.operationGroups[0].operations[0].resource .split('/') .pop(), }; let diskDetails; try { switch (recommendation.recommenderSubtype) { case RecommendationsTypes_1.RECOMMENDATION_TYPES.STOP_VM: const instanceDetails = await this.getInstanceDetails(recommendation, projectId, zone); const computeCO2eEstimatedSavings = await this.getCO2EstimatedSavingsForInstance(projectId, instanceDetails, zone); const storageFootprintEstimates = await Promise.all(instanceDetails.disks.map(async (disk) => { diskDetails = await this.googleServiceWrapper.getDiskDetails(projectId, disk.source.split('disks/').pop(), zone); return await this.getCO2EstimatesSavingsForDisk(diskDetails, zone); })); const storageKilowattHoursSavings = ramda_1.default.sum(storageFootprintEstimates.map((estimate) => estimate.kilowattHours)); const storageCo2eSavings = ramda_1.default.sum(storageFootprintEstimates.map((estimate) => estimate.co2e)); footprintEstimate = { co2e: computeCO2eEstimatedSavings.co2e + storageCo2eSavings, kilowattHours: computeCO2eEstimatedSavings.kilowattHours + storageKilowattHoursSavings, timestamp: undefined, }; resourceDetails = { resourceId: instanceDetails.id.toString(), resourceName: instanceDetails.name, }; return [footprintEstimate, resourceDetails]; case RecommendationsTypes_1.RECOMMENDATION_TYPES.CHANGE_MACHINE_TYPE: const currentMachineType = recommendation.description .replace('.', '') .split(' ')[7]; const currentComputeCO2e = await this.getCO2EstimatedSavingsForMachineType(projectId, zone, currentMachineType); const newMachineType = recommendation.description .replace('.', '') .split(' ')[9]; const newComputeCO2e = await this.getCO2EstimatedSavingsForMachineType(projectId, zone, newMachineType); footprintEstimate = { co2e: currentComputeCO2e.co2e - newComputeCO2e.co2e, kilowattHours: currentComputeCO2e.kilowattHours - newComputeCO2e.kilowattHours, timestamp: undefined, }; const currentMachineInstanceDetails = await this.getInstanceDetails(recommendation, projectId, zone); resourceDetails = { resourceId: currentMachineInstanceDetails.id.toString(), resourceName: currentMachineInstanceDetails.name, }; return [footprintEstimate, resourceDetails]; case RecommendationsTypes_1.RECOMMENDATION_TYPES.SNAPSHOT_AND_DELETE_DISK: case RecommendationsTypes_1.RECOMMENDATION_TYPES.DELETE_DISK: diskDetails = await this.googleServiceWrapper.getDiskDetails(projectId, recommendation.description.split("'")[1], zone); footprintEstimate = await this.getCO2EstimatesSavingsForDisk(diskDetails, zone); resourceDetails = { resourceId: diskDetails.id.toString(), resourceName: diskDetails.name, }; return [footprintEstimate, resourceDetails]; case RecommendationsTypes_1.RECOMMENDATION_TYPES.DELETE_IMAGE: const imageId = recommendation.description.split("'")[1]; const imageDetails = await this.googleServiceWrapper.getImageDetails(projectId, imageId); const imageArchiveSizeGigabytes = (0, common_1.convertBytesToGigabytes)(parseFloat(imageDetails.archiveSizeBytes.toString())); footprintEstimate = await this.estimateStorageCO2eSavings(imageArchiveSizeGigabytes, this.parseRegionFromZone(zone)); resourceDetails = { resourceId: imageDetails.id.toString(), resourceName: imageDetails.name, }; return [footprintEstimate, resourceDetails]; case RecommendationsTypes_1.RECOMMENDATION_TYPES.DELETE_ADDRESS: const addressId = recommendation.description.split("'")[1]; const addressDetails = await this.googleServiceWrapper.getAddressDetails(projectId, addressId, zone); resourceDetails = { resourceId: addressDetails.id.toString(), resourceName: addressDetails.name, }; return [footprintEstimate, resourceDetails]; default: this.recommendationsLogger.warn(`Unknown/unsupported Recommender Type: ${recommendation.recommenderSubtype}`); return [footprintEstimate, resourceDetails]; } } catch (err) { this.recommendationsLogger.warn(`There was an error in estimating CO2e Savings and getting Resource ID/Name: ${recommendation.name}. Error: ${err.message}.`); return [footprintEstimate, resourceDetails]; } } async getCO2EstimatedSavingsForMachineType(projectId, zone, currentMachineType) { const currentMachineTypeDetails = await this.googleServiceWrapper.getMachineTypeDetails(projectId, currentMachineType, zone); const currentMachineTypeVCPus = Object.keys(MachineTypes_1.SHARED_CORE_PROCESSORS_BASELINE_UTILIZATION).includes(currentMachineType) ? MachineTypes_1.SHARED_CORE_PROCESSORS_BASELINE_UTILIZATION[currentMachineType] / domain_1.GCP_CLOUD_CONSTANTS.AVG_CPU_UTILIZATION_2020 : currentMachineTypeDetails.guestCpus; return await this.estimateComputeCO2eSavings(currentMachineType.split('-')[0], currentMachineTypeVCPus, this.parseRegionFromZone(zone)); } async getCO2EstimatesSavingsForDisk(diskDetails, zone) { const storageType = this.googleServiceWrapper.getStorageTypeFromDiskName(diskDetails.type.split('/').pop()); return await this.estimateStorageCO2eSavings(parseFloat(diskDetails.sizeGb.toString()), this.parseRegionFromZone(zone), storageType); } async getCO2EstimatedSavingsForInstance(projectId, instanceDetails, zone) { const machineType = instanceDetails.machineType.split('/').pop(); const machineTypeDetails = await this.googleServiceWrapper.getMachineTypeDetails(projectId, machineType, zone); return await this.estimateComputeCO2eSavings(machineType.split('-')[0], machineTypeDetails.guestCpus, this.parseRegionFromZone(zone)); } async getInstanceDetails(recommendation, projectId, zone) { const instanceId = recommendation.content.operationGroups[0].operations[0].resource .split('/') .pop(); return await this.googleServiceWrapper.getInstanceDetails(projectId, instanceId, zone); } async estimateComputeCO2eSavings(machineTypeFamily, vCpus, region) { const vCpuHours = vCpus * (0, common_1.getHoursInMonth)(); const computeUsage = { cpuUtilizationAverage: domain_1.GCP_CLOUD_CONSTANTS.AVG_CPU_UTILIZATION_2020, vCpuHours: vCpuHours, usesAverageCPUConstant: true, }; const computeProcessors = MachineTypes_1.INSTANCE_TYPE_COMPUTE_PROCESSOR_MAPPING[machineTypeFamily] || [core_1.COMPUTE_PROCESSOR_TYPES.UNKNOWN]; const computeConstants = { minWatts: domain_1.GCP_CLOUD_CONSTANTS.getMinWatts(computeProcessors), maxWatts: domain_1.GCP_CLOUD_CONSTANTS.getMaxWatts(computeProcessors), powerUsageEffectiveness: domain_1.GCP_CLOUD_CONSTANTS.getPUE(region), }; const emissionsFactors = await this.getEmissionsFactors(region, this.recommendationsLogger); return this.computeEstimator.estimate([computeUsage], region, emissionsFactors, computeConstants)[0]; } async estimateStorageCO2eSavings(storageGigabytes, region, storageType) { const storageUsage = { terabyteHours: ((0, common_1.getHoursInMonth)() * storageGigabytes) / 1000, }; const storageConstants = { powerUsageEffectiveness: domain_1.GCP_CLOUD_CONSTANTS.getPUE(region), }; const emissionsFactors = await this.getEmissionsFactors(region, this.recommendationsLogger); const storageEstimator = storageType === 'SSD' ? this.ssdStorageEstimator : this.hddStorageEstimator; return { usesAverageCPUConstant: false, ...storageEstimator.estimate([storageUsage], region, emissionsFactors, storageConstants)[0], }; } getEstimatedCostSavings(rec) { let impact = rec.primaryImpact; if (rec.primaryImpact.category === this.primaryImpactPerformance) { impact = rec.additionalImpact[0]; } return Math.abs((parseInt(impact.costProjection.cost.units) + impact.costProjection.cost.nanos / 1000000000) * -1); } parseRegionFromZone(zone) { const zoneArray = zone.split('-'); return zone === 'global' ? GCPRegions_1.GCP_REGIONS.UNKNOWN : `${zoneArray[0]}-${zoneArray[1]}`; } async getEmissionsFactors(region, logger) { return await (0, common_1.getEmissionsFactors)(region, new Date().toISOString(), (0, domain_1.getGCPEmissionsFactors)(), GCPRegions_1.GCP_MAPPED_REGIONS_TO_ELECTRICITY_MAPS_ZONES, logger); } } exports.default = Recommendations; //# sourceMappingURL=Recommendations.js.map