[APM] Implement improvements to AWS Lambda metrics view (#143113)

* [APM] Refactoring metrics route

* apis

* refactoring memory function

* fixing merge conflicts

* refactoring

* [CI] Auto-commit changed files from 'node scripts/eslint --no-cache --fix'

* addin API test

* new settings

* cost settings

* removing cost settings

* refactoring api

* adding test

* Update oss_plugins.json

* refactoring apis

* new page

* fixing build

* metrics ui

* adding filter by serverlessFunctionName

* aws details

* refactoring

* fixing sorting

* tests

* test

* renaming api

* fixing test

* fixing ci

* refactoring

* reverting

* fixing breadcrumb

* addressing pr comments

* sanity check

* fixing merge conflicts

* fixing tests and ci

Co-authored-by: kibanamachine <42973632+kibanamachine@users.noreply.github.com>
This commit is contained in:
Cauê Marcondes 2022-10-25 14:36:23 -04:00 committed by GitHub
parent c5cbfee06a
commit aa97bff4d4
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
43 changed files with 2539 additions and 682 deletions

View file

@ -33,7 +33,6 @@ export class Serverless extends BaseSpan {
...fields,
'metricset.name': 'app',
'faas.execution': faasExection,
'faas.id': fields['service.name'],
});
}

View file

@ -7,7 +7,6 @@
*/
import { Entity } from '../entity';
import { generateShortId } from '../utils/generate_id';
import { ApmFields } from './apm_fields';
import { ServerlessInstance } from './serverless_instance';
@ -33,7 +32,7 @@ export function serverlessFunction({
agentName: string;
serviceName?: string;
}) {
const faasId = `arn:aws:lambda:us-west-2:${generateShortId()}:function:${functionName}`;
const faasId = `arn:aws:lambda:us-west-2:001:function:${functionName}`;
return new ServerlessFunction({
'service.name': serviceName || faasId,
'faas.id': faasId,

View file

@ -75,6 +75,8 @@ exports[`Error FAAS_DURATION 1`] = `undefined`;
exports[`Error FAAS_ID 1`] = `undefined`;
exports[`Error FAAS_NAME 1`] = `undefined`;
exports[`Error FAAS_TRIGGER_TYPE 1`] = `undefined`;
exports[`Error HOST 1`] = `
@ -330,6 +332,8 @@ exports[`Span FAAS_DURATION 1`] = `undefined`;
exports[`Span FAAS_ID 1`] = `undefined`;
exports[`Span FAAS_NAME 1`] = `undefined`;
exports[`Span FAAS_TRIGGER_TYPE 1`] = `undefined`;
exports[`Span HOST 1`] = `undefined`;
@ -581,6 +585,8 @@ exports[`Transaction FAAS_DURATION 1`] = `undefined`;
exports[`Transaction FAAS_ID 1`] = `undefined`;
exports[`Transaction FAAS_NAME 1`] = `undefined`;
exports[`Transaction FAAS_TRIGGER_TYPE 1`] = `undefined`;
exports[`Transaction HOST 1`] = `

View file

@ -147,6 +147,7 @@ export const USER_AGENT_DEVICE = 'user_agent.device.name';
export const USER_AGENT_OS = 'user_agent.os.name';
export const FAAS_ID = 'faas.id';
export const FAAS_NAME = 'faas.name';
export const FAAS_COLDSTART = 'faas.coldstart';
export const FAAS_TRIGGER_TYPE = 'faas.trigger.type';
export const FAAS_DURATION = 'faas.duration';

View file

@ -0,0 +1,26 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import { getServerlessFunctionNameFromId } from './serverless';
describe('getServerlessFunctionNameFromId', () => {
it('returns serverlessId when regex does not match', () => {
expect(getServerlessFunctionNameFromId('foo')).toEqual('foo');
});
it('returns correct serverless function name', () => {
expect(
getServerlessFunctionNameFromId(
'arn:aws:lambda:us-west-2:123456789012:function:my-function'
)
).toEqual('my-function');
expect(
getServerlessFunctionNameFromId(
'arn:aws:lambda:us-west-2:123456789012:function:my:function'
)
).toEqual('my:function');
});
});

View file

@ -0,0 +1,17 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
/**
* Gets the serverless function name from serverless id.
* Serverless id example: arn:aws:lambda:us-west-2:123456789012:function:my-function
* The function name is the last part after "function:"
*/
const serverlessIdRegex = /function:(.*)/;
export function getServerlessFunctionNameFromId(serverlessId: string) {
const match = serverlessIdRegex.exec(serverlessId);
return match ? match[1] : serverlessId;
}

View file

@ -6,17 +6,30 @@
*/
import React from 'react';
import { isJavaAgentName, isJRubyAgent } from '../../../../common/agent_name';
import {
isJavaAgentName,
isJRubyAgent,
isServerlessAgent,
} from '../../../../common/agent_name';
import { useApmServiceContext } from '../../../context/apm_service/use_apm_service_context';
import { ServerlessMetrics } from './serverless_metrics';
import { ServiceMetrics } from './service_metrics';
import { JvmMetricsOverview } from './jvm_metrics_overview';
export function Metrics() {
const { agentName, runtimeName } = useApmServiceContext();
const isServerless = isServerlessAgent(runtimeName);
if (isJavaAgentName(agentName) || isJRubyAgent(agentName, runtimeName)) {
if (
!isServerless &&
(isJavaAgentName(agentName) || isJRubyAgent(agentName, runtimeName))
) {
return <JvmMetricsOverview />;
}
if (isServerless) {
return <ServerlessMetrics />;
}
return <ServiceMetrics />;
}

View file

@ -0,0 +1,40 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import { EuiFlexGroup, EuiFlexItem } from '@elastic/eui';
import React from 'react';
import { ServerlessFunctions } from './serverless_functions';
import { ServerlessSummary } from './serverless_summary';
import { ServerlessActiveInstances } from './serverless_active_instances';
import { ServerlessMetricsCharts } from './serverless_metrics_charts';
import { ChartPointerEventContextProvider } from '../../../../context/chart_pointer_event/chart_pointer_event_context';
interface Props {
serverlessId?: string;
}
export function ServerlessMetrics({ serverlessId }: Props) {
return (
<EuiFlexGroup direction="column" gutterSize="m">
<EuiFlexItem>
<ServerlessSummary serverlessId={serverlessId} />
</EuiFlexItem>
{!serverlessId && (
<EuiFlexItem>
<ServerlessFunctions />
</EuiFlexItem>
)}
<ChartPointerEventContextProvider>
<EuiFlexItem>
<ServerlessMetricsCharts serverlessId={serverlessId} />
</EuiFlexItem>
<EuiFlexItem>
<ServerlessActiveInstances serverlessId={serverlessId} />
</EuiFlexItem>
</ChartPointerEventContextProvider>
</EuiFlexGroup>
);
}

View file

@ -0,0 +1,223 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import {
EuiBasicTableColumn,
EuiFlexGroup,
EuiFlexItem,
EuiInMemoryTable,
euiPaletteColorBlind,
EuiPanel,
EuiTitle,
PropertySort,
} from '@elastic/eui';
import { i18n } from '@kbn/i18n';
import React, { useMemo } from 'react';
import {
asDynamicBytes,
asInteger,
asMillisecondDuration,
} from '../../../../../common/utils/formatters';
import { Coordinate, TimeSeries } from '../../../../../typings/timeseries';
import { useApmServiceContext } from '../../../../context/apm_service/use_apm_service_context';
import { useApmParams } from '../../../../hooks/use_apm_params';
import { FETCH_STATUS, useFetcher } from '../../../../hooks/use_fetcher';
import { useTimeRange } from '../../../../hooks/use_time_range';
import { APIReturnType } from '../../../../services/rest/create_call_apm_api';
import { TimeseriesChart } from '../../../shared/charts/timeseries_chart';
import { ListMetric } from '../../../shared/list_metric';
import { ServerlessFunctionNameLink } from './serverless_function_name_link';
type ServerlessActiveInstances =
APIReturnType<'GET /internal/apm/services/{serviceName}/metrics/serverless/active_instances'>;
const palette = euiPaletteColorBlind({ rotations: 2 });
interface Props {
serverlessId?: string;
}
export function ServerlessActiveInstances({ serverlessId }: Props) {
const {
query: { environment, kuery, rangeFrom, rangeTo },
} = useApmParams('/services/{serviceName}/metrics');
const { start, end } = useTimeRange({ rangeFrom, rangeTo });
const { serviceName } = useApmServiceContext();
const { data = { activeInstances: [], timeseries: [] }, status } = useFetcher(
(callApmApi) => {
if (!start || !end) {
return undefined;
}
return callApmApi(
'GET /internal/apm/services/{serviceName}/metrics/serverless/active_instances',
{
params: {
path: {
serviceName,
},
query: {
kuery,
environment,
start,
end,
serverlessId,
},
},
}
);
},
[kuery, environment, serviceName, start, end, serverlessId]
);
const isLoading = status === FETCH_STATUS.LOADING;
const columns: Array<
EuiBasicTableColumn<ServerlessActiveInstances['activeInstances'][0]>
> = [
{
field: 'serverlessFunctionName',
name: i18n.translate(
'xpack.apm.serverlessMetrics.activeInstances.functionName',
{ defaultMessage: 'Function name' }
),
sortable: true,
truncateText: true,
render: (_, item) => {
return (
<ServerlessFunctionNameLink
serverlessFunctionName={item.serverlessFunctionName}
serverlessId={item.serverlessId}
/>
);
},
},
{
field: 'activeInstanceName',
name: i18n.translate('xpack.apm.serverlessMetrics.activeInstances.name', {
defaultMessage: 'Name',
}),
sortable: true,
},
{
field: 'serverlessDurationAvg',
name: i18n.translate(
'xpack.apm.serverlessMetrics.serverlessFunctions.functionDuration',
{ defaultMessage: 'Function duration' }
),
sortable: true,
render: (_, { serverlessDurationAvg, timeseries }) => {
return (
<ListMetric
isLoading={isLoading}
series={timeseries.serverlessDuration}
color={palette[1]}
valueLabel={asMillisecondDuration(serverlessDurationAvg)}
/>
);
},
},
{
field: 'billedDurationAvg',
name: i18n.translate(
'xpack.apm.serverlessMetrics.activeInstances.billedDuration',
{ defaultMessage: 'Billed duration' }
),
sortable: true,
render: (_, { billedDurationAvg, timeseries }) => {
return (
<ListMetric
isLoading={isLoading}
series={timeseries.billedDuration}
color={palette[2]}
valueLabel={asMillisecondDuration(billedDurationAvg)}
/>
);
},
},
{
field: 'avgMemoryUsed',
name: i18n.translate(
'xpack.apm.serverlessMetrics.activeInstances.memoryUsageAvg',
{ defaultMessage: 'Memory usage avg.' }
),
sortable: true,
render: (_, { avgMemoryUsed }) => {
return asDynamicBytes(avgMemoryUsed);
},
},
{
field: 'memorySize',
name: i18n.translate(
'xpack.apm.serverlessMetrics.activeInstances.memorySize',
{ defaultMessage: 'Memory size' }
),
sortable: true,
render: (_, { memorySize }) => {
return asDynamicBytes(memorySize);
},
},
];
const sorting = useMemo(
() => ({
sort: {
field: 'serverlessDurationAvg',
direction: 'desc',
} as PropertySort,
}),
[]
);
const charts: Array<TimeSeries<Coordinate>> = useMemo(
() => [
{
title: i18n.translate(
'xpack.apm.serverlessMetrics.activeInstances.title',
{ defaultMessage: 'Active instances' }
),
data: data.timeseries,
type: 'bar',
color: palette[2],
},
],
[data.timeseries]
);
return (
<EuiPanel hasBorder={true}>
<EuiFlexGroup direction="column" gutterSize="xs">
<EuiFlexItem>
<EuiTitle size="xs">
<h2>
{i18n.translate(
'xpack.apm.serverlessMetrics.activeInstances.title',
{ defaultMessage: 'Active instances' }
)}
</h2>
</EuiTitle>
</EuiFlexItem>
<EuiFlexItem>
<TimeseriesChart
timeseries={charts}
id="activeInstances"
fetchStatus={status}
yLabelFormat={asInteger}
/>
</EuiFlexItem>
<EuiFlexItem>
<EuiInMemoryTable
loading={isLoading}
items={data.activeInstances}
columns={columns}
pagination={{ showPerPageOptions: false, pageSize: 5 }}
sorting={sorting}
/>
</EuiFlexItem>
</EuiFlexGroup>
</EuiPanel>
);
}

View file

@ -0,0 +1,42 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import { EuiLink } from '@elastic/eui';
import { euiStyled } from '@kbn/kibana-react-plugin/common';
import React from 'react';
import { useApmServiceContext } from '../../../../context/apm_service/use_apm_service_context';
import { useApmParams } from '../../../../hooks/use_apm_params';
import { useApmRouter } from '../../../../hooks/use_apm_router';
import { truncate } from '../../../../utils/style';
const StyledLink = euiStyled(EuiLink)`${truncate('100%')};`;
interface Props {
serverlessFunctionName: string;
serverlessId: string;
}
export function ServerlessFunctionNameLink({
serverlessFunctionName,
serverlessId,
}: Props) {
const { serviceName } = useApmServiceContext();
const { query } = useApmParams('/services/{serviceName}/metrics');
const { link } = useApmRouter();
return (
<StyledLink
href={link('/services/{serviceName}/metrics/{id}', {
path: {
serviceName,
id: serverlessId,
},
query,
})}
>
{serverlessFunctionName}
</StyledLink>
);
}

View file

@ -0,0 +1,178 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import {
EuiBasicTableColumn,
EuiFlexGroup,
EuiFlexItem,
EuiInMemoryTable,
EuiPanel,
EuiTitle,
PropertySort,
} from '@elastic/eui';
import { i18n } from '@kbn/i18n';
import React, { useMemo } from 'react';
import {
asDynamicBytes,
asMillisecondDuration,
} from '../../../../../common/utils/formatters';
import { useApmServiceContext } from '../../../../context/apm_service/use_apm_service_context';
import { useApmParams } from '../../../../hooks/use_apm_params';
import { FETCH_STATUS, useFetcher } from '../../../../hooks/use_fetcher';
import { useTimeRange } from '../../../../hooks/use_time_range';
import { APIReturnType } from '../../../../services/rest/create_call_apm_api';
import { ServerlessFunctionNameLink } from './serverless_function_name_link';
type ServerlessFunctionOverview =
APIReturnType<'GET /internal/apm/services/{serviceName}/metrics/serverless/functions_overview'>['serverlessFunctionsOverview'][0];
export function ServerlessFunctions() {
const {
query: { environment, kuery, rangeFrom, rangeTo },
} = useApmParams('/services/{serviceName}/metrics');
const { start, end } = useTimeRange({ rangeFrom, rangeTo });
const { serviceName } = useApmServiceContext();
const { data = { serverlessFunctionsOverview: [] }, status } = useFetcher(
(callApmApi) => {
if (!start || !end) {
return undefined;
}
return callApmApi(
'GET /internal/apm/services/{serviceName}/metrics/serverless/functions_overview',
{
params: {
path: {
serviceName,
},
query: {
kuery,
environment,
start,
end,
},
},
}
);
},
[kuery, environment, serviceName, start, end]
);
const columns: Array<EuiBasicTableColumn<ServerlessFunctionOverview>> = [
{
field: 'serverlessFunctionName',
name: i18n.translate(
'xpack.apm.serverlessMetrics.serverlessFunctions.functionName',
{ defaultMessage: 'Function name' }
),
sortable: true,
truncateText: true,
render: (_, item) => {
return (
<ServerlessFunctionNameLink
serverlessFunctionName={item.serverlessFunctionName}
serverlessId={item.serverlessId}
/>
);
},
},
{
field: 'serverlessDurationAvg',
name: i18n.translate(
'xpack.apm.serverlessMetrics.serverlessFunctions.functionDuration',
{ defaultMessage: 'Function duration' }
),
sortable: true,
render: (_, { serverlessDurationAvg }) => {
return asMillisecondDuration(serverlessDurationAvg);
},
},
{
field: 'billedDurationAvg',
name: i18n.translate(
'xpack.apm.serverlessMetrics.serverlessFunctions.billedDuration',
{ defaultMessage: 'Billed duration' }
),
sortable: true,
render: (_, { billedDurationAvg }) => {
return asMillisecondDuration(billedDurationAvg);
},
},
{
field: 'avgMemoryUsed',
name: i18n.translate(
'xpack.apm.serverlessMetrics.serverlessFunctions.memoryUsageAvg',
{ defaultMessage: 'Memory usage avg.' }
),
sortable: true,
render: (_, { avgMemoryUsed }) => {
return asDynamicBytes(avgMemoryUsed);
},
},
{
field: 'memorySize',
name: i18n.translate(
'xpack.apm.serverlessMetrics.serverlessFunctions.memorySize',
{ defaultMessage: 'Memory size' }
),
sortable: true,
render: (_, { memorySize }) => {
return asDynamicBytes(memorySize);
},
},
{
field: 'coldStartCount',
name: i18n.translate(
'xpack.apm.serverlessMetrics.serverlessFunctions.coldStart',
{ defaultMessage: 'Cold start' }
),
sortable: true,
},
];
const isLoading = status === FETCH_STATUS.LOADING;
const sorting = useMemo(
() => ({
sort: {
field: 'serverlessDurationAvg',
direction: 'desc',
} as PropertySort,
}),
[]
);
return (
<EuiPanel hasBorder={true}>
<EuiFlexGroup direction="column">
<EuiFlexItem>
<EuiFlexGroup alignItems="center">
<EuiFlexItem grow={false}>
<EuiTitle size="s">
<h2>
{i18n.translate(
'xpack.apm.serverlessMetrics.serverlessFunctions.title',
{ defaultMessage: 'Lambda functions' }
)}
</h2>
</EuiTitle>
</EuiFlexItem>
</EuiFlexGroup>
</EuiFlexItem>
<EuiFlexItem>
<EuiInMemoryTable
loading={isLoading}
items={data.serverlessFunctionsOverview}
columns={columns}
pagination={{ showPerPageOptions: false, pageSize: 5 }}
sorting={sorting}
/>
</EuiFlexItem>
</EuiFlexGroup>
</EuiPanel>
);
}

View file

@ -0,0 +1,111 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import { EuiFlexGrid, EuiFlexGroup, EuiFlexItem, EuiPanel } from '@elastic/eui';
import { isEmpty, keyBy } from 'lodash';
import React from 'react';
import { useApmServiceContext } from '../../../../context/apm_service/use_apm_service_context';
import { useApmParams } from '../../../../hooks/use_apm_params';
import { useFetcher } from '../../../../hooks/use_fetcher';
import { useTimeRange } from '../../../../hooks/use_time_range';
import { MetricsChart } from '../../../shared/charts/metrics_chart';
interface Props {
serverlessId?: string;
}
const INITIAL_STATE = {
firstLineCharts: [],
secondLineCharts: [],
};
export function ServerlessMetricsCharts({ serverlessId }: Props) {
const {
query: { environment, kuery, rangeFrom, rangeTo },
} = useApmParams('/services/{serviceName}/metrics');
const { start, end } = useTimeRange({ rangeFrom, rangeTo });
const { serviceName } = useApmServiceContext();
const { data = INITIAL_STATE, status } = useFetcher(
(callApmApi) => {
if (!start || !end) {
return undefined;
}
return callApmApi(
'GET /internal/apm/services/{serviceName}/metrics/serverless/charts',
{
params: {
path: {
serviceName,
},
query: {
kuery,
environment,
start,
end,
serverlessId,
},
},
}
).then((resp) => {
const chartsByKey = keyBy(resp.charts, 'key');
if (isEmpty(chartsByKey)) {
return { firstLineCharts: [], secondLineCharts: [] };
}
return {
firstLineCharts: [
chartsByKey.avg_duration,
chartsByKey.cold_start_duration,
chartsByKey.cold_start_count,
],
secondLineCharts: [
chartsByKey.compute_usage,
chartsByKey.memory_usage_chart,
],
};
});
},
[kuery, environment, serviceName, start, end, serverlessId]
);
return (
<EuiFlexGroup direction="column" gutterSize="m">
<EuiFlexItem>
<EuiFlexGrid columns={3} gutterSize="m">
{data.firstLineCharts.map((chart) => (
<EuiFlexItem key={chart.key}>
<EuiPanel hasBorder={true}>
<MetricsChart
start={start}
end={end}
chart={chart}
fetchStatus={status}
/>
</EuiPanel>
</EuiFlexItem>
))}
</EuiFlexGrid>
</EuiFlexItem>
<EuiFlexItem>
<EuiFlexGrid columns={2} gutterSize="m">
{data.secondLineCharts.map((chart) => (
<EuiFlexItem key={chart.key}>
<EuiPanel hasBorder={true}>
<MetricsChart
start={start}
end={end}
chart={chart}
fetchStatus={status}
/>
</EuiPanel>
</EuiFlexItem>
))}
</EuiFlexGrid>
</EuiFlexItem>
</EuiFlexGroup>
);
}

View file

@ -0,0 +1,169 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import {
EuiFlexGroup,
EuiFlexItem,
EuiLink,
EuiPanel,
EuiSpacer,
EuiStat,
EuiTitle,
} from '@elastic/eui';
import { i18n } from '@kbn/i18n';
import React from 'react';
import styled from 'styled-components';
import {
asMillisecondDuration,
asPercent,
} from '../../../../../common/utils/formatters';
import { useApmServiceContext } from '../../../../context/apm_service/use_apm_service_context';
import { useApmParams } from '../../../../hooks/use_apm_params';
import { useBreakpoints } from '../../../../hooks/use_breakpoints';
import { useFetcher, FETCH_STATUS } from '../../../../hooks/use_fetcher';
import { useTimeRange } from '../../../../hooks/use_time_range';
interface Props {
serverlessId?: string;
}
const CentralizedContainer = styled.div`
display: flex;
align-items: center;
`;
const Border = styled.div`
height: 55px;
border-right: 1px solid ${({ theme }) => theme.eui.euiColorLightShade};
`;
function VerticalRule() {
return (
<CentralizedContainer>
<Border />
</CentralizedContainer>
);
}
export function ServerlessSummary({ serverlessId }: Props) {
const breakpoints = useBreakpoints();
const {
query: { environment, kuery, rangeFrom, rangeTo },
} = useApmParams('/services/{serviceName}/metrics');
const { start, end } = useTimeRange({ rangeFrom, rangeTo });
const { serviceName } = useApmServiceContext();
const { data, status } = useFetcher(
(callApmApi) => {
if (!start || !end) {
return undefined;
}
return callApmApi(
'GET /internal/apm/services/{serviceName}/metrics/serverless/summary',
{
params: {
path: {
serviceName,
},
query: {
kuery,
environment,
start,
end,
serverlessId,
},
},
}
);
},
[kuery, environment, serviceName, start, end, serverlessId]
);
const showVerticalRule = !breakpoints.isSmall;
const isLoading = status === FETCH_STATUS.LOADING;
return (
<EuiPanel hasBorder={true}>
<EuiFlexGroup>
<EuiFlexItem>
<EuiTitle size="xs">
<h2>
{i18n.translate('xpack.apm.serverlessMetrics.summary.title', {
defaultMessage: 'Summary',
})}
</h2>
</EuiTitle>
</EuiFlexItem>
<EuiFlexItem grow={false}>
<EuiLink href="https://ela.st/feedback-aws-lambda" target="_blank">
{i18n.translate('xpack.apm.serverlessMetrics.summary.feedback', {
defaultMessage: 'Send feedback',
})}
</EuiLink>
</EuiFlexItem>
</EuiFlexGroup>
<EuiSpacer size="m" />
<EuiFlexGroup gutterSize="xl">
<EuiFlexItem grow={false}>
<EuiStat
isLoading={isLoading}
title={data?.serverlessFunctionsTotal}
titleSize="s"
description={i18n.translate(
'xpack.apm.serverlessMetrics.summary.lambdaFunctions',
{
defaultMessage:
'Lambda {serverlessFunctionsTotal, plural, one {function} other {functions}}',
values: {
serverlessFunctionsTotal: data?.serverlessFunctionsTotal,
},
}
)}
reverse
/>
</EuiFlexItem>
{showVerticalRule && <VerticalRule />}
<EuiFlexItem grow={false}>
<EuiStat
isLoading={isLoading}
title={asMillisecondDuration(data?.serverlessDurationAvg)}
titleSize="s"
description={i18n.translate(
'xpack.apm.serverlessMetrics.summary.functionDurationAvg',
{ defaultMessage: 'Function duration avg.' }
)}
reverse
/>
</EuiFlexItem>
<EuiFlexItem grow={false}>
<EuiStat
isLoading={isLoading}
title={asMillisecondDuration(data?.billedDurationAvg)}
titleSize="s"
description={i18n.translate(
'xpack.apm.serverlessMetrics.summary.billedDurationAvg',
{ defaultMessage: 'Billed duration avg.' }
)}
reverse
/>
</EuiFlexItem>
<EuiFlexItem grow={false}>
<EuiStat
isLoading={isLoading}
title={asPercent(data?.memoryUsageAvgRate, 1)}
titleSize="s"
description={i18n.translate(
'xpack.apm.serverlessMetrics.summary.memoryUsageAvg',
{ defaultMessage: 'Memory usage avg.' }
)}
reverse
/>
</EuiFlexItem>
{showVerticalRule && <VerticalRule />}
</EuiFlexGroup>
</EuiPanel>
);
}

View file

@ -5,8 +5,21 @@
* 2.0.
*/
import React from 'react';
import { isServerlessAgent } from '../../../../common/agent_name';
import { useApmServiceContext } from '../../../context/apm_service/use_apm_service_context';
import { useApmParams } from '../../../hooks/use_apm_params';
import { ServerlessMetricsDetails } from './serverless_metrics_details';
import { ServiceNodeMetrics } from './service_node_metrics';
export function MetricsDetails() {
return <ServiceNodeMetrics />;
const {
path: { id },
} = useApmParams('/services/{serviceName}/metrics/{id}');
const { runtimeName } = useApmServiceContext();
if (isServerlessAgent(runtimeName)) {
return <ServerlessMetricsDetails serverlessId={id} />;
}
return <ServiceNodeMetrics serviceNodeName={id} />;
}

View file

@ -0,0 +1,51 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import React, { useMemo } from 'react';
import { EuiFlexGroup, EuiFlexItem, EuiTitle } from '@elastic/eui';
import { ServerlessMetrics } from '../../metrics/serverless_metrics';
import { getServerlessFunctionNameFromId } from '../../../../../common/serverless';
import { useBreadcrumb } from '../../../../context/breadcrumbs/use_breadcrumb';
import { useApmRouter } from '../../../../hooks/use_apm_router';
import { useApmParams } from '../../../../hooks/use_apm_params';
interface Props {
serverlessId: string;
}
export function ServerlessMetricsDetails({ serverlessId }: Props) {
const apmRouter = useApmRouter();
const { path, query } = useApmParams('/services/{serviceName}/metrics/{id}');
const serverlessFunctionName = useMemo(
() => getServerlessFunctionNameFromId(serverlessId),
[serverlessId]
);
useBreadcrumb(
() => ({
title: serverlessFunctionName,
href: apmRouter.link('/services/{serviceName}/metrics/{id}', {
path,
query,
}),
}),
[apmRouter, path, query, serverlessFunctionName]
);
return (
<EuiFlexGroup direction="column">
<EuiFlexItem>
<EuiTitle>
<h2>{serverlessFunctionName}</h2>
</EuiTitle>
</EuiFlexItem>
<EuiFlexItem>
<ServerlessMetrics serverlessId={serverlessId} />
</EuiFlexItem>
</EuiFlexGroup>
);
}

View file

@ -16,7 +16,7 @@ describe('ServiceNodeMetrics', () => {
expect(() =>
shallow(
<MockApmPluginContextWrapper>
<ServiceNodeMetrics />
<ServiceNodeMetrics serviceNodeName="foo" />
</MockApmPluginContextWrapper>
)
).not.toThrowError();

View file

@ -46,15 +46,16 @@ const Truncate = euiStyled.span`
${truncate(unit * 12)}
`;
export function ServiceNodeMetrics() {
interface Props {
serviceNodeName: string;
}
export function ServiceNodeMetrics({ serviceNodeName }: Props) {
const { agentName, serviceName } = useApmServiceContext();
const apmRouter = useApmRouter();
const {
path: { id: serviceNodeName },
query,
} = useApmParams('/services/{serviceName}/metrics/{id}');
const { query } = useApmParams('/services/{serviceName}/metrics/{id}');
const { environment, kuery, rangeFrom, rangeTo } = query;

View file

@ -50,7 +50,7 @@ export function SparkPlot({
valueLabel: React.ReactNode;
compact?: boolean;
comparisonSeries?: Coordinate[];
comparisonSeriesColor: string;
comparisonSeriesColor?: string;
}) {
return (
<EuiFlexGroup
@ -90,7 +90,7 @@ function SparkPlotItem({
series?: Coordinate[] | null;
compact?: boolean;
comparisonSeries?: Coordinate[];
comparisonSeriesColor: string;
comparisonSeriesColor?: string;
}) {
const theme = useTheme();
const defaultChartTheme = useChartTheme();

View file

@ -32,7 +32,7 @@ export function useServiceMetricChartsFetcher({
} = useApmParams('/services/{serviceName}');
const { start, end } = useTimeRange({ rangeFrom, rangeTo });
const { agentName, serviceName, runtimeName } = useApmServiceContext();
const { agentName, serviceName } = useApmServiceContext();
const {
data = INITIAL_DATA,
@ -53,23 +53,13 @@ export function useServiceMetricChartsFetcher({
start,
end,
agentName,
serviceRuntimeName: runtimeName,
},
},
}
);
}
},
[
environment,
kuery,
serviceName,
start,
end,
agentName,
serviceNodeName,
runtimeName,
]
[environment, kuery, serviceName, start, end, agentName, serviceNodeName]
);
return {

View file

@ -1,122 +0,0 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import { i18n } from '@kbn/i18n';
import { kqlQuery, rangeQuery } from '@kbn/observability-plugin/server';
import { euiLightVars as theme } from '@kbn/ui-theme';
import { APMConfig } from '../../../..';
import {
SERVICE_NAME,
SERVICE_NODE_NAME,
} from '../../../../../common/elasticsearch_fieldnames';
import { environmentQuery } from '../../../../../common/utils/environment_query';
import { APMEventClient } from '../../../../lib/helpers/create_es_client/create_apm_event_client';
import { getMetricsDateHistogramParams } from '../../../../lib/helpers/metrics';
import {
getDocumentTypeFilterForTransactions,
getProcessorEventForTransactions,
} from '../../../../lib/helpers/transactions';
import { GenericMetricsChart } from '../../fetch_and_transform_metrics';
export async function getActiveInstances({
environment,
kuery,
config,
apmEventClient,
serviceName,
start,
end,
searchAggregatedTransactions,
}: {
environment: string;
kuery: string;
config: APMConfig;
apmEventClient: APMEventClient;
serviceName: string;
start: number;
end: number;
searchAggregatedTransactions: boolean;
}): Promise<GenericMetricsChart> {
const aggs = {
activeInstances: {
cardinality: {
field: SERVICE_NODE_NAME,
},
},
};
const params = {
apm: {
events: [getProcessorEventForTransactions(searchAggregatedTransactions)],
},
body: {
track_total_hits: false,
size: 0,
query: {
bool: {
filter: [
{ term: { [SERVICE_NAME]: serviceName } },
...rangeQuery(start, end),
...environmentQuery(environment),
...kqlQuery(kuery),
...getDocumentTypeFilterForTransactions(
searchAggregatedTransactions
),
],
},
},
aggs: {
...aggs,
timeseriesData: {
date_histogram: getMetricsDateHistogramParams({
start,
end,
metricsInterval: config.metricsInterval,
}),
aggs,
},
},
},
};
const { aggregations } = await apmEventClient.search(
'get_active_instances',
params
);
return {
title: i18n.translate('xpack.apm.agentMetrics.serverless.activeInstances', {
defaultMessage: 'Active instances',
}),
key: 'active_instances',
yUnit: 'integer',
description: i18n.translate(
'xpack.apm.agentMetrics.serverless.activeInstances.description',
{
defaultMessage:
'This chart shows the number of active instances of your serverless function over time. Multiple active instances may be a result of provisioned concurrency for your function or an increase in concurrent load that scales your function on-demand. An increase in active instance can be an indicator for an increase in concurrent invocations.',
}
),
series: [
{
title: i18n.translate(
'xpack.apm.agentMetrics.serverless.series.activeInstances',
{ defaultMessage: 'Active instances' }
),
key: 'active_instances',
type: 'bar',
color: theme.euiColorVis1,
overallValue: aggregations?.activeInstances.value ?? 0,
data:
aggregations?.timeseriesData.buckets.map((timeseriesBucket) => ({
x: timeseriesBucket.key,
y: timeseriesBucket.activeInstances.value,
})) || [],
},
],
};
}

View file

@ -88,7 +88,7 @@ export async function getMemoryChartData({
apmEventClient,
serviceName,
serviceNodeName,
faasId,
serverlessId,
start,
end,
}: {
@ -98,7 +98,7 @@ export async function getMemoryChartData({
apmEventClient: APMEventClient;
serviceName: string;
serviceNodeName?: string;
faasId?: string;
serverlessId?: string;
start: number;
end: number;
}) {
@ -119,7 +119,7 @@ export async function getMemoryChartData({
},
additionalFilters: [
{ exists: { field: METRIC_CGROUP_MEMORY_USAGE_BYTES } },
...termQuery(FAAS_ID, faasId),
...termQuery(FAAS_ID, serverlessId),
],
operationName: 'get_cgroup_memory_metrics_charts',
});
@ -142,7 +142,7 @@ export async function getMemoryChartData({
additionalFilters: [
{ exists: { field: METRIC_SYSTEM_FREE_MEMORY } },
{ exists: { field: METRIC_SYSTEM_TOTAL_MEMORY } },
...termQuery(FAAS_ID, faasId),
...termQuery(FAAS_ID, serverlessId),
],
operationName: 'get_system_memory_metrics_charts',
});

View file

@ -7,9 +7,8 @@
import { getJavaMetricsCharts } from './by_agent/java';
import { getDefaultMetricsCharts } from './by_agent/default';
import { isJavaAgentName, isServerlessAgent } from '../../../common/agent_name';
import { isJavaAgentName } from '../../../common/agent_name';
import { GenericMetricsChart } from './fetch_and_transform_metrics';
import { getServerlessAgentMetricCharts } from './by_agent/serverless';
import { APMConfig } from '../..';
import { APMEventClient } from '../../lib/helpers/create_es_client/create_apm_event_client';
@ -23,7 +22,6 @@ export async function getMetricsChartDataByAgent({
agentName,
start,
end,
serviceRuntimeName,
}: {
environment: string;
kuery: string;
@ -34,7 +32,6 @@ export async function getMetricsChartDataByAgent({
agentName: string;
start: number;
end: number;
serviceRuntimeName?: string;
}): Promise<GenericMetricsChart[]> {
const options = {
environment,
@ -45,18 +42,13 @@ export async function getMetricsChartDataByAgent({
start,
end,
};
const serverlessAgent = isServerlessAgent(serviceRuntimeName);
if (isJavaAgentName(agentName) && !serverlessAgent) {
if (isJavaAgentName(agentName)) {
return getJavaMetricsCharts({
...options,
serviceNodeName,
});
}
if (serverlessAgent) {
return getServerlessAgentMetricCharts(options);
}
return getDefaultMetricsCharts(options);
}

View file

@ -13,6 +13,7 @@ import { environmentRt, kueryRt, rangeRt } from '../default_api_types';
import { FetchAndTransformMetrics } from './fetch_and_transform_metrics';
import { getMetricsChartDataByAgent } from './get_metrics_chart_data_by_agent';
import { getServiceNodes } from './get_service_nodes';
import { metricsServerlessRouteRepository } from './serverless/route';
const metricsChartsRoute = createApmServerRoute({
endpoint: 'GET /internal/apm/services/{serviceName}/metrics/charts',
@ -26,7 +27,6 @@ const metricsChartsRoute = createApmServerRoute({
}),
t.partial({
serviceNodeName: t.string,
serviceRuntimeName: t.string,
}),
environmentRt,
kueryRt,
@ -45,15 +45,8 @@ const metricsChartsRoute = createApmServerRoute({
getApmEventClient(resources),
]);
const { serviceName } = params.path;
const {
agentName,
environment,
kuery,
serviceNodeName,
start,
end,
serviceRuntimeName,
} = params.query;
const { agentName, environment, kuery, serviceNodeName, start, end } =
params.query;
const charts = await getMetricsChartDataByAgent({
environment,
@ -65,7 +58,6 @@ const metricsChartsRoute = createApmServerRoute({
serviceNodeName,
start,
end,
serviceRuntimeName,
});
return { charts };
@ -113,4 +105,5 @@ const serviceMetricsJvm = createApmServerRoute({
export const metricsRouteRepository = {
...metricsChartsRoute,
...serviceMetricsJvm,
...metricsServerlessRouteRepository,
};

View file

@ -0,0 +1,191 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import { ProcessorEvent } from '@kbn/observability-plugin/common';
import {
kqlQuery,
rangeQuery,
termQuery,
} from '@kbn/observability-plugin/server';
import {
FAAS_BILLED_DURATION,
FAAS_DURATION,
FAAS_ID,
METRICSET_NAME,
METRIC_SYSTEM_FREE_MEMORY,
METRIC_SYSTEM_TOTAL_MEMORY,
SERVICE_NAME,
SERVICE_NODE_NAME,
} from '../../../../common/elasticsearch_fieldnames';
import { getServerlessFunctionNameFromId } from '../../../../common/serverless';
import { environmentQuery } from '../../../../common/utils/environment_query';
import { Coordinate } from '../../../../typings/timeseries';
import { getBucketSize } from '../../../lib/helpers/get_bucket_size';
import { calcMemoryUsed } from './helper';
import { APMEventClient } from '../../../lib/helpers/create_es_client/create_apm_event_client';
interface ActiveInstanceTimeseries {
serverlessDuration: Coordinate[];
billedDuration: Coordinate[];
}
export interface ActiveInstanceOverview {
activeInstanceName: string;
serverlessId: string;
serverlessFunctionName: string;
timeseries: ActiveInstanceTimeseries;
serverlessDurationAvg: number | null;
billedDurationAvg: number | null;
avgMemoryUsed?: number | null;
memorySize: number | null;
}
export async function getServerlessActiveInstancesOverview({
end,
environment,
kuery,
serviceName,
start,
serverlessId,
apmEventClient,
}: {
environment: string;
kuery: string;
serviceName: string;
start: number;
end: number;
serverlessId?: string;
apmEventClient: APMEventClient;
}) {
const { intervalString } = getBucketSize({
start,
end,
numBuckets: 20,
});
const aggs = {
faasDurationAvg: { avg: { field: FAAS_DURATION } },
faasBilledDurationAvg: { avg: { field: FAAS_BILLED_DURATION } },
};
const params = {
apm: {
events: [ProcessorEvent.metric],
},
body: {
track_total_hits: 1,
size: 0,
query: {
bool: {
filter: [
...termQuery(METRICSET_NAME, 'app'),
{ term: { [SERVICE_NAME]: serviceName } },
...rangeQuery(start, end),
...environmentQuery(environment),
...kqlQuery(kuery),
...termQuery(FAAS_ID, serverlessId),
],
},
},
aggs: {
activeInstances: {
terms: { field: SERVICE_NODE_NAME },
aggs: {
serverlessFunctions: {
terms: { field: FAAS_ID },
aggs: {
...{
...aggs,
maxTotalMemory: {
max: { field: METRIC_SYSTEM_TOTAL_MEMORY },
},
avgTotalMemory: {
avg: { field: METRIC_SYSTEM_TOTAL_MEMORY },
},
avgFreeMemory: { avg: { field: METRIC_SYSTEM_FREE_MEMORY } },
},
timeseries: {
date_histogram: {
field: '@timestamp',
fixed_interval: intervalString,
min_doc_count: 0,
extended_bounds: {
min: start,
max: end,
},
},
aggs,
},
},
},
},
},
},
},
};
const response = await apmEventClient.search(
'ger_serverless_active_instances_overview',
params
);
return (
response.aggregations?.activeInstances?.buckets?.flatMap((bucket) => {
const activeInstanceName = bucket.key as string;
const serverlessFunctionsDetails =
bucket.serverlessFunctions.buckets.reduce<ActiveInstanceOverview[]>(
(acc, curr) => {
const currentServerlessId = curr.key as string;
const timeseries =
curr.timeseries.buckets.reduce<ActiveInstanceTimeseries>(
(timeseriesAcc, timeseriesCurr) => {
return {
serverlessDuration: [
...timeseriesAcc.serverlessDuration,
{
x: timeseriesCurr.key,
y: timeseriesCurr.faasDurationAvg.value,
},
],
billedDuration: [
...timeseriesAcc.billedDuration,
{
x: timeseriesCurr.key,
y: timeseriesCurr.faasBilledDurationAvg.value,
},
],
};
},
{
serverlessDuration: [],
billedDuration: [],
}
);
return [
...acc,
{
activeInstanceName,
serverlessId: currentServerlessId,
serverlessFunctionName:
getServerlessFunctionNameFromId(currentServerlessId),
timeseries,
serverlessDurationAvg: curr.faasDurationAvg.value,
billedDurationAvg: curr.faasBilledDurationAvg.value,
avgMemoryUsed: calcMemoryUsed({
memoryFree: curr.avgFreeMemory.value,
memoryTotal: curr.avgTotalMemory.value,
}),
memorySize: curr.avgTotalMemory.value,
},
];
},
[]
);
return serverlessFunctionsDetails;
}) || []
);
}

View file

@ -0,0 +1,97 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import {
kqlQuery,
rangeQuery,
termQuery,
} from '@kbn/observability-plugin/server';
import { ProcessorEvent } from '@kbn/observability-plugin/common';
import {
FAAS_ID,
METRICSET_NAME,
SERVICE_NAME,
SERVICE_NODE_NAME,
} from '../../../../common/elasticsearch_fieldnames';
import { environmentQuery } from '../../../../common/utils/environment_query';
import { Coordinate } from '../../../../typings/timeseries';
import { getMetricsDateHistogramParams } from '../../../lib/helpers/metrics';
import { APMEventClient } from '../../../lib/helpers/create_es_client/create_apm_event_client';
import { APMConfig } from '../../..';
export async function getActiveInstancesTimeseries({
environment,
kuery,
serviceName,
start,
end,
serverlessId,
config,
apmEventClient,
}: {
environment: string;
kuery: string;
serviceName: string;
start: number;
end: number;
serverlessId?: string;
config: APMConfig;
apmEventClient: APMEventClient;
}): Promise<Coordinate[]> {
const aggs = {
activeInstances: {
cardinality: {
field: SERVICE_NODE_NAME,
},
},
};
const params = {
apm: {
events: [ProcessorEvent.metric],
},
body: {
track_total_hits: false,
size: 0,
query: {
bool: {
filter: [
...termQuery(METRICSET_NAME, 'app'),
{ term: { [SERVICE_NAME]: serviceName } },
...rangeQuery(start, end),
...environmentQuery(environment),
...kqlQuery(kuery),
...termQuery(FAAS_ID, serverlessId),
],
},
},
aggs: {
...aggs,
timeseriesData: {
date_histogram: getMetricsDateHistogramParams({
start,
end,
metricsInterval: config.metricsInterval,
}),
aggs,
},
},
},
};
const { aggregations } = await apmEventClient.search(
'get_active_instances',
params
);
return (
aggregations?.timeseriesData?.buckets?.map((timeseriesBucket) => ({
x: timeseriesBucket.key,
y: timeseriesBucket.activeInstances.value,
})) || []
);
}

View file

@ -8,18 +8,19 @@
import { i18n } from '@kbn/i18n';
import { termQuery } from '@kbn/observability-plugin/server';
import { euiLightVars as theme } from '@kbn/ui-theme';
import { APMConfig } from '../../../..';
import { APMConfig } from '../../..';
import {
FAAS_COLDSTART,
FAAS_ID,
METRICSET_NAME,
} from '../../../../../common/elasticsearch_fieldnames';
import { APMEventClient } from '../../../../lib/helpers/create_es_client/create_apm_event_client';
import { fetchAndTransformMetrics } from '../../fetch_and_transform_metrics';
import { ChartBase } from '../../types';
} from '../../../../common/elasticsearch_fieldnames';
import { fetchAndTransformMetrics } from '../fetch_and_transform_metrics';
import { ChartBase } from '../types';
import { APMEventClient } from '../../../lib/helpers/create_es_client/create_apm_event_client';
const chartBase: ChartBase = {
title: i18n.translate('xpack.apm.agentMetrics.serverless.coldStart', {
defaultMessage: 'Cold start',
title: i18n.translate('xpack.apm.agentMetrics.serverless.coldStart.title', {
defaultMessage: 'Cold starts',
}),
key: 'cold_start_count',
type: 'bar',
@ -34,7 +35,7 @@ const chartBase: ChartBase = {
},
};
export function getColdStartCount({
export function getColdStartCountChart({
environment,
kuery,
config,
@ -42,6 +43,7 @@ export function getColdStartCount({
serviceName,
start,
end,
serverlessId,
}: {
environment: string;
kuery: string;
@ -50,6 +52,7 @@ export function getColdStartCount({
serviceName: string;
start: number;
end: number;
serverlessId?: string;
}) {
return fetchAndTransformMetrics({
environment,
@ -63,7 +66,8 @@ export function getColdStartCount({
aggs: { coldStart: { sum: { field: FAAS_COLDSTART } } },
additionalFilters: [
...termQuery(FAAS_COLDSTART, true),
...termQuery(METRICSET_NAME, 'transaction'),
...termQuery(FAAS_ID, serverlessId),
...termQuery(METRICSET_NAME, 'app'),
],
operationName: 'get_cold_start_count',
});

View file

@ -7,12 +7,17 @@
import { i18n } from '@kbn/i18n';
import { euiLightVars as theme } from '@kbn/ui-theme';
import { FAAS_COLDSTART_DURATION } from '../../../../../common/elasticsearch_fieldnames';
import { fetchAndTransformMetrics } from '../../fetch_and_transform_metrics';
import { ChartBase } from '../../types';
import { isFiniteNumber } from '../../../../../common/utils/is_finite_number';
import { APMConfig } from '../../../..';
import { APMEventClient } from '../../../../lib/helpers/create_es_client/create_apm_event_client';
import { termQuery } from '@kbn/observability-plugin/server';
import {
FAAS_COLDSTART_DURATION,
FAAS_ID,
METRICSET_NAME,
} from '../../../../common/elasticsearch_fieldnames';
import { fetchAndTransformMetrics } from '../fetch_and_transform_metrics';
import { ChartBase } from '../types';
import { isFiniteNumber } from '../../../../common/utils/is_finite_number';
import { APMConfig } from '../../..';
import { APMEventClient } from '../../../lib/helpers/create_es_client/create_apm_event_client';
const chartBase: ChartBase = {
title: i18n.translate('xpack.apm.agentMetrics.serverless.coldStartDuration', {
@ -39,7 +44,7 @@ const chartBase: ChartBase = {
),
};
export async function getColdStartDuration({
export async function getColdStartDurationChart({
environment,
kuery,
config,
@ -47,6 +52,7 @@ export async function getColdStartDuration({
serviceName,
start,
end,
serverlessId,
}: {
environment: string;
kuery: string;
@ -55,6 +61,7 @@ export async function getColdStartDuration({
serviceName: string;
start: number;
end: number;
serverlessId?: string;
}) {
const coldStartDurationMetric = await fetchAndTransformMetrics({
environment,
@ -66,13 +73,17 @@ export async function getColdStartDuration({
end,
chartBase,
aggs: { coldStart: { avg: { field: FAAS_COLDSTART_DURATION } } },
additionalFilters: [{ exists: { field: FAAS_COLDSTART_DURATION } }],
additionalFilters: [
{ exists: { field: FAAS_COLDSTART_DURATION } },
...termQuery(FAAS_ID, serverlessId),
...termQuery(METRICSET_NAME, 'app'),
],
operationName: 'get_cold_start_duration',
});
const [series] = coldStartDurationMetric.series;
const data = series.data.map(({ x, y }) => ({
const data = series?.data?.map(({ x, y }) => ({
x,
// Cold start duration duration is stored in ms, convert it to microseconds so it uses the same unit as the other charts
y: isFiniteNumber(y) ? y * 1000 : y,
@ -80,13 +91,15 @@ export async function getColdStartDuration({
return {
...coldStartDurationMetric,
series: [
{
...series,
// Cold start duration duration is stored in ms, convert it to microseconds
overallValue: series.overallValue * 1000,
data,
},
],
series: series
? [
{
...series,
// Cold start duration duration is stored in ms, convert it to microseconds
overallValue: series.overallValue * 1000,
data,
},
]
: [],
};
}

View file

@ -13,18 +13,19 @@ import {
termQuery,
} from '@kbn/observability-plugin/server';
import { euiLightVars as theme } from '@kbn/ui-theme';
import { APMConfig } from '../../../..';
import { APMConfig } from '../../..';
import {
FAAS_BILLED_DURATION,
FAAS_ID,
METRICSET_NAME,
METRIC_SYSTEM_TOTAL_MEMORY,
SERVICE_NAME,
} from '../../../../../common/elasticsearch_fieldnames';
import { environmentQuery } from '../../../../../common/utils/environment_query';
import { isFiniteNumber } from '../../../../../common/utils/is_finite_number';
import { APMEventClient } from '../../../../lib/helpers/create_es_client/create_apm_event_client';
import { getMetricsDateHistogramParams } from '../../../../lib/helpers/metrics';
import { GenericMetricsChart } from '../../fetch_and_transform_metrics';
} from '../../../../common/elasticsearch_fieldnames';
import { environmentQuery } from '../../../../common/utils/environment_query';
import { isFiniteNumber } from '../../../../common/utils/is_finite_number';
import { getMetricsDateHistogramParams } from '../../../lib/helpers/metrics';
import { GenericMetricsChart } from '../fetch_and_transform_metrics';
import { APMEventClient } from '../../../lib/helpers/create_es_client/create_apm_event_client';
/**
* To calculate the compute usage we need to multiply the "system.memory.total" by "faas.billed_duration".
@ -48,7 +49,7 @@ function calculateComputeUsageGBSeconds({
return totalMemoryGB * faasBilledDurationSec;
}
export async function getComputeUsage({
export async function getComputeUsageChart({
environment,
kuery,
config,
@ -56,6 +57,7 @@ export async function getComputeUsage({
serviceName,
start,
end,
serverlessId,
}: {
environment: string;
kuery: string;
@ -64,6 +66,7 @@ export async function getComputeUsage({
serviceName: string;
start: number;
end: number;
serverlessId?: string;
}): Promise<GenericMetricsChart> {
const aggs = {
avgFaasBilledDuration: { avg: { field: FAAS_BILLED_DURATION } },
@ -86,6 +89,7 @@ export async function getComputeUsage({
...kqlQuery(kuery),
{ exists: { field: FAAS_BILLED_DURATION } },
...termQuery(METRICSET_NAME, 'app'),
...termQuery(FAAS_ID, serverlessId),
],
},
},

View file

@ -5,18 +5,17 @@
* 2.0.
*/
import { withApmSpan } from '../../../../utils/with_apm_span';
import { getServerlessFunctionLatency } from './serverless_function_latency';
import { getColdStartDuration } from './cold_start_duration';
import { getMemoryChartData } from '../shared/memory';
import { getComputeUsage } from './compute_usage';
import { getActiveInstances } from './active_instances';
import { getColdStartCount } from './cold_start_count';
import { getSearchTransactionsEvents } from '../../../../lib/helpers/transactions';
import { APMConfig } from '../../../..';
import { APMEventClient } from '../../../../lib/helpers/create_es_client/create_apm_event_client';
import { getSearchTransactionsEvents } from '../../../lib/helpers/transactions';
import { withApmSpan } from '../../../utils/with_apm_span';
import { getMemoryChartData } from '../by_agent/shared/memory';
import { getColdStartCountChart } from './get_cold_start_count_chart';
import { getColdStartDurationChart } from './get_cold_start_duration_chart';
import { getComputeUsageChart } from './get_compute_usage_chart';
import { getServerlessFunctionLatencyChart } from './get_serverless_function_latency_chart';
import { APMConfig } from '../../..';
import { APMEventClient } from '../../../lib/helpers/create_es_client/create_apm_event_client';
export function getServerlessAgentMetricCharts({
export function getServerlessAgentMetricsCharts({
environment,
kuery,
config,
@ -24,6 +23,7 @@ export function getServerlessAgentMetricCharts({
serviceName,
start,
end,
serverlessId,
}: {
environment: string;
kuery: string;
@ -32,6 +32,7 @@ export function getServerlessAgentMetricCharts({
serviceName: string;
start: number;
end: number;
serverlessId?: string;
}) {
return withApmSpan('get_serverless_agent_metric_charts', async () => {
const searchAggregatedTransactions = await getSearchTransactionsEvents({
@ -50,17 +51,17 @@ export function getServerlessAgentMetricCharts({
serviceName,
start,
end,
serverlessId,
};
return await Promise.all([
getServerlessFunctionLatency({
getServerlessFunctionLatencyChart({
...options,
searchAggregatedTransactions,
}),
getMemoryChartData(options),
getColdStartDuration(options),
getColdStartCount(options),
getComputeUsage(options),
getActiveInstances({ ...options, searchAggregatedTransactions }),
getColdStartDurationChart(options),
getColdStartCountChart(options),
getComputeUsageChart(options),
]);
});
}

View file

@ -7,18 +7,24 @@
import { i18n } from '@kbn/i18n';
import { euiLightVars as theme } from '@kbn/ui-theme';
import { APMConfig } from '../../../..';
import { FAAS_BILLED_DURATION } from '../../../../../common/elasticsearch_fieldnames';
import { LatencyAggregationType } from '../../../../../common/latency_aggregation_types';
import { isFiniteNumber } from '../../../../../common/utils/is_finite_number';
import { getVizColorForIndex } from '../../../../../common/viz_colors';
import { APMEventClient } from '../../../../lib/helpers/create_es_client/create_apm_event_client';
import { getLatencyTimeseries } from '../../../transactions/get_latency_charts';
import { termQuery } from '@kbn/observability-plugin/server';
import { isEmpty } from 'lodash';
import {
FAAS_BILLED_DURATION,
FAAS_ID,
METRICSET_NAME,
} from '../../../../common/elasticsearch_fieldnames';
import { LatencyAggregationType } from '../../../../common/latency_aggregation_types';
import { isFiniteNumber } from '../../../../common/utils/is_finite_number';
import { getVizColorForIndex } from '../../../../common/viz_colors';
import { getLatencyTimeseries } from '../../transactions/get_latency_charts';
import { APMConfig } from '../../..';
import { APMEventClient } from '../../../lib/helpers/create_es_client/create_apm_event_client';
import {
fetchAndTransformMetrics,
GenericMetricsChart,
} from '../../fetch_and_transform_metrics';
import { ChartBase } from '../../types';
} from '../fetch_and_transform_metrics';
import { ChartBase } from '../types';
const billedDurationAvg = {
title: i18n.translate('xpack.apm.agentMetrics.serverless.billedDurationAvg', {
@ -28,7 +34,7 @@ const billedDurationAvg = {
const chartBase: ChartBase = {
title: i18n.translate('xpack.apm.agentMetrics.serverless.avgDuration', {
defaultMessage: 'Avg. Duration',
defaultMessage: 'Lambda Duration',
}),
key: 'avg_duration',
type: 'linemark',
@ -50,6 +56,7 @@ async function getServerlessLantecySeries({
serviceName,
start,
end,
serverlessId,
searchAggregatedTransactions,
}: {
environment: string;
@ -58,6 +65,7 @@ async function getServerlessLantecySeries({
serviceName: string;
start: number;
end: number;
serverlessId?: string;
searchAggregatedTransactions: boolean;
}): Promise<GenericMetricsChart['series']> {
const transactionLatency = await getLatencyTimeseries({
@ -69,6 +77,7 @@ async function getServerlessLantecySeries({
latencyAggregationType: LatencyAggregationType.avg,
start,
end,
serverlessId,
});
return [
@ -86,7 +95,7 @@ async function getServerlessLantecySeries({
];
}
export async function getServerlessFunctionLatency({
export async function getServerlessFunctionLatencyChart({
environment,
kuery,
config,
@ -94,6 +103,7 @@ export async function getServerlessFunctionLatency({
serviceName,
start,
end,
serverlessId,
searchAggregatedTransactions,
}: {
environment: string;
@ -103,6 +113,7 @@ export async function getServerlessFunctionLatency({
serviceName: string;
start: number;
end: number;
serverlessId?: string;
searchAggregatedTransactions: boolean;
}): Promise<GenericMetricsChart> {
const options = {
@ -122,29 +133,43 @@ export async function getServerlessFunctionLatency({
aggs: {
billedDurationAvg: { avg: { field: FAAS_BILLED_DURATION } },
},
additionalFilters: [{ exists: { field: FAAS_BILLED_DURATION } }],
additionalFilters: [
{ exists: { field: FAAS_BILLED_DURATION } },
...termQuery(FAAS_ID, serverlessId),
...termQuery(METRICSET_NAME, 'app'),
],
operationName: 'get_billed_duration',
}),
getServerlessLantecySeries({ ...options, searchAggregatedTransactions }),
getServerlessLantecySeries({
...options,
serverlessId,
searchAggregatedTransactions,
}),
]);
const [series] = billedDurationMetrics.series;
const data = series.data.map(({ x, y }) => ({
x,
// Billed duration is stored in ms, convert it to microseconds so it uses the same unit as the other chart
y: isFiniteNumber(y) ? y * 1000 : y,
}));
const series = [];
const [billedDurationSeries] = billedDurationMetrics.series;
if (billedDurationSeries) {
const data = billedDurationSeries.data?.map(({ x, y }) => ({
x,
// Billed duration is stored in ms, convert it to microseconds so it uses the same unit as the other chart
y: isFiniteNumber(y) ? y * 1000 : y,
}));
series.push({
...billedDurationSeries,
// Billed duration is stored in ms, convert it to microseconds
overallValue: billedDurationSeries.overallValue * 1000,
data: data || [],
});
}
if (!isEmpty(serverlessDurationSeries[0].data)) {
series.push(...serverlessDurationSeries);
}
return {
...billedDurationMetrics,
series: [
{
...series,
// Billed duration is stored in ms, convert it to microseconds
overallValue: series.overallValue * 1000,
data,
},
...serverlessDurationSeries,
],
series,
};
}

View file

@ -0,0 +1,100 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import { ProcessorEvent } from '@kbn/observability-plugin/common';
import {
kqlQuery,
rangeQuery,
termQuery,
} from '@kbn/observability-plugin/server';
import {
FAAS_BILLED_DURATION,
FAAS_COLDSTART,
FAAS_DURATION,
FAAS_ID,
METRICSET_NAME,
METRIC_SYSTEM_FREE_MEMORY,
METRIC_SYSTEM_TOTAL_MEMORY,
SERVICE_NAME,
} from '../../../../common/elasticsearch_fieldnames';
import { getServerlessFunctionNameFromId } from '../../../../common/serverless';
import { environmentQuery } from '../../../../common/utils/environment_query';
import { calcMemoryUsed } from './helper';
import { APMEventClient } from '../../../lib/helpers/create_es_client/create_apm_event_client';
export async function getServerlessFunctionsOverview({
end,
environment,
kuery,
serviceName,
start,
apmEventClient,
}: {
environment: string;
kuery: string;
serviceName: string;
start: number;
end: number;
apmEventClient: APMEventClient;
}) {
const params = {
apm: {
events: [ProcessorEvent.metric],
},
body: {
track_total_hits: false,
size: 0,
query: {
bool: {
filter: [
...termQuery(METRICSET_NAME, 'app'),
{ term: { [SERVICE_NAME]: serviceName } },
...rangeQuery(start, end),
...environmentQuery(environment),
...kqlQuery(kuery),
],
},
},
aggs: {
serverlessFunctions: {
terms: { field: FAAS_ID },
aggs: {
faasDurationAvg: { avg: { field: FAAS_DURATION } },
faasBilledDurationAvg: { avg: { field: FAAS_BILLED_DURATION } },
coldStartCount: { sum: { field: FAAS_COLDSTART } },
maxTotalMemory: { max: { field: METRIC_SYSTEM_TOTAL_MEMORY } },
avgTotalMemory: { avg: { field: METRIC_SYSTEM_TOTAL_MEMORY } },
avgFreeMemory: { avg: { field: METRIC_SYSTEM_FREE_MEMORY } },
},
},
},
},
};
const response = await apmEventClient.search(
'ger_serverless_functions_overview',
params
);
const serverlessFunctionsOverview =
response.aggregations?.serverlessFunctions?.buckets?.map((bucket) => {
const serverlessId = bucket.key as string;
return {
serverlessId,
serverlessFunctionName: getServerlessFunctionNameFromId(serverlessId),
serverlessDurationAvg: bucket.faasDurationAvg.value,
billedDurationAvg: bucket.faasBilledDurationAvg.value,
coldStartCount: bucket.coldStartCount.value,
avgMemoryUsed: calcMemoryUsed({
memoryFree: bucket.avgFreeMemory.value,
memoryTotal: bucket.avgTotalMemory.value,
}),
memorySize: bucket.maxTotalMemory.value,
};
});
return serverlessFunctionsOverview || [];
}

View file

@ -0,0 +1,86 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import { ProcessorEvent } from '@kbn/observability-plugin/common';
import {
termQuery,
kqlQuery,
rangeQuery,
} from '@kbn/observability-plugin/server';
import {
FAAS_BILLED_DURATION,
FAAS_DURATION,
FAAS_ID,
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';
export async function getServerlessSummary({
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.metric],
},
body: {
track_total_hits: false,
size: 0,
query: {
bool: {
filter: [
...termQuery(METRICSET_NAME, 'app'),
{ term: { [SERVICE_NAME]: serviceName } },
...rangeQuery(start, end),
...environmentQuery(environment),
...kqlQuery(kuery),
...termQuery(FAAS_ID, serverlessId),
],
},
},
aggs: {
totalFunctions: { cardinality: { field: FAAS_ID } },
faasDurationAvg: { avg: { field: FAAS_DURATION } },
faasBilledDurationAvg: { avg: { field: FAAS_BILLED_DURATION } },
avgTotalMemory: { avg: { field: METRIC_SYSTEM_TOTAL_MEMORY } },
avgFreeMemory: { avg: { field: METRIC_SYSTEM_FREE_MEMORY } },
},
},
};
const response = await apmEventClient.search(
'ger_serverless_summary',
params
);
return {
memoryUsageAvgRate: calcMemoryUsedRate({
memoryFree: response.aggregations?.avgFreeMemory?.value,
memoryTotal: response.aggregations?.avgTotalMemory?.value,
}),
serverlessFunctionsTotal: response.aggregations?.totalFunctions?.value,
serverlessDurationAvg: response.aggregations?.faasDurationAvg?.value,
billedDurationAvg: response.aggregations?.faasBilledDurationAvg?.value,
};
}

View file

@ -0,0 +1,40 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import { calcMemoryUsed, calcMemoryUsedRate } from './helper';
describe('calcMemoryUsed', () => {
it('returns undefined when memory values are no a number', () => {
[
{ memoryFree: null, memoryTotal: null },
{ memoryFree: undefined, memoryTotal: undefined },
{ memoryFree: 100, memoryTotal: undefined },
{ memoryFree: undefined, memoryTotal: 100 },
].forEach(({ memoryFree, memoryTotal }) => {
expect(calcMemoryUsed({ memoryFree, memoryTotal })).toBeUndefined();
});
});
it('returns correct memory used', () => {
expect(calcMemoryUsed({ memoryFree: 50, memoryTotal: 100 })).toBe(50);
});
});
describe('calcMemoryUsedRate', () => {
it('returns undefined when memory values are no a number', () => {
[
{ memoryFree: null, memoryTotal: null },
{ memoryFree: undefined, memoryTotal: undefined },
{ memoryFree: 100, memoryTotal: undefined },
{ memoryFree: undefined, memoryTotal: 100 },
].forEach(({ memoryFree, memoryTotal }) => {
expect(calcMemoryUsedRate({ memoryFree, memoryTotal })).toBeUndefined();
});
});
it('returns correct memory used rate', () => {
expect(calcMemoryUsedRate({ memoryFree: 50, memoryTotal: 100 })).toBe(0.5);
});
});

View file

@ -0,0 +1,35 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import { isFiniteNumber } from '../../../../common/utils/is_finite_number';
export function calcMemoryUsedRate({
memoryFree,
memoryTotal,
}: {
memoryFree?: number | null;
memoryTotal?: number | null;
}) {
if (!isFiniteNumber(memoryFree) || !isFiniteNumber(memoryTotal)) {
return undefined;
}
return (memoryTotal - memoryFree) / memoryTotal;
}
export function calcMemoryUsed({
memoryFree,
memoryTotal,
}: {
memoryFree?: number | null;
memoryTotal?: number | null;
}) {
if (!isFiniteNumber(memoryFree) || !isFiniteNumber(memoryTotal)) {
return undefined;
}
return memoryTotal - memoryFree;
}

View file

@ -0,0 +1,189 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import * as t from 'io-ts';
import { setupRequest } from '../../../lib/helpers/setup_request';
import { createApmServerRoute } from '../../apm_routes/create_apm_server_route';
import { environmentRt, kueryRt, rangeRt } from '../../default_api_types';
import { getServerlessAgentMetricsCharts } from './get_serverless_agent_metrics_chart';
import { getServerlessActiveInstancesOverview } from './get_active_instances_overview';
import { getServerlessFunctionsOverview } from './get_serverless_functions_overview';
import { getServerlessSummary } from './get_serverless_summary';
import { getActiveInstancesTimeseries } from './get_active_instances_timeseries';
import { getApmEventClient } from '../../../lib/helpers/get_apm_event_client';
const serverlessMetricsChartsRoute = createApmServerRoute({
endpoint:
'GET /internal/apm/services/{serviceName}/metrics/serverless/charts',
params: t.type({
path: t.type({
serviceName: t.string,
}),
query: t.intersection([
environmentRt,
kueryRt,
rangeRt,
t.partial({ serverlessId: t.string }),
]),
}),
options: { tags: ['access:apm'] },
handler: async (
resources
): Promise<{
charts: Awaited<ReturnType<typeof getServerlessAgentMetricsCharts>>;
}> => {
const { params } = resources;
const [setup, apmEventClient] = await Promise.all([
setupRequest(resources),
getApmEventClient(resources),
]);
const { serviceName } = params.path;
const { environment, kuery, start, end, serverlessId } = params.query;
const charts = await getServerlessAgentMetricsCharts({
environment,
start,
end,
kuery,
config: setup.config,
apmEventClient,
serviceName,
serverlessId,
});
return { charts };
},
});
const serverlessMetricsActiveInstancesRoute = createApmServerRoute({
endpoint:
'GET /internal/apm/services/{serviceName}/metrics/serverless/active_instances',
params: t.type({
path: t.type({
serviceName: t.string,
}),
query: t.intersection([
environmentRt,
kueryRt,
rangeRt,
t.partial({ serverlessId: t.string }),
]),
}),
options: { tags: ['access:apm'] },
handler: async (
resources
): Promise<{
activeInstances: Awaited<
ReturnType<typeof getServerlessActiveInstancesOverview>
>;
timeseries: Awaited<ReturnType<typeof getActiveInstancesTimeseries>>;
}> => {
const { params } = resources;
const [setup, apmEventClient] = await Promise.all([
setupRequest(resources),
getApmEventClient(resources),
]);
const { serviceName } = params.path;
const { environment, kuery, start, end, serverlessId } = params.query;
const options = {
environment,
start,
end,
kuery,
setup,
serviceName,
serverlessId,
apmEventClient,
};
const [activeInstances, timeseries] = await Promise.all([
getServerlessActiveInstancesOverview(options),
getActiveInstancesTimeseries({ ...options, config: setup.config }),
]);
return { activeInstances, timeseries };
},
});
const serverlessMetricsFunctionsOverviewRoute = createApmServerRoute({
endpoint:
'GET /internal/apm/services/{serviceName}/metrics/serverless/functions_overview',
params: t.type({
path: t.type({
serviceName: t.string,
}),
query: t.intersection([environmentRt, kueryRt, rangeRt]),
}),
options: { tags: ['access:apm'] },
handler: async (
resources
): Promise<{
serverlessFunctionsOverview: Awaited<
ReturnType<typeof getServerlessFunctionsOverview>
>;
}> => {
const { params } = resources;
const apmEventClient = await getApmEventClient(resources);
const { serviceName } = params.path;
const { environment, kuery, start, end } = params.query;
const serverlessFunctionsOverview = await getServerlessFunctionsOverview({
environment,
start,
end,
kuery,
apmEventClient,
serviceName,
});
return { serverlessFunctionsOverview };
},
});
const serverlessMetricsSummaryRoute = createApmServerRoute({
endpoint:
'GET /internal/apm/services/{serviceName}/metrics/serverless/summary',
params: t.type({
path: t.type({
serviceName: t.string,
}),
query: t.intersection([
environmentRt,
kueryRt,
rangeRt,
t.partial({ serverlessId: t.string }),
]),
}),
options: { tags: ['access:apm'] },
handler: async (
resources
): Promise<Awaited<ReturnType<typeof getServerlessSummary>>> => {
const { params } = resources;
const apmEventClient = await getApmEventClient(resources);
const { serviceName } = params.path;
const { environment, kuery, start, end, serverlessId } = params.query;
return getServerlessSummary({
environment,
start,
end,
kuery,
apmEventClient,
serviceName,
serverlessId,
});
},
});
export const metricsServerlessRouteRepository = {
...serverlessMetricsChartsRoute,
...serverlessMetricsSummaryRoute,
...serverlessMetricsFunctionsOverviewRoute,
...serverlessMetricsActiveInstancesRoute,
};

View file

@ -11,6 +11,7 @@ import {
termQuery,
} from '@kbn/observability-plugin/server';
import {
FAAS_ID,
SERVICE_NAME,
TRANSACTION_NAME,
TRANSACTION_TYPE,
@ -47,6 +48,7 @@ function searchLatency({
start,
end,
offset,
serverlessId,
}: {
environment: string;
kuery: string;
@ -59,6 +61,7 @@ function searchLatency({
start: number;
end: number;
offset?: string;
serverlessId?: string;
}) {
const { startWithOffset, endWithOffset } = getOffsetInMs({
start,
@ -95,6 +98,7 @@ function searchLatency({
...kqlQuery(kuery),
...termQuery(TRANSACTION_NAME, transactionName),
...termQuery(TRANSACTION_TYPE, transactionType),
...termQuery(FAAS_ID, serverlessId),
],
},
},
@ -131,6 +135,7 @@ export async function getLatencyTimeseries({
start,
end,
offset,
serverlessId,
}: {
environment: string;
kuery: string;
@ -143,6 +148,7 @@ export async function getLatencyTimeseries({
start: number;
end: number;
offset?: string;
serverlessId?: string;
}) {
const response = await searchLatency({
environment,
@ -156,6 +162,7 @@ export async function getLatencyTimeseries({
start,
end,
offset,
serverlessId,
});
if (!response.aggregations) {

View file

@ -0,0 +1,120 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import { apm, timerange } from '@kbn/apm-synthtrace';
import type { ApmSynthtraceEsClient } from '@kbn/apm-synthtrace';
export const config = {
memoryTotal: 536870912, // 0.5gb
memoryFree: 94371840, // ~0.08 gb
billedDurationMs: 4000,
faasTimeoutMs: 10000,
coldStartDurationPython: 4000,
faasDuration: 4000,
transactionDuration: 1000,
pythonServerlessFunctionNames: ['fn-lambda-python', 'fn-lambda-python-2'],
pythonInstanceName: 'instance_A',
serverlessId: 'arn:aws:lambda:us-west-2:001:function:',
};
export const expectedValues = {
expectedMemoryUsedRate: (config.memoryTotal - config.memoryFree) / config.memoryTotal,
expectedMemoryUsed: config.memoryTotal - config.memoryFree,
};
export async function generateData({
synthtraceEsClient,
start,
end,
}: {
synthtraceEsClient: ApmSynthtraceEsClient;
start: number;
end: number;
}) {
const {
memoryTotal,
memoryFree,
billedDurationMs,
faasTimeoutMs,
coldStartDurationPython,
faasDuration,
transactionDuration,
pythonServerlessFunctionNames,
pythonInstanceName,
} = config;
const cloudFields = {
'cloud.provider': 'aws',
'cloud.service.name': 'lambda',
'cloud.region': 'us-west-2',
};
const [instanceLambdaPython, instanceLambdaPython2] = pythonServerlessFunctionNames.map(
(functionName) => {
return apm
.serverlessFunction({
serviceName: 'lambda-python',
environment: 'test',
agentName: 'python',
functionName,
})
.instance({ instanceName: pythonInstanceName, ...cloudFields });
}
);
const instanceLambdaNode = apm
.serverlessFunction({
serviceName: 'lambda-node',
environment: 'test',
agentName: 'nodejs',
functionName: 'fn-lambda-node',
})
.instance({ instanceName: 'instance_B', ...cloudFields });
const systemMemory = {
free: memoryFree,
total: memoryTotal,
};
const transactionsEvents = timerange(start, end)
.ratePerMinute(1)
.generator((timestamp) => [
instanceLambdaPython
.invocation()
.billedDuration(billedDurationMs)
.coldStart(true)
.coldStartDuration(coldStartDurationPython)
.faasDuration(faasDuration)
.faasTimeout(faasTimeoutMs)
.memory(systemMemory)
.timestamp(timestamp)
.duration(transactionDuration)
.success(),
instanceLambdaPython2
.invocation()
.billedDuration(billedDurationMs)
.coldStart(true)
.coldStartDuration(coldStartDurationPython)
.faasDuration(faasDuration)
.faasTimeout(faasTimeoutMs)
.memory(systemMemory)
.timestamp(timestamp)
.duration(transactionDuration)
.success(),
instanceLambdaNode
.invocation()
.billedDuration(billedDurationMs)
.coldStart(false)
.faasDuration(faasDuration)
.faasTimeout(faasTimeoutMs)
.memory(systemMemory)
.timestamp(timestamp)
.duration(transactionDuration)
.success(),
]);
await synthtraceEsClient.index(transactionsEvents);
}

View file

@ -0,0 +1,121 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import { APIReturnType } from '@kbn/apm-plugin/public/services/rest/create_call_apm_api';
import expect from '@kbn/expect';
import { sumBy } from 'lodash';
import { FtrProviderContext } from '../../../common/ftr_provider_context';
import { config, expectedValues, generateData } from './generate_data';
export default function ApiTest({ getService }: FtrProviderContext) {
const registry = getService('registry');
const apmApiClient = getService('apmApiClient');
const synthtraceEsClient = getService('synthtraceEsClient');
const start = new Date('2021-01-01T00:00:00.000Z').getTime();
const end = new Date('2021-01-01T00:15:00.000Z').getTime() - 1;
const numberOfTransactionsCreated = 15;
async function callApi(serviceName: string, serverlessId?: string) {
return await apmApiClient.readUser({
endpoint: `GET /internal/apm/services/{serviceName}/metrics/serverless/active_instances`,
params: {
path: { serviceName },
query: {
environment: 'test',
kuery: '',
start: new Date(start).toISOString(),
end: new Date(end).toISOString(),
...(serverlessId ? { serverlessId } : {}),
},
},
});
}
registry.when('Serverless active instances', { config: 'basic', archives: [] }, () => {
const {
memoryTotal,
billedDurationMs,
pythonServerlessFunctionNames,
faasDuration,
serverlessId,
} = config;
const { expectedMemoryUsed } = expectedValues;
before(async () => {
await generateData({ start, end, synthtraceEsClient });
});
after(() => synthtraceEsClient.clean());
describe('Python service', () => {
let activeInstances: APIReturnType<'GET /internal/apm/services/{serviceName}/metrics/serverless/active_instances'>;
before(async () => {
const response = await callApi('lambda-python');
activeInstances = response.body;
});
it('returns correct values for all serverless functions', () => {
pythonServerlessFunctionNames.forEach((name) => {
const activeInstanceOverview = activeInstances.activeInstances.find(
(item) => item.serverlessFunctionName === name
);
expect(activeInstanceOverview?.serverlessId).to.eql(`${serverlessId}${name}`);
expect(activeInstanceOverview?.serverlessDurationAvg).to.eql(faasDuration);
expect(activeInstanceOverview?.billedDurationAvg).to.eql(billedDurationMs);
expect(activeInstanceOverview?.avgMemoryUsed).to.eql(expectedMemoryUsed);
expect(activeInstanceOverview?.memorySize).to.eql(memoryTotal);
});
});
describe('timeseries', () => {
it('returns correct sum value', () => {
const sumValue = sumBy(
activeInstances?.timeseries?.filter((item) => item.y !== 0),
'y'
);
expect(sumValue).to.equal(numberOfTransactionsCreated);
});
});
});
describe('detailed metrics', () => {
let activeInstances: APIReturnType<'GET /internal/apm/services/{serviceName}/metrics/serverless/active_instances'>;
before(async () => {
const response = await callApi(
'lambda-python',
`${serverlessId}${pythonServerlessFunctionNames[0]}`
);
activeInstances = response.body;
});
it('returns correct values for all serverless functions', () => {
const activeInstanceOverview = activeInstances.activeInstances.find(
(item) => item.serverlessFunctionName === pythonServerlessFunctionNames[0]
);
expect(activeInstanceOverview?.serverlessId).to.eql(
`${serverlessId}${pythonServerlessFunctionNames[0]}`
);
expect(activeInstanceOverview?.serverlessDurationAvg).to.eql(faasDuration);
expect(activeInstanceOverview?.billedDurationAvg).to.eql(billedDurationMs);
expect(activeInstanceOverview?.avgMemoryUsed).to.eql(expectedMemoryUsed);
expect(activeInstanceOverview?.memorySize).to.eql(memoryTotal);
});
describe('timeseries', () => {
it('returns correct sum value', () => {
const sumValue = sumBy(
activeInstances?.timeseries?.filter((item) => item.y !== 0),
'y'
);
expect(sumValue).to.equal(numberOfTransactionsCreated);
});
});
});
});
}

View file

@ -0,0 +1,82 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import { APIReturnType } from '@kbn/apm-plugin/public/services/rest/create_call_apm_api';
import expect from '@kbn/expect';
import { FtrProviderContext } from '../../../common/ftr_provider_context';
import { config, expectedValues, generateData } from './generate_data';
export default function ApiTest({ getService }: FtrProviderContext) {
const registry = getService('registry');
const apmApiClient = getService('apmApiClient');
const synthtraceEsClient = getService('synthtraceEsClient');
const start = new Date('2021-01-01T00:00:00.000Z').getTime();
const end = new Date('2021-01-01T00:15:00.000Z').getTime() - 1;
const numberOfTransactionsCreated = 15;
async function callApi(serviceName: string) {
return await apmApiClient.readUser({
endpoint: `GET /internal/apm/services/{serviceName}/metrics/serverless/functions_overview`,
params: {
path: { serviceName },
query: {
environment: 'test',
kuery: '',
start: new Date(start).toISOString(),
end: new Date(end).toISOString(),
},
},
});
}
registry.when('Serverless functions overview', { config: 'basic', archives: [] }, () => {
const {
memoryTotal,
billedDurationMs,
pythonServerlessFunctionNames,
faasDuration,
serverlessId,
} = config;
const { expectedMemoryUsed } = expectedValues;
before(async () => {
await generateData({ start, end, synthtraceEsClient });
});
after(() => synthtraceEsClient.clean());
describe('Python service', () => {
let functionsOverview: APIReturnType<'GET /internal/apm/services/{serviceName}/metrics/serverless/functions_overview'>;
before(async () => {
const response = await callApi('lambda-python');
functionsOverview = response.body;
});
it('returns correct number of serverless functions', () => {
expect(
functionsOverview.serverlessFunctionsOverview.map((item) => {
return item.serverlessFunctionName;
})
).to.eql(pythonServerlessFunctionNames);
});
it('returns correct values for all serverless functions', () => {
pythonServerlessFunctionNames.forEach((name) => {
const functionOverview = functionsOverview.serverlessFunctionsOverview.find(
(item) => item.serverlessFunctionName === name
);
expect(functionOverview?.serverlessId).to.eql(`${serverlessId}${name}`);
expect(functionOverview?.serverlessDurationAvg).to.eql(faasDuration);
expect(functionOverview?.billedDurationAvg).to.eql(billedDurationMs);
expect(functionOverview?.coldStartCount).to.eql(numberOfTransactionsCreated);
expect(functionOverview?.avgMemoryUsed).to.eql(expectedMemoryUsed);
expect(functionOverview?.memorySize).to.eql(memoryTotal);
});
});
});
});
}

View file

@ -0,0 +1,322 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import expect from '@kbn/expect';
import { meanBy, sumBy } from 'lodash';
import { Coordinate } from '@kbn/apm-plugin/typings/timeseries';
import { APIReturnType } from '@kbn/apm-plugin/public/services/rest/create_call_apm_api';
import { FtrProviderContext } from '../../../common/ftr_provider_context';
import { generateData, config } from './generate_data';
function isNotNullOrZeroCoordinate(coordinate: Coordinate) {
return coordinate.y !== null && coordinate.y !== 0;
}
export default function ApiTest({ getService }: FtrProviderContext) {
const registry = getService('registry');
const apmApiClient = getService('apmApiClient');
const synthtraceEsClient = getService('synthtraceEsClient');
const start = new Date('2021-01-01T00:00:00.000Z').getTime();
const end = new Date('2021-01-01T00:15:00.000Z').getTime() - 1;
const numberOfTransactionsCreated = 15;
async function callApi(serviceName: string, serverlessId?: string) {
return await apmApiClient.readUser({
endpoint: 'GET /internal/apm/services/{serviceName}/metrics/serverless/charts',
params: {
path: { serviceName },
query: {
environment: 'test',
kuery: '',
start: new Date(start).toISOString(),
end: new Date(end).toISOString(),
...(serverlessId ? { serverlessId } : {}),
},
},
});
}
registry.when(
'Serverless metrics charts when data is not loaded',
{ config: 'basic', archives: [] },
() => {
let serverlessMetrics: APIReturnType<'GET /internal/apm/services/{serviceName}/metrics/serverless/charts'>;
before(async () => {
const response = await callApi('lambda-python');
serverlessMetrics = response.body;
});
it('returns empty', () => {
serverlessMetrics.charts.forEach((chart) => {
expect(chart.series).to.be.empty();
});
});
}
);
registry.when('Serverless metrics charts', { config: 'basic', archives: [] }, () => {
const {
memoryTotal,
memoryFree,
billedDurationMs,
coldStartDurationPython,
transactionDuration,
pythonServerlessFunctionNames,
serverlessId,
} = config;
before(async () => {
await generateData({ start, end, synthtraceEsClient });
});
after(() => synthtraceEsClient.clean());
describe('Python service', () => {
let serverlessMetrics: APIReturnType<'GET /internal/apm/services/{serviceName}/metrics/serverless/charts'>;
before(async () => {
const response = await callApi('lambda-python');
serverlessMetrics = response.body;
});
it('returns all metrics chart', () => {
expect(serverlessMetrics.charts.length).to.be.greaterThan(0);
expect(serverlessMetrics.charts.map(({ title }) => title).sort()).to.eql([
'Cold start duration',
'Cold starts',
'Compute usage',
'Lambda Duration',
'System memory usage',
]);
});
describe('Avg. Duration', () => {
const transactionDurationInMicroSeconds = transactionDuration * 1000;
[
{ title: 'Billed Duration', expectedValue: billedDurationMs * 1000 },
{ title: 'Transaction Duration', expectedValue: transactionDurationInMicroSeconds },
].map(({ title, expectedValue }) =>
it(`returns correct ${title} value`, () => {
const avgDurationMetric = serverlessMetrics.charts.find((chart) => {
return chart.key === 'avg_duration';
});
const series = avgDurationMetric?.series.find((item) => item.title === title);
expect(series?.overallValue).to.eql(expectedValue);
const meanValue = meanBy(series?.data.filter(isNotNullOrZeroCoordinate), 'y');
expect(meanValue).to.eql(expectedValue);
})
);
});
let metricsChart: typeof serverlessMetrics.charts[0] | undefined;
describe('Cold start duration', () => {
before(() => {
metricsChart = serverlessMetrics.charts.find((chart) => {
return chart.key === 'cold_start_duration';
});
});
it('returns correct overall value', () => {
expect(metricsChart?.series[0].overallValue).to.equal(coldStartDurationPython * 1000);
});
it('returns correct mean value', () => {
const meanValue = meanBy(
metricsChart?.series[0]?.data.filter(isNotNullOrZeroCoordinate),
'y'
);
expect(meanValue).to.equal(coldStartDurationPython * 1000);
});
});
describe('Cold start count', () => {
before(() => {
metricsChart = serverlessMetrics.charts.find((chart) => {
return chart.key === 'cold_start_count';
});
});
it('returns correct overall value', () => {
expect(metricsChart?.series[0].overallValue).to.equal(
numberOfTransactionsCreated * pythonServerlessFunctionNames.length
);
});
it('returns correct sum value', () => {
const sumValue = sumBy(
metricsChart?.series[0]?.data.filter(isNotNullOrZeroCoordinate),
'y'
);
expect(sumValue).to.equal(
numberOfTransactionsCreated * pythonServerlessFunctionNames.length
);
});
});
describe('memory usage', () => {
const expectedFreeMemory = 1 - memoryFree / memoryTotal;
[
{ title: 'Max', expectedValue: expectedFreeMemory },
{ title: 'Average', expectedValue: expectedFreeMemory },
].map(({ title, expectedValue }) =>
it(`returns correct ${title} value`, () => {
const memoryUsageMetric = serverlessMetrics.charts.find((chart) => {
return chart.key === 'memory_usage_chart';
});
const series = memoryUsageMetric?.series.find((item) => item.title === title);
expect(series?.overallValue).to.eql(expectedValue);
const meanValue = meanBy(series?.data.filter(isNotNullOrZeroCoordinate), 'y');
expect(meanValue).to.eql(expectedValue);
})
);
});
describe('Compute usage', () => {
const GBSeconds = 1024 * 1024 * 1024 * 1000;
const expectedValue = (memoryTotal * billedDurationMs) / GBSeconds;
let computeUsageMetric: typeof serverlessMetrics.charts[0] | undefined;
before(() => {
computeUsageMetric = serverlessMetrics.charts.find((chart) => {
return chart.key === 'compute_usage';
});
});
it('returns correct overall value', () => {
expect(computeUsageMetric?.series[0].overallValue).to.equal(expectedValue);
});
it('returns correct mean value', () => {
const meanValue = meanBy(
computeUsageMetric?.series[0]?.data.filter((item) => item.y !== 0),
'y'
);
expect(meanValue).to.equal(expectedValue);
});
});
});
describe('detailed metrics', () => {
let serverlessMetrics: APIReturnType<'GET /internal/apm/services/{serviceName}/metrics/serverless/charts'>;
before(async () => {
const response = await callApi(
'lambda-python',
`${serverlessId}${pythonServerlessFunctionNames[0]}`
);
serverlessMetrics = response.body;
});
it('returns all metrics chart', () => {
expect(serverlessMetrics.charts.length).to.be.greaterThan(0);
expect(serverlessMetrics.charts.map(({ title }) => title).sort()).to.eql([
'Cold start duration',
'Cold starts',
'Compute usage',
'Lambda Duration',
'System memory usage',
]);
});
describe('Avg. Duration', () => {
const transactionDurationInMicroSeconds = transactionDuration * 1000;
[
{ title: 'Billed Duration', expectedValue: billedDurationMs * 1000 },
{ title: 'Transaction Duration', expectedValue: transactionDurationInMicroSeconds },
].map(({ title, expectedValue }) =>
it(`returns correct ${title} value`, () => {
const avgDurationMetric = serverlessMetrics.charts.find((chart) => {
return chart.key === 'avg_duration';
});
const series = avgDurationMetric?.series.find((item) => item.title === title);
expect(series?.overallValue).to.eql(expectedValue);
const meanValue = meanBy(series?.data.filter(isNotNullOrZeroCoordinate), 'y');
expect(meanValue).to.eql(expectedValue);
})
);
});
let metricsChart: typeof serverlessMetrics.charts[0] | undefined;
describe('Cold start duration', () => {
before(() => {
metricsChart = serverlessMetrics.charts.find((chart) => {
return chart.key === 'cold_start_duration';
});
});
it('returns correct overall value', () => {
expect(metricsChart?.series[0].overallValue).to.equal(coldStartDurationPython * 1000);
});
it('returns correct mean value', () => {
const meanValue = meanBy(
metricsChart?.series[0]?.data.filter(isNotNullOrZeroCoordinate),
'y'
);
expect(meanValue).to.equal(coldStartDurationPython * 1000);
});
});
describe('Cold start count', () => {
before(() => {
metricsChart = serverlessMetrics.charts.find((chart) => {
return chart.key === 'cold_start_count';
});
});
it('returns correct overall value', () => {
expect(metricsChart?.series[0].overallValue).to.equal(numberOfTransactionsCreated);
});
it('returns correct sum value', () => {
const sumValue = sumBy(
metricsChart?.series[0]?.data.filter(isNotNullOrZeroCoordinate),
'y'
);
expect(sumValue).to.equal(numberOfTransactionsCreated);
});
});
describe('memory usage', () => {
const expectedFreeMemory = 1 - memoryFree / memoryTotal;
[
{ title: 'Max', expectedValue: expectedFreeMemory },
{ title: 'Average', expectedValue: expectedFreeMemory },
].map(({ title, expectedValue }) =>
it(`returns correct ${title} value`, () => {
const memoryUsageMetric = serverlessMetrics.charts.find((chart) => {
return chart.key === 'memory_usage_chart';
});
const series = memoryUsageMetric?.series.find((item) => item.title === title);
expect(series?.overallValue).to.eql(expectedValue);
const meanValue = meanBy(series?.data.filter(isNotNullOrZeroCoordinate), 'y');
expect(meanValue).to.eql(expectedValue);
})
);
});
describe('Compute usage', () => {
const GBSeconds = 1024 * 1024 * 1024 * 1000;
const expectedValue = (memoryTotal * billedDurationMs) / GBSeconds;
let computeUsageMetric: typeof serverlessMetrics.charts[0] | undefined;
before(() => {
computeUsageMetric = serverlessMetrics.charts.find((chart) => {
return chart.key === 'compute_usage';
});
});
it('returns correct overall value', () => {
expect(computeUsageMetric?.series[0].overallValue).to.equal(expectedValue);
});
it('returns correct mean value', () => {
const meanValue = meanBy(
computeUsageMetric?.series[0]?.data.filter((item) => item.y !== 0),
'y'
);
expect(meanValue).to.equal(expectedValue);
});
});
});
});
}

View file

@ -0,0 +1,109 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import { APIReturnType } from '@kbn/apm-plugin/public/services/rest/create_call_apm_api';
import expect from '@kbn/expect';
import { FtrProviderContext } from '../../../common/ftr_provider_context';
import { config, expectedValues, generateData } from './generate_data';
export default function ApiTest({ getService }: FtrProviderContext) {
const registry = getService('registry');
const apmApiClient = getService('apmApiClient');
const synthtraceEsClient = getService('synthtraceEsClient');
const start = new Date('2021-01-01T00:00:00.000Z').getTime();
const end = new Date('2021-01-01T00:15:00.000Z').getTime() - 1;
async function callApi(serviceName: string, serverlessId?: string) {
return await apmApiClient.readUser({
endpoint: `GET /internal/apm/services/{serviceName}/metrics/serverless/summary`,
params: {
path: { serviceName },
query: {
environment: 'test',
kuery: '',
start: new Date(start).toISOString(),
end: new Date(end).toISOString(),
...(serverlessId ? { serverlessId } : {}),
},
},
});
}
registry.when(
'Serverless overview when data is not loaded',
{ config: 'basic', archives: [] },
() => {
let serverlessSummary: APIReturnType<'GET /internal/apm/services/{serviceName}/metrics/serverless/summary'>;
before(async () => {
const response = await callApi('lambda-python');
serverlessSummary = response.body;
});
it('returns empty', () => {
expect(serverlessSummary).to.be.empty();
});
}
);
registry.when('Serverless overview', { config: 'basic', archives: [] }, () => {
const { billedDurationMs, pythonServerlessFunctionNames, faasDuration, serverlessId } = config;
const { expectedMemoryUsedRate } = expectedValues;
before(async () => {
await generateData({ start, end, synthtraceEsClient });
});
// after(() => synthtraceEsClient.clean());
describe('Python service', () => {
let serverlessSummary: APIReturnType<'GET /internal/apm/services/{serviceName}/metrics/serverless/summary'>;
before(async () => {
const response = await callApi('lambda-python');
serverlessSummary = response.body;
});
it('returns correct memory avg', () => {
expect(serverlessSummary.memoryUsageAvgRate).to.eql(expectedMemoryUsedRate);
});
it('returns correct serverless function total', () => {
expect(serverlessSummary.serverlessFunctionsTotal).to.eql(
pythonServerlessFunctionNames.length
);
});
it('returns correct serverless duration avg', () => {
expect(serverlessSummary.serverlessDurationAvg).to.eql(faasDuration);
});
it('returns correct billed duration avg', () => {
expect(serverlessSummary.billedDurationAvg).to.eql(billedDurationMs);
});
});
describe('detailed metrics', () => {
let serverlessSummary: APIReturnType<'GET /internal/apm/services/{serviceName}/metrics/serverless/summary'>;
before(async () => {
const response = await callApi(
'lambda-python',
`${serverlessId}${pythonServerlessFunctionNames[0]}`
);
serverlessSummary = response.body;
});
it('returns correct memory avg', () => {
expect(serverlessSummary.memoryUsageAvgRate).to.eql(expectedMemoryUsedRate);
});
it('returns correct serverless function total', () => {
expect(serverlessSummary.serverlessFunctionsTotal).to.eql(1);
});
it('returns correct serverless duration avg', () => {
expect(serverlessSummary.serverlessDurationAvg).to.eql(faasDuration);
});
it('returns correct billed duration avg', () => {
expect(serverlessSummary.billedDurationAvg).to.eql(billedDurationMs);
});
});
});
}

View file

@ -1,431 +0,0 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import { APIReturnType } from '@kbn/apm-plugin/public/services/rest/create_call_apm_api';
import { apm, timerange } from '@kbn/apm-synthtrace';
import expect from '@kbn/expect';
import { meanBy, sumBy } from 'lodash';
import { FtrProviderContext } from '../../common/ftr_provider_context';
export default function ApiTest({ getService }: FtrProviderContext) {
const registry = getService('registry');
const apmApiClient = getService('apmApiClient');
const synthtraceEsClient = getService('synthtraceEsClient');
const start = new Date('2021-01-01T00:00:00.000Z').getTime();
const end = new Date('2021-01-01T00:15:00.000Z').getTime() - 1;
async function callApi(serviceName: string, agentName: string) {
return await apmApiClient.readUser({
endpoint: `GET /internal/apm/services/{serviceName}/metrics/charts`,
params: {
path: { serviceName },
query: {
environment: 'test',
agentName,
kuery: '',
start: new Date(start).toISOString(),
end: new Date(end).toISOString(),
serviceRuntimeName: 'aws_lambda',
},
},
});
}
registry.when(
'Serverless metrics charts when data is loaded',
{ config: 'basic', archives: [] },
() => {
const MEMORY_TOTAL = 536870912; // 0.5gb;
const MEMORY_FREE = 94371840; // ~0.08 gb;
const BILLED_DURATION_MS = 4000;
const FAAS_TIMEOUT_MS = 10000;
const COLD_START_DURATION_PYTHON = 4000;
const COLD_START_DURATION_NODE = 0;
const FAAS_DURATION = 4000;
const TRANSACTION_DURATION = 1000;
const numberOfTransactionsCreated = 15;
const numberOfPythonInstances = 2;
before(async () => {
const cloudFields = {
'cloud.provider': 'aws',
'cloud.service.name': 'lambda',
'cloud.region': 'us-west-2',
};
const instanceLambdaPython = apm
.serverlessFunction({
serviceName: 'lambda-python',
environment: 'test',
agentName: 'python',
functionName: 'fn-lambda-python',
})
.instance({ instanceName: 'instance python', ...cloudFields });
const instanceLambdaPython2 = apm
.serverlessFunction({
serviceName: 'lambda-python',
environment: 'test',
agentName: 'python',
functionName: 'fn-lambda-python-2',
})
.instance({ instanceName: 'instance python 2', ...cloudFields });
const instanceLambdaNode = apm
.serverlessFunction({
serviceName: 'lambda-node',
environment: 'test',
agentName: 'nodejs',
functionName: 'fn-lambda-node',
})
.instance({ instanceName: 'instance node', ...cloudFields });
const systemMemory = {
free: MEMORY_FREE,
total: MEMORY_TOTAL,
};
const transactionsEvents = timerange(start, end)
.interval('1m')
.rate(1)
.generator((timestamp) => [
instanceLambdaPython
.invocation()
.billedDuration(BILLED_DURATION_MS)
.coldStart(true)
.coldStartDuration(COLD_START_DURATION_PYTHON)
.faasDuration(FAAS_DURATION)
.faasTimeout(FAAS_TIMEOUT_MS)
.memory(systemMemory)
.timestamp(timestamp)
.duration(TRANSACTION_DURATION)
.success(),
instanceLambdaPython2
.invocation()
.billedDuration(BILLED_DURATION_MS)
.coldStart(true)
.coldStartDuration(COLD_START_DURATION_PYTHON)
.faasDuration(FAAS_DURATION)
.faasTimeout(FAAS_TIMEOUT_MS)
.memory(systemMemory)
.timestamp(timestamp)
.duration(TRANSACTION_DURATION)
.success(),
instanceLambdaNode
.invocation()
.billedDuration(BILLED_DURATION_MS)
.coldStart(false)
.coldStartDuration(COLD_START_DURATION_NODE)
.faasDuration(FAAS_DURATION)
.faasTimeout(FAAS_TIMEOUT_MS)
.memory(systemMemory)
.timestamp(timestamp)
.duration(TRANSACTION_DURATION)
.success(),
]);
await synthtraceEsClient.index(transactionsEvents);
});
after(() => synthtraceEsClient.clean());
describe('python', () => {
let metrics: APIReturnType<'GET /internal/apm/services/{serviceName}/metrics/charts'>;
before(async () => {
const { status, body } = await callApi('lambda-python', 'python');
expect(status).to.be(200);
metrics = body;
});
it('returns all metrics chart', () => {
expect(metrics.charts.length).to.be.greaterThan(0);
expect(metrics.charts.map(({ title }) => title).sort()).to.eql([
'Active instances',
'Avg. Duration',
'Cold start',
'Cold start duration',
'Compute usage',
'System memory usage',
]);
});
describe('Avg. Duration', () => {
const transactionDurationInMicroSeconds = TRANSACTION_DURATION * 1000;
[
{ title: 'Billed Duration', expectedValue: BILLED_DURATION_MS * 1000 },
{ title: 'Transaction Duration', expectedValue: transactionDurationInMicroSeconds },
].map(({ title, expectedValue }) =>
it(`returns correct ${title} value`, () => {
const avgDurationMetric = metrics.charts.find((chart) => {
return chart.key === 'avg_duration';
});
const series = avgDurationMetric?.series.find((item) => item.title === title);
expect(series?.overallValue).to.eql(expectedValue);
const meanValue = meanBy(
series?.data.filter((item) => item.y !== null),
'y'
);
expect(meanValue).to.eql(expectedValue);
})
);
});
describe('Cold start duration', () => {
let coldStartDurationMetric: typeof metrics['charts'][0] | undefined;
before(() => {
coldStartDurationMetric = metrics.charts.find((chart) => {
return chart.key === 'cold_start_duration';
});
});
it('returns correct overall value', () => {
expect(coldStartDurationMetric?.series[0].overallValue).to.equal(
COLD_START_DURATION_PYTHON * 1000
);
});
it('returns correct mean value', () => {
const meanValue = meanBy(
coldStartDurationMetric?.series[0]?.data.filter((item) => item.y !== null),
'y'
);
expect(meanValue).to.equal(COLD_START_DURATION_PYTHON * 1000);
});
});
describe('Cold start count', () => {
let coldStartCountMetric: typeof metrics['charts'][0] | undefined;
before(() => {
coldStartCountMetric = metrics.charts.find((chart) => {
return chart.key === 'cold_start_count';
});
});
it('returns correct overall value', () => {
expect(coldStartCountMetric?.series[0].overallValue).to.equal(
numberOfTransactionsCreated * numberOfPythonInstances
);
});
it('returns correct sum value', () => {
const sumValue = sumBy(
coldStartCountMetric?.series[0]?.data.filter((item) => item.y !== null),
'y'
);
expect(sumValue).to.equal(numberOfTransactionsCreated * numberOfPythonInstances);
});
});
describe('memory usage', () => {
const expectedFreeMemory = 1 - MEMORY_FREE / MEMORY_TOTAL;
[
{ title: 'Max', expectedValue: expectedFreeMemory },
{ title: 'Average', expectedValue: expectedFreeMemory },
].map(({ title, expectedValue }) =>
it(`returns correct ${title} value`, () => {
const memoryUsageMetric = metrics.charts.find((chart) => {
return chart.key === 'memory_usage_chart';
});
const series = memoryUsageMetric?.series.find((item) => item.title === title);
expect(series?.overallValue).to.eql(expectedValue);
const meanValue = meanBy(
series?.data.filter((item) => item.y !== null),
'y'
);
expect(meanValue).to.eql(expectedValue);
})
);
});
describe('Compute usage', () => {
const GBSeconds = 1024 * 1024 * 1024 * 1000;
const expectedValue = (MEMORY_TOTAL * BILLED_DURATION_MS) / GBSeconds;
let computeUsageMetric: typeof metrics['charts'][0] | undefined;
before(() => {
computeUsageMetric = metrics.charts.find((chart) => {
return chart.key === 'compute_usage';
});
});
it('returns correct overall value', () => {
expect(computeUsageMetric?.series[0].overallValue).to.equal(expectedValue);
});
it('returns correct mean value', () => {
const meanValue = meanBy(
computeUsageMetric?.series[0]?.data.filter((item) => item.y !== 0),
'y'
);
expect(meanValue).to.equal(expectedValue);
});
});
describe('Active instances', () => {
let activeInstancesMetric: typeof metrics['charts'][0] | undefined;
before(() => {
activeInstancesMetric = metrics.charts.find((chart) => {
return chart.key === 'active_instances';
});
});
it('returns correct overall value', () => {
expect(activeInstancesMetric?.series[0].overallValue).to.equal(numberOfPythonInstances);
});
it('returns correct sum value', () => {
const sumValue = sumBy(
activeInstancesMetric?.series[0]?.data.filter((item) => item.y !== 0),
'y'
);
expect(sumValue).to.equal(numberOfTransactionsCreated * numberOfPythonInstances);
});
});
});
describe('nodejs', () => {
let metrics: APIReturnType<'GET /internal/apm/services/{serviceName}/metrics/charts'>;
before(async () => {
const { status, body } = await callApi('lambda-node', 'nodejs');
expect(status).to.be(200);
metrics = body;
});
it('returns all metrics chart', () => {
expect(metrics.charts.length).to.be.greaterThan(0);
expect(metrics.charts.map(({ title }) => title).sort()).to.eql([
'Active instances',
'Avg. Duration',
'Cold start',
'Cold start duration',
'Compute usage',
'System memory usage',
]);
});
describe('Avg. Duration', () => {
const transactionDurationInMicroSeconds = TRANSACTION_DURATION * 1000;
[
{ title: 'Billed Duration', expectedValue: BILLED_DURATION_MS * 1000 },
{ title: 'Transaction Duration', expectedValue: transactionDurationInMicroSeconds },
].map(({ title, expectedValue }) =>
it(`returns correct ${title} value`, () => {
const avgDurationMetric = metrics.charts.find((chart) => {
return chart.key === 'avg_duration';
});
const series = avgDurationMetric?.series.find((item) => item.title === title);
expect(series?.overallValue).to.eql(expectedValue);
const meanValue = meanBy(
series?.data.filter((item) => item.y !== null),
'y'
);
expect(meanValue).to.eql(expectedValue);
})
);
});
describe('Cold start duration', () => {
let coldStartDurationMetric: typeof metrics['charts'][0] | undefined;
before(() => {
coldStartDurationMetric = metrics.charts.find((chart) => {
return chart.key === 'cold_start_duration';
});
});
it('returns 0 overall value', () => {
expect(coldStartDurationMetric?.series[0].overallValue).to.equal(
COLD_START_DURATION_NODE * 1000
);
});
it('returns 0 mean value', () => {
const meanValue = meanBy(
coldStartDurationMetric?.series[0]?.data.filter((item) => item.y !== null),
'y'
);
expect(meanValue).to.equal(COLD_START_DURATION_NODE * 1000);
});
});
describe('Cold start count', () => {
let coldStartCountMetric: typeof metrics['charts'][0] | undefined;
before(() => {
coldStartCountMetric = metrics.charts.find((chart) => {
return chart.key === 'cold_start_count';
});
});
it('does not return cold start count', () => {
expect(coldStartCountMetric?.series).to.be.empty();
});
});
describe('memory usage', () => {
const expectedFreeMemory = 1 - MEMORY_FREE / MEMORY_TOTAL;
[
{ title: 'Max', expectedValue: expectedFreeMemory },
{ title: 'Average', expectedValue: expectedFreeMemory },
].map(({ title, expectedValue }) =>
it(`returns correct ${title} value`, () => {
const memoryUsageMetric = metrics.charts.find((chart) => {
return chart.key === 'memory_usage_chart';
});
const series = memoryUsageMetric?.series.find((item) => item.title === title);
expect(series?.overallValue).to.eql(expectedValue);
const meanValue = meanBy(
series?.data.filter((item) => item.y !== null),
'y'
);
expect(meanValue).to.eql(expectedValue);
})
);
});
describe('Compute usage', () => {
const GBSeconds = 1024 * 1024 * 1024 * 1000;
const expectedValue = (MEMORY_TOTAL * BILLED_DURATION_MS) / GBSeconds;
let computeUsageMetric: typeof metrics['charts'][0] | undefined;
before(() => {
computeUsageMetric = metrics.charts.find((chart) => {
return chart.key === 'compute_usage';
});
});
it('returns correct overall value', () => {
expect(computeUsageMetric?.series[0].overallValue).to.equal(expectedValue);
});
it('returns correct mean value', () => {
const meanValue = meanBy(
computeUsageMetric?.series[0]?.data.filter((item) => item.y !== 0),
'y'
);
expect(meanValue).to.equal(expectedValue);
});
});
describe('Active instances', () => {
let activeInstancesMetric: typeof metrics['charts'][0] | undefined;
before(() => {
activeInstancesMetric = metrics.charts.find((chart) => {
return chart.key === 'active_instances';
});
});
it('returns correct overall value', () => {
// there's only one node instance
expect(activeInstancesMetric?.series[0].overallValue).to.equal(1);
});
it('returns correct sum value', () => {
const sumValue = sumBy(
activeInstancesMetric?.series[0]?.data.filter((item) => item.y !== 0),
'y'
);
expect(sumValue).to.equal(numberOfTransactionsCreated);
});
});
});
}
);
}