Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[APM] AWS lambda estimated cost #143986

Merged
merged 12 commits into from
Oct 31, 2022
1 change: 1 addition & 0 deletions packages/kbn-apm-synthtrace/src/lib/apm/apm_fields.ts
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,7 @@ export type ApmFields = Fields &
'error.grouping_name': string;
'error.grouping_key': string;
'host.name': string;
'host.architecture': string;
'host.hostname': string;
'http.request.method': string;
'http.response.status_code': number;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,11 +26,13 @@ export function serverlessFunction({
serviceName,
environment,
agentName,
architecture = 'arm',
}: {
functionName: string;
environment: string;
agentName: string;
serviceName?: string;
architecture?: string;
}) {
const faasId = `arn:aws:lambda:us-west-2:001:function:${functionName}`;
return new ServerlessFunction({
Expand All @@ -40,5 +42,6 @@ export function serverlessFunction({
'service.environment': environment,
'agent.name': agentName,
'service.runtime.name': 'AWS_lambda',
'host.architecture': architecture,
});
}
Original file line number Diff line number Diff line change
Expand Up @@ -442,6 +442,14 @@ export const stackManagementSchema: MakeSchemaFrom<UsageStats> = {
type: 'boolean',
_meta: { description: 'Non-default value of setting.' },
},
'observability:apmAWSLambdaPriceFactor': {
type: 'text',
_meta: { description: 'Non-default value of setting.' },
},
'observability:apmAWSLambdaRequestCostPerMillion': {
type: 'integer',
_meta: { description: 'Non-default value of setting.' },
},
'banners:placement': {
type: 'keyword',
_meta: { description: 'Non-default value of setting.' },
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,8 @@ export interface UsageStats {
'observability:enableComparisonByDefault': boolean;
'observability:enableServiceGroups': boolean;
'observability:apmEnableServiceMetrics': boolean;
'observability:apmAWSLambdaPriceFactor': string;
'observability:apmAWSLambdaRequestCostPerMillion': number;
'observability:enableInfrastructureHostsView': boolean;
'visualize:enableLabs': boolean;
'visualization:heatmap:maxBuckets': number;
Expand Down
12 changes: 12 additions & 0 deletions src/plugins/telemetry/schema/oss_plugins.json
Original file line number Diff line number Diff line change
Expand Up @@ -8708,6 +8708,18 @@
"description": "Non-default value of setting."
}
},
"observability:apmAWSLambdaPriceFactor": {
"type": "text",
"_meta": {
"description": "Non-default value of setting."
}
},
"observability:apmAWSLambdaRequestCostPerMillion": {
"type": "integer",
"_meta": {
"description": "Non-default value of setting."
}
},
"banners:placement": {
"type": "keyword",
"_meta": {
Expand Down

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions x-pack/plugins/apm/common/elasticsearch_fieldnames.ts
Original file line number Diff line number Diff line change
Expand Up @@ -123,6 +123,7 @@ export const HOST = 'host';
export const HOST_HOSTNAME = 'host.hostname'; // Do not use. Please use `HOST_NAME` instead.
export const HOST_NAME = 'host.name';
export const HOST_OS_PLATFORM = 'host.os.platform';
export const HOST_ARCHITECTURE = 'host.architecture';
export const CONTAINER_ID = 'container.id';
export const CONTAINER = 'container';
export const CONTAINER_IMAGE = 'container.image.name';
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -163,6 +163,18 @@ export function ServerlessSummary({ serverlessId }: Props) {
/>
</EuiFlexItem>
{showVerticalRule && <VerticalRule />}
<EuiFlexItem grow={false}>
<EuiStat
isLoading={isLoading}
title={`$${data?.estimedCost}`}
titleSize="s"
description={i18n.translate(
'xpack.apm.serverlessMetrics.summary.estimatedCost',
{ defaultMessage: 'Estimated costs avg.' }
Copy link
Contributor

@kpatticha kpatticha Oct 27, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think it would be useful to have a tooltip to inform the user how the avg cost is calculated

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@boriskirov can you come up with a copy, please?

)}
reverse
/>
</EuiFlexItem>
</EuiFlexGroup>
</EuiPanel>
);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,8 @@ import {
defaultApmServiceEnvironment,
enableComparisonByDefault,
enableInspectEsQueries,
apmAWSLambdaPriceFactor,
apmAWSLambdaRequestCostPerMillion,
} from '@kbn/observability-plugin/common';
import { isEmpty } from 'lodash';
import React from 'react';
Expand All @@ -30,6 +32,8 @@ const apmSettingsKeys = [
apmServiceGroupMaxNumberOfServices,
enableInspectEsQueries,
apmLabsButton,
apmAWSLambdaPriceFactor,
apmAWSLambdaRequestCostPerMillion,
];

export function GeneralSettings() {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -72,5 +72,17 @@ export function ServerlessDetails({ serverless }: Props) {
});
}

if (serverless.hostArchitecture) {
listItems.push({
title: i18n.translate(
'xpack.apm.serviceIcons.serviceDetails.cloud.architecture',
{ defaultMessage: 'Architecture' }
),
description: (
<EuiBadge color="hollow">{serverless.hostArchitecture}</EuiBadge>
),
});
}

return <EuiDescriptionList textStyle="reverse" listItems={listItems} />;
}
Original file line number Diff line number Diff line change
Expand Up @@ -14,14 +14,67 @@ import {
FAAS_BILLED_DURATION,
FAAS_DURATION,
FAAS_ID,
HOST_ARCHITECTURE,
METRICSET_NAME,
METRIC_SYSTEM_FREE_MEMORY,
METRIC_SYSTEM_TOTAL_MEMORY,
SERVICE_NAME,
} from '../../../../common/elasticsearch_fieldnames';
import { environmentQuery } from '../../../../common/utils/environment_query';
import { calcMemoryUsedRate } from './helper';
import { APMEventClient } from '../../../lib/helpers/create_es_client/create_apm_event_client';
import { MetricRaw } from '../../../../typings/es_schemas/raw/metric_raw';
import { calcEstimatedCost, calcMemoryUsedRate } from './helper';

export type AwsLambdaArchitecture = 'arm' | 'x86_64';

export type AWSLambdaPriceFactor = Record<AwsLambdaArchitecture, number>;

async function getTransactionThroughput({
end,
environment,
kuery,
serviceName,
start,
serverlessId,
apmEventClient,
}: {
environment: string;
kuery: string;
serviceName: string;
start: number;
end: number;
serverlessId?: string;
awsLambdaPriceFactor?: AWSLambdaPriceFactor;
apmEventClient: APMEventClient;
}) {
const params = {
apm: {
events: [ProcessorEvent.transaction],
},
body: {
track_total_hits: true,
size: 0,
query: {
bool: {
filter: [
...termQuery(SERVICE_NAME, serviceName),
...rangeQuery(start, end),
...environmentQuery(environment),
...kqlQuery(kuery),
...termQuery(FAAS_ID, serverlessId),
],
},
},
},
};

const response = await apmEventClient.search(
'ger_transaction_throughout',
params
);

return response.hits.total.value;
}

export async function getServerlessSummary({
end,
Expand All @@ -31,6 +84,8 @@ export async function getServerlessSummary({
start,
serverlessId,
apmEventClient,
awsLambdaPriceFactor,
awsLambdaRequestCostPerMillion,
}: {
environment: string;
kuery: string;
Expand All @@ -39,6 +94,8 @@ export async function getServerlessSummary({
end: number;
serverlessId?: string;
apmEventClient: APMEventClient;
awsLambdaPriceFactor?: AWSLambdaPriceFactor;
awsLambdaRequestCostPerMillion?: number;
}) {
const params = {
apm: {
Expand All @@ -65,14 +122,29 @@ export async function getServerlessSummary({
faasBilledDurationAvg: { avg: { field: FAAS_BILLED_DURATION } },
avgTotalMemory: { avg: { field: METRIC_SYSTEM_TOTAL_MEMORY } },
avgFreeMemory: { avg: { field: METRIC_SYSTEM_FREE_MEMORY } },
sample: {
top_hits: {
size: 1,
_source: [HOST_ARCHITECTURE],
sort: [{ '@timestamp': { order: 'desc' as const } }],
},
},
},
},
};

const response = await apmEventClient.search(
'ger_serverless_summary',
params
);
const [response, transactionThroughput] = await Promise.all([
apmEventClient.search('ger_serverless_summary', params),
getTransactionThroughput({
end,
environment,
kuery,
serviceName,
apmEventClient,
start,
serverlessId,
}),
]);

return {
memoryUsageAvgRate: calcMemoryUsedRate({
Expand All @@ -82,5 +154,17 @@ export async function getServerlessSummary({
serverlessFunctionsTotal: response.aggregations?.totalFunctions?.value,
serverlessDurationAvg: response.aggregations?.faasDurationAvg?.value,
billedDurationAvg: response.aggregations?.faasBilledDurationAvg?.value,
estimedCost: calcEstimatedCost({
awsLambdaPriceFactor,
awsLambdaRequestCostPerMillion,
architecture: (
response.aggregations?.sample?.hits?.hits?.[0]?._source as
| MetricRaw
| undefined
)?.host?.architecture as AwsLambdaArchitecture,
transactionThroughput,
billedDuration: response.aggregations?.faasBilledDurationAvg.value,
totalMemory: response.aggregations?.avgTotalMemory.value,
}),
};
}
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,11 @@
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import { calcMemoryUsed, calcMemoryUsedRate } from './helper';
import {
calcMemoryUsed,
calcMemoryUsedRate,
calcEstimatedCost,
} from './helper';
describe('calcMemoryUsed', () => {
it('returns undefined when memory values are no a number', () => {
[
Expand Down Expand Up @@ -38,3 +42,85 @@ describe('calcMemoryUsedRate', () => {
expect(calcMemoryUsedRate({ memoryFree: 50, memoryTotal: 100 })).toBe(0.5);
});
});

const AWS_LAMBDA_PRICE_FACTOR = {
x86_64: 0.0000166667,
arm: 0.0000133334,
};

describe('calcEstimatedCost', () => {
it('returns 0 when price factor is not defined', () => {
expect(
calcEstimatedCost({
totalMemory: 1,
billedDuration: 1,
transactionThroughput: 1,
architecture: 'arm',
})
).toEqual(0);
});

it('returns 0 when architecture is not defined', () => {
expect(
calcEstimatedCost({
totalMemory: 1,
billedDuration: 1,
transactionThroughput: 1,
awsLambdaPriceFactor: AWS_LAMBDA_PRICE_FACTOR,
})
).toEqual(0);
});

it('returns 0 when compute usage is not defined', () => {
expect(
calcEstimatedCost({
transactionThroughput: 1,
awsLambdaPriceFactor: AWS_LAMBDA_PRICE_FACTOR,
architecture: 'arm',
})
).toEqual(0);
});

it('returns 0 when request cost per million is not defined', () => {
expect(
calcEstimatedCost({
totalMemory: 1,
billedDuration: 1,
transactionThroughput: 1,
awsLambdaPriceFactor: AWS_LAMBDA_PRICE_FACTOR,
architecture: 'arm',
})
).toEqual(0);
});

describe('x86_64 architecture', () => {
const architecture = 'x86_64';
it('returns correct cost when usage is less than 6 b gb-sec', () => {
expect(
calcEstimatedCost({
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think it'd be useful to have several invocations here, it would make it more clear from just reading the test how different factors influence the final result.

awsLambdaPriceFactor: AWS_LAMBDA_PRICE_FACTOR,
architecture,
billedDuration: 4000,
totalMemory: 536870912, // 0.5gb
transactionThroughput: 100000,
awsLambdaRequestCostPerMillion: 0.2,
})
).toEqual(0.03);
});
});
describe('arm architecture', () => {
const architecture = 'arm';
it('returns correct cost', () => {
expect(
calcEstimatedCost({
awsLambdaPriceFactor: AWS_LAMBDA_PRICE_FACTOR,
architecture,
billedDuration: 8000,
totalMemory: 536870912, // 0.5gb
transactionThroughput: 200000,
awsLambdaRequestCostPerMillion: 0.2,
})
).toEqual(0.05);
});
});
});
Loading