@cloud-carbon-footprint/aws
Version:
The core logic to get cloud usage data and estimate energy and carbon emissions from Amazon Web Services.
454 lines (412 loc) • 11.3 kB
text/typescript
/*
* © 2021 Thoughtworks, Inc.
*/
import AWSMock from 'aws-sdk-mock'
import AWS, { CloudWatchLogs, CostExplorer, CloudWatch, S3 } from 'aws-sdk'
import { estimateCo2 } from '@cloud-carbon-footprint/core'
import Lambda from '../lib/Lambda'
import { ServiceWrapper } from '../lib/ServiceWrapper'
import { buildCostExplorerGetCostResponse } from './fixtures/builders'
import { AWS_EMISSIONS_FACTORS_METRIC_TON_PER_KWH } from '../domain'
describe('Lambda', () => {
beforeAll(() => {
AWSMock.setSDKInstance(AWS)
})
afterEach(() => {
AWSMock.restore()
jest.restoreAllMocks()
startQuerySpy.mockClear()
})
const startDate = '2020-08-09T00:00:00Z'
const endDate = '2020-08-10T00:00:00Z'
const dayThree = '2020-08-11T00:00:00Z'
const region = 'us-west-1'
const queryResponse = {
queryId: '321db1cd-5790-47aa-a3ab-e5036ffdd16f',
}
const logGroup = generateLogGroups(1).map((groupName) => ({
logGroupName: groupName,
}))
const runningQueries: CloudWatchLogs.QueryInfo[] = [
{
queryId: 'test',
status: 'Running',
},
]
const getServiceWrapper = () =>
new ServiceWrapper(
new CloudWatch(),
new CloudWatchLogs(),
new CostExplorer(),
new S3(),
)
it('gets Lambda usage for one function and one day', async () => {
const logGroups = logGroup
const results = {
results: [
[
{
field: 'Date',
value: startDate,
},
{
field: 'Watts',
value: '0.10',
},
],
],
status: 'Complete',
}
mockDescribeLogGroups(logGroups)
mockDescribeQueries(runningQueries)
mockStartQuery(queryResponse)
mockGetResults(results)
const lambdaService = new Lambda(60000, 1000, getServiceWrapper())
const result = await lambdaService.getEstimates(
new Date(startDate),
new Date(endDate),
region,
)
const expectedKilowattHours = 0.00011350000000000001
expect(result).toEqual([
{
timestamp: new Date(startDate),
kilowattHours: expectedKilowattHours,
co2e: estimateCo2(
expectedKilowattHours,
region,
AWS_EMISSIONS_FACTORS_METRIC_TON_PER_KWH,
),
},
])
})
it('gets Lambda usage for one function and two days', async () => {
const logGroups = logGroup
const results = {
results: [
[
{
field: 'Date',
value: startDate,
},
{
field: 'Watts',
value: '0.10',
},
],
[
{
field: 'Date',
value: endDate,
},
{
field: 'Watts',
value: '0.40',
},
],
],
status: 'Complete',
}
mockDescribeLogGroups(logGroups)
mockDescribeQueries(runningQueries)
mockStartQuery(queryResponse)
mockGetResults(results)
const lambdaService = new Lambda(60000, 1000, getServiceWrapper())
const result = await lambdaService.getEstimates(
new Date(startDate),
new Date(dayThree),
region,
)
const expectedKilowattHoursOne = 0.00011350000000000001
const expectedKilowattHoursTwo = 0.00045400000000000003
expect(result).toEqual([
{
timestamp: new Date(startDate),
kilowattHours: expectedKilowattHoursOne,
co2e: estimateCo2(
expectedKilowattHoursOne,
region,
AWS_EMISSIONS_FACTORS_METRIC_TON_PER_KWH,
),
},
{
timestamp: new Date(endDate),
kilowattHours: expectedKilowattHoursTwo,
co2e: estimateCo2(
expectedKilowattHoursTwo,
region,
AWS_EMISSIONS_FACTORS_METRIC_TON_PER_KWH,
),
},
])
})
it('gets results from 2 Lambda log group names', async () => {
const logGroups = generateLogGroups(2).map((groupName) => ({
logGroupName: groupName,
}))
const results = {
results: [
[
{
field: 'Date',
value: startDate,
},
{
field: 'Watts',
value: '0.20',
},
],
[
{
field: 'Date',
value: startDate,
},
{
field: 'Watts',
value: '0.23',
},
],
],
status: 'Complete',
}
mockDescribeLogGroups(logGroups)
mockDescribeQueries(runningQueries)
mockStartQuery(queryResponse)
mockGetResults(results)
const lambdaService = new Lambda(60000, 1000, getServiceWrapper())
const result = await lambdaService.getEstimates(
new Date(startDate),
new Date(endDate),
region,
)
expect(startQuerySpy).toHaveBeenCalledWith(
{
startTime: expect.anything(),
endTime: expect.anything(),
queryString: expect.anything(),
logGroupNames: generateLogGroups(2),
},
expect.anything(),
)
const expectedKilowattHoursOne = 0.00022700000000000002
const expectedKilowattHoursTwo = 0.00026105000000000003
expect(result).toEqual([
{
timestamp: new Date(startDate),
kilowattHours: expectedKilowattHoursOne,
co2e: estimateCo2(
expectedKilowattHoursOne,
region,
AWS_EMISSIONS_FACTORS_METRIC_TON_PER_KWH,
),
},
{
timestamp: new Date(startDate),
kilowattHours: expectedKilowattHoursTwo,
co2e: estimateCo2(
expectedKilowattHoursTwo,
region,
AWS_EMISSIONS_FACTORS_METRIC_TON_PER_KWH,
),
},
])
})
it('gets results from 21 Lambda log group names', async () => {
const logGroups = generateLogGroups(21).map((groupName) => ({
logGroupName: groupName,
}))
const results = {
results: [
[
{
field: 'Date',
value: startDate,
},
{
field: 'Watts',
value: '0.23',
},
],
],
status: 'Complete',
}
mockDescribeLogGroups(logGroups)
mockDescribeQueries(runningQueries)
mockStartQuery(queryResponse)
mockGetResults(results)
const lambdaService = new Lambda(60000, 1000, getServiceWrapper())
const result = await lambdaService.getEstimates(
new Date(startDate),
new Date(endDate),
region,
)
expect(startQuerySpy).toHaveBeenNthCalledWith(
1,
{
startTime: expect.anything(),
endTime: expect.anything(),
queryString: expect.anything(),
logGroupNames: generateLogGroups(20),
},
expect.anything(),
)
expect(startQuerySpy).toHaveBeenNthCalledWith(
2,
{
startTime: expect.anything(),
endTime: expect.anything(),
queryString: expect.anything(),
logGroupNames: ['/aws/lambda/sample-function-name-21'],
},
expect.anything(),
)
const expectedKilowattHours = 0.00026105000000000003
expect(result).toEqual([
{
timestamp: new Date(startDate),
kilowattHours: expectedKilowattHours,
co2e: estimateCo2(
expectedKilowattHours,
region,
AWS_EMISSIONS_FACTORS_METRIC_TON_PER_KWH,
),
},
{
timestamp: new Date(startDate),
kilowattHours: expectedKilowattHours,
co2e: estimateCo2(
expectedKilowattHours,
region,
AWS_EMISSIONS_FACTORS_METRIC_TON_PER_KWH,
),
},
])
})
it('gets Lambda usage for one function and one day when there are no group names for that region', async () => {
mockDescribeLogGroups([])
mockDescribeQueries([])
const lambdaService = new Lambda(60000, 1000, getServiceWrapper())
const result = await lambdaService.getEstimates(
new Date(startDate),
new Date(endDate),
region,
)
expect(result).toEqual([])
})
it('throws an error if status is not complete after 100 ms', async () => {
const logGroups = logGroup
const results = {
results: [
[
{
field: 'Date',
value: startDate,
},
{
field: 'Watts',
value: '0.10',
},
],
],
status: 'Running',
}
mockDescribeLogGroups(logGroups)
mockDescribeQueries(runningQueries)
mockStartQuery(queryResponse)
mockGetResults(results)
const lambdaService = new Lambda(100, 50, getServiceWrapper())
const expectedError = new Error(
'CloudWatchLog request failed, status: Running',
)
try {
await lambdaService.getEstimates(
new Date(startDate),
new Date(endDate),
region,
)
} catch (error) {
expect(error).toEqual(expectedError)
}
})
it('gets Lambda cost', async () => {
AWSMock.mock(
'CostExplorer',
'getCostAndUsage',
(
params: CostExplorer.GetCostAndUsageRequest,
callback: (a: Error, response: any) => any,
) => {
callback(
null,
buildCostExplorerGetCostResponse([
{ start: startDate, amount: 100.0, keys: ['AWS Lambda'] },
{ start: endDate, amount: 50.0, keys: ['test'] },
]),
)
},
)
const lambdaService = new Lambda(60000, 1000, getServiceWrapper())
const lambdaCosts = await lambdaService.getCosts(
new Date(startDate),
new Date(endDate),
'us-east-1',
)
expect(lambdaCosts).toEqual([
{ amount: 100.0, currency: 'USD', timestamp: new Date(startDate) },
{ amount: 50.0, currency: 'USD', timestamp: new Date(endDate) },
])
})
function mockDescribeLogGroups(logGroups: { logGroupName: string }[]) {
AWSMock.mock(
'CloudWatchLogs',
'describeLogGroups',
(
params: CloudWatchLogs.DescribeLogGroupsRequest,
callback: (a: Error, response: any) => any,
) => {
callback(null, {
logGroups: logGroups,
})
},
)
}
function mockDescribeQueries(queries: CloudWatchLogs.QueryInfo[]) {
AWSMock.mock(
'CloudWatchLogs',
'describeQueries',
(
params: CloudWatchLogs.DescribeQueriesRequest,
callback: (a: Error, response: any) => any,
) => {
callback(null, {
queries: queries,
})
},
)
}
const startQuerySpy = jest.fn()
function mockStartQuery(response: { queryId: string }) {
startQuerySpy.mockResolvedValue(response)
return AWSMock.mock('CloudWatchLogs', 'startQuery', startQuerySpy)
}
function mockGetResults(results: {
results: { field: string; value: string }[][]
status: string
}) {
AWSMock.mock(
'CloudWatchLogs',
'getQueryResults',
(
params: CloudWatchLogs.GetQueryResultsRequest,
callback: (a: Error, response: any) => any,
) => {
callback(null, results)
},
)
}
})
function generateLogGroups(numberOfLogGroups: number): string[] {
return [...Array(numberOfLogGroups).keys()].map(
(i) => `/aws/lambda/sample-function-name-${i + 1}`,
)
}