Skip to content
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 @@ -8785,6 +8785,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 HOST_OS_VERSION = 'host.os.version';
export const CONTAINER_ID = 'container.id';
export const CONTAINER = 'container';
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -163,6 +163,20 @@ export function ServerlessSummary({ serverlessId }: Props) {
/>
</EuiFlexItem>
{showVerticalRule && <VerticalRule />}
{data?.estimatedCost && (
<EuiFlexItem grow={false}>
<EuiStat
isLoading={isLoading}
title={`$${data.estimatedCost}`}
titleSize="s"
description={i18n.translate(
'xpack.apm.serverlessMetrics.summary.estimatedCost',
{ defaultMessage: 'Estimated costs avg.' }
)}
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,65 @@ 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 { calcEstimatedCost, calcMemoryUsedRate } from './helper';

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

export type AWSLambdaPriceFactor = Record<AwsLambdaArchitecture, number>;

async function getServerlessTransactionThroughput({
end,
environment,
kuery,
serviceName,
start,
serverlessId,
apmEventClient,
}: {
environment: string;
kuery: string;
serviceName: string;
start: number;
end: number;
serverlessId?: string;
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(
'get_serverless_transaction_throughout',
params
);

return response.hits.total.value;
}

export async function getServerlessSummary({
end,
Expand All @@ -31,6 +82,8 @@ export async function getServerlessSummary({
start,
serverlessId,
apmEventClient,
awsLambdaPriceFactor,
awsLambdaRequestCostPerMillion,
}: {
environment: string;
kuery: string;
Expand All @@ -39,6 +92,8 @@ export async function getServerlessSummary({
end: number;
serverlessId?: string;
apmEventClient: APMEventClient;
awsLambdaPriceFactor?: AWSLambdaPriceFactor;
awsLambdaRequestCostPerMillion?: number;
}) {
const params = {
apm: {
Expand All @@ -65,14 +120,28 @@ 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_metrics: {
metrics: [{ field: 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('get_serverless_summary', params),
getServerlessTransactionThroughput({
end,
environment,
kuery,
serviceName,
apmEventClient,
start,
serverlessId,
}),
]);

return {
memoryUsageAvgRate: calcMemoryUsedRate({
Expand All @@ -82,5 +151,15 @@ export async function getServerlessSummary({
serverlessFunctionsTotal: response.aggregations?.totalFunctions?.value,
serverlessDurationAvg: response.aggregations?.faasDurationAvg?.value,
billedDurationAvg: response.aggregations?.faasBilledDurationAvg?.value,
estimatedCost: calcEstimatedCost({
awsLambdaPriceFactor,
awsLambdaRequestCostPerMillion,
architecture: response.aggregations?.sample?.top?.[0]?.metrics?.[
HOST_ARCHITECTURE
] as AwsLambdaArchitecture | undefined,
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 undefined when price factor is not defined', () => {
expect(
calcEstimatedCost({
totalMemory: 1,
billedDuration: 1,
transactionThroughput: 1,
architecture: 'arm',
})
).toBeUndefined();
});

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

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

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

describe('x86_64 architecture', () => {
const architecture = 'x86_64';
it('returns correct cost', () => {
expect(
calcEstimatedCost({
Copy link
Copy Markdown
Contributor

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