mirror of
https://github.com/elastic/kibana.git
synced 2025-04-23 09:19:04 -04:00
[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:
parent
c5cbfee06a
commit
aa97bff4d4
43 changed files with 2539 additions and 682 deletions
|
@ -33,7 +33,6 @@ export class Serverless extends BaseSpan {
|
|||
...fields,
|
||||
'metricset.name': 'app',
|
||||
'faas.execution': faasExection,
|
||||
'faas.id': fields['service.name'],
|
||||
});
|
||||
}
|
||||
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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`] = `
|
||||
|
|
|
@ -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';
|
||||
|
|
26
x-pack/plugins/apm/common/serverless.test.ts
Normal file
26
x-pack/plugins/apm/common/serverless.test.ts
Normal 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');
|
||||
});
|
||||
});
|
17
x-pack/plugins/apm/common/serverless.ts
Normal file
17
x-pack/plugins/apm/common/serverless.ts
Normal 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;
|
||||
}
|
|
@ -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 />;
|
||||
}
|
||||
|
|
|
@ -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>
|
||||
);
|
||||
}
|
|
@ -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>
|
||||
);
|
||||
}
|
|
@ -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>
|
||||
);
|
||||
}
|
|
@ -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>
|
||||
);
|
||||
}
|
|
@ -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>
|
||||
);
|
||||
}
|
|
@ -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>
|
||||
);
|
||||
}
|
|
@ -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} />;
|
||||
}
|
||||
|
|
|
@ -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>
|
||||
);
|
||||
}
|
|
@ -16,7 +16,7 @@ describe('ServiceNodeMetrics', () => {
|
|||
expect(() =>
|
||||
shallow(
|
||||
<MockApmPluginContextWrapper>
|
||||
<ServiceNodeMetrics />
|
||||
<ServiceNodeMetrics serviceNodeName="foo" />
|
||||
</MockApmPluginContextWrapper>
|
||||
)
|
||||
).not.toThrowError();
|
||||
|
|
|
@ -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;
|
||||
|
||||
|
|
|
@ -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();
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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,
|
||||
})) || [],
|
||||
},
|
||||
],
|
||||
};
|
||||
}
|
|
@ -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',
|
||||
});
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
|
|
@ -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,
|
||||
};
|
||||
|
|
|
@ -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;
|
||||
}) || []
|
||||
);
|
||||
}
|
|
@ -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,
|
||||
})) || []
|
||||
);
|
||||
}
|
|
@ -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',
|
||||
});
|
|
@ -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,
|
||||
},
|
||||
]
|
||||
: [],
|
||||
};
|
||||
}
|
|
@ -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),
|
||||
],
|
||||
},
|
||||
},
|
|
@ -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),
|
||||
]);
|
||||
});
|
||||
}
|
|
@ -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,
|
||||
};
|
||||
}
|
|
@ -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 || [];
|
||||
}
|
|
@ -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,
|
||||
};
|
||||
}
|
|
@ -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);
|
||||
});
|
||||
});
|
|
@ -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;
|
||||
}
|
189
x-pack/plugins/apm/server/routes/metrics/serverless/route.ts
Normal file
189
x-pack/plugins/apm/server/routes/metrics/serverless/route.ts
Normal 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,
|
||||
};
|
|
@ -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) {
|
||||
|
|
|
@ -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);
|
||||
}
|
|
@ -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);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
|
@ -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);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
|
@ -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);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
|
@ -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);
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
|
@ -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);
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
);
|
||||
}
|
Loading…
Add table
Add a link
Reference in a new issue