mirror of
https://github.com/elastic/kibana.git
synced 2025-04-23 17:28:26 -04:00
APM UI changes for serverless services / AWS lambda (#122775)
* Adapted service UI for AWS lambda / serverless services * Add unit tests for isServerlessAgent function * Add story for cold start badge * Add unit tests for service icons and icon details * Add aws_lambda checks to isMetricsTabHidden and isJVMsTabHidden unit tests * Add API test for coldstart_rate chart * Change service icon API tests to use synthrace and test for serverless property * Change service details API tests to use synthrace and add test for serverless * Add e2e tests for cold start rate chart * Add cold start badge to transaction flyout * Add beta badge to cloud details in a lambda context * Add support for multiple lambda functions in a single service Co-authored-by: Alexander Wert <alexander.wert@elastic.co> Co-authored-by: Casper Hübertz <casper@formgeist.com>
This commit is contained in:
parent
ecba4787f5
commit
78643f495c
48 changed files with 2240 additions and 247 deletions
|
@ -63,8 +63,12 @@ export type ApmFields = Fields &
|
|||
};
|
||||
'transaction.sampled': true;
|
||||
'service.name': string;
|
||||
'service.version': string;
|
||||
'service.environment': string;
|
||||
'service.node.name': string;
|
||||
'service.runtime.name': string;
|
||||
'service.runtime.version': string;
|
||||
'service.framework.name': string;
|
||||
'span.id': string;
|
||||
'span.name': string;
|
||||
'span.type': string;
|
||||
|
@ -77,5 +81,17 @@ export type ApmFields = Fields &
|
|||
'span.destination.service.response_time.count': number;
|
||||
'span.self_time.count': number;
|
||||
'span.self_time.sum.us': number;
|
||||
'cloud.provider': string;
|
||||
'cloud.project.name': string;
|
||||
'cloud.service.name': string;
|
||||
'cloud.availability_zone': string;
|
||||
'cloud.machine.type': string;
|
||||
'cloud.region': string;
|
||||
'host.os.platform': string;
|
||||
'faas.id': string;
|
||||
'faas.coldstart': boolean;
|
||||
'faas.execution': string;
|
||||
'faas.trigger.type': string;
|
||||
'faas.trigger.request_id': string;
|
||||
}> &
|
||||
ApmApplicationMetricFields;
|
||||
|
|
|
@ -51,6 +51,20 @@ export function getTransactionMetrics(events: ApmFields[]) {
|
|||
'host.name',
|
||||
'container.id',
|
||||
'kubernetes.pod.name',
|
||||
'cloud.account.id',
|
||||
'cloud.account.name',
|
||||
'cloud.machine.type',
|
||||
'cloud.project.id',
|
||||
'cloud.project.name',
|
||||
'cloud.service.name',
|
||||
'service.language.name',
|
||||
'service.language.version',
|
||||
'service.runtime.name',
|
||||
'service.runtime.version',
|
||||
'host.os.platform',
|
||||
'faas.id',
|
||||
'faas.coldstart',
|
||||
'faas.trigger.type',
|
||||
]);
|
||||
|
||||
return metricsets.map((metricset) => {
|
||||
|
|
|
@ -37,6 +37,8 @@ exports[`Error CLOUD_PROVIDER 1`] = `"gcp"`;
|
|||
|
||||
exports[`Error CLOUD_REGION 1`] = `"europe-west1"`;
|
||||
|
||||
exports[`Error CLOUD_SERVICE_NAME 1`] = `undefined`;
|
||||
|
||||
exports[`Error CLS_FIELD 1`] = `undefined`;
|
||||
|
||||
exports[`Error CONTAINER_ID 1`] = `undefined`;
|
||||
|
@ -63,6 +65,12 @@ exports[`Error ERROR_PAGE_URL 1`] = `undefined`;
|
|||
|
||||
exports[`Error EVENT_OUTCOME 1`] = `undefined`;
|
||||
|
||||
exports[`Error FAAS_COLDSTART 1`] = `undefined`;
|
||||
|
||||
exports[`Error FAAS_ID 1`] = `undefined`;
|
||||
|
||||
exports[`Error FAAS_TRIGGER_TYPE 1`] = `undefined`;
|
||||
|
||||
exports[`Error FCP_FIELD 1`] = `undefined`;
|
||||
|
||||
exports[`Error FID_FIELD 1`] = `undefined`;
|
||||
|
@ -282,6 +290,8 @@ exports[`Span CLOUD_PROVIDER 1`] = `"gcp"`;
|
|||
|
||||
exports[`Span CLOUD_REGION 1`] = `"europe-west1"`;
|
||||
|
||||
exports[`Span CLOUD_SERVICE_NAME 1`] = `undefined`;
|
||||
|
||||
exports[`Span CLS_FIELD 1`] = `undefined`;
|
||||
|
||||
exports[`Span CONTAINER_ID 1`] = `undefined`;
|
||||
|
@ -308,6 +318,12 @@ exports[`Span ERROR_PAGE_URL 1`] = `undefined`;
|
|||
|
||||
exports[`Span EVENT_OUTCOME 1`] = `"unknown"`;
|
||||
|
||||
exports[`Span FAAS_COLDSTART 1`] = `undefined`;
|
||||
|
||||
exports[`Span FAAS_ID 1`] = `undefined`;
|
||||
|
||||
exports[`Span FAAS_TRIGGER_TYPE 1`] = `undefined`;
|
||||
|
||||
exports[`Span FCP_FIELD 1`] = `undefined`;
|
||||
|
||||
exports[`Span FID_FIELD 1`] = `undefined`;
|
||||
|
@ -519,6 +535,8 @@ exports[`Transaction CLOUD_PROVIDER 1`] = `"gcp"`;
|
|||
|
||||
exports[`Transaction CLOUD_REGION 1`] = `"europe-west1"`;
|
||||
|
||||
exports[`Transaction CLOUD_SERVICE_NAME 1`] = `undefined`;
|
||||
|
||||
exports[`Transaction CLS_FIELD 1`] = `undefined`;
|
||||
|
||||
exports[`Transaction CONTAINER_ID 1`] = `"container1234567890abcdef"`;
|
||||
|
@ -545,6 +563,12 @@ exports[`Transaction ERROR_PAGE_URL 1`] = `undefined`;
|
|||
|
||||
exports[`Transaction EVENT_OUTCOME 1`] = `"unknown"`;
|
||||
|
||||
exports[`Transaction FAAS_COLDSTART 1`] = `undefined`;
|
||||
|
||||
exports[`Transaction FAAS_ID 1`] = `undefined`;
|
||||
|
||||
exports[`Transaction FAAS_TRIGGER_TYPE 1`] = `undefined`;
|
||||
|
||||
exports[`Transaction FCP_FIELD 1`] = `undefined`;
|
||||
|
||||
exports[`Transaction FID_FIELD 1`] = `undefined`;
|
||||
|
|
|
@ -5,7 +5,12 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
import { isJavaAgentName, isRumAgentName, isIosAgentName } from './agent_name';
|
||||
import {
|
||||
isJavaAgentName,
|
||||
isRumAgentName,
|
||||
isIosAgentName,
|
||||
isServerlessAgent,
|
||||
} from './agent_name';
|
||||
|
||||
describe('agent name helpers', () => {
|
||||
describe('isJavaAgentName', () => {
|
||||
|
@ -79,4 +84,30 @@ describe('agent name helpers', () => {
|
|||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('isServerlessAgent', () => {
|
||||
describe('when the runtime name is AWS_LAMBDA', () => {
|
||||
it('returns true', () => {
|
||||
expect(isServerlessAgent('AWS_LAMBDA')).toEqual(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe('when the runtime name is aws_lambda', () => {
|
||||
it('returns true', () => {
|
||||
expect(isServerlessAgent('aws_lambda')).toEqual(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe('when the runtime name is aws_lambda_test', () => {
|
||||
it('returns true', () => {
|
||||
expect(isServerlessAgent('aws_lambda_test')).toEqual(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe('when the runtime name is something else', () => {
|
||||
it('returns false', () => {
|
||||
expect(isServerlessAgent('not_aws_lambda')).toEqual(false);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -90,3 +90,7 @@ export function isIosAgentName(agentName?: string) {
|
|||
export function isJRubyAgent(agentName?: string, runtimeName?: string) {
|
||||
return agentName === 'ruby' && runtimeName?.toLowerCase() === 'jruby';
|
||||
}
|
||||
|
||||
export function isServerlessAgent(runtimeName?: string) {
|
||||
return runtimeName?.toLowerCase().startsWith('aws_lambda');
|
||||
}
|
||||
|
|
|
@ -13,6 +13,7 @@ export const CLOUD_MACHINE_TYPE = 'cloud.machine.type';
|
|||
export const CLOUD_ACCOUNT_ID = 'cloud.account.id';
|
||||
export const CLOUD_INSTANCE_ID = 'cloud.instance.id';
|
||||
export const CLOUD_INSTANCE_NAME = 'cloud.instance.name';
|
||||
export const CLOUD_SERVICE_NAME = 'cloud.service.name';
|
||||
|
||||
export const SERVICE = 'service';
|
||||
export const SERVICE_NAME = 'service.name';
|
||||
|
@ -152,3 +153,7 @@ export const PROFILE_ALLOC_OBJECTS = 'profile.alloc_objects.count';
|
|||
export const PROFILE_ALLOC_SPACE = 'profile.alloc_space.bytes';
|
||||
export const PROFILE_INUSE_OBJECTS = 'profile.inuse_objects.count';
|
||||
export const PROFILE_INUSE_SPACE = 'profile.inuse_space.bytes';
|
||||
|
||||
export const FAAS_ID = 'faas.id';
|
||||
export const FAAS_COLDSTART = 'faas.coldstart';
|
||||
export const FAAS_TRIGGER_TYPE = 'faas.trigger.type';
|
||||
|
|
|
@ -0,0 +1,53 @@
|
|||
/*
|
||||
* 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 url from 'url';
|
||||
import { synthtrace } from '../../../../../synthtrace';
|
||||
import { generateData } from './generate_data';
|
||||
|
||||
const start = '2021-10-10T00:00:00.000Z';
|
||||
const end = '2021-10-10T00:15:00.000Z';
|
||||
|
||||
const serviceOverviewHref = url.format({
|
||||
pathname: '/app/apm/services/synth-python/overview',
|
||||
query: { rangeFrom: start, rangeTo: end },
|
||||
});
|
||||
|
||||
const apiToIntercept = {
|
||||
endpoint:
|
||||
'/internal/apm/services/synth-python/transactions/charts/coldstart_rate?*',
|
||||
name: 'coldStartRequest',
|
||||
};
|
||||
|
||||
describe('Service overview - aws lambda', () => {
|
||||
before(async () => {
|
||||
await synthtrace.index(
|
||||
generateData({
|
||||
start: new Date(start).getTime(),
|
||||
end: new Date(end).getTime(),
|
||||
})
|
||||
);
|
||||
});
|
||||
|
||||
after(async () => {
|
||||
await synthtrace.clean();
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
cy.loginAsReadOnlyUser();
|
||||
});
|
||||
|
||||
it('displays a cold start rate chart and not a transaction breakdown chart', () => {
|
||||
const { endpoint, name } = apiToIntercept;
|
||||
|
||||
cy.intercept('GET', endpoint).as(name);
|
||||
cy.visit(serviceOverviewHref);
|
||||
cy.wait(`@${name}`);
|
||||
|
||||
cy.contains('Cold start rate');
|
||||
cy.contains('Time spent by span type').should('not.exist');
|
||||
});
|
||||
});
|
|
@ -0,0 +1,41 @@
|
|||
/*
|
||||
* 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 '@elastic/apm-synthtrace';
|
||||
|
||||
const dataConfig = {
|
||||
serviceName: 'synth-python',
|
||||
rate: 10,
|
||||
transaction: {
|
||||
name: 'GET /apple 🍎',
|
||||
duration: 1000,
|
||||
},
|
||||
};
|
||||
|
||||
export function generateData({ start, end }: { start: number; end: number }) {
|
||||
const { rate, transaction, serviceName } = dataConfig;
|
||||
const instance = apm
|
||||
.service(serviceName, 'production', 'python')
|
||||
.instance('instance-a');
|
||||
|
||||
const traceEvents = timerange(start, end)
|
||||
.interval('1m')
|
||||
.rate(rate)
|
||||
.flatMap((timestamp) => [
|
||||
...instance
|
||||
.transaction(transaction.name)
|
||||
.defaults({
|
||||
'service.runtime.name': 'AWS_Lambda_python3.8',
|
||||
'faas.coldstart': true,
|
||||
})
|
||||
.timestamp(timestamp)
|
||||
.duration(transaction.duration)
|
||||
.success()
|
||||
.serialize(),
|
||||
]);
|
||||
|
||||
return traceEvents;
|
||||
}
|
|
@ -9,13 +9,18 @@ import { EuiFlexGroup, EuiFlexItem, EuiLink, EuiPanel } from '@elastic/eui';
|
|||
import { i18n } from '@kbn/i18n';
|
||||
import React from 'react';
|
||||
import { useHistory } from 'react-router-dom';
|
||||
import { isRumAgentName, isIosAgentName } from '../../../../common/agent_name';
|
||||
import {
|
||||
isRumAgentName,
|
||||
isIosAgentName,
|
||||
isServerlessAgent,
|
||||
} from '../../../../common/agent_name';
|
||||
import { AnnotationsContextProvider } from '../../../context/annotations/annotations_context';
|
||||
import { useApmServiceContext } from '../../../context/apm_service/use_apm_service_context';
|
||||
import { ChartPointerEventContextProvider } from '../../../context/chart_pointer_event/chart_pointer_event_context';
|
||||
import { useBreakpoints } from '../../../hooks/use_breakpoints';
|
||||
import { LatencyChart } from '../../shared/charts/latency_chart';
|
||||
import { TransactionBreakdownChart } from '../../shared/charts/transaction_breakdown_chart';
|
||||
import { TransactionColdstartRateChart } from '../../shared/charts/transaction_coldstart_rate_chart';
|
||||
import { FailedTransactionRateChart } from '../../shared/charts/failed_transaction_rate_chart';
|
||||
import { ServiceOverviewDependenciesTable } from './service_overview_dependencies_table';
|
||||
import { ServiceOverviewErrorsTable } from './service_overview_errors_table';
|
||||
|
@ -35,8 +40,13 @@ import { replace } from '../../shared/links/url_helpers';
|
|||
export const chartHeight = 288;
|
||||
|
||||
export function ServiceOverview() {
|
||||
const { agentName, serviceName, transactionType, fallbackToTransactions } =
|
||||
useApmServiceContext();
|
||||
const {
|
||||
agentName,
|
||||
serviceName,
|
||||
transactionType,
|
||||
fallbackToTransactions,
|
||||
runtimeName,
|
||||
} = useApmServiceContext();
|
||||
const {
|
||||
query,
|
||||
query: {
|
||||
|
@ -69,7 +79,7 @@ export function ServiceOverview() {
|
|||
const rowDirection = isSingleColumn ? 'column' : 'row';
|
||||
const isRumAgent = isRumAgentName(agentName);
|
||||
const isIosAgent = isIosAgentName(agentName);
|
||||
|
||||
const isServerless = isServerlessAgent(runtimeName);
|
||||
const router = useApmRouter();
|
||||
const dependenciesLink = router.link('/services/{serviceName}/dependencies', {
|
||||
path: {
|
||||
|
@ -152,13 +162,23 @@ export function ServiceOverview() {
|
|||
gutterSize="s"
|
||||
responsive={false}
|
||||
>
|
||||
<EuiFlexItem grow={3}>
|
||||
<TransactionBreakdownChart
|
||||
showAnnotations={false}
|
||||
environment={environment}
|
||||
kuery={kuery}
|
||||
/>
|
||||
</EuiFlexItem>
|
||||
{isServerless ? (
|
||||
<EuiFlexItem grow={3}>
|
||||
<TransactionColdstartRateChart
|
||||
showAnnotations={false}
|
||||
environment={environment}
|
||||
kuery={kuery}
|
||||
/>
|
||||
</EuiFlexItem>
|
||||
) : (
|
||||
<EuiFlexItem grow={3}>
|
||||
<TransactionBreakdownChart
|
||||
showAnnotations={false}
|
||||
environment={environment}
|
||||
kuery={kuery}
|
||||
/>
|
||||
</EuiFlexItem>
|
||||
)}
|
||||
{!isRumAgent && (
|
||||
<EuiFlexItem grow={7}>
|
||||
<EuiPanel hasBorder={true}>
|
||||
|
@ -180,7 +200,7 @@ export function ServiceOverview() {
|
|||
)}
|
||||
</EuiFlexGroup>
|
||||
</EuiFlexItem>
|
||||
{!isRumAgent && !isIosAgent && (
|
||||
{!isRumAgent && !isIosAgent && !isServerless && (
|
||||
<EuiFlexItem>
|
||||
<EuiFlexGroup
|
||||
direction="column"
|
||||
|
|
|
@ -18,6 +18,7 @@ import { AggregatedTransactionsBadge } from '../../shared/aggregated_transaction
|
|||
import { TransactionCharts } from '../../shared/charts/transaction_charts';
|
||||
import { replace } from '../../shared/links/url_helpers';
|
||||
import { TransactionDetailsTabs } from './transaction_details_tabs';
|
||||
import { isServerlessAgent } from '../../../../common/agent_name';
|
||||
|
||||
export function TransactionDetails() {
|
||||
const { path, query } = useApmParams(
|
||||
|
@ -28,10 +29,13 @@ export function TransactionDetails() {
|
|||
rangeFrom,
|
||||
rangeTo,
|
||||
transactionType: transactionTypeFromUrl,
|
||||
comparisonEnabled,
|
||||
comparisonType,
|
||||
} = query;
|
||||
const { start, end } = useTimeRange({ rangeFrom, rangeTo });
|
||||
const apmRouter = useApmRouter();
|
||||
const { transactionType, fallbackToTransactions } = useApmServiceContext();
|
||||
const { transactionType, fallbackToTransactions, runtimeName } =
|
||||
useApmServiceContext();
|
||||
|
||||
const history = useHistory();
|
||||
|
||||
|
@ -48,6 +52,8 @@ export function TransactionDetails() {
|
|||
}),
|
||||
});
|
||||
|
||||
const isServerless = isServerlessAgent(runtimeName);
|
||||
|
||||
return (
|
||||
<>
|
||||
{fallbackToTransactions && <AggregatedTransactionsBadge />}
|
||||
|
@ -66,6 +72,9 @@ export function TransactionDetails() {
|
|||
start={start}
|
||||
end={end}
|
||||
transactionName={transactionName}
|
||||
isServerlessContext={isServerless}
|
||||
comparisonEnabled={comparisonEnabled}
|
||||
comparisonType={comparisonType}
|
||||
/>
|
||||
</ChartPointerEventContextProvider>
|
||||
|
||||
|
|
|
@ -0,0 +1,18 @@
|
|||
/*
|
||||
* 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 from 'react';
|
||||
import { ColdStartBadge } from './cold_start_badge';
|
||||
|
||||
export default {
|
||||
title: 'app/TransactionDetails/Waterfall/Badge/ColdStartBadge',
|
||||
component: ColdStartBadge,
|
||||
};
|
||||
|
||||
export function Example() {
|
||||
return <ColdStartBadge />;
|
||||
}
|
|
@ -0,0 +1,20 @@
|
|||
/*
|
||||
* 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 { EuiBadge } from '@elastic/eui';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import React from 'react';
|
||||
|
||||
export function ColdStartBadge() {
|
||||
return (
|
||||
<EuiBadge color="warning">
|
||||
{i18n.translate('xpack.apm.transactionDetails.coldstartBadge', {
|
||||
defaultMessage: 'cold start',
|
||||
})}
|
||||
</EuiBadge>
|
||||
);
|
||||
}
|
|
@ -9,7 +9,7 @@ import React from 'react';
|
|||
import { SyncBadge, SyncBadgeProps } from './sync_badge';
|
||||
|
||||
export default {
|
||||
title: 'app/TransactionDetails/SyncBadge',
|
||||
title: 'app/TransactionDetails/Waterfall/Badge/SyncBadge',
|
||||
component: SyncBadge,
|
||||
argTypes: {
|
||||
sync: {
|
|
@ -8,7 +8,7 @@
|
|||
import { EuiBadge } from '@elastic/eui';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import React from 'react';
|
||||
import { AgentName } from '../../../../../../../typings/es_schemas/ui/fields/agent';
|
||||
import { AgentName } from '../../../../../../../../typings/es_schemas/ui/fields/agent';
|
||||
|
||||
export interface SyncBadgeProps {
|
||||
/**
|
|
@ -34,7 +34,7 @@ import { DurationSummaryItem } from '../../../../../../shared/summary/duration_s
|
|||
import { HttpInfoSummaryItem } from '../../../../../../shared/summary/http_info_summary_item';
|
||||
import { TimestampTooltip } from '../../../../../../shared/timestamp_tooltip';
|
||||
import { ResponsiveFlyout } from '../responsive_flyout';
|
||||
import { SyncBadge } from '../sync_badge';
|
||||
import { SyncBadge } from '../badge/sync_badge';
|
||||
import { SpanDatabase } from './span_db';
|
||||
import { StickySpanProperties } from './sticky_span_properties';
|
||||
import { FailureBadge } from '../failure_badge';
|
||||
|
|
|
@ -87,6 +87,7 @@ export function TransactionFlyout({
|
|||
transaction={transactionDoc}
|
||||
totalDuration={rootTransactionDuration}
|
||||
errorCount={errorCount}
|
||||
coldStartBadge={transactionDoc.faas?.coldstart}
|
||||
/>
|
||||
<EuiHorizontalRule margin="m" />
|
||||
<DroppedSpansWarning transactionDoc={transactionDoc} />
|
||||
|
|
|
@ -18,7 +18,8 @@ import {
|
|||
import { asDuration } from '../../../../../../../common/utils/formatters';
|
||||
import { Margins } from '../../../../../shared/charts/timeline';
|
||||
import { TruncateWithTooltip } from '../../../../../shared/truncate_with_tooltip';
|
||||
import { SyncBadge } from './sync_badge';
|
||||
import { SyncBadge } from './badge/sync_badge';
|
||||
import { ColdStartBadge } from './badge/cold_start_badge';
|
||||
import { IWaterfallSpanOrTransaction } from './waterfall_helpers/waterfall_helpers';
|
||||
import { FailureBadge } from './failure_badge';
|
||||
import { useApmRouter } from '../../../../../../hooks/use_apm_router';
|
||||
|
@ -200,6 +201,8 @@ export function WaterfallItem({
|
|||
|
||||
const isCompositeSpan = item.docType === 'span' && item.doc.span.composite;
|
||||
const itemBarStyle = getItemBarStyle(item, color, width, left);
|
||||
const isServerlessColdstart =
|
||||
item.docType === 'transaction' && item.doc.faas?.coldstart;
|
||||
|
||||
return (
|
||||
<Container
|
||||
|
@ -234,6 +237,7 @@ export function WaterfallItem({
|
|||
agentName={item.doc.agent.name}
|
||||
/>
|
||||
)}
|
||||
{isServerlessColdstart && <ColdStartBadge />}
|
||||
</ItemText>
|
||||
</Container>
|
||||
);
|
||||
|
|
|
@ -226,6 +226,9 @@ export const simpleTrace = {
|
|||
timestamp: {
|
||||
us: 1584975868787052,
|
||||
},
|
||||
faas: {
|
||||
coldstart: true,
|
||||
},
|
||||
},
|
||||
{
|
||||
parent: {
|
||||
|
|
|
@ -19,9 +19,18 @@ import {
|
|||
traceWithErrors,
|
||||
urlParams as testUrlParams,
|
||||
} from './waterfall_container.stories.data';
|
||||
import type { ApmPluginContextValue } from '../../../../../context/apm_plugin/apm_plugin_context';
|
||||
|
||||
type Args = ComponentProps<typeof WaterfallContainer>;
|
||||
|
||||
const apmPluginContextMock = {
|
||||
core: {
|
||||
http: {
|
||||
basePath: { prepend: () => {} },
|
||||
},
|
||||
},
|
||||
} as unknown as ApmPluginContextValue;
|
||||
|
||||
const stories: Meta<Args> = {
|
||||
title: 'app/TransactionDetails/waterfall',
|
||||
component: WaterfallContainer,
|
||||
|
@ -32,7 +41,7 @@ const stories: Meta<Args> = {
|
|||
'/services/{serviceName}/transactions/view?rangeFrom=now-15m&rangeTo=now&transactionName=testTransactionName',
|
||||
]}
|
||||
>
|
||||
<MockApmPluginContextWrapper>
|
||||
<MockApmPluginContextWrapper value={apmPluginContextMock}>
|
||||
<StoryComponent />
|
||||
</MockApmPluginContextWrapper>
|
||||
</MemoryRouter>
|
||||
|
|
|
@ -15,6 +15,7 @@ import { AggregatedTransactionsBadge } from '../../shared/aggregated_transaction
|
|||
import { TransactionCharts } from '../../shared/charts/transaction_charts';
|
||||
import { replace } from '../../shared/links/url_helpers';
|
||||
import { TransactionsTable } from '../../shared/transactions_table';
|
||||
import { isServerlessAgent } from '../../../../common/agent_name';
|
||||
|
||||
export function TransactionOverview() {
|
||||
const {
|
||||
|
@ -24,12 +25,14 @@ export function TransactionOverview() {
|
|||
rangeFrom,
|
||||
rangeTo,
|
||||
transactionType: transactionTypeFromUrl,
|
||||
comparisonEnabled,
|
||||
comparisonType,
|
||||
},
|
||||
} = useApmParams('/services/{serviceName}/transactions');
|
||||
|
||||
const { start, end } = useTimeRange({ rangeFrom, rangeTo });
|
||||
|
||||
const { transactionType, serviceName, fallbackToTransactions } =
|
||||
const { transactionType, serviceName, fallbackToTransactions, runtimeName } =
|
||||
useApmServiceContext();
|
||||
|
||||
const history = useHistory();
|
||||
|
@ -45,6 +48,8 @@ export function TransactionOverview() {
|
|||
return null;
|
||||
}
|
||||
|
||||
const isServerless = isServerlessAgent(runtimeName);
|
||||
|
||||
return (
|
||||
<>
|
||||
{fallbackToTransactions && (
|
||||
|
@ -62,6 +67,9 @@ export function TransactionOverview() {
|
|||
environment={environment}
|
||||
start={start}
|
||||
end={end}
|
||||
isServerlessContext={isServerless}
|
||||
comparisonEnabled={comparisonEnabled}
|
||||
comparisonType={comparisonType}
|
||||
/>
|
||||
<EuiSpacer size="s" />
|
||||
<EuiPanel hasBorder={true}>
|
||||
|
|
|
@ -19,6 +19,7 @@ describe('APM service template', () => {
|
|||
{ agentName: 'ios/swift' },
|
||||
{ agentName: 'opentelemetry/swift' },
|
||||
{ agentName: 'ruby', runtimeName: 'jruby' },
|
||||
{ runtimeName: 'aws_lambda' },
|
||||
].map((input) => {
|
||||
it(`when input ${JSON.stringify(input)}`, () => {
|
||||
expect(isMetricsTabHidden(input)).toBeTruthy();
|
||||
|
@ -52,6 +53,7 @@ describe('APM service template', () => {
|
|||
{ agentName: 'nodejs' },
|
||||
{ agentName: 'php' },
|
||||
{ agentName: 'python' },
|
||||
{ runtimeName: 'aws_lambda' },
|
||||
].map((input) => {
|
||||
it(`when input ${JSON.stringify(input)}`, () => {
|
||||
expect(isJVMsTabHidden(input)).toBeTruthy();
|
||||
|
|
|
@ -21,6 +21,7 @@ import {
|
|||
isJavaAgentName,
|
||||
isJRubyAgent,
|
||||
isRumAgentName,
|
||||
isServerlessAgent,
|
||||
} from '../../../../../common/agent_name';
|
||||
import { useApmPluginContext } from '../../../../context/apm_plugin/use_apm_plugin_context';
|
||||
import { ApmServiceContextProvider } from '../../../../context/apm_service/apm_service_context';
|
||||
|
@ -144,7 +145,8 @@ export function isMetricsTabHidden({
|
|||
isRumAgentName(agentName) ||
|
||||
isJavaAgentName(agentName) ||
|
||||
isIosAgentName(agentName) ||
|
||||
isJRubyAgent(agentName, runtimeName)
|
||||
isJRubyAgent(agentName, runtimeName) ||
|
||||
isServerlessAgent(runtimeName)
|
||||
);
|
||||
}
|
||||
|
||||
|
@ -155,7 +157,10 @@ export function isJVMsTabHidden({
|
|||
agentName?: string;
|
||||
runtimeName?: string;
|
||||
}) {
|
||||
return !(isJavaAgentName(agentName) || isJRubyAgent(agentName, runtimeName));
|
||||
return (
|
||||
!(isJavaAgentName(agentName) || isJRubyAgent(agentName, runtimeName)) ||
|
||||
isServerlessAgent(runtimeName)
|
||||
);
|
||||
}
|
||||
|
||||
function useTabs({ selectedTab }: { selectedTab: Tab['key'] }) {
|
||||
|
|
|
@ -12,7 +12,9 @@ import { ChartPointerEventContextProvider } from '../../../../context/chart_poin
|
|||
import { ServiceOverviewThroughputChart } from '../../../app/service_overview/service_overview_throughput_chart';
|
||||
import { LatencyChart } from '../latency_chart';
|
||||
import { TransactionBreakdownChart } from '../transaction_breakdown_chart';
|
||||
import { TransactionColdstartRateChart } from '../transaction_coldstart_rate_chart';
|
||||
import { FailedTransactionRateChart } from '../failed_transaction_rate_chart';
|
||||
import { TimeRangeComparisonType } from '../../../../../common/runtime_types/comparison_type_rt';
|
||||
|
||||
export function TransactionCharts({
|
||||
kuery,
|
||||
|
@ -20,12 +22,18 @@ export function TransactionCharts({
|
|||
start,
|
||||
end,
|
||||
transactionName,
|
||||
isServerlessContext,
|
||||
comparisonEnabled,
|
||||
comparisonType,
|
||||
}: {
|
||||
kuery: string;
|
||||
environment: string;
|
||||
start: string;
|
||||
end: string;
|
||||
transactionName?: string;
|
||||
isServerlessContext?: boolean;
|
||||
comparisonEnabled?: boolean;
|
||||
comparisonType?: TimeRangeComparisonType;
|
||||
}) {
|
||||
return (
|
||||
<>
|
||||
|
@ -56,12 +64,24 @@ export function TransactionCharts({
|
|||
<EuiFlexItem>
|
||||
<FailedTransactionRateChart kuery={kuery} />
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem>
|
||||
<TransactionBreakdownChart
|
||||
kuery={kuery}
|
||||
environment={environment}
|
||||
/>
|
||||
</EuiFlexItem>
|
||||
{isServerlessContext ? (
|
||||
<EuiFlexItem>
|
||||
<TransactionColdstartRateChart
|
||||
kuery={kuery}
|
||||
transactionName={transactionName}
|
||||
environment={environment}
|
||||
comparisonEnabled={comparisonEnabled}
|
||||
comparisonType={comparisonType}
|
||||
/>
|
||||
</EuiFlexItem>
|
||||
) : (
|
||||
<EuiFlexItem>
|
||||
<TransactionBreakdownChart
|
||||
kuery={kuery}
|
||||
environment={environment}
|
||||
/>
|
||||
</EuiFlexItem>
|
||||
)}
|
||||
</EuiFlexGrid>
|
||||
</ChartPointerEventContextProvider>
|
||||
</AnnotationsContextProvider>
|
||||
|
|
|
@ -0,0 +1,184 @@
|
|||
/*
|
||||
* 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 {
|
||||
EuiPanel,
|
||||
EuiTitle,
|
||||
EuiFlexGroup,
|
||||
EuiFlexItem,
|
||||
EuiIconTip,
|
||||
} from '@elastic/eui';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import React from 'react';
|
||||
import { APIReturnType } from '../../../../services/rest/create_call_apm_api';
|
||||
import { asPercent } from '../../../../../common/utils/formatters';
|
||||
import { useFetcher } from '../../../../hooks/use_fetcher';
|
||||
import { useTheme } from '../../../../hooks/use_theme';
|
||||
import { TimeseriesChart } from '../timeseries_chart';
|
||||
import { useApmServiceContext } from '../../../../context/apm_service/use_apm_service_context';
|
||||
import {
|
||||
getComparisonChartTheme,
|
||||
getTimeRangeComparison,
|
||||
} from '../../time_comparison/get_time_range_comparison';
|
||||
import { useApmParams } from '../../../../hooks/use_apm_params';
|
||||
import { useTimeRange } from '../../../../hooks/use_time_range';
|
||||
import { TimeRangeComparisonType } from '../../../../../common/runtime_types/comparison_type_rt';
|
||||
|
||||
function yLabelFormat(y?: number | null) {
|
||||
return asPercent(y || 0, 1);
|
||||
}
|
||||
|
||||
interface Props {
|
||||
height?: number;
|
||||
showAnnotations?: boolean;
|
||||
kuery: string;
|
||||
environment: string;
|
||||
transactionName?: string;
|
||||
comparisonEnabled?: boolean;
|
||||
comparisonType?: TimeRangeComparisonType;
|
||||
}
|
||||
|
||||
type ColdstartRate =
|
||||
APIReturnType<'GET /internal/apm/services/{serviceName}/transactions/charts/coldstart_rate'>;
|
||||
|
||||
const INITIAL_STATE: ColdstartRate = {
|
||||
currentPeriod: {
|
||||
transactionColdstartRate: [],
|
||||
average: null,
|
||||
},
|
||||
previousPeriod: {
|
||||
transactionColdstartRate: [],
|
||||
average: null,
|
||||
},
|
||||
};
|
||||
|
||||
export function TransactionColdstartRateChart({
|
||||
height,
|
||||
showAnnotations = true,
|
||||
environment,
|
||||
kuery,
|
||||
transactionName,
|
||||
comparisonEnabled,
|
||||
comparisonType,
|
||||
}: Props) {
|
||||
const theme = useTheme();
|
||||
|
||||
const {
|
||||
query: { rangeFrom, rangeTo },
|
||||
} = useApmParams('/services/{serviceName}');
|
||||
|
||||
const { start, end } = useTimeRange({ rangeFrom, rangeTo });
|
||||
|
||||
const { serviceName, transactionType } = useApmServiceContext();
|
||||
const comparisonChartThem = getComparisonChartTheme();
|
||||
const { comparisonStart, comparisonEnd } = getTimeRangeComparison({
|
||||
start,
|
||||
end,
|
||||
comparisonType,
|
||||
comparisonEnabled,
|
||||
});
|
||||
|
||||
const endpoint = transactionName
|
||||
? ('GET /internal/apm/services/{serviceName}/transactions/charts/coldstart_rate_by_transaction_name' as const)
|
||||
: ('GET /internal/apm/services/{serviceName}/transactions/charts/coldstart_rate' as const);
|
||||
|
||||
const { data = INITIAL_STATE, status } = useFetcher(
|
||||
(callApmApi) => {
|
||||
if (transactionType && serviceName && start && end) {
|
||||
return callApmApi(endpoint, {
|
||||
params: {
|
||||
path: {
|
||||
serviceName,
|
||||
},
|
||||
query: {
|
||||
environment,
|
||||
kuery,
|
||||
start,
|
||||
end,
|
||||
transactionType,
|
||||
comparisonStart,
|
||||
comparisonEnd,
|
||||
...(transactionName ? { transactionName } : {}),
|
||||
},
|
||||
},
|
||||
});
|
||||
}
|
||||
},
|
||||
[
|
||||
environment,
|
||||
kuery,
|
||||
serviceName,
|
||||
start,
|
||||
end,
|
||||
transactionType,
|
||||
transactionName,
|
||||
comparisonStart,
|
||||
comparisonEnd,
|
||||
endpoint,
|
||||
]
|
||||
);
|
||||
|
||||
const timeseries = [
|
||||
{
|
||||
data: data.currentPeriod.transactionColdstartRate,
|
||||
type: 'linemark',
|
||||
color: theme.eui.euiColorVis5,
|
||||
title: i18n.translate('xpack.apm.coldstartRate.chart.coldstartRate', {
|
||||
defaultMessage: 'Cold start rate (avg.)',
|
||||
}),
|
||||
},
|
||||
...(comparisonEnabled
|
||||
? [
|
||||
{
|
||||
data: data.previousPeriod.transactionColdstartRate,
|
||||
type: 'area',
|
||||
color: theme.eui.euiColorMediumShade,
|
||||
title: i18n.translate(
|
||||
'xpack.apm.coldstartRate.chart.coldstartRate.previousPeriodLabel',
|
||||
{ defaultMessage: 'Previous period' }
|
||||
),
|
||||
},
|
||||
]
|
||||
: []),
|
||||
];
|
||||
|
||||
return (
|
||||
<EuiPanel hasBorder={true}>
|
||||
<EuiFlexGroup alignItems="center" gutterSize="s" responsive={false}>
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiTitle size="xs">
|
||||
<h2>
|
||||
{i18n.translate('xpack.apm.coldstartRate', {
|
||||
defaultMessage: 'Cold start rate',
|
||||
})}
|
||||
</h2>
|
||||
</EuiTitle>
|
||||
</EuiFlexItem>
|
||||
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiIconTip
|
||||
content={i18n.translate('xpack.apm.serviceOverview.coldstartHelp', {
|
||||
defaultMessage:
|
||||
'The cold start rate indicates the percentage of requests that trigger a cold start of a serverless function.',
|
||||
})}
|
||||
position="right"
|
||||
/>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
<TimeseriesChart
|
||||
id="coldstartRate"
|
||||
height={height}
|
||||
showAnnotations={showAnnotations}
|
||||
fetchStatus={status}
|
||||
timeseries={timeseries}
|
||||
yLabelFormat={yLabelFormat}
|
||||
yDomain={{ min: 0, max: 1 }}
|
||||
customTheme={comparisonChartThem}
|
||||
/>
|
||||
</EuiPanel>
|
||||
);
|
||||
}
|
|
@ -5,7 +5,13 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
import { EuiBadge, EuiDescriptionList } from '@elastic/eui';
|
||||
import {
|
||||
EuiBadge,
|
||||
EuiDescriptionList,
|
||||
EuiBetaBadge,
|
||||
EuiFlexGroup,
|
||||
EuiFlexItem,
|
||||
} from '@elastic/eui';
|
||||
import { EuiDescriptionListProps } from '@elastic/eui/src/components/description_list/description_list';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import React from 'react';
|
||||
|
@ -16,9 +22,10 @@ type ServiceDetailsReturnType =
|
|||
|
||||
interface Props {
|
||||
cloud: ServiceDetailsReturnType['cloud'];
|
||||
isServerless: boolean;
|
||||
}
|
||||
|
||||
export function CloudDetails({ cloud }: Props) {
|
||||
export function CloudDetails({ cloud, isServerless }: Props) {
|
||||
if (!cloud) {
|
||||
return null;
|
||||
}
|
||||
|
@ -36,6 +43,43 @@ export function CloudDetails({ cloud }: Props) {
|
|||
});
|
||||
}
|
||||
|
||||
if (cloud.serviceName) {
|
||||
listItems.push({
|
||||
title: i18n.translate(
|
||||
'xpack.apm.serviceIcons.serviceDetails.cloud.serviceNameLabel',
|
||||
{
|
||||
defaultMessage: 'Cloud service',
|
||||
}
|
||||
),
|
||||
description: (
|
||||
<EuiFlexGroup alignItems="center" gutterSize="xs">
|
||||
<EuiFlexItem grow={false}>{cloud.serviceName}</EuiFlexItem>
|
||||
{isServerless && (
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiBetaBadge
|
||||
label={i18n.translate(
|
||||
'xpack.apm.serviceIcons.serviceDetails.cloud.betaLabel',
|
||||
{
|
||||
defaultMessage: 'Beta',
|
||||
}
|
||||
)}
|
||||
tooltipContent={i18n.translate(
|
||||
'xpack.apm.serviceIcons.serviceDetails.cloud.betaTooltip',
|
||||
{
|
||||
defaultMessage:
|
||||
'AWS Lambda support is not GA. Please help us by reporting bugs.',
|
||||
}
|
||||
)}
|
||||
size="s"
|
||||
iconType="beaker"
|
||||
/>
|
||||
</EuiFlexItem>
|
||||
)}
|
||||
</EuiFlexGroup>
|
||||
),
|
||||
});
|
||||
}
|
||||
|
||||
if (!!cloud.availabilityZones?.length) {
|
||||
listItems.push({
|
||||
title: i18n.translate(
|
||||
|
@ -58,7 +102,29 @@ export function CloudDetails({ cloud }: Props) {
|
|||
});
|
||||
}
|
||||
|
||||
if (cloud.machineTypes) {
|
||||
if (!!cloud.regions?.length) {
|
||||
listItems.push({
|
||||
title: i18n.translate(
|
||||
'xpack.apm.serviceIcons.serviceDetails.cloud.regionLabel',
|
||||
{
|
||||
defaultMessage:
|
||||
'{regions, plural, =0 {Region} one {Region} other {Regions}} ',
|
||||
values: { regions: cloud.regions.length },
|
||||
}
|
||||
),
|
||||
description: (
|
||||
<ul>
|
||||
{cloud.regions.map((region, index) => (
|
||||
<li key={index}>
|
||||
<EuiBadge color="hollow">{region}</EuiBadge>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
),
|
||||
});
|
||||
}
|
||||
|
||||
if (!!cloud.machineTypes?.length) {
|
||||
listItems.push({
|
||||
title: i18n.translate(
|
||||
'xpack.apm.serviceIcons.serviceDetails.cloud.machineTypesLabel',
|
||||
|
|
|
@ -189,6 +189,7 @@ describe('ServiceIcons', () => {
|
|||
data: {
|
||||
agentName: 'java',
|
||||
containerType: 'Kubernetes',
|
||||
serverlessType: 'lambda',
|
||||
cloudProvider: 'gcp',
|
||||
},
|
||||
status: fetcherHook.FETCH_STATUS.SUCCESS,
|
||||
|
@ -220,6 +221,7 @@ describe('ServiceIcons', () => {
|
|||
expect(queryAllByTestId('loading')).toHaveLength(0);
|
||||
expect(getByTestId('service')).toBeInTheDocument();
|
||||
expect(getByTestId('container')).toBeInTheDocument();
|
||||
expect(getByTestId('serverless')).toBeInTheDocument();
|
||||
expect(getByTestId('cloud')).toBeInTheDocument();
|
||||
fireEvent.click(getByTestId('popover_Service'));
|
||||
expect(getByTestId('loading-content')).toBeInTheDocument();
|
||||
|
@ -231,6 +233,7 @@ describe('ServiceIcons', () => {
|
|||
data: {
|
||||
agentName: 'java',
|
||||
containerType: 'Kubernetes',
|
||||
serverlessType: '',
|
||||
cloudProvider: 'gcp',
|
||||
},
|
||||
status: fetcherHook.FETCH_STATUS.SUCCESS,
|
||||
|
@ -269,5 +272,118 @@ describe('ServiceIcons', () => {
|
|||
expect(getByText('Service')).toBeInTheDocument();
|
||||
expect(getByText('v1.0.0')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('shows serverless content', () => {
|
||||
const apisMockData = {
|
||||
'GET /internal/apm/services/{serviceName}/metadata/icons': {
|
||||
data: {
|
||||
agentName: 'java',
|
||||
containerType: 'Kubernetes',
|
||||
serverlessType: 'lambda',
|
||||
cloudProvider: 'gcp',
|
||||
},
|
||||
status: fetcherHook.FETCH_STATUS.SUCCESS,
|
||||
refetch: jest.fn(),
|
||||
},
|
||||
'GET /internal/apm/services/{serviceName}/metadata/details': {
|
||||
data: {
|
||||
serverless: {
|
||||
type: '',
|
||||
functionNames: ['lambda-java-dev'],
|
||||
faasTriggerTypes: ['datasource', 'http'],
|
||||
},
|
||||
},
|
||||
status: fetcherHook.FETCH_STATUS.SUCCESS,
|
||||
refetch: jest.fn(),
|
||||
},
|
||||
};
|
||||
jest
|
||||
.spyOn(fetcherHook, 'useFetcher')
|
||||
.mockImplementation((func: Function, deps: string[]) => {
|
||||
return func(callApmApi(apisMockData)) || {};
|
||||
});
|
||||
|
||||
const { queryAllByTestId, getByTestId, getByText } = render(
|
||||
<Wrapper>
|
||||
<EuiThemeProvider>
|
||||
<ServiceIcons
|
||||
serviceName="foo"
|
||||
start="2021-08-20T10:00:00.000Z"
|
||||
end="2021-08-20T10:15:00.000Z"
|
||||
/>
|
||||
</EuiThemeProvider>
|
||||
</Wrapper>
|
||||
);
|
||||
expect(queryAllByTestId('loading')).toHaveLength(0);
|
||||
expect(getByTestId('service')).toBeInTheDocument();
|
||||
expect(getByTestId('container')).toBeInTheDocument();
|
||||
expect(getByTestId('serverless')).toBeInTheDocument();
|
||||
expect(getByTestId('cloud')).toBeInTheDocument();
|
||||
|
||||
fireEvent.click(getByTestId('popover_Serverless'));
|
||||
expect(queryAllByTestId('loading-content')).toHaveLength(0);
|
||||
expect(getByText('Serverless')).toBeInTheDocument();
|
||||
expect(getByText('lambda-java-dev')).toBeInTheDocument();
|
||||
expect(getByText('datasource')).toBeInTheDocument();
|
||||
expect(getByText('http')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('shows cloud content', () => {
|
||||
const apisMockData = {
|
||||
'GET /internal/apm/services/{serviceName}/metadata/icons': {
|
||||
data: {
|
||||
agentName: 'java',
|
||||
containerType: 'Kubernetes',
|
||||
serverlessType: 'lambda',
|
||||
cloudProvider: 'gcp',
|
||||
},
|
||||
status: fetcherHook.FETCH_STATUS.SUCCESS,
|
||||
refetch: jest.fn(),
|
||||
},
|
||||
'GET /internal/apm/services/{serviceName}/metadata/details': {
|
||||
data: {
|
||||
cloud: {
|
||||
provider: 'aws',
|
||||
projectName: '',
|
||||
serviceName: 'lambda',
|
||||
availabilityZones: [],
|
||||
regions: ['us-east-1'],
|
||||
machineTypes: [],
|
||||
},
|
||||
},
|
||||
status: fetcherHook.FETCH_STATUS.SUCCESS,
|
||||
refetch: jest.fn(),
|
||||
},
|
||||
};
|
||||
jest
|
||||
.spyOn(fetcherHook, 'useFetcher')
|
||||
.mockImplementation((func: Function, deps: string[]) => {
|
||||
return func(callApmApi(apisMockData)) || {};
|
||||
});
|
||||
|
||||
const { queryAllByTestId, getByTestId, getByText } = render(
|
||||
<Wrapper>
|
||||
<EuiThemeProvider>
|
||||
<ServiceIcons
|
||||
serviceName="foo"
|
||||
start="2021-08-20T10:00:00.000Z"
|
||||
end="2021-08-20T10:15:00.000Z"
|
||||
/>
|
||||
</EuiThemeProvider>
|
||||
</Wrapper>
|
||||
);
|
||||
expect(queryAllByTestId('loading')).toHaveLength(0);
|
||||
expect(getByTestId('service')).toBeInTheDocument();
|
||||
expect(getByTestId('container')).toBeInTheDocument();
|
||||
expect(getByTestId('serverless')).toBeInTheDocument();
|
||||
expect(getByTestId('cloud')).toBeInTheDocument();
|
||||
|
||||
fireEvent.click(getByTestId('popover_Cloud'));
|
||||
expect(queryAllByTestId('loading-content')).toHaveLength(0);
|
||||
expect(getByText('Cloud')).toBeInTheDocument();
|
||||
expect(getByText('aws')).toBeInTheDocument();
|
||||
expect(getByText('lambda')).toBeInTheDocument();
|
||||
expect(getByText('us-east-1')).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -13,6 +13,7 @@ import { ContainerType } from '../../../../common/service_metadata';
|
|||
import { FETCH_STATUS, useFetcher } from '../../../hooks/use_fetcher';
|
||||
import { getAgentIcon } from '../agent_icon/get_agent_icon';
|
||||
import { CloudDetails } from './cloud_details';
|
||||
import { ServerlessDetails } from './serverless_details';
|
||||
import { ContainerDetails } from './container_details';
|
||||
import { IconPopover } from './icon_popover';
|
||||
import { ServiceDetails } from './service_details';
|
||||
|
@ -47,7 +48,7 @@ export function getContainerIcon(container?: ContainerType) {
|
|||
}
|
||||
}
|
||||
|
||||
type Icons = 'service' | 'container' | 'cloud' | 'alerts';
|
||||
type Icons = 'service' | 'container' | 'serverless' | 'cloud' | 'alerts';
|
||||
|
||||
export interface PopoverItem {
|
||||
key: Icons;
|
||||
|
@ -130,6 +131,17 @@ export function ServiceIcons({ start, end, serviceName }: Props) {
|
|||
}),
|
||||
component: <ContainerDetails container={details?.container} />,
|
||||
},
|
||||
{
|
||||
key: 'serverless',
|
||||
icon: {
|
||||
type: getAgentIcon(icons?.serverlessType, theme.darkMode) || 'node',
|
||||
},
|
||||
isVisible: !!icons?.serverlessType,
|
||||
title: i18n.translate('xpack.apm.serviceIcons.serverless', {
|
||||
defaultMessage: 'Serverless',
|
||||
}),
|
||||
component: <ServerlessDetails serverless={details?.serverless} />,
|
||||
},
|
||||
{
|
||||
key: 'cloud',
|
||||
icon: {
|
||||
|
@ -139,7 +151,12 @@ export function ServiceIcons({ start, end, serviceName }: Props) {
|
|||
title: i18n.translate('xpack.apm.serviceIcons.cloud', {
|
||||
defaultMessage: 'Cloud',
|
||||
}),
|
||||
component: <CloudDetails cloud={details?.cloud} />,
|
||||
component: (
|
||||
<CloudDetails
|
||||
cloud={details?.cloud}
|
||||
isServerless={!!details?.serverless}
|
||||
/>
|
||||
),
|
||||
},
|
||||
];
|
||||
|
||||
|
|
|
@ -0,0 +1,73 @@
|
|||
/*
|
||||
* 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 { EuiBadge, EuiDescriptionList } from '@elastic/eui';
|
||||
import { EuiDescriptionListProps } from '@elastic/eui/src/components/description_list/description_list';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import React from 'react';
|
||||
import { APIReturnType } from '../../../services/rest/create_call_apm_api';
|
||||
|
||||
type ServiceDetailsReturnType =
|
||||
APIReturnType<'GET /internal/apm/services/{serviceName}/metadata/details'>;
|
||||
|
||||
interface Props {
|
||||
serverless: ServiceDetailsReturnType['serverless'];
|
||||
}
|
||||
|
||||
export function ServerlessDetails({ serverless }: Props) {
|
||||
if (!serverless) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const listItems: EuiDescriptionListProps['listItems'] = [];
|
||||
|
||||
if (!!serverless.functionNames?.length) {
|
||||
listItems.push({
|
||||
title: i18n.translate(
|
||||
'xpack.apm.serviceIcons.serviceDetails.cloud.functionNameLabel',
|
||||
{
|
||||
defaultMessage:
|
||||
'{functionNames, plural, =0 {Function name} one {Function name} other {Function names}} ',
|
||||
values: { functionNames: serverless.functionNames.length },
|
||||
}
|
||||
),
|
||||
description: (
|
||||
<ul>
|
||||
{serverless.functionNames.map((type, index) => (
|
||||
<li key={index}>
|
||||
<EuiBadge color="hollow">{type}</EuiBadge>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
),
|
||||
});
|
||||
}
|
||||
|
||||
if (!!serverless.faasTriggerTypes?.length) {
|
||||
listItems.push({
|
||||
title: i18n.translate(
|
||||
'xpack.apm.serviceIcons.serviceDetails.cloud.faasTriggerTypeLabel',
|
||||
{
|
||||
defaultMessage:
|
||||
'{triggerTypes, plural, =0 {Trigger type} one {Trigger type} other {Trigger types}} ',
|
||||
values: { triggerTypes: serverless.faasTriggerTypes.length },
|
||||
}
|
||||
),
|
||||
description: (
|
||||
<ul>
|
||||
{serverless.faasTriggerTypes.map((type, index) => (
|
||||
<li key={index}>
|
||||
<EuiBadge color="hollow">{type}</EuiBadge>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
),
|
||||
});
|
||||
}
|
||||
|
||||
return <EuiDescriptionList textStyle="reverse" listItems={listItems} />;
|
||||
}
|
|
@ -14,11 +14,13 @@ import { ErrorCountSummaryItemBadge } from './error_count_summary_item_badge';
|
|||
import { HttpInfoSummaryItem } from './http_info_summary_item';
|
||||
import { TransactionResultSummaryItem } from './transaction_result_summary_item';
|
||||
import { UserAgentSummaryItem } from './user_agent_summary_item';
|
||||
import { ColdStartBadge } from '../../app/transaction_details/waterfall_with_summary/waterfall_container/waterfall/badge/cold_start_badge';
|
||||
|
||||
interface Props {
|
||||
transaction: Transaction;
|
||||
totalDuration: number | undefined;
|
||||
errorCount: number;
|
||||
coldStartBadge?: boolean;
|
||||
}
|
||||
|
||||
function getTransactionResultSummaryItem(transaction: Transaction) {
|
||||
|
@ -39,7 +41,12 @@ function getTransactionResultSummaryItem(transaction: Transaction) {
|
|||
return null;
|
||||
}
|
||||
|
||||
function TransactionSummary({ transaction, totalDuration, errorCount }: Props) {
|
||||
function TransactionSummary({
|
||||
transaction,
|
||||
totalDuration,
|
||||
errorCount,
|
||||
coldStartBadge,
|
||||
}: Props) {
|
||||
const items = [
|
||||
<TimestampTooltip time={transaction.timestamp.us / 1000} />,
|
||||
<DurationSummaryItem
|
||||
|
@ -52,6 +59,7 @@ function TransactionSummary({ transaction, totalDuration, errorCount }: Props) {
|
|||
transaction.user_agent ? (
|
||||
<UserAgentSummaryItem {...transaction.user_agent} />
|
||||
) : null,
|
||||
coldStartBadge ? <ColdStartBadge /> : null,
|
||||
];
|
||||
|
||||
return <Summary items={items} />;
|
||||
|
|
|
@ -0,0 +1,67 @@
|
|||
/*
|
||||
* 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 { FAAS_COLDSTART } from '../../../common/elasticsearch_fieldnames';
|
||||
import {
|
||||
AggregationOptionsByType,
|
||||
AggregationResultOf,
|
||||
} from '../../../../../../src/core/types/elasticsearch';
|
||||
|
||||
export const getColdstartAggregation = () => ({
|
||||
terms: {
|
||||
field: FAAS_COLDSTART,
|
||||
},
|
||||
});
|
||||
|
||||
type ColdstartAggregation = ReturnType<typeof getColdstartAggregation>;
|
||||
|
||||
export const getTimeseriesAggregation = (
|
||||
start: number,
|
||||
end: number,
|
||||
intervalString: string
|
||||
) => ({
|
||||
date_histogram: {
|
||||
field: '@timestamp',
|
||||
fixed_interval: intervalString,
|
||||
min_doc_count: 0,
|
||||
extended_bounds: { min: start, max: end },
|
||||
},
|
||||
aggs: { coldstartStates: getColdstartAggregation() },
|
||||
});
|
||||
|
||||
export function calculateTransactionColdstartRate(
|
||||
coldstartStatesResponse: AggregationResultOf<ColdstartAggregation, {}>
|
||||
) {
|
||||
const coldstartStates = Object.fromEntries(
|
||||
coldstartStatesResponse.buckets.map(({ key, doc_count: count }) => [
|
||||
key === 1 ? 'true' : 'false',
|
||||
count,
|
||||
])
|
||||
);
|
||||
|
||||
const coldstarts = coldstartStates.true ?? 0;
|
||||
const warmstarts = coldstartStates.false ?? 0;
|
||||
|
||||
return coldstarts / (coldstarts + warmstarts);
|
||||
}
|
||||
|
||||
export function getTransactionColdstartRateTimeSeries(
|
||||
buckets: AggregationResultOf<
|
||||
{
|
||||
date_histogram: AggregationOptionsByType['date_histogram'];
|
||||
aggs: { coldstartStates: ColdstartAggregation };
|
||||
},
|
||||
{}
|
||||
>['buckets']
|
||||
) {
|
||||
return buckets.map((dateBucket) => {
|
||||
return {
|
||||
x: dateBucket.key,
|
||||
y: calculateTransactionColdstartRate(dateBucket.coldstartStates),
|
||||
};
|
||||
});
|
||||
}
|
|
@ -0,0 +1,181 @@
|
|||
/*
|
||||
* 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 {
|
||||
FAAS_COLDSTART,
|
||||
SERVICE_NAME,
|
||||
TRANSACTION_NAME,
|
||||
TRANSACTION_TYPE,
|
||||
} from '../../../common/elasticsearch_fieldnames';
|
||||
import { offsetPreviousPeriodCoordinates } from '../../../common/utils/offset_previous_period_coordinate';
|
||||
import { kqlQuery, rangeQuery } from '../../../../observability/server';
|
||||
import { environmentQuery } from '../../../common/utils/environment_query';
|
||||
import { Coordinate } from '../../../typings/timeseries';
|
||||
import {
|
||||
getDocumentTypeFilterForTransactions,
|
||||
getProcessorEventForTransactions,
|
||||
} from '../helpers/transactions';
|
||||
import { getBucketSizeForAggregatedTransactions } from '../helpers/get_bucket_size_for_aggregated_transactions';
|
||||
import { Setup } from '../helpers/setup_request';
|
||||
import {
|
||||
calculateTransactionColdstartRate,
|
||||
getColdstartAggregation,
|
||||
getTransactionColdstartRateTimeSeries,
|
||||
} from '../helpers/transaction_coldstart_rate';
|
||||
import { termQuery } from '../../../../observability/server';
|
||||
|
||||
export async function getColdstartRate({
|
||||
environment,
|
||||
kuery,
|
||||
serviceName,
|
||||
transactionType,
|
||||
transactionName,
|
||||
setup,
|
||||
searchAggregatedTransactions,
|
||||
start,
|
||||
end,
|
||||
}: {
|
||||
environment: string;
|
||||
kuery: string;
|
||||
serviceName: string;
|
||||
transactionType?: string;
|
||||
transactionName: string;
|
||||
setup: Setup;
|
||||
searchAggregatedTransactions: boolean;
|
||||
start: number;
|
||||
end: number;
|
||||
}): Promise<{
|
||||
transactionColdstartRate: Coordinate[];
|
||||
average: number | null;
|
||||
}> {
|
||||
const { apmEventClient } = setup;
|
||||
|
||||
const filter = [
|
||||
...termQuery(SERVICE_NAME, serviceName),
|
||||
{ exists: { field: FAAS_COLDSTART } },
|
||||
...(transactionName ? termQuery(TRANSACTION_NAME, transactionName) : []),
|
||||
...termQuery(TRANSACTION_TYPE, transactionType),
|
||||
...getDocumentTypeFilterForTransactions(searchAggregatedTransactions),
|
||||
...rangeQuery(start, end),
|
||||
...environmentQuery(environment),
|
||||
...kqlQuery(kuery),
|
||||
];
|
||||
|
||||
const coldstartStates = getColdstartAggregation();
|
||||
|
||||
const params = {
|
||||
apm: {
|
||||
events: [getProcessorEventForTransactions(searchAggregatedTransactions)],
|
||||
},
|
||||
body: {
|
||||
size: 0,
|
||||
query: { bool: { filter } },
|
||||
aggs: {
|
||||
coldstartStates,
|
||||
timeseries: {
|
||||
date_histogram: {
|
||||
field: '@timestamp',
|
||||
fixed_interval: getBucketSizeForAggregatedTransactions({
|
||||
start,
|
||||
end,
|
||||
searchAggregatedTransactions,
|
||||
}).intervalString,
|
||||
min_doc_count: 0,
|
||||
extended_bounds: { min: start, max: end },
|
||||
},
|
||||
aggs: {
|
||||
coldstartStates,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const resp = await apmEventClient.search(
|
||||
'get_transaction_group_coldstart_rate',
|
||||
params
|
||||
);
|
||||
|
||||
if (!resp.aggregations) {
|
||||
return { transactionColdstartRate: [], average: null };
|
||||
}
|
||||
|
||||
const transactionColdstartRate = getTransactionColdstartRateTimeSeries(
|
||||
resp.aggregations.timeseries.buckets
|
||||
);
|
||||
|
||||
const average = calculateTransactionColdstartRate(
|
||||
resp.aggregations.coldstartStates
|
||||
);
|
||||
|
||||
return { transactionColdstartRate, average };
|
||||
}
|
||||
|
||||
export async function getColdstartRatePeriods({
|
||||
environment,
|
||||
kuery,
|
||||
serviceName,
|
||||
transactionType,
|
||||
transactionName = '',
|
||||
setup,
|
||||
searchAggregatedTransactions,
|
||||
comparisonStart,
|
||||
comparisonEnd,
|
||||
start,
|
||||
end,
|
||||
}: {
|
||||
environment: string;
|
||||
kuery: string;
|
||||
serviceName: string;
|
||||
transactionType?: string;
|
||||
transactionName?: string;
|
||||
setup: Setup;
|
||||
searchAggregatedTransactions: boolean;
|
||||
comparisonStart?: number;
|
||||
comparisonEnd?: number;
|
||||
start: number;
|
||||
end: number;
|
||||
}) {
|
||||
const commonProps = {
|
||||
environment,
|
||||
kuery,
|
||||
serviceName,
|
||||
transactionType,
|
||||
transactionName,
|
||||
setup,
|
||||
searchAggregatedTransactions,
|
||||
};
|
||||
|
||||
const currentPeriodPromise = getColdstartRate({ ...commonProps, start, end });
|
||||
|
||||
const previousPeriodPromise =
|
||||
comparisonStart && comparisonEnd
|
||||
? getColdstartRate({
|
||||
...commonProps,
|
||||
start: comparisonStart,
|
||||
end: comparisonEnd,
|
||||
})
|
||||
: { transactionColdstartRate: [], average: null };
|
||||
|
||||
const [currentPeriod, previousPeriod] = await Promise.all([
|
||||
currentPeriodPromise,
|
||||
previousPeriodPromise,
|
||||
]);
|
||||
|
||||
const firstCurrentPeriod = currentPeriod.transactionColdstartRate;
|
||||
|
||||
return {
|
||||
currentPeriod,
|
||||
previousPeriod: {
|
||||
...previousPeriod,
|
||||
transactionColdstartRate: offsetPreviousPeriodCoordinates({
|
||||
currentPeriodTimeseries: firstCurrentPeriod,
|
||||
previousPeriodTimeseries: previousPeriod.transactionColdstartRate,
|
||||
}),
|
||||
},
|
||||
};
|
||||
}
|
|
@ -10,7 +10,9 @@ import {
|
|||
AGENT,
|
||||
CLOUD,
|
||||
CLOUD_AVAILABILITY_ZONE,
|
||||
CLOUD_REGION,
|
||||
CLOUD_MACHINE_TYPE,
|
||||
CLOUD_SERVICE_NAME,
|
||||
CONTAINER_ID,
|
||||
HOST,
|
||||
KUBERNETES,
|
||||
|
@ -18,6 +20,8 @@ import {
|
|||
SERVICE_NAME,
|
||||
SERVICE_NODE_NAME,
|
||||
SERVICE_VERSION,
|
||||
FAAS_ID,
|
||||
FAAS_TRIGGER_TYPE,
|
||||
} from '../../../common/elasticsearch_fieldnames';
|
||||
import { ContainerType } from '../../../common/service_metadata';
|
||||
import { rangeQuery } from '../../../../observability/server';
|
||||
|
@ -50,11 +54,18 @@ export interface ServiceMetadataDetails {
|
|||
totalNumberInstances?: number;
|
||||
type?: ContainerType;
|
||||
};
|
||||
serverless?: {
|
||||
type?: string;
|
||||
functionNames?: string[];
|
||||
faasTriggerTypes?: string[];
|
||||
};
|
||||
cloud?: {
|
||||
provider?: string;
|
||||
availabilityZones?: string[];
|
||||
regions?: string[];
|
||||
machineTypes?: string[];
|
||||
projectName?: string;
|
||||
serviceName?: string;
|
||||
};
|
||||
}
|
||||
|
||||
|
@ -104,12 +115,36 @@ export async function getServiceMetadataDetails({
|
|||
size: 10,
|
||||
},
|
||||
},
|
||||
regions: {
|
||||
terms: {
|
||||
field: CLOUD_REGION,
|
||||
size: 10,
|
||||
},
|
||||
},
|
||||
cloudServices: {
|
||||
terms: {
|
||||
field: CLOUD_SERVICE_NAME,
|
||||
size: 1,
|
||||
},
|
||||
},
|
||||
machineTypes: {
|
||||
terms: {
|
||||
field: CLOUD_MACHINE_TYPE,
|
||||
size: 10,
|
||||
},
|
||||
},
|
||||
faasTriggerTypes: {
|
||||
terms: {
|
||||
field: FAAS_TRIGGER_TYPE,
|
||||
size: 10,
|
||||
},
|
||||
},
|
||||
faasFunctionNames: {
|
||||
terms: {
|
||||
field: FAAS_ID,
|
||||
size: 10,
|
||||
},
|
||||
},
|
||||
totalNumberInstances: { cardinality: { field: SERVICE_NODE_NAME } },
|
||||
},
|
||||
},
|
||||
|
@ -153,13 +188,30 @@ export async function getServiceMetadataDetails({
|
|||
}
|
||||
: undefined;
|
||||
|
||||
const serverlessDetails =
|
||||
!!response.aggregations?.faasTriggerTypes?.buckets.length && cloud
|
||||
? {
|
||||
type: cloud.service?.name,
|
||||
functionNames: response.aggregations?.faasFunctionNames.buckets
|
||||
.map((bucket) => getLambdaFunctionNameFromARN(bucket.key as string))
|
||||
.filter((name) => name),
|
||||
faasTriggerTypes: response.aggregations?.faasTriggerTypes.buckets.map(
|
||||
(bucket) => bucket.key as string
|
||||
),
|
||||
}
|
||||
: undefined;
|
||||
|
||||
const cloudDetails = cloud
|
||||
? {
|
||||
provider: cloud.provider,
|
||||
projectName: cloud.project?.name,
|
||||
serviceName: cloud.service?.name,
|
||||
availabilityZones: response.aggregations?.availabilityZones.buckets.map(
|
||||
(bucket) => bucket.key as string
|
||||
),
|
||||
regions: response.aggregations?.regions.buckets.map(
|
||||
(bucket) => bucket.key as string
|
||||
),
|
||||
machineTypes: response.aggregations?.machineTypes.buckets.map(
|
||||
(bucket) => bucket.key as string
|
||||
),
|
||||
|
@ -169,6 +221,12 @@ export async function getServiceMetadataDetails({
|
|||
return {
|
||||
service: serviceMetadataDetails,
|
||||
container: containerDetails,
|
||||
serverless: serverlessDetails,
|
||||
cloud: cloudDetails,
|
||||
};
|
||||
}
|
||||
|
||||
function getLambdaFunctionNameFromARN(arn: string) {
|
||||
// Lambda function ARN example: arn:aws:lambda:us-west-2:123456789012:function:my-function
|
||||
return arn.split(':')[6] || '';
|
||||
}
|
||||
|
|
|
@ -9,6 +9,7 @@ import { ProcessorEvent } from '../../../common/processor_event';
|
|||
import {
|
||||
AGENT_NAME,
|
||||
CLOUD_PROVIDER,
|
||||
CLOUD_SERVICE_NAME,
|
||||
CONTAINER_ID,
|
||||
KUBERNETES,
|
||||
SERVICE_NAME,
|
||||
|
@ -29,6 +30,7 @@ type ServiceMetadataIconsRaw = Pick<
|
|||
export interface ServiceMetadataIcons {
|
||||
agentName?: string;
|
||||
containerType?: ContainerType;
|
||||
serverlessType?: string;
|
||||
cloudProvider?: string;
|
||||
}
|
||||
|
||||
|
@ -70,7 +72,13 @@ export async function getServiceMetadataIcons({
|
|||
},
|
||||
body: {
|
||||
size: 1,
|
||||
_source: [KUBERNETES, CLOUD_PROVIDER, CONTAINER_ID, AGENT_NAME],
|
||||
_source: [
|
||||
KUBERNETES,
|
||||
CLOUD_PROVIDER,
|
||||
CONTAINER_ID,
|
||||
AGENT_NAME,
|
||||
CLOUD_SERVICE_NAME,
|
||||
],
|
||||
query: { bool: { filter, should } },
|
||||
},
|
||||
};
|
||||
|
@ -85,6 +93,7 @@ export async function getServiceMetadataIcons({
|
|||
agentName: undefined,
|
||||
containerType: undefined,
|
||||
cloudProvider: undefined,
|
||||
serverlessType: undefined,
|
||||
};
|
||||
}
|
||||
|
||||
|
@ -98,9 +107,15 @@ export async function getServiceMetadataIcons({
|
|||
containerType = 'Docker';
|
||||
}
|
||||
|
||||
let serverlessType: string | undefined;
|
||||
if (cloud?.provider === 'aws' && cloud?.service?.name === 'lambda') {
|
||||
serverlessType = 'lambda';
|
||||
}
|
||||
|
||||
return {
|
||||
agentName: agent?.name,
|
||||
containerType,
|
||||
serverlessType,
|
||||
cloudProvider: cloud?.provider,
|
||||
};
|
||||
}
|
||||
|
|
|
@ -19,6 +19,7 @@ import { getTransactionBreakdown } from './breakdown';
|
|||
import { getTransactionTraceSamples } from './trace_samples';
|
||||
import { getLatencyPeriods } from './get_latency_charts';
|
||||
import { getFailedTransactionRatePeriods } from './get_failed_transaction_rate_periods';
|
||||
import { getColdstartRatePeriods } from '../../lib/transaction_groups/get_coldstart_rate';
|
||||
import { createApmServerRoute } from '../apm_routes/create_apm_server_route';
|
||||
import {
|
||||
comparisonRangeRt,
|
||||
|
@ -461,6 +462,158 @@ const transactionChartsErrorRateRoute = createApmServerRoute({
|
|||
},
|
||||
});
|
||||
|
||||
const transactionChartsColdstartRateRoute = createApmServerRoute({
|
||||
endpoint:
|
||||
'GET /internal/apm/services/{serviceName}/transactions/charts/coldstart_rate',
|
||||
params: t.type({
|
||||
path: t.type({
|
||||
serviceName: t.string,
|
||||
}),
|
||||
query: t.intersection([
|
||||
t.type({ transactionType: t.string }),
|
||||
t.intersection([environmentRt, kueryRt, rangeRt, comparisonRangeRt]),
|
||||
]),
|
||||
}),
|
||||
options: { tags: ['access:apm'] },
|
||||
handler: async (
|
||||
resources
|
||||
): Promise<{
|
||||
currentPeriod: {
|
||||
transactionColdstartRate: Array<
|
||||
import('../../../typings/timeseries').Coordinate
|
||||
>;
|
||||
average: number | null;
|
||||
};
|
||||
previousPeriod:
|
||||
| {
|
||||
transactionColdstartRate: Array<{
|
||||
x: number;
|
||||
y: import('../../../typings/common').Maybe<number>;
|
||||
}>;
|
||||
average: number | null;
|
||||
}
|
||||
| {
|
||||
transactionColdstartRate: Array<{
|
||||
x: number;
|
||||
y: import('../../../typings/common').Maybe<number>;
|
||||
}>;
|
||||
average: null;
|
||||
};
|
||||
}> => {
|
||||
const setup = await setupRequest(resources);
|
||||
|
||||
const { params } = resources;
|
||||
const { serviceName } = params.path;
|
||||
const {
|
||||
environment,
|
||||
kuery,
|
||||
transactionType,
|
||||
comparisonStart,
|
||||
comparisonEnd,
|
||||
start,
|
||||
end,
|
||||
} = params.query;
|
||||
|
||||
const searchAggregatedTransactions = await getSearchAggregatedTransactions({
|
||||
...setup,
|
||||
kuery,
|
||||
start,
|
||||
end,
|
||||
});
|
||||
|
||||
return getColdstartRatePeriods({
|
||||
environment,
|
||||
kuery,
|
||||
serviceName,
|
||||
transactionType,
|
||||
setup,
|
||||
searchAggregatedTransactions,
|
||||
comparisonStart,
|
||||
comparisonEnd,
|
||||
start,
|
||||
end,
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
const transactionChartsColdstartRateByTransactionNameRoute =
|
||||
createApmServerRoute({
|
||||
endpoint:
|
||||
'GET /internal/apm/services/{serviceName}/transactions/charts/coldstart_rate_by_transaction_name',
|
||||
params: t.type({
|
||||
path: t.type({
|
||||
serviceName: t.string,
|
||||
}),
|
||||
query: t.intersection([
|
||||
t.type({ transactionType: t.string, transactionName: t.string }),
|
||||
t.intersection([environmentRt, kueryRt, rangeRt, comparisonRangeRt]),
|
||||
]),
|
||||
}),
|
||||
options: { tags: ['access:apm'] },
|
||||
handler: async (
|
||||
resources
|
||||
): Promise<{
|
||||
currentPeriod: {
|
||||
transactionColdstartRate: Array<
|
||||
import('../../../typings/timeseries').Coordinate
|
||||
>;
|
||||
average: number | null;
|
||||
};
|
||||
previousPeriod:
|
||||
| {
|
||||
transactionColdstartRate: Array<{
|
||||
x: number;
|
||||
y: import('../../../typings/common').Maybe<number>;
|
||||
}>;
|
||||
average: number | null;
|
||||
}
|
||||
| {
|
||||
transactionColdstartRate: Array<{
|
||||
x: number;
|
||||
y: import('../../../typings/common').Maybe<number>;
|
||||
}>;
|
||||
average: null;
|
||||
};
|
||||
}> => {
|
||||
const setup = await setupRequest(resources);
|
||||
|
||||
const { params } = resources;
|
||||
const { serviceName } = params.path;
|
||||
const {
|
||||
environment,
|
||||
kuery,
|
||||
transactionType,
|
||||
transactionName,
|
||||
comparisonStart,
|
||||
comparisonEnd,
|
||||
start,
|
||||
end,
|
||||
} = params.query;
|
||||
|
||||
const searchAggregatedTransactions =
|
||||
await getSearchAggregatedTransactions({
|
||||
...setup,
|
||||
kuery,
|
||||
start,
|
||||
end,
|
||||
});
|
||||
|
||||
return getColdstartRatePeriods({
|
||||
environment,
|
||||
kuery,
|
||||
serviceName,
|
||||
transactionType,
|
||||
transactionName,
|
||||
setup,
|
||||
searchAggregatedTransactions,
|
||||
comparisonStart,
|
||||
comparisonEnd,
|
||||
start,
|
||||
end,
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
export const transactionRouteRepository = {
|
||||
...transactionGroupsMainStatisticsRoute,
|
||||
...transactionGroupsDetailedStatisticsRoute,
|
||||
|
@ -468,4 +621,6 @@ export const transactionRouteRepository = {
|
|||
...transactionTraceSamplesRoute,
|
||||
...transactionChartsBreakdownRoute,
|
||||
...transactionChartsErrorRateRoute,
|
||||
...transactionChartsColdstartRateRoute,
|
||||
...transactionChartsColdstartRateByTransactionNameRoute,
|
||||
};
|
||||
|
|
|
@ -27,4 +27,7 @@ export interface Cloud {
|
|||
image?: {
|
||||
id: string;
|
||||
};
|
||||
service?: {
|
||||
name: string;
|
||||
};
|
||||
}
|
||||
|
|
16
x-pack/plugins/apm/typings/es_schemas/raw/fields/faas.ts
Normal file
16
x-pack/plugins/apm/typings/es_schemas/raw/fields/faas.ts
Normal file
|
@ -0,0 +1,16 @@
|
|||
/*
|
||||
* 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.
|
||||
*/
|
||||
|
||||
export interface Faas {
|
||||
id: string;
|
||||
coldstart?: boolean;
|
||||
execution?: string;
|
||||
trigger?: {
|
||||
type?: string;
|
||||
request_id?: string;
|
||||
};
|
||||
}
|
|
@ -19,6 +19,7 @@ import { TimestampUs } from './fields/timestamp_us';
|
|||
import { Url } from './fields/url';
|
||||
import { User } from './fields/user';
|
||||
import { UserAgent } from './fields/user_agent';
|
||||
import { Faas } from './fields/faas';
|
||||
|
||||
interface Processor {
|
||||
name: 'transaction';
|
||||
|
@ -69,4 +70,5 @@ export interface TransactionRaw extends APMBaseDoc {
|
|||
user?: User;
|
||||
user_agent?: UserAgent;
|
||||
cloud?: Cloud;
|
||||
faas?: Faas;
|
||||
}
|
||||
|
|
|
@ -0,0 +1,205 @@
|
|||
/*
|
||||
* 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 { first, last } from 'lodash';
|
||||
import moment from 'moment';
|
||||
import { FtrProviderContext } from '../../common/ftr_provider_context';
|
||||
import { dataConfig, generateData } from './generate_data';
|
||||
import {
|
||||
APIReturnType,
|
||||
APIClientRequestParamsOf,
|
||||
} from '../../../../plugins/apm/public/services/rest/create_call_apm_api';
|
||||
import { RecursivePartial } from '../../../../plugins/apm/typings/common';
|
||||
import { isFiniteNumber } from '../../../../plugins/apm/common/utils/is_finite_number';
|
||||
|
||||
type ColdStartRate =
|
||||
APIReturnType<'GET /internal/apm/services/{serviceName}/transactions/charts/coldstart_rate'>;
|
||||
|
||||
export default function ApiTest({ getService }: FtrProviderContext) {
|
||||
const registry = getService('registry');
|
||||
const apmApiClient = getService('apmApiClient');
|
||||
const synthtraceEsClient = getService('synthtraceEsClient');
|
||||
|
||||
const { serviceName } = dataConfig;
|
||||
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(
|
||||
overrides?: RecursivePartial<
|
||||
APIClientRequestParamsOf<'GET /internal/apm/services/{serviceName}/transactions/charts/coldstart_rate'>['params']
|
||||
>
|
||||
) {
|
||||
return await apmApiClient.readUser({
|
||||
endpoint: 'GET /internal/apm/services/{serviceName}/transactions/charts/coldstart_rate',
|
||||
params: {
|
||||
path: { serviceName },
|
||||
query: {
|
||||
transactionType: 'request',
|
||||
environment: 'ENVIRONMENT_ALL',
|
||||
start: new Date(start).toISOString(),
|
||||
end: new Date(end).toISOString(),
|
||||
kuery: '',
|
||||
...overrides?.query,
|
||||
},
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
registry.when(
|
||||
'Cold start rate when data is not loaded',
|
||||
{ config: 'basic', archives: [] },
|
||||
() => {
|
||||
it('handles empty state', async () => {
|
||||
const { status, body } = await callApi();
|
||||
|
||||
expect(status).to.be(200);
|
||||
expect(body.currentPeriod.transactionColdstartRate).to.empty();
|
||||
expect(body.currentPeriod.average).to.be(null);
|
||||
|
||||
expect(body.previousPeriod.transactionColdstartRate).to.empty();
|
||||
expect(body.previousPeriod.average).to.be(null);
|
||||
});
|
||||
}
|
||||
);
|
||||
|
||||
registry.when(
|
||||
'Cold start rate when data is generated',
|
||||
{ config: 'basic', archives: ['apm_mappings_only_8.0.0'] },
|
||||
() => {
|
||||
describe('without comparison', () => {
|
||||
let body: ColdStartRate;
|
||||
let status: number;
|
||||
|
||||
before(async () => {
|
||||
await generateData({
|
||||
synthtraceEsClient,
|
||||
start,
|
||||
end,
|
||||
coldStartRate: 10,
|
||||
warmStartRate: 30,
|
||||
});
|
||||
const response = await callApi();
|
||||
body = response.body;
|
||||
status = response.status;
|
||||
});
|
||||
|
||||
after(() => synthtraceEsClient.clean());
|
||||
|
||||
it('returns correct HTTP status', () => {
|
||||
expect(status).to.be(200);
|
||||
});
|
||||
|
||||
it('returns an array of transaction cold start rates', () => {
|
||||
expect(body).to.have.property('currentPeriod');
|
||||
expect(body.currentPeriod.transactionColdstartRate).to.have.length(15);
|
||||
expect(body.currentPeriod.transactionColdstartRate.every(({ y }) => y === 0.25)).to.be(
|
||||
true
|
||||
);
|
||||
});
|
||||
|
||||
it('returns correct average rate', () => {
|
||||
expect(body.currentPeriod.average).to.be(0.25);
|
||||
});
|
||||
|
||||
it("doesn't have data for the previous period", () => {
|
||||
expect(body).to.have.property('previousPeriod');
|
||||
expect(body.previousPeriod.transactionColdstartRate).to.have.length(0);
|
||||
expect(body.previousPeriod.average).to.be(null);
|
||||
});
|
||||
});
|
||||
|
||||
describe('with comparison', () => {
|
||||
let body: ColdStartRate;
|
||||
let status: number;
|
||||
|
||||
before(async () => {
|
||||
const startDate = moment(start).add(6, 'minutes');
|
||||
const endDate = moment(start).add(9, 'minutes');
|
||||
const comparisonStartDate = new Date(start);
|
||||
const comparisonEndDate = moment(start).add(3, 'minutes');
|
||||
|
||||
await generateData({
|
||||
synthtraceEsClient,
|
||||
start: startDate.valueOf(),
|
||||
end: endDate.valueOf(),
|
||||
coldStartRate: 10,
|
||||
warmStartRate: 30,
|
||||
});
|
||||
await generateData({
|
||||
synthtraceEsClient,
|
||||
start: comparisonStartDate.getTime(),
|
||||
end: comparisonEndDate.valueOf(),
|
||||
coldStartRate: 20,
|
||||
warmStartRate: 20,
|
||||
});
|
||||
|
||||
const response = await callApi({
|
||||
query: {
|
||||
start: startDate.toISOString(),
|
||||
end: endDate.subtract(1, 'seconds').toISOString(),
|
||||
comparisonStart: comparisonStartDate.toISOString(),
|
||||
comparisonEnd: comparisonEndDate.subtract(1, 'seconds').toISOString(),
|
||||
},
|
||||
});
|
||||
body = response.body;
|
||||
status = response.status;
|
||||
});
|
||||
|
||||
after(() => synthtraceEsClient.clean());
|
||||
|
||||
it('returns correct HTTP status', () => {
|
||||
expect(status).to.be(200);
|
||||
});
|
||||
|
||||
it('returns some data', () => {
|
||||
expect(body.currentPeriod.average).not.to.be(null);
|
||||
expect(body.currentPeriod.transactionColdstartRate.length).to.be.greaterThan(0);
|
||||
const hasCurrentPeriodData = body.currentPeriod.transactionColdstartRate.some(({ y }) =>
|
||||
isFiniteNumber(y)
|
||||
);
|
||||
expect(hasCurrentPeriodData).to.equal(true);
|
||||
|
||||
expect(body.previousPeriod.average).not.to.be(null);
|
||||
expect(body.previousPeriod.transactionColdstartRate.length).to.be.greaterThan(0);
|
||||
const hasPreviousPeriodData = body.previousPeriod.transactionColdstartRate.some(({ y }) =>
|
||||
isFiniteNumber(y)
|
||||
);
|
||||
expect(hasPreviousPeriodData).to.equal(true);
|
||||
});
|
||||
|
||||
it('has same start time for both periods', () => {
|
||||
expect(first(body.currentPeriod.transactionColdstartRate)?.x).to.equal(
|
||||
first(body.previousPeriod.transactionColdstartRate)?.x
|
||||
);
|
||||
});
|
||||
|
||||
it('has same end time for both periods', () => {
|
||||
expect(last(body.currentPeriod.transactionColdstartRate)?.x).to.equal(
|
||||
last(body.previousPeriod.transactionColdstartRate)?.x
|
||||
);
|
||||
});
|
||||
|
||||
it('returns an array of transaction cold start rates', () => {
|
||||
expect(body.currentPeriod.transactionColdstartRate).to.have.length(3);
|
||||
expect(body.currentPeriod.transactionColdstartRate.every(({ y }) => y === 0.25)).to.be(
|
||||
true
|
||||
);
|
||||
|
||||
expect(body.previousPeriod.transactionColdstartRate).to.have.length(3);
|
||||
expect(body.previousPeriod.transactionColdstartRate.every(({ y }) => y === 0.5)).to.be(
|
||||
true
|
||||
);
|
||||
});
|
||||
|
||||
it('has same average value for both periods', () => {
|
||||
expect(body.currentPeriod.average).to.be(0.25);
|
||||
expect(body.previousPeriod.average).to.be(0.5);
|
||||
});
|
||||
});
|
||||
}
|
||||
);
|
||||
}
|
|
@ -0,0 +1,207 @@
|
|||
/*
|
||||
* 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 { first, last } from 'lodash';
|
||||
import moment from 'moment';
|
||||
import { FtrProviderContext } from '../../../common/ftr_provider_context';
|
||||
import { dataConfig, generateData } from './generate_data';
|
||||
import {
|
||||
APIReturnType,
|
||||
APIClientRequestParamsOf,
|
||||
} from '../../../../../plugins/apm/public/services/rest/create_call_apm_api';
|
||||
import { RecursivePartial } from '../../../../../plugins/apm/typings/common';
|
||||
import { isFiniteNumber } from '../../../../../plugins/apm/common/utils/is_finite_number';
|
||||
|
||||
type ColdStartRate =
|
||||
APIReturnType<'GET /internal/apm/services/{serviceName}/transactions/charts/coldstart_rate_by_transaction_name'>;
|
||||
|
||||
export default function ApiTest({ getService }: FtrProviderContext) {
|
||||
const registry = getService('registry');
|
||||
const apmApiClient = getService('apmApiClient');
|
||||
const synthtraceEsClient = getService('synthtraceEsClient');
|
||||
|
||||
const { serviceName, transactionName } = dataConfig;
|
||||
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(
|
||||
overrides?: RecursivePartial<
|
||||
APIClientRequestParamsOf<'GET /internal/apm/services/{serviceName}/transactions/charts/coldstart_rate'>['params']
|
||||
>
|
||||
) {
|
||||
return await apmApiClient.readUser({
|
||||
endpoint:
|
||||
'GET /internal/apm/services/{serviceName}/transactions/charts/coldstart_rate_by_transaction_name',
|
||||
params: {
|
||||
path: { serviceName },
|
||||
query: {
|
||||
transactionType: 'request',
|
||||
transactionName,
|
||||
environment: 'ENVIRONMENT_ALL',
|
||||
start: new Date(start).toISOString(),
|
||||
end: new Date(end).toISOString(),
|
||||
kuery: '',
|
||||
...overrides?.query,
|
||||
},
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
registry.when(
|
||||
'Cold start rate by transaction name when data is not loaded',
|
||||
{ config: 'basic', archives: [] },
|
||||
() => {
|
||||
it('handles empty state', async () => {
|
||||
const { status, body } = await callApi();
|
||||
|
||||
expect(status).to.be(200);
|
||||
expect(body.currentPeriod.transactionColdstartRate).to.empty();
|
||||
expect(body.currentPeriod.average).to.be(null);
|
||||
|
||||
expect(body.previousPeriod.transactionColdstartRate).to.empty();
|
||||
expect(body.previousPeriod.average).to.be(null);
|
||||
});
|
||||
}
|
||||
);
|
||||
|
||||
registry.when(
|
||||
'Cold start rate by transaction name when data is generated',
|
||||
{ config: 'basic', archives: ['apm_mappings_only_8.0.0'] },
|
||||
() => {
|
||||
describe('without comparison', () => {
|
||||
let body: ColdStartRate;
|
||||
let status: number;
|
||||
|
||||
before(async () => {
|
||||
await generateData({
|
||||
synthtraceEsClient,
|
||||
start,
|
||||
end,
|
||||
coldStartRate: 10,
|
||||
warmStartRate: 30,
|
||||
});
|
||||
const response = await callApi();
|
||||
body = response.body;
|
||||
status = response.status;
|
||||
});
|
||||
|
||||
after(() => synthtraceEsClient.clean());
|
||||
|
||||
it('returns correct HTTP status', () => {
|
||||
expect(status).to.be(200);
|
||||
});
|
||||
|
||||
it('returns an array of transaction cold start rates', () => {
|
||||
expect(body).to.have.property('currentPeriod');
|
||||
expect(body.currentPeriod.transactionColdstartRate).to.have.length(15);
|
||||
expect(body.currentPeriod.transactionColdstartRate.every(({ y }) => y === 0.25)).to.be(
|
||||
true
|
||||
);
|
||||
});
|
||||
|
||||
it('returns correct average rate', () => {
|
||||
expect(body.currentPeriod.average).to.be(0.25);
|
||||
});
|
||||
|
||||
it("doesn't have data for the previous period", () => {
|
||||
expect(body).to.have.property('previousPeriod');
|
||||
expect(body.previousPeriod.transactionColdstartRate).to.have.length(0);
|
||||
expect(body.previousPeriod.average).to.be(null);
|
||||
});
|
||||
});
|
||||
|
||||
describe('with comparison', () => {
|
||||
let body: ColdStartRate;
|
||||
let status: number;
|
||||
|
||||
before(async () => {
|
||||
const startDate = moment(start).add(6, 'minutes');
|
||||
const endDate = moment(start).add(9, 'minutes');
|
||||
const comparisonStartDate = new Date(start);
|
||||
const comparisonEndDate = moment(start).add(3, 'minutes');
|
||||
|
||||
await generateData({
|
||||
synthtraceEsClient,
|
||||
start: startDate.valueOf(),
|
||||
end: endDate.valueOf(),
|
||||
coldStartRate: 10,
|
||||
warmStartRate: 30,
|
||||
});
|
||||
await generateData({
|
||||
synthtraceEsClient,
|
||||
start: comparisonStartDate.getTime(),
|
||||
end: comparisonEndDate.valueOf(),
|
||||
coldStartRate: 20,
|
||||
warmStartRate: 20,
|
||||
});
|
||||
|
||||
const response = await callApi({
|
||||
query: {
|
||||
start: startDate.toISOString(),
|
||||
end: endDate.subtract(1, 'seconds').toISOString(),
|
||||
comparisonStart: comparisonStartDate.toISOString(),
|
||||
comparisonEnd: comparisonEndDate.subtract(1, 'seconds').toISOString(),
|
||||
},
|
||||
});
|
||||
body = response.body;
|
||||
status = response.status;
|
||||
});
|
||||
|
||||
after(() => synthtraceEsClient.clean());
|
||||
|
||||
it('returns correct HTTP status', () => {
|
||||
expect(status).to.be(200);
|
||||
});
|
||||
|
||||
it('returns some data', () => {
|
||||
expect(body.currentPeriod.average).not.to.be(null);
|
||||
expect(body.currentPeriod.transactionColdstartRate.length).to.be.greaterThan(0);
|
||||
const hasCurrentPeriodData = body.currentPeriod.transactionColdstartRate.some(({ y }) =>
|
||||
isFiniteNumber(y)
|
||||
);
|
||||
expect(hasCurrentPeriodData).to.equal(true);
|
||||
|
||||
expect(body.previousPeriod.average).not.to.be(null);
|
||||
expect(body.previousPeriod.transactionColdstartRate.length).to.be.greaterThan(0);
|
||||
const hasPreviousPeriodData = body.previousPeriod.transactionColdstartRate.some(({ y }) =>
|
||||
isFiniteNumber(y)
|
||||
);
|
||||
expect(hasPreviousPeriodData).to.equal(true);
|
||||
});
|
||||
|
||||
it('has same start time for both periods', () => {
|
||||
expect(first(body.currentPeriod.transactionColdstartRate)?.x).to.equal(
|
||||
first(body.previousPeriod.transactionColdstartRate)?.x
|
||||
);
|
||||
});
|
||||
|
||||
it('has same end time for both periods', () => {
|
||||
expect(last(body.currentPeriod.transactionColdstartRate)?.x).to.equal(
|
||||
last(body.previousPeriod.transactionColdstartRate)?.x
|
||||
);
|
||||
});
|
||||
|
||||
it('returns an array of transaction cold start rates', () => {
|
||||
expect(body.currentPeriod.transactionColdstartRate).to.have.length(3);
|
||||
expect(body.currentPeriod.transactionColdstartRate.every(({ y }) => y === 0.25)).to.be(
|
||||
true
|
||||
);
|
||||
|
||||
expect(body.previousPeriod.transactionColdstartRate).to.have.length(3);
|
||||
expect(body.previousPeriod.transactionColdstartRate.every(({ y }) => y === 0.5)).to.be(
|
||||
true
|
||||
);
|
||||
});
|
||||
|
||||
it('has same average value for both periods', () => {
|
||||
expect(body.currentPeriod.average).to.be(0.25);
|
||||
expect(body.previousPeriod.average).to.be(0.5);
|
||||
});
|
||||
});
|
||||
}
|
||||
);
|
||||
}
|
|
@ -0,0 +1,64 @@
|
|||
/*
|
||||
* 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 '@elastic/apm-synthtrace';
|
||||
import type { ApmSynthtraceEsClient } from '@elastic/apm-synthtrace';
|
||||
|
||||
export const dataConfig = {
|
||||
serviceName: 'synth-go',
|
||||
transactionName: 'GET /apple 🍎',
|
||||
duration: 1000,
|
||||
};
|
||||
|
||||
export async function generateData({
|
||||
synthtraceEsClient,
|
||||
start,
|
||||
end,
|
||||
coldStartRate,
|
||||
warmStartRate,
|
||||
}: {
|
||||
synthtraceEsClient: ApmSynthtraceEsClient;
|
||||
start: number;
|
||||
end: number;
|
||||
coldStartRate: number;
|
||||
warmStartRate: number;
|
||||
}) {
|
||||
const { transactionName, duration, serviceName } = dataConfig;
|
||||
const instance = apm.service(serviceName, 'production', 'go').instance('instance-a');
|
||||
|
||||
const traceEvents = [
|
||||
...timerange(start, end)
|
||||
.interval('1m')
|
||||
.rate(coldStartRate)
|
||||
.flatMap((timestamp) => [
|
||||
...instance
|
||||
.transaction(transactionName)
|
||||
.defaults({
|
||||
'faas.coldstart': true,
|
||||
})
|
||||
.timestamp(timestamp)
|
||||
.duration(duration)
|
||||
.success()
|
||||
.serialize(),
|
||||
]),
|
||||
...timerange(start, end)
|
||||
.interval('1m')
|
||||
.rate(warmStartRate)
|
||||
.flatMap((timestamp) => [
|
||||
...instance
|
||||
.transaction(transactionName)
|
||||
.defaults({
|
||||
'faas.coldstart': false,
|
||||
})
|
||||
.timestamp(timestamp)
|
||||
.duration(duration)
|
||||
.success()
|
||||
.serialize(),
|
||||
]),
|
||||
];
|
||||
|
||||
await synthtraceEsClient.index(traceEvents);
|
||||
}
|
|
@ -0,0 +1,70 @@
|
|||
/*
|
||||
* 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 '@elastic/apm-synthtrace';
|
||||
import type { ApmSynthtraceEsClient } from '@elastic/apm-synthtrace';
|
||||
|
||||
export const dataConfig = {
|
||||
serviceName: 'synth-go',
|
||||
coldStartTransaction: {
|
||||
name: 'GET /apple 🍎',
|
||||
duration: 1000,
|
||||
},
|
||||
warmStartTransaction: {
|
||||
name: 'GET /banana 🍌',
|
||||
duration: 2000,
|
||||
},
|
||||
};
|
||||
|
||||
export async function generateData({
|
||||
synthtraceEsClient,
|
||||
start,
|
||||
end,
|
||||
coldStartRate,
|
||||
warmStartRate,
|
||||
}: {
|
||||
synthtraceEsClient: ApmSynthtraceEsClient;
|
||||
start: number;
|
||||
end: number;
|
||||
coldStartRate: number;
|
||||
warmStartRate: number;
|
||||
}) {
|
||||
const { coldStartTransaction, warmStartTransaction, serviceName } = dataConfig;
|
||||
const instance = apm.service(serviceName, 'production', 'go').instance('instance-a');
|
||||
|
||||
const traceEvents = [
|
||||
...timerange(start, end)
|
||||
.interval('1m')
|
||||
.rate(coldStartRate)
|
||||
.flatMap((timestamp) => [
|
||||
...instance
|
||||
.transaction(coldStartTransaction.name)
|
||||
.defaults({
|
||||
'faas.coldstart': true,
|
||||
})
|
||||
.timestamp(timestamp)
|
||||
.duration(coldStartTransaction.duration)
|
||||
.success()
|
||||
.serialize(),
|
||||
]),
|
||||
...timerange(start, end)
|
||||
.interval('1m')
|
||||
.rate(warmStartRate)
|
||||
.flatMap((timestamp) => [
|
||||
...instance
|
||||
.transaction(warmStartTransaction.name)
|
||||
.defaults({
|
||||
'faas.coldstart': false,
|
||||
})
|
||||
.timestamp(timestamp)
|
||||
.duration(warmStartTransaction.duration)
|
||||
.success()
|
||||
.serialize(),
|
||||
]),
|
||||
];
|
||||
|
||||
await synthtraceEsClient.index(traceEvents);
|
||||
}
|
|
@ -1,135 +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 expect from '@kbn/expect';
|
||||
import url from 'url';
|
||||
import { FtrProviderContext } from '../../common/ftr_provider_context';
|
||||
import archives from '../../common/fixtures/es_archiver/archives_metadata';
|
||||
|
||||
export default function ApiTest({ getService }: FtrProviderContext) {
|
||||
const registry = getService('registry');
|
||||
const supertest = getService('legacySupertestAsApmReadUser');
|
||||
|
||||
const archiveName = 'apm_8.0.0';
|
||||
const { start, end } = archives[archiveName];
|
||||
|
||||
registry.when(
|
||||
'Service details when data is not loaded',
|
||||
{ config: 'basic', archives: [] },
|
||||
() => {
|
||||
it('handles the empty state', async () => {
|
||||
const response = await supertest.get(
|
||||
url.format({
|
||||
pathname: `/internal/apm/services/opbeans-java/metadata/details`,
|
||||
query: { start, end },
|
||||
})
|
||||
);
|
||||
|
||||
expect(response.status).to.be(200);
|
||||
expect(response.body).to.eql({});
|
||||
});
|
||||
}
|
||||
);
|
||||
|
||||
registry.when(
|
||||
'Service details when data is loaded',
|
||||
{ config: 'basic', archives: [archiveName] },
|
||||
() => {
|
||||
it('returns java service details', async () => {
|
||||
const response = await supertest.get(
|
||||
url.format({
|
||||
pathname: `/internal/apm/services/opbeans-java/metadata/details`,
|
||||
query: { start, end },
|
||||
})
|
||||
);
|
||||
|
||||
expect(response.status).to.be(200);
|
||||
|
||||
expectSnapshot(response.body).toMatchInline(`
|
||||
Object {
|
||||
"cloud": Object {
|
||||
"availabilityZones": Array [
|
||||
"europe-west1-c",
|
||||
],
|
||||
"machineTypes": Array [
|
||||
"n1-standard-4",
|
||||
],
|
||||
"projectName": "elastic-observability",
|
||||
"provider": "gcp",
|
||||
},
|
||||
"container": Object {
|
||||
"isContainerized": true,
|
||||
"os": "Linux",
|
||||
"totalNumberInstances": 1,
|
||||
"type": "Kubernetes",
|
||||
},
|
||||
"service": Object {
|
||||
"agent": Object {
|
||||
"ephemeral_id": "2745d454-f57f-4473-a09b-fe6bef295860",
|
||||
"name": "java",
|
||||
"version": "1.25.1-SNAPSHOT.UNKNOWN",
|
||||
},
|
||||
"runtime": Object {
|
||||
"name": "Java",
|
||||
"version": "11.0.11",
|
||||
},
|
||||
"versions": Array [
|
||||
"2021-08-03 04:26:27",
|
||||
],
|
||||
},
|
||||
}
|
||||
`);
|
||||
});
|
||||
|
||||
it('returns python service details', async () => {
|
||||
const response = await supertest.get(
|
||||
url.format({
|
||||
pathname: `/internal/apm/services/opbeans-python/metadata/details`,
|
||||
query: { start, end },
|
||||
})
|
||||
);
|
||||
|
||||
expect(response.status).to.be(200);
|
||||
|
||||
expectSnapshot(response.body).toMatchInline(`
|
||||
Object {
|
||||
"cloud": Object {
|
||||
"availabilityZones": Array [
|
||||
"europe-west1-c",
|
||||
],
|
||||
"machineTypes": Array [
|
||||
"n1-standard-4",
|
||||
],
|
||||
"projectName": "elastic-observability",
|
||||
"provider": "gcp",
|
||||
},
|
||||
"container": Object {
|
||||
"isContainerized": true,
|
||||
"os": "linux",
|
||||
"totalNumberInstances": 1,
|
||||
"type": "Kubernetes",
|
||||
},
|
||||
"service": Object {
|
||||
"agent": Object {
|
||||
"name": "python",
|
||||
"version": "6.3.3",
|
||||
},
|
||||
"framework": "django",
|
||||
"runtime": Object {
|
||||
"name": "CPython",
|
||||
"version": "3.9.6",
|
||||
},
|
||||
"versions": Array [
|
||||
"2021-08-03 04:26:25",
|
||||
],
|
||||
},
|
||||
}
|
||||
`);
|
||||
});
|
||||
}
|
||||
);
|
||||
}
|
|
@ -0,0 +1,130 @@
|
|||
/*
|
||||
* 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 '@elastic/apm-synthtrace';
|
||||
import type { ApmSynthtraceEsClient } from '@elastic/apm-synthtrace';
|
||||
|
||||
export const dataConfig = {
|
||||
rate: 10,
|
||||
transaction: {
|
||||
name: 'GET /apple 🍎',
|
||||
duration: 1000,
|
||||
},
|
||||
service: {
|
||||
name: 'lambda-python-dev-hello',
|
||||
version: '$LATEST',
|
||||
runtime: {
|
||||
name: 'AWS_Lambda_python3.8',
|
||||
version: '3.8.11',
|
||||
},
|
||||
framework: 'AWS Lambda',
|
||||
agent: {
|
||||
name: 'python',
|
||||
version: '6.6.0',
|
||||
},
|
||||
},
|
||||
containerOs: 'linux',
|
||||
serverless: {
|
||||
firstFunctionName: 'my-function-1',
|
||||
secondFunctionName: 'my-function-2',
|
||||
faasTriggerType: 'other',
|
||||
},
|
||||
cloud: {
|
||||
provider: 'aws',
|
||||
availabilityZone: 'us-central1-c',
|
||||
region: 'us-east-1',
|
||||
machineType: 'e2-standard-4',
|
||||
projectName: 'elastic-observability',
|
||||
serviceName: 'lambda',
|
||||
},
|
||||
};
|
||||
|
||||
export async function generateData({
|
||||
synthtraceEsClient,
|
||||
start,
|
||||
end,
|
||||
}: {
|
||||
synthtraceEsClient: ApmSynthtraceEsClient;
|
||||
start: number;
|
||||
end: number;
|
||||
}) {
|
||||
const { rate, service, containerOs, serverless, cloud, transaction } = dataConfig;
|
||||
const {
|
||||
provider,
|
||||
availabilityZone,
|
||||
region,
|
||||
machineType,
|
||||
projectName,
|
||||
serviceName: cloudServiceName,
|
||||
} = cloud;
|
||||
const { faasTriggerType, firstFunctionName, secondFunctionName } = serverless;
|
||||
const { version, runtime, framework, agent, name: serviceName } = service;
|
||||
const { name: serviceRunTimeName, version: serviceRunTimeVersion } = runtime;
|
||||
const { name: agentName, version: agentVersion } = agent;
|
||||
|
||||
const instance = apm.service(serviceName, 'production', agentName).instance('instance-a');
|
||||
|
||||
const traceEvents = [
|
||||
...timerange(start, end)
|
||||
.interval('30s')
|
||||
.rate(rate)
|
||||
.flatMap((timestamp) =>
|
||||
instance
|
||||
.transaction(transaction.name)
|
||||
.timestamp(timestamp)
|
||||
.defaults({
|
||||
'cloud.provider': provider,
|
||||
'cloud.project.name': projectName,
|
||||
'cloud.service.name': cloudServiceName,
|
||||
'cloud.availability_zone': availabilityZone,
|
||||
'cloud.machine.type': machineType,
|
||||
'cloud.region': region,
|
||||
'faas.id': `arn:aws:lambda:us-west-2:123456789012:function:${firstFunctionName}`,
|
||||
'faas.trigger.type': faasTriggerType,
|
||||
'host.os.platform': containerOs,
|
||||
'kubernetes.pod.uid': '48f4c5a5-0625-4bea-9d94-77ee94a17e70',
|
||||
'service.version': version,
|
||||
'service.runtime.name': serviceRunTimeName,
|
||||
'service.runtime.version': serviceRunTimeVersion,
|
||||
'service.framework.name': framework,
|
||||
'agent.version': agentVersion,
|
||||
})
|
||||
.duration(transaction.duration)
|
||||
.success()
|
||||
.serialize()
|
||||
),
|
||||
...timerange(start, end)
|
||||
.interval('30s')
|
||||
.rate(rate)
|
||||
.flatMap((timestamp) =>
|
||||
instance
|
||||
.transaction(transaction.name)
|
||||
.timestamp(timestamp)
|
||||
.defaults({
|
||||
'cloud.provider': provider,
|
||||
'cloud.project.name': projectName,
|
||||
'cloud.service.name': cloudServiceName,
|
||||
'cloud.availability_zone': availabilityZone,
|
||||
'cloud.machine.type': machineType,
|
||||
'cloud.region': region,
|
||||
'faas.id': `arn:aws:lambda:us-west-2:123456789012:function:${secondFunctionName}`,
|
||||
'faas.trigger.type': faasTriggerType,
|
||||
'host.os.platform': containerOs,
|
||||
'kubernetes.pod.uid': '48f4c5a5-0625-4bea-9d94-77ee94a17e70',
|
||||
'service.version': version,
|
||||
'service.runtime.name': serviceRunTimeName,
|
||||
'service.runtime.version': serviceRunTimeVersion,
|
||||
'service.framework.name': framework,
|
||||
'agent.version': agentVersion,
|
||||
})
|
||||
.duration(transaction.duration)
|
||||
.success()
|
||||
.serialize()
|
||||
),
|
||||
];
|
||||
|
||||
await synthtraceEsClient.index(traceEvents);
|
||||
}
|
|
@ -0,0 +1,128 @@
|
|||
/*
|
||||
* 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 { first } from 'lodash';
|
||||
import { FtrProviderContext } from '../../../common/ftr_provider_context';
|
||||
import { dataConfig, generateData } from './generate_data';
|
||||
import { APIReturnType } from '../../../../../plugins/apm/public/services/rest/create_call_apm_api';
|
||||
|
||||
type ServiceDetails = APIReturnType<'GET /internal/apm/services/{serviceName}/metadata/details'>;
|
||||
|
||||
export default function ApiTest({ getService }: FtrProviderContext) {
|
||||
const registry = getService('registry');
|
||||
const apmApiClient = getService('apmApiClient');
|
||||
const synthtraceEsClient = getService('synthtraceEsClient');
|
||||
|
||||
const {
|
||||
service: { name: serviceName },
|
||||
} = dataConfig;
|
||||
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() {
|
||||
return await apmApiClient.readUser({
|
||||
endpoint: 'GET /internal/apm/services/{serviceName}/metadata/details',
|
||||
params: {
|
||||
path: { serviceName },
|
||||
query: {
|
||||
start: new Date(start).toISOString(),
|
||||
end: new Date(end).toISOString(),
|
||||
},
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
registry.when(
|
||||
'Service details when data is not loaded',
|
||||
{ config: 'basic', archives: [] },
|
||||
() => {
|
||||
it('handles empty state', async () => {
|
||||
const { status, body } = await callApi();
|
||||
|
||||
expect(status).to.be(200);
|
||||
expect(body).to.empty();
|
||||
});
|
||||
}
|
||||
);
|
||||
|
||||
registry.when(
|
||||
'Service details when data is generated',
|
||||
{ config: 'basic', archives: ['apm_mappings_only_8.0.0'] },
|
||||
() => {
|
||||
let body: ServiceDetails;
|
||||
let status: number;
|
||||
|
||||
before(async () => {
|
||||
await generateData({ synthtraceEsClient, start, end });
|
||||
const response = await callApi();
|
||||
body = response.body;
|
||||
status = response.status;
|
||||
});
|
||||
|
||||
after(() => synthtraceEsClient.clean());
|
||||
|
||||
it('returns correct HTTP status', () => {
|
||||
expect(status).to.be(200);
|
||||
});
|
||||
|
||||
it('returns correct cloud details', () => {
|
||||
const { cloud } = dataConfig;
|
||||
const {
|
||||
provider,
|
||||
availabilityZone,
|
||||
region,
|
||||
machineType,
|
||||
projectName,
|
||||
serviceName: cloudServiceName,
|
||||
} = cloud;
|
||||
|
||||
expect(first(body?.cloud?.availabilityZones)).to.be(availabilityZone);
|
||||
expect(first(body?.cloud?.machineTypes)).to.be(machineType);
|
||||
expect(body?.cloud?.provider).to.be(provider);
|
||||
expect(body?.cloud?.projectName).to.be(projectName);
|
||||
expect(body?.cloud?.serviceName).to.be(cloudServiceName);
|
||||
expect(first(body?.cloud?.regions)).to.be(region);
|
||||
});
|
||||
|
||||
it('returns correct container details', () => {
|
||||
const { containerOs } = dataConfig;
|
||||
|
||||
expect(body?.container?.isContainerized).to.be(true);
|
||||
expect(body?.container?.os).to.be(containerOs);
|
||||
expect(body?.container?.totalNumberInstances).to.be(1);
|
||||
expect(body?.container?.type).to.be('Kubernetes');
|
||||
});
|
||||
|
||||
it('returns correct serverless details', () => {
|
||||
const { cloud, serverless } = dataConfig;
|
||||
const { serviceName: cloudServiceName } = cloud;
|
||||
const { faasTriggerType, firstFunctionName, secondFunctionName } = serverless;
|
||||
|
||||
expect(body?.serverless?.type).to.be(cloudServiceName);
|
||||
expect(body?.serverless?.functionNames).to.have.length(2);
|
||||
expect(body?.serverless?.functionNames).to.contain(firstFunctionName);
|
||||
expect(body?.serverless?.functionNames).to.contain(secondFunctionName);
|
||||
expect(first(body?.serverless?.faasTriggerTypes)).to.be(faasTriggerType);
|
||||
});
|
||||
|
||||
it('returns correct service details', () => {
|
||||
const { service } = dataConfig;
|
||||
const { version, runtime, framework, agent } = service;
|
||||
const { name: runTimeName, version: runTimeVersion } = runtime;
|
||||
const { name: agentName, version: agentVersion } = agent;
|
||||
|
||||
expect(body?.service?.framework).to.be(framework);
|
||||
expect(body?.service?.agent.name).to.be(agentName);
|
||||
expect(body?.service?.agent.version).to.be(agentVersion);
|
||||
expect(body?.service?.runtime?.name).to.be(runTimeName);
|
||||
expect(body?.service?.runtime?.version).to.be(runTimeVersion);
|
||||
expect(first(body?.service?.versions)).to.be(version);
|
||||
});
|
||||
}
|
||||
);
|
||||
}
|
|
@ -1,77 +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 expect from '@kbn/expect';
|
||||
import url from 'url';
|
||||
import { FtrProviderContext } from '../../common/ftr_provider_context';
|
||||
import archives from '../../common/fixtures/es_archiver/archives_metadata';
|
||||
|
||||
export default function ApiTest({ getService }: FtrProviderContext) {
|
||||
const registry = getService('registry');
|
||||
const supertest = getService('legacySupertestAsApmReadUser');
|
||||
|
||||
const archiveName = 'apm_8.0.0';
|
||||
const { start, end } = archives[archiveName];
|
||||
|
||||
registry.when('Service icons when data is not loaded', { config: 'basic', archives: [] }, () => {
|
||||
it('handles the empty state', async () => {
|
||||
const response = await supertest.get(
|
||||
url.format({
|
||||
pathname: `/internal/apm/services/opbeans-java/metadata/icons`,
|
||||
query: { start, end },
|
||||
})
|
||||
);
|
||||
|
||||
expect(response.status).to.be(200);
|
||||
expect(response.body).to.eql({});
|
||||
});
|
||||
});
|
||||
|
||||
registry.when(
|
||||
'Service icons when data is not loaded',
|
||||
{ config: 'basic', archives: [archiveName] },
|
||||
() => {
|
||||
it('returns java service icons', async () => {
|
||||
const response = await supertest.get(
|
||||
url.format({
|
||||
pathname: `/internal/apm/services/opbeans-java/metadata/icons`,
|
||||
query: { start, end },
|
||||
})
|
||||
);
|
||||
|
||||
expect(response.status).to.be(200);
|
||||
|
||||
expectSnapshot(response.body).toMatchInline(`
|
||||
Object {
|
||||
"agentName": "java",
|
||||
"cloudProvider": "gcp",
|
||||
"containerType": "Kubernetes",
|
||||
}
|
||||
`);
|
||||
});
|
||||
|
||||
it('returns python service icons', async () => {
|
||||
const response = await supertest.get(
|
||||
url.format({
|
||||
pathname: `/internal/apm/services/opbeans-python/metadata/icons`,
|
||||
query: { start, end },
|
||||
})
|
||||
);
|
||||
|
||||
expect(response.status).to.be(200);
|
||||
|
||||
expectSnapshot(response.body).toMatchInline(`
|
||||
Object {
|
||||
"agentName": "python",
|
||||
"cloudProvider": "gcp",
|
||||
"containerType": "Kubernetes",
|
||||
}
|
||||
`);
|
||||
});
|
||||
}
|
||||
);
|
||||
}
|
|
@ -0,0 +1,56 @@
|
|||
/*
|
||||
* 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 '@elastic/apm-synthtrace';
|
||||
import type { ApmSynthtraceEsClient } from '@elastic/apm-synthtrace';
|
||||
|
||||
export const dataConfig = {
|
||||
serviceName: 'synth-node',
|
||||
rate: 10,
|
||||
transaction: {
|
||||
name: 'GET /apple 🍎',
|
||||
duration: 1000,
|
||||
},
|
||||
agentName: 'node',
|
||||
cloud: {
|
||||
provider: 'aws',
|
||||
serviceName: 'lambda',
|
||||
},
|
||||
};
|
||||
|
||||
export async function generateData({
|
||||
synthtraceEsClient,
|
||||
start,
|
||||
end,
|
||||
}: {
|
||||
synthtraceEsClient: ApmSynthtraceEsClient;
|
||||
start: number;
|
||||
end: number;
|
||||
}) {
|
||||
const { serviceName, agentName, rate, cloud, transaction } = dataConfig;
|
||||
const { provider, serviceName: cloudServiceName } = cloud;
|
||||
|
||||
const instance = apm.service(serviceName, 'production', agentName).instance('instance-a');
|
||||
|
||||
const traceEvents = timerange(start, end)
|
||||
.interval('30s')
|
||||
.rate(rate)
|
||||
.flatMap((timestamp) =>
|
||||
instance
|
||||
.transaction(transaction.name)
|
||||
.defaults({
|
||||
'kubernetes.pod.uid': 'test',
|
||||
'cloud.provider': provider,
|
||||
'cloud.service.name': cloudServiceName,
|
||||
})
|
||||
.timestamp(timestamp)
|
||||
.duration(transaction.duration)
|
||||
.success()
|
||||
.serialize()
|
||||
);
|
||||
|
||||
await synthtraceEsClient.index(traceEvents);
|
||||
}
|
|
@ -0,0 +1,77 @@
|
|||
/*
|
||||
* 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 { FtrProviderContext } from '../../../common/ftr_provider_context';
|
||||
import { dataConfig, generateData } from './generate_data';
|
||||
import { APIReturnType } from '../../../../../plugins/apm/public/services/rest/create_call_apm_api';
|
||||
|
||||
type ServiceIconMetadata = APIReturnType<'GET /internal/apm/services/{serviceName}/metadata/icons'>;
|
||||
|
||||
export default function ApiTest({ getService }: FtrProviderContext) {
|
||||
const registry = getService('registry');
|
||||
const apmApiClient = getService('apmApiClient');
|
||||
const synthtraceEsClient = getService('synthtraceEsClient');
|
||||
|
||||
const { serviceName } = dataConfig;
|
||||
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() {
|
||||
return await apmApiClient.readUser({
|
||||
endpoint: 'GET /internal/apm/services/{serviceName}/metadata/icons',
|
||||
params: {
|
||||
path: { serviceName },
|
||||
query: {
|
||||
start: new Date(start).toISOString(),
|
||||
end: new Date(end).toISOString(),
|
||||
},
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
registry.when('Service icons when data is not loaded', { config: 'basic', archives: [] }, () => {
|
||||
it('handles empty state', async () => {
|
||||
const { status, body } = await callApi();
|
||||
|
||||
expect(status).to.be(200);
|
||||
expect(body).to.empty();
|
||||
});
|
||||
});
|
||||
|
||||
registry.when(
|
||||
'Service icons when data is generated',
|
||||
{ config: 'basic', archives: ['apm_mappings_only_8.0.0'] },
|
||||
() => {
|
||||
let body: ServiceIconMetadata;
|
||||
let status: number;
|
||||
|
||||
before(async () => {
|
||||
await generateData({ synthtraceEsClient, start, end });
|
||||
const response = await callApi();
|
||||
body = response.body;
|
||||
status = response.status;
|
||||
});
|
||||
|
||||
after(() => synthtraceEsClient.clean());
|
||||
|
||||
it('returns correct HTTP status', () => {
|
||||
expect(status).to.be(200);
|
||||
});
|
||||
|
||||
it('returns correct metadata', () => {
|
||||
const { agentName, cloud } = dataConfig;
|
||||
const { provider, serviceName: cloudServiceName } = cloud;
|
||||
|
||||
expect(body.agentName).to.be(agentName);
|
||||
expect(body.cloudProvider).to.be(provider);
|
||||
expect(body.containerType).to.be('Kubernetes');
|
||||
expect(body.serverlessType).to.be(cloudServiceName);
|
||||
});
|
||||
}
|
||||
);
|
||||
}
|
Loading…
Add table
Add a link
Reference in a new issue