UNPKG

@cloud-carbon-footprint/aws

Version:

The core logic to get cloud usage data and estimate energy and carbon emissions from Amazon Web Services.

406 lines (374 loc) 11.6 kB
/* * © 2021 Thoughtworks, Inc. */ import { mockClient } from 'aws-sdk-client-mock' import { ServiceWrapper } from '../lib' import mockAWSCloudWatchGetMetricDataCall from '../lib/mockAWSCloudWatchGetMetricDataCall' import EC2 from '../lib/EC2' import { buildCostExplorerGetCostResponse } from './fixtures/builders' import { AWS_CLOUD_CONSTANTS } from '../domain' import { S3Client } from '@aws-sdk/client-s3' import { CostExplorerClient, GetCostAndUsageCommand, } from '@aws-sdk/client-cost-explorer' import { CloudWatchLogsClient } from '@aws-sdk/client-cloudwatch-logs' import { CloudWatchClient } from '@aws-sdk/client-cloudwatch' const costExplorerMock = mockClient(CostExplorerClient) describe('EC2', () => { afterEach(() => { costExplorerMock.reset() }) const dayOneHourOne = '2020-07-10T21:00:00.000Z' const dayOneHourTwo = '2020-07-10T22:00:00.000Z' const dayOneHourThree = '2020-07-10T23:00:00.000Z' const dayTwoHourOne = '2020-07-11T00:00:00.000Z' const dayTwoHourTwo = '2020-07-11T01:00:00.000Z' const dayTwoHourThree = '2020-07-11T02:00:00.000Z' const region = 'us-east-one' const startDate = '2020-07-10' const endDate = '2020-07-11' const AVG_CPU_UTILIZATION_2020 = AWS_CLOUD_CONSTANTS.AVG_CPU_UTILIZATION_2020 const metricDataQueries = [ { Id: 'cpuUtilizationWithEmptyValues', Expression: "SEARCH('{AWS/EC2,InstanceId} MetricName=\"CPUUtilization\"', 'Average', 3600)", ReturnData: false, }, { Id: 'cpuUtilization', Expression: 'REMOVE_EMPTY(cpuUtilizationWithEmptyValues)', }, { Id: 'vCPUs', Expression: 'SEARCH(\'{AWS/Usage,Resource,Type,Service,Class } Resource="vCPU" MetricName="ResourceCount"\', \'Average\', 3600)', }, ] const getServiceWrapper = () => new ServiceWrapper( new CloudWatchClient(), new CloudWatchLogsClient(), new CostExplorerClient(), new S3Client(), ) it('gets EC2 usage', async () => { const response: any = { MetricDataResults: [ { Id: 'cpuUtilization', Label: 'AWS/EC2 i-01914bfb56d65a9ae CPUUtilization', Timestamps: [dayOneHourOne, dayOneHourTwo], Values: [22.983333333333334, 31.435897435897434], StatusCode: 'Complete', Messages: [], }, { Id: 'cpuUtilization', Label: 'AWS/EC2 i-0462587efbbf601c5 CPUUtilization', Timestamps: [dayOneHourTwo, dayTwoHourOne, dayTwoHourTwo], Values: [11.576923076923077, 9.716666666666667, 20.46153846153846], StatusCode: 'Complete', Messages: [], }, { Id: 'cpuUtilization', Label: 'AWS/EC2 i-0489a00f5a4835dc8 CPUUtilization', Timestamps: [dayOneHourTwo, dayTwoHourOne, dayTwoHourTwo], Values: [9.63265306122449, 13.083333333333334, 32.44444444444444], StatusCode: 'Complete', Messages: [], }, { Id: 'cpuUtilization', Label: 'AWS/EC2 i-057b1d36294c2ff23 CPUUtilization', Timestamps: [dayTwoHourTwo], Values: [10.26923076923077], StatusCode: 'Complete', Messages: [], }, { Id: 'cpuUtilization', Label: 'AWS/EC2 i-0990709d4aafe0be8 CPUUtilization', Timestamps: [dayTwoHourTwo], Values: [9.75], StatusCode: 'Complete', Messages: [], }, { Id: 'cpuUtilization', Label: 'AWS/EC2 i-0d1808334c391e056 CPUUtilization', Timestamps: [dayOneHourOne, dayOneHourTwo], Values: [11.566666666666666, 24.25], StatusCode: 'Complete', Messages: [], }, { Id: 'vCPUs', Label: 'AWS/Usage Standard/OnDemand vCPU EC2 Resource ResourceCount', Timestamps: [ dayOneHourOne, dayOneHourTwo, dayTwoHourOne, dayTwoHourTwo, ], Values: [4, 4.5, 4, 4.333333333333333], StatusCode: 'Complete', Messages: [], }, ], Messages: [], } mockAWSCloudWatchGetMetricDataCall( new Date(dayTwoHourOne), new Date(dayTwoHourThree), response, metricDataQueries, ) const ec2Service = new EC2(getServiceWrapper()) const result = await ec2Service.getUsage( new Date('2020-07-11T00:00:00Z'), new Date('2020-07-11T02:00:00Z'), ) expect(result).toEqual([ { cpuUtilizationAverage: (22.983333333333334 + 11.566666666666666) / 2, //should be the average of CPUUtilization accross vcpus per hour vCpuHours: 4, timestamp: new Date(dayOneHourOne), usesAverageCPUConstant: false, }, { cpuUtilizationAverage: (31.435897435897434 + 11.576923076923077 + 9.63265306122449 + 24.25) / 4, vCpuHours: 4.5, timestamp: new Date(dayOneHourTwo), usesAverageCPUConstant: false, }, { cpuUtilizationAverage: (9.716666666666667 + 13.083333333333334) / 2, vCpuHours: 4, timestamp: new Date(dayTwoHourOne), usesAverageCPUConstant: false, }, { cpuUtilizationAverage: (20.46153846153846 + 32.44444444444444 + 10.26923076923077 + 9.75) / 4, vCpuHours: 4.333333333333333, timestamp: new Date(dayTwoHourTwo), usesAverageCPUConstant: false, }, ]) }) it('check for PartialData', async () => { const response: any = { MetricDataResults: [ { Id: 'cpuUtilization', Label: 'AWS/EC2 i-01914bfb56d65a9ae CPUUtilization', Timestamps: [dayOneHourOne, dayOneHourTwo], Values: [22.983333333333334, 31.435897435897434], StatusCode: 'PartialData', Messages: [], }, { Id: 'cpuUtilization', Label: 'AWS/EC2 i-0462587efbbf601c5 CPUUtilization', Timestamps: [dayOneHourTwo, dayTwoHourOne, dayTwoHourTwo], Values: [11.576923076923077, 9.716666666666667, 20.46153846153846], StatusCode: 'Complete', Messages: [], }, { Id: 'vCPUs', Label: 'AWS/Usage Standard/OnDemand vCPU EC2 Resource ResourceCount', Timestamps: [ dayOneHourOne, dayOneHourTwo, dayTwoHourOne, dayTwoHourTwo, ], Values: [4, 4.5, 4, 4.333333333333333], StatusCode: 'Complete', Messages: [], }, ], Messages: [], } mockAWSCloudWatchGetMetricDataCall( new Date(dayTwoHourOne), new Date(dayTwoHourThree), response, metricDataQueries, ) const ec2Service = new EC2(getServiceWrapper()) const getEC2Usage = async () => await ec2Service.getUsage( new Date(dayTwoHourOne), new Date(dayTwoHourThree), ) await expect(getEC2Usage).rejects.toThrow('Partial Data Returned from AWS') }) describe('missing CPU utilization', () => { it('uses average CPU utilization for every missing timestamp', async () => { const response: any = { MetricDataResults: [ { Id: 'cpuUtilization', Timestamps: [dayOneHourOne], Values: [1], }, { Id: 'vCPUs', Timestamps: [dayOneHourOne, dayOneHourTwo], Values: [1, 1], }, ], } mockAWSCloudWatchGetMetricDataCall( new Date(dayOneHourOne), new Date(dayTwoHourOne), response, metricDataQueries, ) const ec2Service = new EC2(getServiceWrapper()) const result = await ec2Service.getUsage( new Date(dayOneHourOne), new Date(dayTwoHourOne), ) expect(result).toEqual([ { cpuUtilizationAverage: 1, vCpuHours: 1, timestamp: new Date(dayOneHourOne), usesAverageCPUConstant: false, }, { cpuUtilizationAverage: AVG_CPU_UTILIZATION_2020, vCpuHours: 1, timestamp: new Date(dayOneHourTwo), usesAverageCPUConstant: true, }, ]) }) it('uses average CPU utilization for every timestamp present for vCPUs', async () => { const response: any = { MetricDataResults: [ { Id: 'vCPUs', Timestamps: [dayOneHourOne, dayOneHourTwo], Values: [4, 3], }, ], } mockAWSCloudWatchGetMetricDataCall( new Date(dayOneHourOne), new Date(dayTwoHourOne), response, metricDataQueries, ) const ec2Service = new EC2(getServiceWrapper()) const result = await ec2Service.getUsage( new Date(dayOneHourOne), new Date(dayTwoHourOne), ) expect(result).toEqual([ { cpuUtilizationAverage: AVG_CPU_UTILIZATION_2020, vCpuHours: 4, timestamp: new Date(dayOneHourOne), usesAverageCPUConstant: true, }, { cpuUtilizationAverage: AVG_CPU_UTILIZATION_2020, vCpuHours: 3, timestamp: new Date(dayOneHourTwo), usesAverageCPUConstant: true, }, ]) }) }) it('should not return an estimate for a given timestamp if no vCPU data is provided for that timestamp', async () => { const response: any = { MetricDataResults: [ { Id: 'cpuUtilization', Timestamps: [dayTwoHourOne], Values: [1], }, { Id: 'cpuUtilization', Timestamps: [dayTwoHourTwo], Values: [1], }, { Id: 'vCPUs', Timestamps: [dayTwoHourTwo], Values: [1], }, ], } mockAWSCloudWatchGetMetricDataCall( new Date(dayTwoHourOne), new Date(dayTwoHourThree), response, metricDataQueries, ) const ec2Service = new EC2(getServiceWrapper()) const result = await ec2Service.getUsage( new Date(dayTwoHourOne), new Date(dayTwoHourThree), ) expect(result).toEqual([ { cpuUtilizationAverage: 1, vCpuHours: 1, timestamp: new Date(dayTwoHourTwo), usesAverageCPUConstant: false, }, ]) }) it('should return an empty array if no vCPUs', async () => { const response: any = { MetricDataResults: [ { Id: 'cpuUtilization', Timestamps: [], Values: [], }, ], } mockAWSCloudWatchGetMetricDataCall( new Date(dayOneHourOne), new Date(dayOneHourThree), response, metricDataQueries, ) const ec2Service = new EC2(getServiceWrapper()) const result = await ec2Service.getUsage( new Date(dayOneHourOne), new Date(dayOneHourThree), ) expect(result).toEqual([]) }) it('gets ec2 cost', async () => { costExplorerMock.on(GetCostAndUsageCommand).resolves( buildCostExplorerGetCostResponse([ { start: startDate, amount: 100.0, keys: ['EC2: Running Hours'] }, { start: endDate, amount: 50.0, keys: ['test'] }, ]), ) const ec2Service = new EC2(getServiceWrapper()) const ec2Costs = await ec2Service.getCosts( new Date(startDate), new Date(endDate), region, ) expect(ec2Costs).toEqual([ { amount: 100.0, currency: 'USD', timestamp: new Date(startDate) }, { amount: 50.0, currency: 'USD', timestamp: new Date(endDate) }, ]) }) })