@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
text/typescript
/*
* © 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'],
}
}