@cloud-carbon-footprint/aws
Version:
The core logic to get cloud usage data and estimate energy and carbon emissions from Amazon Web Services.
417 lines (377 loc) • 10.6 kB
text/typescript
/*
* © 2021 Thoughtworks, Inc.
*/
import AWSMock from 'aws-sdk-mock'
import AWS, { CloudWatch, CloudWatchLogs, CostExplorer, S3 } from 'aws-sdk'
import RDSComputeService from '../lib/RDSCompute'
import { ServiceWrapper } from '../lib/ServiceWrapper'
import mockAWSCloudWatchGetMetricDataCall from '../lib/mockAWSCloudWatchGetMetricDataCall'
import {
buildCostExplorerGetCostRequest,
buildCostExplorerGetCostResponse,
buildCostExplorerGetUsageResponse,
} from './fixtures/builders'
import { AWS_CLOUD_CONSTANTS } from '../domain'
beforeAll(() => {
AWSMock.setSDKInstance(AWS)
})
describe('RDS Compute', function () {
afterEach(() => {
AWSMock.restore()
})
const metricDataQueries = [
{
Id: 'cpuUtilizationWithEmptyValues',
Expression:
"SEARCH('{AWS/RDS} MetricName=\"CPUUtilization\"', 'Average', 3600)",
ReturnData: false,
},
{
Id: 'cpuUtilization',
Expression: 'REMOVE_EMPTY(cpuUtilizationWithEmptyValues)',
},
]
const getServiceWrapper = () =>
new ServiceWrapper(
new CloudWatch(),
new CloudWatchLogs(),
new CostExplorer(),
new S3(),
)
it('should get RDS CPU utilization for two hours of different days', async () => {
const start_date_string = '2020-01-25T00:00:00.000Z'
const end_date_string = '2020-01-27T00:00:00.000Z'
const cloudwatchResponse = buildCloudwatchCPUUtilizationResponse(
[
new Date('2020-01-25T05:00:00.000Z'),
new Date('2020-01-26T23:00:00.000Z'),
],
[32.34, 12.65],
)
mockAWSCloudWatchGetMetricDataCall(
new Date(start_date_string),
new Date(end_date_string),
cloudwatchResponse,
metricDataQueries,
)
const costExplorerRequest = buildRdsCostExplorerGetUsageRequest(
start_date_string.substr(0, 10),
end_date_string.substr(0, 10),
'us-east-1',
)
AWSMock.mock(
'CostExplorer',
'getCostAndUsage',
(
params: AWS.CostExplorer.GetCostAndUsageRequest,
callback: (a: Error, response: any) => any,
) => {
expect(params).toEqual(costExplorerRequest)
callback(
null,
buildCostExplorerGetUsageResponse([
{
start: '2020-01-25',
amount: 1,
keys: ['USW1-InstanceUsage:db.t3.medium'],
},
{
start: '2020-01-26',
amount: 1,
keys: ['USW1-InstanceUsage:db.r5.24xlarge'],
},
]),
)
},
)
const rdsService = new RDSComputeService(getServiceWrapper())
const usageByHour = await rdsService.getUsage(
new Date(start_date_string),
new Date(end_date_string),
'us-east-1',
)
expect(usageByHour).toEqual([
{
cpuUtilizationAverage: 32.34,
vCpuHours: 2,
timestamp: new Date('2020-01-25T00:00:00.000Z'),
usesAverageCPUConstant: false,
},
{
cpuUtilizationAverage: 12.65,
vCpuHours: 96,
timestamp: new Date('2020-01-26T00:00:00.000Z'),
usesAverageCPUConstant: false,
},
])
})
it('uses the cpu utilization constant for missing cpu utilization data', async () => {
const start_date_string = '2020-01-25T00:00:00.000Z'
const end_date_string = '2020-01-27T00:00:00.000Z'
const cloudwatchResponse = buildCloudwatchCPUUtilizationResponse(
[new Date('2020-01-25T05:00:00.000Z')],
[32.34],
)
mockAWSCloudWatchGetMetricDataCall(
new Date(start_date_string),
new Date(end_date_string),
cloudwatchResponse,
metricDataQueries,
)
const costExplorerRequest = buildRdsCostExplorerGetUsageRequest(
start_date_string.substr(0, 10),
end_date_string.substr(0, 10),
'us-east-1',
)
AWSMock.mock(
'CostExplorer',
'getCostAndUsage',
(
params: AWS.CostExplorer.GetCostAndUsageRequest,
callback: (a: Error, response: any) => any,
) => {
expect(params).toEqual(costExplorerRequest)
callback(
null,
buildCostExplorerGetUsageResponse([
{
start: '2020-01-25',
amount: 1,
keys: ['USW1-InstanceUsage:db.t3.medium'],
},
{
start: '2020-01-26',
amount: 1,
keys: ['USW1-InstanceUsage:db.r5.24xlarge'],
},
]),
)
},
)
const rdsService = new RDSComputeService(getServiceWrapper())
const usageByHour = await rdsService.getUsage(
new Date(start_date_string),
new Date(end_date_string),
'us-east-1',
)
expect(usageByHour).toEqual([
{
cpuUtilizationAverage: 32.34,
vCpuHours: 2,
timestamp: new Date('2020-01-25T00:00:00.000Z'),
usesAverageCPUConstant: false,
},
{
cpuUtilizationAverage: AWS_CLOUD_CONSTANTS.AVG_CPU_UTILIZATION_2020,
vCpuHours: 96,
timestamp: new Date('2020-01-26T00:00:00.000Z'),
usesAverageCPUConstant: true,
},
])
})
it('returns an empty list when there is no usage', async () => {
const start_date_string = '2020-01-25T00:00:00.000Z'
const end_date_string = '2020-01-27T00:00:00.000Z'
mockAWSCloudWatchGetMetricDataCall(
new Date(start_date_string),
new Date(end_date_string),
{ MetricDataResults: [] },
metricDataQueries,
)
const costExplorerRequest = buildRdsCostExplorerGetUsageRequest(
start_date_string.substr(0, 10),
end_date_string.substr(0, 10),
'us-east-1',
)
AWSMock.mock(
'CostExplorer',
'getCostAndUsage',
(
params: AWS.CostExplorer.GetCostAndUsageRequest,
callback: (a: Error, response: any) => any,
) => {
expect(params).toEqual(costExplorerRequest)
callback(null, {
ResultsByTime: [
{
TimePeriod: {
Start: start_date_string,
End: end_date_string,
},
Total: {
UsageQuantity: {
Amount: 0,
},
},
Groups: [],
},
],
})
},
)
const rdsService = new RDSComputeService(getServiceWrapper())
const usageByHour = await rdsService.getUsage(
new Date(start_date_string),
new Date(end_date_string),
'us-east-1',
)
expect(usageByHour).toEqual([])
})
it('should throw PartialData error when AWS returns PartialData', async () => {
const start_date_string = '2020-01-25T00:00:00.000Z'
const end_date_string = '2020-01-27T00:00:00.000Z'
const cloudwatchResponse = buildCloudwatchCPUUtilizationResponse(
[new Date('2020-01-25T05:00:00.000Z')],
[32.34],
'PartialData',
)
mockAWSCloudWatchGetMetricDataCall(
new Date(start_date_string),
new Date(end_date_string),
cloudwatchResponse,
metricDataQueries,
)
const costExplorerRequest = buildRdsCostExplorerGetUsageRequest(
start_date_string.substr(0, 10),
end_date_string.substr(0, 10),
'us-east-1',
)
AWSMock.mock(
'CostExplorer',
'getCostAndUsage',
(
params: AWS.CostExplorer.GetCostAndUsageRequest,
callback: (a: Error, response: any) => any,
) => {
expect(params).toEqual(costExplorerRequest)
callback(null, {
ResultsByTime: [
{
TimePeriod: {
Start: start_date_string,
End: end_date_string,
},
Total: {
UsageQuantity: {
Amount: 0,
},
},
Groups: [],
},
],
})
},
)
const rdsService = new RDSComputeService(getServiceWrapper())
const getUsageByHour = async () =>
await rdsService.getUsage(
new Date(start_date_string),
new Date(end_date_string),
'us-east-1',
)
await expect(getUsageByHour).rejects.toThrow(
'Partial Data Returned from AWS',
)
})
it('should get rds cost', async () => {
const start = '2020-01-25T00:00:00.000Z'
const end = '2020-01-27T00:00:00.000Z'
AWSMock.mock(
'CostExplorer',
'getCostAndUsage',
(
params: AWS.CostExplorer.GetCostAndUsageRequest,
callback: (a: Error, response: any) => any,
) => {
expect(params).toEqual(
buildCostExplorerGetCostRequest(
start.substr(0, 10),
end.substr(0, 10),
'us-east-1',
['RDS: Running Hours'],
),
)
callback(
null,
buildCostExplorerGetCostResponse([
{
start: '2020-01-25',
amount: 2.3081821243,
keys: ['USW1-InstanceUsage:db.t3.medium'],
},
{
start: '2020-01-26',
amount: 2.3081821243,
keys: ['USW1-InstanceUsage:db.t3.medium'],
},
]),
)
},
)
const rdsService = new RDSComputeService(getServiceWrapper())
const rdsCosts = await rdsService.getCosts(
new Date(start),
new Date(end),
'us-east-1',
)
expect(rdsCosts).toEqual([
{
amount: 2.3081821243,
currency: 'USD',
timestamp: new Date('2020-01-25T00:00:00.000Z'),
},
{
amount: 2.3081821243,
currency: 'USD',
timestamp: new Date('2020-01-26T00:00:00.000Z'),
},
])
})
})
function buildCloudwatchCPUUtilizationResponse(
timestamps: Date[],
values: number[],
statusCode = 'Complete',
) {
return {
MetricDataResults: [
{
Id: 'cpuUtilization',
Timestamps: timestamps,
Values: values,
StatusCode: statusCode,
},
],
}
}
function buildRdsCostExplorerGetUsageRequest(
startDate: string,
endDate: string,
region: string,
) {
return {
TimePeriod: {
Start: startDate,
End: endDate,
},
Filter: {
And: [
{ Dimensions: { Key: 'REGION', Values: [region] } },
{
Dimensions: {
Key: 'USAGE_TYPE_GROUP',
Values: ['RDS: Running Hours'],
},
},
],
},
Granularity: 'DAILY',
GroupBy: [
{
Key: 'USAGE_TYPE',
Type: 'DIMENSION',
},
],
Metrics: ['UsageQuantity'],
}
}