UNPKG

@cloud-carbon-footprint/aws

Version:

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

466 lines (421 loc) 11.2 kB
/* * © 2021 Thoughtworks, Inc. */ import { mockClient } from 'aws-sdk-client-mock' import ElastiCache from '../lib/ElastiCache' import { ServiceWrapper } from '../lib' import mockAWSCloudWatchGetMetricDataCall from '../lib/mockAWSCloudWatchGetMetricDataCall' 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('ElastiCache', () => { const startDate = '2020-07-10' const dayTwo = '2020-07-11' const endDate = '2020-07-12' const region = 'us-west-1' const metrics = [ { Id: 'cpuUtilizationWithEmptyValues', Expression: "SEARCH('{AWS/ElastiCache} MetricName=\"CPUUtilization\"', 'Average', 3600)", ReturnData: false, }, { Id: 'cpuUtilization', Expression: 'REMOVE_EMPTY(cpuUtilizationWithEmptyValues)', }, ] const getServiceWrapper = () => new ServiceWrapper( new CloudWatchClient(), new CloudWatchLogsClient(), new CostExplorerClient(), new S3Client(), ) afterEach(() => { costExplorerMock.reset() }) it('should return the usage of two hours of different days', async () => { const response: any = { MetricDataResults: [ { Id: 'cpuUtilization', Label: 'AWS/ElastiCache CPUUtilization', Timestamps: [new Date(startDate), new Date(dayTwo)], Values: [1.0456, 2.03242], StatusCode: 'Complete', Messages: [], }, ], } mockAWSCloudWatchGetMetricDataCall( new Date(startDate), new Date(endDate), response, metrics, ) costExplorerMock.on(GetCostAndUsageCommand).resolves({ ResultsByTime: [ { TimePeriod: { Start: startDate, End: dayTwo, }, Groups: [ { Keys: ['NodeUsage:cache.t3.medium'], Metrics: { UsageQuantity: { Amount: '2', }, }, }, ], }, { TimePeriod: { Start: dayTwo, End: endDate, }, Groups: [ { Keys: ['NodeUsage:cache.t3.medium'], Metrics: { UsageQuantity: { Amount: '2', }, }, }, ], }, ], }) const elasticacheService = new ElastiCache(getServiceWrapper()) const usageByHour = await elasticacheService.getUsage( new Date(startDate), new Date(endDate), region, ) const calls = costExplorerMock.commandCalls(GetCostAndUsageCommand) expect(calls).toHaveLength(1) expect(calls[0].args[0].input).toEqual( costExplorerRequest(startDate, endDate, region), ) expect(usageByHour).toEqual([ { cpuUtilizationAverage: 1.0456, vCpuHours: 4, timestamp: new Date(startDate), usesAverageCPUConstant: false, }, { cpuUtilizationAverage: 2.03242, vCpuHours: 4, timestamp: new Date(dayTwo), usesAverageCPUConstant: false, }, ]) }) it('should return empty list when no usage', async () => { const response: any = { MetricDataResults: [], } mockAWSCloudWatchGetMetricDataCall( new Date(startDate), new Date(endDate), response, metrics, ) costExplorerMock.on(GetCostAndUsageCommand).resolves({ ResultsByTime: [ { TimePeriod: { Start: startDate, End: endDate, }, Total: { UsageQuantity: { Amount: '0', // }, }, Groups: [], }, ], }) const elasticacheService = new ElastiCache(getServiceWrapper()) const usageByHour = await elasticacheService.getUsage( new Date(startDate), new Date(endDate), region, ) const calls = costExplorerMock.commandCalls(GetCostAndUsageCommand) expect(calls).toHaveLength(1) expect(calls[0].args[0].input).toEqual( costExplorerRequest(startDate, endDate, region), ) expect(usageByHour).toEqual([]) }) it('uses the cpu utilization constant for missing cpu utilization data', async () => { const response: any = { MetricDataResults: [ { Id: 'cpuUtilization', Label: 'cpuUtilization', Timestamps: [], Values: [], StatusCode: 'Complete', }, ], } mockAWSCloudWatchGetMetricDataCall( new Date(startDate), new Date(endDate), response, metrics, ) costExplorerMock.on(GetCostAndUsageCommand).resolves({ ResultsByTime: [ { TimePeriod: { Start: startDate, End: endDate, }, Groups: [ { Keys: ['USE2-NodeUsage:cache.t3.medium'], Metrics: { UsageQuantity: { Amount: '1', }, }, }, ], }, ], }) const elasticacheService = new ElastiCache(getServiceWrapper()) const usageByHour = await elasticacheService.getUsage( new Date(startDate), new Date(endDate), region, ) const calls = costExplorerMock.commandCalls(GetCostAndUsageCommand) expect(calls).toHaveLength(1) expect(calls[0].args[0].input).toEqual( costExplorerRequest(startDate, endDate, region), ) expect(usageByHour).toEqual([ { cpuUtilizationAverage: 50, vCpuHours: 2, timestamp: new Date(startDate), usesAverageCPUConstant: true, }, ]) }) it('should return the usage when two different cache instances types were used', async () => { const response: any = { MetricDataResults: [ { Id: 'cpuUtilization', Label: 'cpuUtilization', Timestamps: [new Date(startDate)], Values: [50], StatusCode: 'Complete', }, ], } mockAWSCloudWatchGetMetricDataCall( new Date(startDate), new Date(endDate), response, metrics, ) costExplorerMock.on(GetCostAndUsageCommand).resolves({ ResultsByTime: [ { TimePeriod: { Start: startDate, End: endDate, }, Groups: [ { Keys: ['USE2-NodeUsage:cache.t3.medium'], Metrics: { UsageQuantity: { Amount: '3', }, }, }, { Keys: ['USE2-NodeUsage:cache.t2.micro'], Metrics: { UsageQuantity: { Amount: '2', }, }, }, ], }, ], }) const elasticacheService = new ElastiCache(getServiceWrapper()) const usageByHour = await elasticacheService.getUsage( new Date(startDate), new Date(endDate), region, ) const calls = costExplorerMock.commandCalls(GetCostAndUsageCommand) expect(calls).toHaveLength(1) expect(calls[0].args[0].input).toEqual( costExplorerRequest(startDate, endDate, region), ) expect(usageByHour).toEqual([ { cpuUtilizationAverage: 50, vCpuHours: 8, timestamp: new Date(startDate), usesAverageCPUConstant: false, }, ]) }) it('should return the usage when two different cache instances types in different hours were used', async () => { const response: any = { MetricDataResults: [ { Id: 'cpuUtilization', Label: 'cpuUtilization', Timestamps: [ new Date(startDate + 'T22:00:00.000Z'), new Date(startDate + 'T22:06:00.000Z'), ], Values: [50, 70], StatusCode: 'Complete', }, ], } mockAWSCloudWatchGetMetricDataCall( new Date(startDate), new Date(endDate), response, metrics, ) costExplorerMock.on(GetCostAndUsageCommand).resolves({ ResultsByTime: [ { TimePeriod: { Start: startDate, End: endDate, }, Groups: [ { Keys: ['USE2-NodeUsage:cache.t3.medium'], Metrics: { UsageQuantity: { Amount: '2', }, }, }, { Keys: ['USE2-NodeUsage:cache.t2.micro'], Metrics: { UsageQuantity: { Amount: '2', }, }, }, ], }, ], }) const elasticacheService = new ElastiCache(getServiceWrapper()) const usageByHour = await elasticacheService.getUsage( new Date(startDate), new Date(endDate), region, ) const calls = costExplorerMock.commandCalls(GetCostAndUsageCommand) expect(calls).toHaveLength(1) expect(calls[0].args[0].input).toEqual( costExplorerRequest(startDate, endDate, region), ) expect(usageByHour).toEqual([ { cpuUtilizationAverage: 60, vCpuHours: 6, timestamp: new Date(startDate), usesAverageCPUConstant: false, }, ]) }) it('should throw PartialData when AWS returns PartialData', async () => { const response: any = { MetricDataResults: [ { Id: 'cpuUtilization', Label: 'cpuUtilization', Timestamps: [ new Date(startDate + 'T22:00:00.000Z'), new Date(startDate + 'T22:06:00.000Z'), ], Values: [50, 70], StatusCode: 'PartialData', }, ], } mockAWSCloudWatchGetMetricDataCall( new Date(startDate), new Date(endDate), response, metrics, ) const elasticacheService = new ElastiCache(getServiceWrapper()) const getUsageByHour = async () => await elasticacheService.getUsage( new Date(startDate), new Date(endDate), region, ) const calls = costExplorerMock.commandCalls(GetCostAndUsageCommand) expect(calls).toHaveLength(0) await expect(getUsageByHour).rejects.toThrow( 'Partial Data Returned from AWS', ) }) }) function costExplorerRequest( startDate: string, endDate: string, region: string, ) { return { TimePeriod: { Start: startDate, End: endDate, }, Filter: { And: [ { Dimensions: { Key: 'USAGE_TYPE_GROUP', Values: ['ElastiCache: Running Hours'], }, }, { Dimensions: { Key: 'REGION', Values: [region] } }, ], }, Granularity: 'DAILY', GroupBy: [ { Key: 'USAGE_TYPE', Type: 'DIMENSION', }, ], Metrics: ['UsageQuantity'], } }