@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
JavaScript
"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