mirror of
https://github.com/elastic/kibana.git
synced 2025-04-23 09:19:04 -04:00
[APM] Aggregated critical path (#144092)
This commit is contained in:
parent
14ce15e6e8
commit
65f0c385d6
26 changed files with 2058 additions and 301 deletions
|
@ -0,0 +1,92 @@
|
|||
/*
|
||||
* 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 { sumBy } from 'lodash';
|
||||
import type { CriticalPathResponse } from '../../server/routes/traces/get_aggregated_critical_path';
|
||||
|
||||
export interface CriticalPathTreeNode {
|
||||
nodeId: string;
|
||||
children: CriticalPathTreeNode[];
|
||||
countInclusive: number;
|
||||
countExclusive: number;
|
||||
}
|
||||
|
||||
export function getAggregatedCriticalPathRootNodes(params: {
|
||||
criticalPath: CriticalPathResponse;
|
||||
}): {
|
||||
rootNodes: CriticalPathTreeNode[];
|
||||
maxDepth: number;
|
||||
numNodes: number;
|
||||
} {
|
||||
let maxDepth = 20; // min max depth
|
||||
|
||||
const { criticalPath } = params;
|
||||
|
||||
let numNodes = 0;
|
||||
|
||||
function mergeNodesWithSameOperationId(
|
||||
nodes: CriticalPathTreeNode[]
|
||||
): CriticalPathTreeNode[] {
|
||||
const nodesByOperationId: Record<string, CriticalPathTreeNode> = {};
|
||||
const mergedNodes = nodes.reduce<CriticalPathTreeNode[]>(
|
||||
(prev, node, index, array) => {
|
||||
const nodeId = node.nodeId;
|
||||
const operationId = criticalPath.operationIdByNodeId[nodeId];
|
||||
if (nodesByOperationId[operationId]) {
|
||||
const prevNode = nodesByOperationId[operationId];
|
||||
prevNode.children.push(...node.children);
|
||||
prevNode.countExclusive += node.countExclusive;
|
||||
prevNode.countInclusive += node.countInclusive;
|
||||
return prev;
|
||||
}
|
||||
|
||||
nodesByOperationId[operationId] = node;
|
||||
|
||||
prev.push(node);
|
||||
return prev;
|
||||
},
|
||||
[]
|
||||
);
|
||||
|
||||
numNodes += mergedNodes.length;
|
||||
|
||||
mergedNodes.forEach((node) => {
|
||||
node.children = mergeNodesWithSameOperationId(node.children);
|
||||
});
|
||||
|
||||
return mergedNodes;
|
||||
}
|
||||
|
||||
function getNode(nodeId: string, depth: number): CriticalPathTreeNode {
|
||||
maxDepth = Math.max(maxDepth, depth);
|
||||
|
||||
const children = criticalPath.nodes[nodeId].map((childNodeId) =>
|
||||
getNode(childNodeId, depth + 1)
|
||||
);
|
||||
|
||||
const nodeCountExclusive = criticalPath.timeByNodeId[nodeId] || 0;
|
||||
const nodeCountInclusive =
|
||||
sumBy(children, (child) => child.countInclusive) + nodeCountExclusive;
|
||||
|
||||
return {
|
||||
nodeId,
|
||||
children,
|
||||
countInclusive: nodeCountInclusive,
|
||||
countExclusive: nodeCountExclusive,
|
||||
};
|
||||
}
|
||||
|
||||
const rootNodes = mergeNodesWithSameOperationId(
|
||||
criticalPath.rootNodes.map((nodeId) => getNode(nodeId, 1))
|
||||
);
|
||||
|
||||
return {
|
||||
rootNodes,
|
||||
maxDepth,
|
||||
numNodes,
|
||||
};
|
||||
}
|
|
@ -37,7 +37,10 @@ export function getCriticalPath(waterfall: IWaterfall): CriticalPath {
|
|||
const orderedChildren = directChildren.concat().sort((a, b) => {
|
||||
const endTimeA = a.offset + a.skew + a.duration;
|
||||
const endTimeB = b.offset + b.skew + b.duration;
|
||||
return endTimeB - endTimeA;
|
||||
if (endTimeA === endTimeB) {
|
||||
return 0;
|
||||
}
|
||||
return endTimeB > endTimeA ? 1 : -1;
|
||||
});
|
||||
|
||||
// For each point in time, determine what child is on the critical path.
|
||||
|
|
9
x-pack/plugins/apm/common/index.ts
Normal file
9
x-pack/plugins/apm/common/index.ts
Normal file
|
@ -0,0 +1,9 @@
|
|||
/*
|
||||
* 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.
|
||||
*/
|
||||
|
||||
// for API tests
|
||||
export { getAggregatedCriticalPathRootNodes } from './critical_path/get_aggregated_critical_path_root_nodes';
|
|
@ -96,7 +96,7 @@ export function DetailView({ errorGroup, urlParams, kuery }: Props) {
|
|||
const method = error.http?.request?.method;
|
||||
const status = error.http?.response?.status_code;
|
||||
|
||||
const traceExplorerLink = router.link('/traces/explorer', {
|
||||
const traceExplorerLink = router.link('/traces/explorer/waterfall', {
|
||||
query: {
|
||||
...query,
|
||||
showCriticalPath: false,
|
||||
|
|
|
@ -49,7 +49,7 @@ export function EdgeContents({ elementData }: ContentsProps) {
|
|||
` [ span where service.name == "${sourceService}" and span.destination.service.resource == "${edgeData.targetData[SPAN_DESTINATION_SERVICE_RESOURCE]}" ]`;
|
||||
}
|
||||
|
||||
const url = apmRouter.link('/traces/explorer', {
|
||||
const url = apmRouter.link('/traces/explorer/waterfall', {
|
||||
query: {
|
||||
...query,
|
||||
type: TraceSearchType.eql,
|
||||
|
|
|
@ -44,18 +44,20 @@ export function TopTracesOverview() {
|
|||
);
|
||||
|
||||
return (
|
||||
<>
|
||||
<SearchBar />
|
||||
<EuiFlexGroup direction="column">
|
||||
<EuiFlexItem grow={false}>
|
||||
<SearchBar />
|
||||
</EuiFlexItem>
|
||||
|
||||
{fallbackToTransactions && (
|
||||
<EuiFlexGroup>
|
||||
<EuiFlexItem>
|
||||
<AggregatedTransactionsBadge />
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
<EuiFlexItem grow={false}>
|
||||
<AggregatedTransactionsBadge />
|
||||
</EuiFlexItem>
|
||||
)}
|
||||
|
||||
<TraceList response={response} />
|
||||
</>
|
||||
<EuiFlexItem grow>
|
||||
<TraceList response={response} />
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -4,7 +4,8 @@
|
|||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
import { EuiFlexGroup, EuiFlexItem } from '@elastic/eui';
|
||||
import { EuiFlexGroup, EuiFlexItem, EuiTab, EuiTabs } from '@elastic/eui';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import React, { useEffect, useMemo, useState } from 'react';
|
||||
import { useHistory } from 'react-router-dom';
|
||||
import {
|
||||
|
@ -12,39 +13,38 @@ import {
|
|||
TraceSearchType,
|
||||
} from '../../../../common/trace_explorer';
|
||||
import { useApmParams } from '../../../hooks/use_apm_params';
|
||||
import { useFetcher } from '../../../hooks/use_fetcher';
|
||||
import { useApmRouter } from '../../../hooks/use_apm_router';
|
||||
import { useApmRoutePath } from '../../../hooks/use_apm_route_path';
|
||||
import { useTimeRange } from '../../../hooks/use_time_range';
|
||||
import { TraceExplorerSamplesFetcherContextProvider } from '../../../hooks/use_trace_explorer_samples';
|
||||
import { APIClientRequestParamsOf } from '../../../services/rest/create_call_apm_api';
|
||||
import { ApmDatePicker } from '../../shared/date_picker/apm_date_picker';
|
||||
import { fromQuery, toQuery, push } from '../../shared/links/url_helpers';
|
||||
import { useWaterfallFetcher } from '../transaction_details/use_waterfall_fetcher';
|
||||
import { WaterfallWithSummary } from '../transaction_details/waterfall_with_summary';
|
||||
import { push } from '../../shared/links/url_helpers';
|
||||
import { TechnicalPreviewBadge } from '../../shared/technical_preview_badge';
|
||||
import { TransactionTab } from '../transaction_details/waterfall_with_summary/transaction_tabs';
|
||||
import { TraceSearchBox } from './trace_search_box';
|
||||
|
||||
export function TraceExplorer() {
|
||||
const [query, setQuery] = useState<TraceSearchQuery>({
|
||||
export function TraceExplorer({ children }: { children: React.ReactElement }) {
|
||||
const [searchQuery, setSearchQuery] = useState<TraceSearchQuery>({
|
||||
query: '',
|
||||
type: TraceSearchType.kql,
|
||||
});
|
||||
|
||||
const {
|
||||
query,
|
||||
query: {
|
||||
rangeFrom,
|
||||
rangeTo,
|
||||
environment,
|
||||
query: queryFromUrlParams,
|
||||
type: typeFromUrlParams,
|
||||
traceId,
|
||||
transactionId,
|
||||
waterfallItemId,
|
||||
detailTab,
|
||||
showCriticalPath,
|
||||
},
|
||||
} = useApmParams('/traces/explorer');
|
||||
|
||||
const history = useHistory();
|
||||
|
||||
useEffect(() => {
|
||||
setQuery({
|
||||
setSearchQuery({
|
||||
query: queryFromUrlParams,
|
||||
type: typeFromUrlParams,
|
||||
});
|
||||
|
@ -55,120 +55,95 @@ export function TraceExplorer() {
|
|||
rangeTo,
|
||||
});
|
||||
|
||||
const { data, status, error } = useFetcher(
|
||||
(callApmApi) => {
|
||||
return callApmApi('GET /internal/apm/traces/find', {
|
||||
params: {
|
||||
query: {
|
||||
start,
|
||||
end,
|
||||
environment,
|
||||
query: queryFromUrlParams,
|
||||
type: typeFromUrlParams,
|
||||
},
|
||||
},
|
||||
});
|
||||
},
|
||||
[start, end, environment, queryFromUrlParams, typeFromUrlParams]
|
||||
);
|
||||
const params = useMemo<
|
||||
APIClientRequestParamsOf<'GET /internal/apm/traces/find'>['params']
|
||||
>(() => {
|
||||
return {
|
||||
query: {
|
||||
start,
|
||||
end,
|
||||
environment,
|
||||
query: queryFromUrlParams,
|
||||
type: typeFromUrlParams,
|
||||
},
|
||||
};
|
||||
}, [start, end, environment, queryFromUrlParams, typeFromUrlParams]);
|
||||
|
||||
useEffect(() => {
|
||||
const nextSample = data?.traceSamples[0];
|
||||
const nextWaterfallItemId = '';
|
||||
history.replace({
|
||||
...history.location,
|
||||
search: fromQuery({
|
||||
...toQuery(history.location.search),
|
||||
traceId: nextSample?.traceId ?? '',
|
||||
transactionId: nextSample?.transactionId,
|
||||
waterfallItemId: nextWaterfallItemId,
|
||||
}),
|
||||
});
|
||||
}, [data, history]);
|
||||
const router = useApmRouter();
|
||||
|
||||
const waterfallFetchResult = useWaterfallFetcher({
|
||||
traceId,
|
||||
transactionId,
|
||||
start,
|
||||
end,
|
||||
});
|
||||
|
||||
const traceSamplesFetchResult = useMemo(
|
||||
() => ({
|
||||
data,
|
||||
status,
|
||||
error,
|
||||
}),
|
||||
[data, status, error]
|
||||
);
|
||||
const routePath = useApmRoutePath();
|
||||
|
||||
return (
|
||||
<EuiFlexGroup direction="column" gutterSize="s">
|
||||
<EuiFlexItem>
|
||||
<EuiFlexGroup direction="row">
|
||||
<EuiFlexItem grow>
|
||||
<TraceSearchBox
|
||||
query={query}
|
||||
error={false}
|
||||
loading={false}
|
||||
onQueryCommit={() => {
|
||||
history.push({
|
||||
...history.location,
|
||||
search: fromQuery({
|
||||
...toQuery(history.location.search),
|
||||
query: query.query,
|
||||
type: query.type,
|
||||
}),
|
||||
});
|
||||
}}
|
||||
onQueryChange={(nextQuery) => {
|
||||
setQuery(nextQuery);
|
||||
}}
|
||||
/>
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem grow={false}>
|
||||
<ApmDatePicker />
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem>
|
||||
<WaterfallWithSummary
|
||||
waterfallFetchResult={waterfallFetchResult}
|
||||
traceSamples={traceSamplesFetchResult.data?.traceSamples}
|
||||
traceSamplesFetchStatus={traceSamplesFetchResult.status}
|
||||
environment={environment}
|
||||
onSampleClick={(sample) => {
|
||||
push(history, {
|
||||
query: {
|
||||
traceId: sample.traceId,
|
||||
transactionId: sample.transactionId,
|
||||
waterfallItemId: '',
|
||||
},
|
||||
});
|
||||
}}
|
||||
onTabClick={(nextDetailTab) => {
|
||||
push(history, {
|
||||
query: {
|
||||
detailTab: nextDetailTab,
|
||||
},
|
||||
});
|
||||
}}
|
||||
detailTab={detailTab}
|
||||
waterfallItemId={waterfallItemId}
|
||||
serviceName={
|
||||
waterfallFetchResult.waterfall.entryWaterfallTransaction?.doc
|
||||
.service.name
|
||||
}
|
||||
showCriticalPath={showCriticalPath}
|
||||
onShowCriticalPathChange={(nextShowCriticalPath) => {
|
||||
push(history, {
|
||||
query: {
|
||||
showCriticalPath: nextShowCriticalPath ? 'true' : 'false',
|
||||
},
|
||||
});
|
||||
}}
|
||||
/>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
<TraceExplorerSamplesFetcherContextProvider params={params}>
|
||||
<EuiFlexGroup direction="column" gutterSize="s">
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiFlexGroup direction="row">
|
||||
<EuiFlexItem grow>
|
||||
<TraceSearchBox
|
||||
query={searchQuery}
|
||||
error={false}
|
||||
loading={false}
|
||||
onQueryCommit={() => {
|
||||
push(history, {
|
||||
query: {
|
||||
query: searchQuery.query,
|
||||
type: searchQuery.type,
|
||||
},
|
||||
});
|
||||
}}
|
||||
onQueryChange={(nextQuery) => {
|
||||
setSearchQuery(nextQuery);
|
||||
}}
|
||||
/>
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem grow={false}>
|
||||
<ApmDatePicker />
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiTabs>
|
||||
<EuiTab
|
||||
href={router.link('/traces/explorer/waterfall', {
|
||||
query: {
|
||||
...query,
|
||||
traceId: '',
|
||||
transactionId: '',
|
||||
waterfallItemId: '',
|
||||
detailTab: TransactionTab.timeline,
|
||||
},
|
||||
})}
|
||||
isSelected={routePath === '/traces/explorer/waterfall'}
|
||||
>
|
||||
{i18n.translate('xpack.apm.traceExplorer.waterfallTab', {
|
||||
defaultMessage: 'Waterfall',
|
||||
})}
|
||||
</EuiTab>
|
||||
<EuiTab
|
||||
href={router.link('/traces/explorer/critical_path', {
|
||||
query,
|
||||
})}
|
||||
isSelected={routePath === '/traces/explorer/critical_path'}
|
||||
>
|
||||
<EuiFlexGroup direction="row" gutterSize="s">
|
||||
<EuiFlexItem grow={false}>
|
||||
{i18n.translate('xpack.apm.traceExplorer.criticalPathTab', {
|
||||
defaultMessage: 'Aggregated critical path',
|
||||
})}
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem grow={false}>
|
||||
<TechnicalPreviewBadge
|
||||
icon="beaker"
|
||||
size="s"
|
||||
style={{ verticalAlign: 'middle' }}
|
||||
/>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
</EuiTab>
|
||||
</EuiTabs>
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem>{children}</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
</TraceExplorerSamplesFetcherContextProvider>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -0,0 +1,37 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
import React, { useMemo } from 'react';
|
||||
import { useApmParams } from '../../../hooks/use_apm_params';
|
||||
import { useTimeRange } from '../../../hooks/use_time_range';
|
||||
import { useTraceExplorerSamples } from '../../../hooks/use_trace_explorer_samples';
|
||||
import { CriticalPathFlamegraph } from '../../shared/critical_path_flamegraph';
|
||||
|
||||
export function TraceExplorerAggregatedCriticalPath() {
|
||||
const {
|
||||
query: { rangeFrom, rangeTo },
|
||||
} = useApmParams('/traces/explorer/critical_path');
|
||||
|
||||
const { start, end } = useTimeRange({ rangeFrom, rangeTo });
|
||||
|
||||
const {
|
||||
data: { traceSamples },
|
||||
status: samplesFetchStatus,
|
||||
} = useTraceExplorerSamples();
|
||||
|
||||
const traceIds = useMemo(() => {
|
||||
return traceSamples.map((sample) => sample.traceId);
|
||||
}, [traceSamples]);
|
||||
|
||||
return (
|
||||
<CriticalPathFlamegraph
|
||||
start={start}
|
||||
end={end}
|
||||
traceIds={traceIds}
|
||||
traceIdsFetchStatus={samplesFetchStatus}
|
||||
/>
|
||||
);
|
||||
}
|
|
@ -0,0 +1,93 @@
|
|||
/*
|
||||
* 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, { useEffect } from 'react';
|
||||
import { useHistory } from 'react-router-dom';
|
||||
import { useApmParams } from '../../../hooks/use_apm_params';
|
||||
import { useTimeRange } from '../../../hooks/use_time_range';
|
||||
import { useTraceExplorerSamples } from '../../../hooks/use_trace_explorer_samples';
|
||||
import { push, replace } from '../../shared/links/url_helpers';
|
||||
import { useWaterfallFetcher } from '../transaction_details/use_waterfall_fetcher';
|
||||
import { WaterfallWithSummary } from '../transaction_details/waterfall_with_summary';
|
||||
|
||||
export function TraceExplorerWaterfall() {
|
||||
const history = useHistory();
|
||||
|
||||
const traceSamplesFetchResult = useTraceExplorerSamples();
|
||||
|
||||
const {
|
||||
query: {
|
||||
traceId,
|
||||
transactionId,
|
||||
waterfallItemId,
|
||||
rangeFrom,
|
||||
rangeTo,
|
||||
environment,
|
||||
showCriticalPath,
|
||||
detailTab,
|
||||
},
|
||||
} = useApmParams('/traces/explorer/waterfall');
|
||||
|
||||
const { start, end } = useTimeRange({ rangeFrom, rangeTo });
|
||||
|
||||
useEffect(() => {
|
||||
const nextSample = traceSamplesFetchResult.data?.traceSamples[0];
|
||||
const nextWaterfallItemId = '';
|
||||
replace(history, {
|
||||
query: {
|
||||
traceId: nextSample?.traceId ?? '',
|
||||
transactionId: nextSample?.transactionId ?? '',
|
||||
waterfallItemId: nextWaterfallItemId,
|
||||
},
|
||||
});
|
||||
}, [traceSamplesFetchResult.data, history]);
|
||||
|
||||
const waterfallFetchResult = useWaterfallFetcher({
|
||||
traceId,
|
||||
transactionId,
|
||||
start,
|
||||
end,
|
||||
});
|
||||
|
||||
return (
|
||||
<WaterfallWithSummary
|
||||
waterfallFetchResult={waterfallFetchResult}
|
||||
traceSamples={traceSamplesFetchResult.data.traceSamples}
|
||||
traceSamplesFetchStatus={traceSamplesFetchResult.status}
|
||||
environment={environment}
|
||||
onSampleClick={(sample) => {
|
||||
push(history, {
|
||||
query: {
|
||||
traceId: sample.traceId,
|
||||
transactionId: sample.transactionId,
|
||||
waterfallItemId: '',
|
||||
},
|
||||
});
|
||||
}}
|
||||
onTabClick={(nextDetailTab) => {
|
||||
push(history, {
|
||||
query: {
|
||||
detailTab: nextDetailTab,
|
||||
},
|
||||
});
|
||||
}}
|
||||
detailTab={detailTab}
|
||||
waterfallItemId={waterfallItemId}
|
||||
serviceName={
|
||||
waterfallFetchResult.waterfall.entryWaterfallTransaction?.doc.service
|
||||
.name
|
||||
}
|
||||
showCriticalPath={showCriticalPath}
|
||||
onShowCriticalPathChange={(nextShowCriticalPath) => {
|
||||
push(history, {
|
||||
query: {
|
||||
showCriticalPath: nextShowCriticalPath ? 'true' : 'false',
|
||||
},
|
||||
});
|
||||
}}
|
||||
/>
|
||||
);
|
||||
}
|
|
@ -4,16 +4,22 @@
|
|||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
import React from 'react';
|
||||
import { EuiFlexGroup, EuiFlexItem, EuiTab, EuiTabs } from '@elastic/eui';
|
||||
import { EuiFlexGroup, EuiFlexItem } from '@elastic/eui';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { useApmRouter } from '../../../hooks/use_apm_router';
|
||||
import { useApmParams } from '../../../hooks/use_apm_params';
|
||||
import { useApmRoutePath } from '../../../hooks/use_apm_route_path';
|
||||
import React from 'react';
|
||||
import { TraceSearchType } from '../../../../common/trace_explorer';
|
||||
import { TransactionTab } from '../transaction_details/waterfall_with_summary/transaction_tabs';
|
||||
import { useApmParams } from '../../../hooks/use_apm_params';
|
||||
import { useApmRouter } from '../../../hooks/use_apm_router';
|
||||
import { useApmRoutePath } from '../../../hooks/use_apm_route_path';
|
||||
import { useTraceExplorerEnabledSetting } from '../../../hooks/use_trace_explorer_enabled_setting';
|
||||
import { ApmMainTemplate } from '../../routing/templates/apm_main_template';
|
||||
import { TechnicalPreviewBadge } from '../../shared/technical_preview_badge';
|
||||
import { Breadcrumb } from '../breadcrumb';
|
||||
import { TransactionTab } from '../transaction_details/waterfall_with_summary/transaction_tabs';
|
||||
|
||||
type Tab = Required<
|
||||
Required<React.ComponentProps<typeof ApmMainTemplate>>['pageHeader']
|
||||
>['tabs'][number];
|
||||
|
||||
export function TraceOverview({ children }: { children: React.ReactElement }) {
|
||||
const isTraceExplorerEnabled = useTraceExplorerEnabledSetting();
|
||||
|
@ -24,11 +30,24 @@ export function TraceOverview({ children }: { children: React.ReactElement }) {
|
|||
|
||||
const routePath = useApmRoutePath();
|
||||
|
||||
if (!isTraceExplorerEnabled) {
|
||||
return children;
|
||||
}
|
||||
const topTracesLink = router.link('/traces', {
|
||||
query: {
|
||||
comparisonEnabled: query.comparisonEnabled,
|
||||
environment: query.environment,
|
||||
kuery: query.kuery,
|
||||
rangeFrom: query.rangeFrom,
|
||||
rangeTo: query.rangeTo,
|
||||
offset: query.offset,
|
||||
refreshInterval: query.refreshInterval,
|
||||
refreshPaused: query.refreshPaused,
|
||||
},
|
||||
});
|
||||
|
||||
const explorerLink = router.link('/traces/explorer', {
|
||||
const title = i18n.translate('xpack.apm.views.traceOverview.title', {
|
||||
defaultMessage: 'Traces',
|
||||
});
|
||||
|
||||
const explorerLink = router.link('/traces/explorer/waterfall', {
|
||||
query: {
|
||||
comparisonEnabled: query.comparisonEnabled,
|
||||
environment: query.environment,
|
||||
|
@ -48,40 +67,55 @@ export function TraceOverview({ children }: { children: React.ReactElement }) {
|
|||
},
|
||||
});
|
||||
|
||||
const topTracesLink = router.link('/traces', {
|
||||
query: {
|
||||
comparisonEnabled: query.comparisonEnabled,
|
||||
environment: query.environment,
|
||||
kuery: query.kuery,
|
||||
rangeFrom: query.rangeFrom,
|
||||
rangeTo: query.rangeTo,
|
||||
offset: query.offset,
|
||||
refreshInterval: query.refreshInterval,
|
||||
refreshPaused: query.refreshPaused,
|
||||
},
|
||||
});
|
||||
const tabs: Tab[] = isTraceExplorerEnabled
|
||||
? [
|
||||
{
|
||||
href: topTracesLink,
|
||||
label: i18n.translate('xpack.apm.traceOverview.topTracesTab', {
|
||||
defaultMessage: 'Top traces',
|
||||
}),
|
||||
isSelected: routePath === '/traces',
|
||||
},
|
||||
{
|
||||
href: explorerLink,
|
||||
label: (
|
||||
<EuiFlexGroup gutterSize="s">
|
||||
<EuiFlexItem grow={false}>
|
||||
{i18n.translate('xpack.apm.traceOverview.traceExplorerTab', {
|
||||
defaultMessage: 'Explorer',
|
||||
})}
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem grow={false}>
|
||||
<TechnicalPreviewBadge
|
||||
icon="beaker"
|
||||
style={{ verticalAlign: 'middle' }}
|
||||
/>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
),
|
||||
isSelected: routePath.startsWith('/traces/explorer'),
|
||||
},
|
||||
]
|
||||
: [];
|
||||
|
||||
return (
|
||||
<EuiFlexGroup direction="column">
|
||||
<EuiFlexItem>
|
||||
<EuiTabs size="l">
|
||||
<EuiTab href={topTracesLink} isSelected={routePath === '/traces'}>
|
||||
{i18n.translate('xpack.apm.traceOverview.topTracesTab', {
|
||||
defaultMessage: 'Top traces',
|
||||
})}
|
||||
</EuiTab>
|
||||
<EuiTab
|
||||
href={explorerLink}
|
||||
append={<TechnicalPreviewBadge icon="beaker" />}
|
||||
isSelected={routePath === '/traces/explorer'}
|
||||
>
|
||||
{i18n.translate('xpack.apm.traceOverview.traceExplorerTab', {
|
||||
defaultMessage: 'Explorer',
|
||||
})}
|
||||
</EuiTab>
|
||||
</EuiTabs>
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem>{children}</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
<Breadcrumb href="/traces" title={title}>
|
||||
<ApmMainTemplate
|
||||
pageTitle={title}
|
||||
pageSectionProps={{
|
||||
contentProps: {
|
||||
style: {
|
||||
display: 'flex',
|
||||
flexGrow: 1,
|
||||
},
|
||||
},
|
||||
}}
|
||||
pageHeader={{
|
||||
tabs,
|
||||
}}
|
||||
>
|
||||
{children}
|
||||
</ApmMainTemplate>
|
||||
</Breadcrumb>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -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 { EuiFlexGroup, EuiFlexItem } from '@elastic/eui';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import React, { useMemo } from 'react';
|
||||
import { useApmParams } from '../../../hooks/use_apm_params';
|
||||
import { useTimeRange } from '../../../hooks/use_time_range';
|
||||
import { CriticalPathFlamegraph } from '../../shared/critical_path_flamegraph';
|
||||
import { TechnicalPreviewBadge } from '../../shared/technical_preview_badge';
|
||||
import { TabContentProps } from './transaction_details_tabs';
|
||||
|
||||
function TransactionDetailAggregatedCriticalPath({
|
||||
traceSamplesFetchResult,
|
||||
}: TabContentProps) {
|
||||
const {
|
||||
path: { serviceName },
|
||||
query: { rangeFrom, rangeTo, transactionName },
|
||||
} = useApmParams('/services/{serviceName}/transactions/view');
|
||||
|
||||
const { start, end } = useTimeRange({ rangeFrom, rangeTo });
|
||||
|
||||
const traceIds = useMemo(() => {
|
||||
return (
|
||||
traceSamplesFetchResult.data?.traceSamples.map(
|
||||
(sample) => sample.traceId
|
||||
) ?? []
|
||||
);
|
||||
}, [traceSamplesFetchResult.data]);
|
||||
|
||||
return (
|
||||
<CriticalPathFlamegraph
|
||||
start={start}
|
||||
end={end}
|
||||
traceIdsFetchStatus={traceSamplesFetchResult.status}
|
||||
traceIds={traceIds}
|
||||
serviceName={serviceName}
|
||||
transactionName={transactionName}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
export const aggregatedCriticalPathTab = {
|
||||
dataTestSubj: 'apmAggregatedCriticalPathTabButton',
|
||||
key: 'aggregatedCriticalPath',
|
||||
label: (
|
||||
<EuiFlexGroup gutterSize="s" direction="row">
|
||||
<EuiFlexItem grow={false}>
|
||||
{i18n.translate(
|
||||
'xpack.apm.transactionDetails.tabs.aggregatedCriticalPathLabel',
|
||||
{
|
||||
defaultMessage: 'Aggregated critical path',
|
||||
}
|
||||
)}
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem>
|
||||
<TechnicalPreviewBadge
|
||||
icon="beaker"
|
||||
size="s"
|
||||
style={{ verticalAlign: 'middle' }}
|
||||
/>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
),
|
||||
component: TransactionDetailAggregatedCriticalPath,
|
||||
};
|
|
@ -27,6 +27,8 @@ import { latencyCorrelationsTab } from './latency_correlations_tab';
|
|||
import { traceSamplesTab } from './trace_samples_tab';
|
||||
import { useSampleChartSelection } from '../../../hooks/use_sample_chart_selection';
|
||||
import { FETCH_STATUS } from '../../../hooks/use_fetcher';
|
||||
import { useCriticalPathFeatureEnabledSetting } from '../../../hooks/use_critical_path_feature_enabled_setting';
|
||||
import { aggregatedCriticalPathTab } from './aggregated_critical_path_tab';
|
||||
|
||||
export interface TabContentProps {
|
||||
clearChartSelection: () => void;
|
||||
|
@ -46,12 +48,18 @@ const tabs = [
|
|||
export function TransactionDetailsTabs() {
|
||||
const { query } = useApmParams('/services/{serviceName}/transactions/view');
|
||||
|
||||
const isCriticalPathFeatureEnabled = useCriticalPathFeatureEnabledSetting();
|
||||
|
||||
const availableTabs = isCriticalPathFeatureEnabled
|
||||
? tabs.concat(aggregatedCriticalPathTab)
|
||||
: tabs;
|
||||
|
||||
const { urlParams } = useLegacyUrlParams();
|
||||
const history = useHistory();
|
||||
|
||||
const [currentTab, setCurrentTab] = useState(traceSamplesTab.key);
|
||||
const { component: TabContent } =
|
||||
tabs.find((tab) => tab.key === currentTab) ?? traceSamplesTab;
|
||||
availableTabs.find((tab) => tab.key === currentTab) ?? traceSamplesTab;
|
||||
|
||||
const { environment, kuery, transactionName } = query;
|
||||
|
||||
|
@ -107,7 +115,7 @@ export function TransactionDetailsTabs() {
|
|||
return (
|
||||
<>
|
||||
<EuiTabs>
|
||||
{tabs.map(({ dataTestSubj, key, label }) => (
|
||||
{availableTabs.map(({ dataTestSubj, key, label }) => (
|
||||
<EuiTab
|
||||
data-test-subj={dataTestSubj}
|
||||
key={key}
|
||||
|
|
|
@ -104,80 +104,85 @@ export function WaterfallWithSummary<TSample extends {}>({
|
|||
const entryTransaction = entryWaterfallTransaction?.doc;
|
||||
|
||||
return (
|
||||
<>
|
||||
<EuiFlexGroup alignItems="center">
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiTitle size="xs">
|
||||
<h5>
|
||||
{i18n.translate('xpack.apm.transactionDetails.traceSampleTitle', {
|
||||
defaultMessage: 'Trace sample',
|
||||
})}
|
||||
</h5>
|
||||
</EuiTitle>
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem>
|
||||
{!!traceSamples?.length && (
|
||||
<EuiPagination
|
||||
pageCount={traceSamples.length}
|
||||
activePage={samplePageIndex}
|
||||
onPageClick={goToSample}
|
||||
compressed
|
||||
/>
|
||||
)}
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem>
|
||||
<EuiFlexGroup justifyContent="flexEnd">
|
||||
<EuiFlexItem grow={false}>
|
||||
<TransactionActionMenu
|
||||
isLoading={isLoading}
|
||||
transaction={entryTransaction}
|
||||
<EuiFlexGroup direction="column" gutterSize="s">
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiFlexGroup alignItems="center">
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiTitle size="xs">
|
||||
<h5>
|
||||
{i18n.translate(
|
||||
'xpack.apm.transactionDetails.traceSampleTitle',
|
||||
{
|
||||
defaultMessage: 'Trace sample',
|
||||
}
|
||||
)}
|
||||
</h5>
|
||||
</EuiTitle>
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem grow>
|
||||
{!!traceSamples?.length && (
|
||||
<EuiPagination
|
||||
pageCount={traceSamples.length}
|
||||
activePage={samplePageIndex}
|
||||
onPageClick={goToSample}
|
||||
compressed
|
||||
/>
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem grow={false}>
|
||||
<MaybeViewTraceLink
|
||||
isLoading={isLoading}
|
||||
transaction={entryTransaction}
|
||||
waterfall={waterfallFetchResult.waterfall}
|
||||
environment={environment}
|
||||
/>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
|
||||
<EuiSpacer size="s" />
|
||||
)}
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiFlexGroup justifyContent="flexEnd">
|
||||
<EuiFlexItem grow={false}>
|
||||
<TransactionActionMenu
|
||||
isLoading={isLoading}
|
||||
transaction={entryTransaction}
|
||||
/>
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem grow={false}>
|
||||
<MaybeViewTraceLink
|
||||
isLoading={isLoading}
|
||||
transaction={entryTransaction}
|
||||
waterfall={waterfallFetchResult.waterfall}
|
||||
environment={environment}
|
||||
/>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
</EuiFlexItem>
|
||||
|
||||
{isLoading || !entryTransaction ? (
|
||||
<>
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiSpacer size="s" />
|
||||
<EuiLoadingContent lines={1} data-test-sub="loading-content" />
|
||||
</>
|
||||
</EuiFlexItem>
|
||||
) : (
|
||||
<TransactionSummary
|
||||
errorCount={
|
||||
waterfallFetchResult.waterfall.apiResponse.errorDocs.length
|
||||
}
|
||||
totalDuration={
|
||||
waterfallFetchResult.waterfall.rootTransaction?.transaction.duration
|
||||
.us
|
||||
}
|
||||
transaction={entryTransaction}
|
||||
/>
|
||||
<EuiFlexItem grow={false}>
|
||||
<TransactionSummary
|
||||
errorCount={
|
||||
waterfallFetchResult.waterfall.apiResponse.errorDocs.length
|
||||
}
|
||||
totalDuration={
|
||||
waterfallFetchResult.waterfall.rootTransaction?.transaction
|
||||
.duration.us
|
||||
}
|
||||
transaction={entryTransaction}
|
||||
/>
|
||||
</EuiFlexItem>
|
||||
)}
|
||||
|
||||
<EuiSpacer size="s" />
|
||||
|
||||
<TransactionTabs
|
||||
transaction={entryTransaction}
|
||||
detailTab={detailTab}
|
||||
serviceName={serviceName}
|
||||
waterfallItemId={waterfallItemId}
|
||||
onTabClick={onTabClick}
|
||||
waterfall={waterfallFetchResult.waterfall}
|
||||
isLoading={isLoading}
|
||||
showCriticalPath={showCriticalPath}
|
||||
onShowCriticalPathChange={onShowCriticalPathChange}
|
||||
/>
|
||||
</>
|
||||
<EuiFlexItem grow={false}>
|
||||
<TransactionTabs
|
||||
transaction={entryTransaction}
|
||||
detailTab={detailTab}
|
||||
serviceName={serviceName}
|
||||
waterfallItemId={waterfallItemId}
|
||||
onTabClick={onTabClick}
|
||||
waterfall={waterfallFetchResult.waterfall}
|
||||
isLoading={isLoading}
|
||||
showCriticalPath={showCriticalPath}
|
||||
onShowCriticalPathChange={onShowCriticalPathChange}
|
||||
/>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -19,6 +19,8 @@ import { ServiceInventory } from '../../app/service_inventory';
|
|||
import { ServiceMapHome } from '../../app/service_map';
|
||||
import { TopTracesOverview } from '../../app/top_traces_overview';
|
||||
import { TraceExplorer } from '../../app/trace_explorer';
|
||||
import { TraceExplorerAggregatedCriticalPath } from '../../app/trace_explorer/trace_explorer_aggregated_critical_path';
|
||||
import { TraceExplorerWaterfall } from '../../app/trace_explorer/trace_explorer_waterfall';
|
||||
import { TraceOverview } from '../../app/trace_overview';
|
||||
import { TransactionTab } from '../../app/transaction_details/waterfall_with_summary/transaction_tabs';
|
||||
import { RedirectTo } from '../redirect_to';
|
||||
|
@ -184,11 +186,7 @@ export const home = {
|
|||
element: <ServiceMapHome />,
|
||||
serviceGroupContextTab: 'service-map',
|
||||
}),
|
||||
...page({
|
||||
path: '/traces',
|
||||
title: i18n.translate('xpack.apm.views.traceOverview.title', {
|
||||
defaultMessage: 'Traces',
|
||||
}),
|
||||
'/traces': {
|
||||
element: (
|
||||
<TraceOverview>
|
||||
<Outlet />
|
||||
|
@ -196,7 +194,42 @@ export const home = {
|
|||
),
|
||||
children: {
|
||||
'/traces/explorer': {
|
||||
element: <TraceExplorer />,
|
||||
element: (
|
||||
<TraceExplorer>
|
||||
<Outlet />
|
||||
</TraceExplorer>
|
||||
),
|
||||
children: {
|
||||
'/traces/explorer/waterfall': {
|
||||
element: <TraceExplorerWaterfall />,
|
||||
params: t.type({
|
||||
query: t.type({
|
||||
traceId: t.string,
|
||||
transactionId: t.string,
|
||||
waterfallItemId: t.string,
|
||||
detailTab: t.union([
|
||||
t.literal(TransactionTab.timeline),
|
||||
t.literal(TransactionTab.metadata),
|
||||
t.literal(TransactionTab.logs),
|
||||
]),
|
||||
}),
|
||||
}),
|
||||
defaults: {
|
||||
query: {
|
||||
waterfallItemId: '',
|
||||
traceId: '',
|
||||
transactionId: '',
|
||||
detailTab: TransactionTab.timeline,
|
||||
},
|
||||
},
|
||||
},
|
||||
'/traces/explorer/critical_path': {
|
||||
element: <TraceExplorerAggregatedCriticalPath />,
|
||||
},
|
||||
'/traces/explorer': {
|
||||
element: <RedirectTo pathname="/traces/explorer/waterfall" />,
|
||||
},
|
||||
},
|
||||
params: t.type({
|
||||
query: t.type({
|
||||
query: t.string,
|
||||
|
@ -204,14 +237,6 @@ export const home = {
|
|||
t.literal(TraceSearchType.kql),
|
||||
t.literal(TraceSearchType.eql),
|
||||
]),
|
||||
waterfallItemId: t.string,
|
||||
traceId: t.string,
|
||||
transactionId: t.string,
|
||||
detailTab: t.union([
|
||||
t.literal(TransactionTab.timeline),
|
||||
t.literal(TransactionTab.metadata),
|
||||
t.literal(TransactionTab.logs),
|
||||
]),
|
||||
showCriticalPath: toBooleanRt,
|
||||
}),
|
||||
}),
|
||||
|
@ -219,10 +244,6 @@ export const home = {
|
|||
query: {
|
||||
query: '',
|
||||
type: TraceSearchType.kql,
|
||||
waterfallItemId: '',
|
||||
traceId: '',
|
||||
transactionId: '',
|
||||
detailTab: TransactionTab.timeline,
|
||||
showCriticalPath: '',
|
||||
},
|
||||
},
|
||||
|
@ -231,7 +252,7 @@ export const home = {
|
|||
element: <TopTracesOverview />,
|
||||
},
|
||||
},
|
||||
}),
|
||||
},
|
||||
...dependencies,
|
||||
...legacyBackends,
|
||||
...storageExplorer,
|
||||
|
|
|
@ -6,17 +6,18 @@
|
|||
*/
|
||||
|
||||
import { EuiPageHeaderProps } from '@elastic/eui';
|
||||
import { useKibana } from '@kbn/kibana-react-plugin/public';
|
||||
import { ObservabilityPageTemplateProps } from '@kbn/observability-plugin/public/components/shared/page_template/page_template';
|
||||
import type { KibanaPageTemplateProps } from '@kbn/shared-ux-page-kibana-template';
|
||||
import React from 'react';
|
||||
import { useLocation } from 'react-router-dom';
|
||||
import { useKibana } from '@kbn/kibana-react-plugin/public';
|
||||
import type { KibanaPageTemplateProps } from '@kbn/shared-ux-page-kibana-template';
|
||||
import { EnvironmentsContextProvider } from '../../../context/environments_context/environments_context';
|
||||
import { useFetcher, FETCH_STATUS } from '../../../hooks/use_fetcher';
|
||||
import { FETCH_STATUS, useFetcher } from '../../../hooks/use_fetcher';
|
||||
import { ApmPluginStartDeps } from '../../../plugin';
|
||||
import { ApmEnvironmentFilter } from '../../shared/environment_filter';
|
||||
import { getNoDataConfig } from './no_data_config';
|
||||
import { ServiceGroupSaveButton } from '../../app/service_groups';
|
||||
import { ServiceGroupsButtonGroup } from '../../app/service_groups/service_groups_button_group';
|
||||
import { ApmEnvironmentFilter } from '../../shared/environment_filter';
|
||||
import { getNoDataConfig } from './no_data_config';
|
||||
|
||||
// Paths that must skip the no data screen
|
||||
const bypassNoDataScreenPaths = ['/settings'];
|
||||
|
@ -48,7 +49,8 @@ export function ApmMainTemplate({
|
|||
showServiceGroupSaveButton?: boolean;
|
||||
showServiceGroupsNav?: boolean;
|
||||
selectedNavButton?: 'serviceGroups' | 'allServices';
|
||||
} & KibanaPageTemplateProps) {
|
||||
} & KibanaPageTemplateProps &
|
||||
Pick<ObservabilityPageTemplateProps, 'pageSectionProps'>) {
|
||||
const location = useLocation();
|
||||
|
||||
const { services } = useKibana<ApmPluginStartDeps>();
|
||||
|
|
|
@ -0,0 +1,120 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
import {
|
||||
EuiBadge,
|
||||
EuiFlexGroup,
|
||||
EuiFlexItem,
|
||||
EuiHorizontalRule,
|
||||
EuiPanel,
|
||||
} from '@elastic/eui';
|
||||
import React from 'react';
|
||||
import { ProcessorEvent } from '@kbn/observability-plugin/common';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import type { CriticalPathResponse } from '../../../../server/routes/traces/get_aggregated_critical_path';
|
||||
import {
|
||||
AGENT_NAME,
|
||||
SERVICE_NAME,
|
||||
SPAN_NAME,
|
||||
SPAN_SUBTYPE,
|
||||
SPAN_TYPE,
|
||||
TRANSACTION_NAME,
|
||||
TRANSACTION_TYPE,
|
||||
} from '../../../../common/elasticsearch_fieldnames';
|
||||
import { SpanIcon } from '../span_icon';
|
||||
import { AgentIcon } from '../agent_icon';
|
||||
import { asPercent } from '../../../../common/utils/formatters';
|
||||
|
||||
export function CriticalPathFlamegraphTooltip({
|
||||
metadata,
|
||||
countInclusive,
|
||||
countExclusive,
|
||||
totalCount,
|
||||
}: {
|
||||
metadata?: CriticalPathResponse['metadata'][string];
|
||||
countInclusive: number;
|
||||
countExclusive: number;
|
||||
totalCount: number;
|
||||
}) {
|
||||
if (!metadata) {
|
||||
return <></>;
|
||||
}
|
||||
|
||||
return (
|
||||
<EuiPanel>
|
||||
<EuiFlexGroup direction="column" gutterSize="s">
|
||||
{metadata['processor.event'] === ProcessorEvent.transaction ? (
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiFlexGroup
|
||||
direction="row"
|
||||
gutterSize="s"
|
||||
style={{ overflowWrap: 'anywhere' }}
|
||||
alignItems="center"
|
||||
>
|
||||
<EuiFlexItem grow={false}>
|
||||
{metadata[TRANSACTION_NAME]}
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiBadge>{metadata[TRANSACTION_TYPE]}</EuiBadge>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
</EuiFlexItem>
|
||||
) : (
|
||||
<EuiFlexItem>
|
||||
<EuiFlexGroup
|
||||
direction="row"
|
||||
gutterSize="s"
|
||||
style={{ overflowWrap: 'anywhere' }}
|
||||
alignItems="center"
|
||||
>
|
||||
<EuiFlexItem grow={false}>
|
||||
<SpanIcon
|
||||
type={metadata[SPAN_TYPE]}
|
||||
subtype={metadata[SPAN_SUBTYPE]}
|
||||
/>
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem grow={false}>{metadata[SPAN_NAME]}</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
</EuiFlexItem>
|
||||
)}
|
||||
<EuiFlexItem>
|
||||
<EuiHorizontalRule margin="none" />
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem>
|
||||
<EuiFlexGroup direction="row" gutterSize="xs" alignItems="center">
|
||||
<EuiFlexItem grow={false}>
|
||||
<AgentIcon agentName={metadata[AGENT_NAME]} />
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem grow={false}>{metadata[SERVICE_NAME]}</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem>
|
||||
<EuiHorizontalRule margin="none" />
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem>
|
||||
<EuiFlexGroup direction="column" gutterSize="s">
|
||||
<EuiFlexItem grow={false}>
|
||||
{i18n.translate('xpack.apm.criticalPathFlameGraph.selfTime', {
|
||||
defaultMessage: 'Self time: {value}',
|
||||
values: {
|
||||
value: asPercent(countExclusive / totalCount, 1),
|
||||
},
|
||||
})}
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem grow={false}>
|
||||
{i18n.translate('xpack.apm.criticalPathFlameGraph.totalTime', {
|
||||
defaultMessage: 'Total time: {value}',
|
||||
values: {
|
||||
value: asPercent(countInclusive / totalCount, 1),
|
||||
},
|
||||
})}
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
</EuiPanel>
|
||||
);
|
||||
}
|
|
@ -0,0 +1,144 @@
|
|||
/*
|
||||
* 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 type { ColumnarViewModel } from '@elastic/charts';
|
||||
import { memoize, sumBy } from 'lodash';
|
||||
import { lighten, parseToRgb } from 'polished';
|
||||
import seedrandom from 'seedrandom';
|
||||
import type { CriticalPathResponse } from '../../../../server/routes/traces/get_aggregated_critical_path';
|
||||
import {
|
||||
CriticalPathTreeNode,
|
||||
getAggregatedCriticalPathRootNodes,
|
||||
} from '../../../../common/critical_path/get_aggregated_critical_path_root_nodes';
|
||||
|
||||
const lightenColor = lighten(0.2);
|
||||
|
||||
export function criticalPathToFlamegraph(
|
||||
params: {
|
||||
criticalPath: CriticalPathResponse;
|
||||
colors: string[];
|
||||
} & ({ serviceName: string; transactionName: string } | {})
|
||||
): {
|
||||
viewModel: ColumnarViewModel;
|
||||
operationId: string[];
|
||||
countExclusive: Float64Array;
|
||||
sum: number;
|
||||
} {
|
||||
let sum = 0;
|
||||
|
||||
const { criticalPath, colors } = params;
|
||||
|
||||
const { rootNodes, maxDepth, numNodes } =
|
||||
getAggregatedCriticalPathRootNodes(params);
|
||||
|
||||
// include the root node
|
||||
const totalSize = numNodes + 1;
|
||||
|
||||
const operationId = new Array<string>(totalSize);
|
||||
const countInclusive = new Float64Array(totalSize);
|
||||
const countExclusive = new Float64Array(totalSize);
|
||||
const label = new Array<string>(totalSize);
|
||||
const position = new Float32Array(totalSize * 2);
|
||||
const size = new Float32Array(totalSize);
|
||||
const color = new Float32Array(totalSize * 4);
|
||||
|
||||
// eslint-disable-next-line guard-for-in
|
||||
for (const nodeId in criticalPath.timeByNodeId) {
|
||||
const count = criticalPath.timeByNodeId[nodeId];
|
||||
sum += count;
|
||||
}
|
||||
|
||||
let maxValue = 0;
|
||||
|
||||
let index = 0;
|
||||
|
||||
const availableColors: Array<[number, number, number, number]> = colors.map(
|
||||
(vizColor) => {
|
||||
const rgb = parseToRgb(lightenColor(vizColor));
|
||||
|
||||
return [rgb.red / 255, rgb.green / 255, rgb.blue / 255, 1];
|
||||
}
|
||||
);
|
||||
|
||||
const pickColor = memoize((identifier: string) => {
|
||||
const idx =
|
||||
Math.abs(seedrandom(identifier).int32()) % availableColors.length;
|
||||
return availableColors[idx];
|
||||
});
|
||||
|
||||
function addNodeToFlamegraph(
|
||||
node: CriticalPathTreeNode,
|
||||
x: number,
|
||||
y: number
|
||||
) {
|
||||
let nodeOperationId: string;
|
||||
let nodeLabel: string;
|
||||
let operationMetadata: CriticalPathResponse['metadata'][string] | undefined;
|
||||
if (node.nodeId === 'root') {
|
||||
nodeOperationId = '';
|
||||
nodeLabel = 'root';
|
||||
} else {
|
||||
nodeOperationId = criticalPath.operationIdByNodeId[node.nodeId];
|
||||
operationMetadata = criticalPath.metadata[nodeOperationId];
|
||||
nodeLabel =
|
||||
operationMetadata['processor.event'] === 'transaction'
|
||||
? operationMetadata['transaction.name']
|
||||
: operationMetadata['span.name'];
|
||||
}
|
||||
|
||||
operationId[index] = nodeOperationId;
|
||||
countInclusive[index] = node.countInclusive;
|
||||
countExclusive[index] = node.countExclusive;
|
||||
label[index] = nodeLabel;
|
||||
position[index * 2] = x / maxValue;
|
||||
position[index * 2 + 1] = 1 - (y + 1) / (maxDepth + 1);
|
||||
size[index] = node.countInclusive / maxValue;
|
||||
|
||||
const identifier =
|
||||
operationMetadata?.['processor.event'] === 'transaction'
|
||||
? operationMetadata['transaction.type']
|
||||
: operationMetadata?.['span.subtype'] ||
|
||||
operationMetadata?.['span.type'] ||
|
||||
'';
|
||||
|
||||
color.set(pickColor(identifier), index * 4);
|
||||
|
||||
index++;
|
||||
|
||||
let childX = x;
|
||||
node.children.forEach((child) => {
|
||||
addNodeToFlamegraph(child, childX, y + 1);
|
||||
childX += child.countInclusive;
|
||||
});
|
||||
}
|
||||
|
||||
const root: CriticalPathTreeNode = {
|
||||
children: rootNodes,
|
||||
nodeId: 'root',
|
||||
countExclusive: 0,
|
||||
countInclusive: sumBy(rootNodes, 'countInclusive'),
|
||||
};
|
||||
|
||||
maxValue = root.countInclusive;
|
||||
|
||||
addNodeToFlamegraph(root, 0, 0);
|
||||
|
||||
return {
|
||||
viewModel: {
|
||||
value: countInclusive,
|
||||
label,
|
||||
color,
|
||||
position0: position,
|
||||
position1: position,
|
||||
size0: size,
|
||||
size1: size,
|
||||
},
|
||||
operationId,
|
||||
countExclusive,
|
||||
sum,
|
||||
};
|
||||
}
|
|
@ -0,0 +1,165 @@
|
|||
/*
|
||||
* 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 { Chart, Datum, Flame, Settings } from '@elastic/charts';
|
||||
import {
|
||||
EuiFlexGroup,
|
||||
EuiFlexItem,
|
||||
EuiLoadingSpinner,
|
||||
euiPaletteColorBlind,
|
||||
} from '@elastic/eui';
|
||||
import { css } from '@emotion/css';
|
||||
import { useChartTheme } from '@kbn/observability-plugin/public';
|
||||
import { uniqueId } from 'lodash';
|
||||
import React, { useMemo, useRef } from 'react';
|
||||
import { FETCH_STATUS, useFetcher } from '../../../hooks/use_fetcher';
|
||||
import { CriticalPathFlamegraphTooltip } from './critical_path_flamegraph_tooltip';
|
||||
import { criticalPathToFlamegraph } from './critical_path_to_flamegraph';
|
||||
|
||||
const chartClassName = css`
|
||||
flex-grow: 1;
|
||||
`;
|
||||
|
||||
export function CriticalPathFlamegraph(
|
||||
props: {
|
||||
start: string;
|
||||
end: string;
|
||||
traceIds: string[];
|
||||
traceIdsFetchStatus: FETCH_STATUS;
|
||||
} & ({ serviceName: string; transactionName: string } | {})
|
||||
) {
|
||||
const { start, end, traceIds, traceIdsFetchStatus } = props;
|
||||
|
||||
const serviceName = 'serviceName' in props ? props.serviceName : null;
|
||||
const transactionName =
|
||||
'transactionName' in props ? props.transactionName : null;
|
||||
|
||||
// Use a reference to time range, to not invalidate the API fetch
|
||||
// we only care for traceIds, start/end are there to limit the search
|
||||
// request to a certain time range. It shouldn't affect the actual results
|
||||
// of the search.
|
||||
const timerange = useRef({ start, end });
|
||||
timerange.current = { start, end };
|
||||
|
||||
const {
|
||||
data: { criticalPath } = { criticalPath: null },
|
||||
status: criticalPathFetchStatus,
|
||||
} = useFetcher(
|
||||
(callApmApi) => {
|
||||
if (!traceIds.length) {
|
||||
return Promise.resolve({ criticalPath: null });
|
||||
}
|
||||
|
||||
return callApmApi('POST /internal/apm/traces/aggregated_critical_path', {
|
||||
params: {
|
||||
body: {
|
||||
start: timerange.current.start,
|
||||
end: timerange.current.end,
|
||||
traceIds,
|
||||
serviceName,
|
||||
transactionName,
|
||||
},
|
||||
},
|
||||
});
|
||||
},
|
||||
[timerange, traceIds, serviceName, transactionName]
|
||||
);
|
||||
|
||||
const chartTheme = useChartTheme();
|
||||
|
||||
const isLoading =
|
||||
traceIdsFetchStatus === FETCH_STATUS.NOT_INITIATED ||
|
||||
traceIdsFetchStatus === FETCH_STATUS.LOADING ||
|
||||
criticalPathFetchStatus === FETCH_STATUS.NOT_INITIATED ||
|
||||
criticalPathFetchStatus === FETCH_STATUS.LOADING;
|
||||
|
||||
const flameGraph = useMemo(() => {
|
||||
if (!criticalPath) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const colors = euiPaletteColorBlind({});
|
||||
|
||||
const flamegraph = criticalPathToFlamegraph({
|
||||
criticalPath,
|
||||
colors,
|
||||
});
|
||||
|
||||
return {
|
||||
...flamegraph,
|
||||
// make sure Flame re-renders when data changes, workaround for https://github.com/elastic/elastic-charts/issues/1766
|
||||
key: uniqueId(),
|
||||
};
|
||||
}, [criticalPath]);
|
||||
|
||||
const themeOverrides = {
|
||||
chartMargins: { top: 0, left: 0, bottom: 0, right: 0 },
|
||||
chartPaddings: { left: 0, right: 0, top: 0, bottom: 0 },
|
||||
};
|
||||
|
||||
return (
|
||||
<EuiFlexGroup
|
||||
direction="column"
|
||||
gutterSize="l"
|
||||
alignItems="stretch"
|
||||
justifyContent="center"
|
||||
style={{ minHeight: 400 }}
|
||||
>
|
||||
{isLoading ? (
|
||||
<EuiFlexItem grow={false} style={{ alignSelf: 'center' }}>
|
||||
<EuiLoadingSpinner size="l" />
|
||||
</EuiFlexItem>
|
||||
) : (
|
||||
flameGraph && (
|
||||
<EuiFlexItem grow>
|
||||
<Chart key={flameGraph.key} className={chartClassName}>
|
||||
<Settings
|
||||
theme={[
|
||||
{
|
||||
chartMargins: themeOverrides.chartMargins,
|
||||
chartPaddings: themeOverrides.chartPaddings,
|
||||
},
|
||||
...chartTheme,
|
||||
]}
|
||||
tooltip={{
|
||||
customTooltip: (tooltipProps) => {
|
||||
const valueIndex = tooltipProps.values[0]
|
||||
.valueAccessor as number;
|
||||
const operationId = flameGraph.operationId[valueIndex];
|
||||
const operationMetadata =
|
||||
criticalPath?.metadata[operationId];
|
||||
const countInclusive =
|
||||
flameGraph.viewModel.value[valueIndex];
|
||||
const countExclusive =
|
||||
flameGraph.countExclusive[valueIndex];
|
||||
|
||||
return (
|
||||
<CriticalPathFlamegraphTooltip
|
||||
metadata={operationMetadata}
|
||||
countInclusive={countInclusive}
|
||||
countExclusive={countExclusive}
|
||||
totalCount={flameGraph.viewModel.value[0]}
|
||||
/>
|
||||
);
|
||||
},
|
||||
}}
|
||||
onElementClick={(elements) => {}}
|
||||
/>
|
||||
<Flame
|
||||
id="aggregated_critical_path"
|
||||
columnarData={flameGraph.viewModel}
|
||||
valueAccessor={(d: Datum) => d.value as number}
|
||||
valueFormatter={(value) => `${value}`}
|
||||
animation={{ duration: 100 }}
|
||||
controlProviderCallback={{}}
|
||||
/>
|
||||
</Chart>
|
||||
</EuiFlexItem>
|
||||
)
|
||||
)}
|
||||
</EuiFlexGroup>
|
||||
);
|
||||
}
|
|
@ -9,11 +9,11 @@ import { EuiBetaBadge, IconType } from '@elastic/eui';
|
|||
import { i18n } from '@kbn/i18n';
|
||||
import React from 'react';
|
||||
|
||||
interface Props {
|
||||
type Props = {
|
||||
icon?: IconType;
|
||||
}
|
||||
} & Pick<React.ComponentProps<typeof EuiBetaBadge>, 'size' | 'style'>;
|
||||
|
||||
export function TechnicalPreviewBadge({ icon }: Props) {
|
||||
export function TechnicalPreviewBadge({ icon, size, style }: Props) {
|
||||
return (
|
||||
<EuiBetaBadge
|
||||
label={i18n.translate('xpack.apm.technicalPreviewBadgeLabel', {
|
||||
|
@ -27,6 +27,8 @@ export function TechnicalPreviewBadge({ icon }: Props) {
|
|||
}
|
||||
)}
|
||||
iconType={icon}
|
||||
size={size}
|
||||
style={style}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -0,0 +1,72 @@
|
|||
/*
|
||||
* 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, { createContext, useContext, useMemo } from 'react';
|
||||
import type { APIEndpoint } from '../../server';
|
||||
import type {
|
||||
APIClientRequestParamsOf,
|
||||
APIReturnType,
|
||||
} from '../services/rest/create_call_apm_api';
|
||||
import { useFetcher, FetcherResult } from './use_fetcher';
|
||||
|
||||
interface SharedUseFetcher<TEndpoint extends APIEndpoint> {
|
||||
useFetcherResult: () => FetcherResult<APIReturnType<TEndpoint>> & {
|
||||
refetch: () => void;
|
||||
};
|
||||
Provider: React.FunctionComponent<
|
||||
{
|
||||
children: React.ReactElement;
|
||||
params: {};
|
||||
} & APIClientRequestParamsOf<TEndpoint>
|
||||
>;
|
||||
}
|
||||
|
||||
export function createSharedUseFetcher<TEndpoint extends APIEndpoint>(
|
||||
endpoint: TEndpoint
|
||||
): SharedUseFetcher<TEndpoint> {
|
||||
const Context = createContext<
|
||||
APIClientRequestParamsOf<APIEndpoint> | undefined
|
||||
>(undefined);
|
||||
|
||||
const returnValue: SharedUseFetcher<TEndpoint> = {
|
||||
useFetcherResult: () => {
|
||||
const context = useContext(Context);
|
||||
|
||||
if (!context) {
|
||||
throw new Error('Context was not found');
|
||||
}
|
||||
|
||||
const params = context.params;
|
||||
|
||||
const result = useFetcher(
|
||||
(callApmApi) => {
|
||||
return callApmApi(
|
||||
...([endpoint, { params }] as Parameters<typeof callApmApi>)
|
||||
);
|
||||
},
|
||||
[params]
|
||||
);
|
||||
|
||||
return result as ReturnType<
|
||||
SharedUseFetcher<TEndpoint>['useFetcherResult']
|
||||
>;
|
||||
},
|
||||
Provider: (props) => {
|
||||
const { children } = props;
|
||||
|
||||
const params = props.params;
|
||||
|
||||
const memoizedParams = useMemo(() => {
|
||||
return { params };
|
||||
}, [params]);
|
||||
return (
|
||||
<Context.Provider value={memoizedParams}>{children}</Context.Provider>
|
||||
);
|
||||
},
|
||||
};
|
||||
|
||||
return returnValue;
|
||||
}
|
|
@ -0,0 +1,29 @@
|
|||
/*
|
||||
* 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 { useMemo } from 'react';
|
||||
import { createSharedUseFetcher } from './create_shared_use_fetcher';
|
||||
|
||||
const sharedUseFetcher = createSharedUseFetcher(
|
||||
'GET /internal/apm/traces/find'
|
||||
);
|
||||
|
||||
const useTraceExplorerSamples = () => {
|
||||
const result = sharedUseFetcher.useFetcherResult();
|
||||
|
||||
return useMemo(() => {
|
||||
return {
|
||||
...result,
|
||||
data: result.data || {
|
||||
traceSamples: [],
|
||||
},
|
||||
};
|
||||
}, [result]);
|
||||
};
|
||||
const TraceExplorerSamplesFetcherContextProvider = sharedUseFetcher.Provider;
|
||||
|
||||
export { useTraceExplorerSamples, TraceExplorerSamplesFetcherContextProvider };
|
|
@ -0,0 +1,406 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import { ProcessorEvent } from '@kbn/observability-plugin/common';
|
||||
import { rangeQuery, termsQuery } from '@kbn/observability-plugin/server';
|
||||
import { Logger } from '@kbn/logging';
|
||||
import {
|
||||
AGENT_NAME,
|
||||
PROCESSOR_EVENT,
|
||||
SERVICE_NAME,
|
||||
SPAN_NAME,
|
||||
SPAN_SUBTYPE,
|
||||
SPAN_TYPE,
|
||||
TRACE_ID,
|
||||
TRANSACTION_NAME,
|
||||
TRANSACTION_TYPE,
|
||||
} from '../../../common/elasticsearch_fieldnames';
|
||||
import { AgentName } from '../../../typings/es_schemas/ui/fields/agent';
|
||||
import { APMEventClient } from '../../lib/helpers/create_es_client/create_apm_event_client';
|
||||
|
||||
type OperationMetadata = {
|
||||
[SERVICE_NAME]: string;
|
||||
[AGENT_NAME]: AgentName;
|
||||
} & (
|
||||
| {
|
||||
[PROCESSOR_EVENT]: ProcessorEvent.transaction;
|
||||
[TRANSACTION_TYPE]: string;
|
||||
[TRANSACTION_NAME]: string;
|
||||
}
|
||||
| {
|
||||
[PROCESSOR_EVENT]: ProcessorEvent.span;
|
||||
[SPAN_NAME]: string;
|
||||
[SPAN_TYPE]: string;
|
||||
[SPAN_SUBTYPE]: string;
|
||||
}
|
||||
);
|
||||
|
||||
type OperationId = string;
|
||||
|
||||
type NodeId = string;
|
||||
|
||||
export interface CriticalPathResponse {
|
||||
metadata: Record<OperationId, OperationMetadata>;
|
||||
timeByNodeId: Record<NodeId, number>;
|
||||
nodes: Record<NodeId, NodeId[]>;
|
||||
rootNodes: NodeId[];
|
||||
operationIdByNodeId: Record<NodeId, OperationId>;
|
||||
}
|
||||
|
||||
const TWO_DAYS_MS = 2 * 24 * 60 * 60 * 1000;
|
||||
|
||||
export async function getAggregatedCriticalPath({
|
||||
traceIds,
|
||||
start,
|
||||
end,
|
||||
apmEventClient,
|
||||
serviceName,
|
||||
transactionName,
|
||||
logger,
|
||||
}: {
|
||||
traceIds: string[];
|
||||
start: number;
|
||||
end: number;
|
||||
apmEventClient: APMEventClient;
|
||||
serviceName: string | null;
|
||||
transactionName: string | null;
|
||||
logger: Logger;
|
||||
}): Promise<{ criticalPath: CriticalPathResponse | null }> {
|
||||
const now = Date.now();
|
||||
|
||||
const response = await apmEventClient.search('get_aggregated_critical_path', {
|
||||
apm: {
|
||||
events: [ProcessorEvent.span, ProcessorEvent.transaction],
|
||||
},
|
||||
body: {
|
||||
size: 0,
|
||||
track_total_hits: false,
|
||||
query: {
|
||||
bool: {
|
||||
filter: [
|
||||
...termsQuery(TRACE_ID, ...traceIds),
|
||||
// we need a range query to allow ES to skip shards based on the time range,
|
||||
// but we need enough padding to make sure we get the full trace
|
||||
...rangeQuery(start - TWO_DAYS_MS, end + TWO_DAYS_MS),
|
||||
],
|
||||
},
|
||||
},
|
||||
aggs: {
|
||||
critical_path: {
|
||||
scripted_metric: {
|
||||
params: {
|
||||
// can't send null parameters to ES. undefined will be removed during JSON serialisation
|
||||
serviceName: serviceName || undefined,
|
||||
transactionName: transactionName || undefined,
|
||||
},
|
||||
init_script: {
|
||||
source: `
|
||||
state.eventsById = [:];
|
||||
state.metadataByOperationId = [:];
|
||||
`,
|
||||
},
|
||||
map_script: {
|
||||
source: `
|
||||
String toHash (def item) {
|
||||
return item.toString();
|
||||
}
|
||||
|
||||
def id;
|
||||
double duration;
|
||||
|
||||
def operationMetadata = [
|
||||
"service.name": doc['service.name'].value,
|
||||
"processor.event": doc['processor.event'].value,
|
||||
"agent.name": doc['agent.name'].value
|
||||
];
|
||||
|
||||
def isSpan = !doc['span.id'].empty;
|
||||
|
||||
if (isSpan) {
|
||||
id = doc['span.id'].value;
|
||||
operationMetadata.put('span.name', doc['span.name'].value);
|
||||
if (!doc['span.type'].empty) {
|
||||
operationMetadata.put('span.type', doc['span.type'].value);
|
||||
}
|
||||
if (!doc['span.subtype'].empty) {
|
||||
operationMetadata.put('span.subtype', doc['span.subtype'].value);
|
||||
}
|
||||
duration = doc['span.duration.us'].value;
|
||||
} else {
|
||||
id = doc['transaction.id'].value;
|
||||
operationMetadata.put('transaction.name', doc['transaction.name'].value);
|
||||
operationMetadata.put('transaction.type', doc['transaction.type'].value);
|
||||
duration = doc['transaction.duration.us'].value;
|
||||
}
|
||||
|
||||
String operationId = toHash(operationMetadata);
|
||||
|
||||
def map = [
|
||||
"traceId": doc['trace.id'].value,
|
||||
"id": id,
|
||||
"parentId": doc['parent.id'].empty ? null : doc['parent.id'].value,
|
||||
"operationId": operationId,
|
||||
"timestamp": doc['timestamp.us'].value,
|
||||
"duration": duration
|
||||
];
|
||||
|
||||
if (state.metadataByOperationId[operationId] == null) {
|
||||
state.metadataByOperationId.put(operationId, operationMetadata);
|
||||
}
|
||||
state.eventsById.put(id, map);
|
||||
`,
|
||||
},
|
||||
combine_script: {
|
||||
source: 'return state;',
|
||||
},
|
||||
reduce_script: {
|
||||
source: `
|
||||
String toHash (def item) {
|
||||
return item.toString();
|
||||
}
|
||||
|
||||
def processEvent (def context, def event) {
|
||||
if (context.processedEvents[event.id] != null) {
|
||||
return context.processedEvents[event.id];
|
||||
}
|
||||
|
||||
def processedEvent = [
|
||||
"children": []
|
||||
];
|
||||
|
||||
if(event.parentId != null) {
|
||||
def parent = context.events[event.parentId];
|
||||
if (parent == null) {
|
||||
return null;
|
||||
}
|
||||
def processedParent = processEvent(context, parent);
|
||||
if (processedParent == null) {
|
||||
return null;
|
||||
}
|
||||
processedParent.children.add(processedEvent);
|
||||
}
|
||||
|
||||
context.processedEvents.put(event.id, processedEvent);
|
||||
|
||||
processedEvent.putAll(event);
|
||||
|
||||
if (context.params.serviceName != null && context.params.transactionName != null) {
|
||||
|
||||
def metadata = context.metadata[event.operationId];
|
||||
|
||||
if (metadata != null
|
||||
&& context.params.serviceName == metadata['service.name']
|
||||
&& metadata['transaction.name'] != null
|
||||
&& context.params.transactionName == metadata['transaction.name']
|
||||
) {
|
||||
context.entryTransactions.add(processedEvent);
|
||||
}
|
||||
|
||||
} else if (event.parentId == null) {
|
||||
context.entryTransactions.add(processedEvent);
|
||||
}
|
||||
|
||||
return processedEvent;
|
||||
}
|
||||
|
||||
double getClockSkew (def context, def item, def parent ) {
|
||||
if (parent == null) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
def processorEvent = context.metadata[item.operationId]['processor.event'];
|
||||
|
||||
def isTransaction = processorEvent == 'transaction';
|
||||
|
||||
if (!isTransaction) {
|
||||
return parent.skew;
|
||||
}
|
||||
|
||||
double parentStart = parent.timestamp + parent.skew;
|
||||
double offsetStart = parentStart - item.timestamp;
|
||||
if (offsetStart > 0) {
|
||||
double latency = Math.round(Math.max(parent.duration - item.duration, 0) / 2);
|
||||
return offsetStart + latency;
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
void setOffsetAndSkew ( def context, def event, def parent, def startOfTrace ) {
|
||||
event.skew = getClockSkew(context, event, parent);
|
||||
event.offset = event.timestamp - startOfTrace;
|
||||
for(child in event.children) {
|
||||
setOffsetAndSkew(context, child, event, startOfTrace);
|
||||
}
|
||||
event.end = event.offset + event.skew + event.duration;
|
||||
}
|
||||
|
||||
void count ( def context, def nodeId, def duration ) {
|
||||
context.timeByNodeId[nodeId] = (context.timeByNodeId[nodeId] ?: 0) + duration;
|
||||
}
|
||||
|
||||
void scan ( def context, def item, def start, def end, def path ) {
|
||||
|
||||
def nodeId = toHash(path);
|
||||
|
||||
def childNodes = context.nodes[nodeId] != null ? context.nodes[nodeId] : [];
|
||||
|
||||
context.nodes[nodeId] = childNodes;
|
||||
|
||||
context.operationIdByNodeId[nodeId] = item.operationId;
|
||||
|
||||
if (item.children.size() == 0) {
|
||||
count(context, nodeId, end - start);
|
||||
return;
|
||||
}
|
||||
|
||||
item.children.sort((a, b) -> {
|
||||
if (b.end === a.end) {
|
||||
return 0;
|
||||
}
|
||||
if (b.end > a.end) {
|
||||
return 1;
|
||||
}
|
||||
return -1;
|
||||
});
|
||||
|
||||
def scanTime = end;
|
||||
|
||||
for(child in item.children) {
|
||||
double normalizedChildStart = Math.max(child.offset + child.skew, start);
|
||||
double childEnd = child.offset + child.skew + child.duration;
|
||||
|
||||
double normalizedChildEnd = Math.min(childEnd, scanTime);
|
||||
|
||||
def isOnCriticalPath = !(
|
||||
normalizedChildStart >= scanTime ||
|
||||
normalizedChildEnd < start ||
|
||||
childEnd > scanTime
|
||||
);
|
||||
|
||||
if (!isOnCriticalPath) {
|
||||
continue;
|
||||
}
|
||||
|
||||
def childPath = path.clone();
|
||||
|
||||
childPath.add(child.operationId);
|
||||
|
||||
def childId = toHash(childPath);
|
||||
|
||||
if(!childNodes.contains(childId)) {
|
||||
childNodes.add(childId);
|
||||
}
|
||||
|
||||
if (normalizedChildEnd < (scanTime - 1000)) {
|
||||
count(context, nodeId, scanTime - normalizedChildEnd);
|
||||
}
|
||||
|
||||
scan(context, child, normalizedChildStart, childEnd, childPath);
|
||||
|
||||
scanTime = normalizedChildStart;
|
||||
}
|
||||
|
||||
if (scanTime > start) {
|
||||
count(context, nodeId, scanTime - start);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
def events = [:];
|
||||
def metadata = [:];
|
||||
def processedEvents = [:];
|
||||
def entryTransactions = [];
|
||||
def timeByNodeId = [:];
|
||||
def nodes = [:];
|
||||
def rootNodes = [];
|
||||
def operationIdByNodeId = [:];
|
||||
|
||||
|
||||
def context = [
|
||||
"events": events,
|
||||
"metadata": metadata,
|
||||
"processedEvents": processedEvents,
|
||||
"entryTransactions": entryTransactions,
|
||||
"timeByNodeId": timeByNodeId,
|
||||
"nodes": nodes,
|
||||
"operationIdByNodeId": operationIdByNodeId,
|
||||
"params": params
|
||||
];
|
||||
|
||||
for(state in states) {
|
||||
if (state.eventsById != null) {
|
||||
events.putAll(state.eventsById);
|
||||
}
|
||||
if (state.metadataByOperationId != null) {
|
||||
metadata.putAll(state.metadataByOperationId);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
for(def event: events.values()) {
|
||||
processEvent(context, event);
|
||||
}
|
||||
|
||||
for(transaction in context.entryTransactions) {
|
||||
transaction.skew = 0;
|
||||
transaction.offset = 0;
|
||||
setOffsetAndSkew(context, transaction, null, transaction.timestamp);
|
||||
|
||||
def path = [];
|
||||
def parent = transaction;
|
||||
while (parent != null) {
|
||||
path.add(parent.operationId);
|
||||
if (parent.parentId == null) {
|
||||
break;
|
||||
}
|
||||
parent = context.processedEvents[parent.parentId];
|
||||
}
|
||||
|
||||
Collections.reverse(path);
|
||||
|
||||
def nodeId = toHash(path);
|
||||
|
||||
scan(context, transaction, 0, transaction.duration, path);
|
||||
|
||||
if (!rootNodes.contains(nodeId)) {
|
||||
rootNodes.add(nodeId);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
return [
|
||||
"timeByNodeId": timeByNodeId,
|
||||
"metadata": metadata,
|
||||
"nodes": nodes,
|
||||
"rootNodes": rootNodes,
|
||||
"operationIdByNodeId": operationIdByNodeId
|
||||
];`,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
logger.debug(
|
||||
`Retrieved critical path in ${Date.now() - now}ms, took: ${response.took}ms`
|
||||
);
|
||||
|
||||
if (!response.aggregations) {
|
||||
return {
|
||||
criticalPath: null,
|
||||
};
|
||||
}
|
||||
|
||||
const criticalPath = response.aggregations?.critical_path
|
||||
.value as CriticalPathResponse;
|
||||
|
||||
return {
|
||||
criticalPath,
|
||||
};
|
||||
}
|
|
@ -6,6 +6,7 @@
|
|||
*/
|
||||
|
||||
import * as t from 'io-ts';
|
||||
import { nonEmptyStringRt } from '@kbn/io-ts-utils';
|
||||
import { TraceSearchType } from '../../../common/trace_explorer';
|
||||
import { setupRequest } from '../../lib/helpers/setup_request';
|
||||
import { getSearchTransactionsEvents } from '../../lib/helpers/transactions';
|
||||
|
@ -23,6 +24,10 @@ import { getTraceItems } from './get_trace_items';
|
|||
import { getTraceSamplesByQuery } from './get_trace_samples_by_query';
|
||||
import { getRandomSampler } from '../../lib/helpers/get_random_sampler';
|
||||
import { getApmEventClient } from '../../lib/helpers/get_apm_event_client';
|
||||
import {
|
||||
CriticalPathResponse,
|
||||
getAggregatedCriticalPath,
|
||||
} from './get_aggregated_critical_path';
|
||||
|
||||
const tracesRoute = createApmServerRoute({
|
||||
endpoint: 'GET /internal/apm/traces',
|
||||
|
@ -194,10 +199,49 @@ const findTracesRoute = createApmServerRoute({
|
|||
},
|
||||
});
|
||||
|
||||
const aggregatedCriticalPathRoute = createApmServerRoute({
|
||||
endpoint: 'POST /internal/apm/traces/aggregated_critical_path',
|
||||
params: t.type({
|
||||
body: t.intersection([
|
||||
t.type({
|
||||
traceIds: t.array(t.string),
|
||||
serviceName: t.union([nonEmptyStringRt, t.null]),
|
||||
transactionName: t.union([nonEmptyStringRt, t.null]),
|
||||
}),
|
||||
rangeRt,
|
||||
]),
|
||||
}),
|
||||
options: {
|
||||
tags: ['access:apm'],
|
||||
},
|
||||
handler: async (
|
||||
resources
|
||||
): Promise<{ criticalPath: CriticalPathResponse | null }> => {
|
||||
const {
|
||||
params: {
|
||||
body: { traceIds, start, end, serviceName, transactionName },
|
||||
},
|
||||
} = resources;
|
||||
|
||||
const apmEventClient = await getApmEventClient(resources);
|
||||
|
||||
return getAggregatedCriticalPath({
|
||||
traceIds,
|
||||
start,
|
||||
end,
|
||||
apmEventClient,
|
||||
serviceName,
|
||||
transactionName,
|
||||
logger: resources.logger,
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
export const traceRouteRepository = {
|
||||
...tracesByIdRoute,
|
||||
...tracesRoute,
|
||||
...rootTransactionByTraceIdRoute,
|
||||
...transactionByIdRoute,
|
||||
...findTracesRoute,
|
||||
...aggregatedCriticalPathRoute,
|
||||
};
|
||||
|
|
|
@ -7,6 +7,7 @@
|
|||
|
||||
import { PartialTheme } from '@elastic/charts';
|
||||
import { EUI_CHARTS_THEME_DARK, EUI_CHARTS_THEME_LIGHT } from '@elastic/eui/dist/eui_charts_theme';
|
||||
import { useMemo } from 'react';
|
||||
import { useTheme } from './use_theme';
|
||||
|
||||
export function useChartTheme(): PartialTheme[] {
|
||||
|
@ -15,24 +16,27 @@ export function useChartTheme(): PartialTheme[] {
|
|||
? EUI_CHARTS_THEME_DARK.theme
|
||||
: EUI_CHARTS_THEME_LIGHT.theme;
|
||||
|
||||
return [
|
||||
{
|
||||
chartMargins: {
|
||||
left: 10,
|
||||
right: 10,
|
||||
top: 10,
|
||||
bottom: 10,
|
||||
return useMemo(
|
||||
() => [
|
||||
{
|
||||
chartMargins: {
|
||||
left: 10,
|
||||
right: 10,
|
||||
top: 10,
|
||||
bottom: 10,
|
||||
},
|
||||
background: {
|
||||
color: 'transparent',
|
||||
},
|
||||
lineSeriesStyle: {
|
||||
point: { visible: false },
|
||||
},
|
||||
areaSeriesStyle: {
|
||||
point: { visible: false },
|
||||
},
|
||||
},
|
||||
background: {
|
||||
color: 'transparent',
|
||||
},
|
||||
lineSeriesStyle: {
|
||||
point: { visible: false },
|
||||
},
|
||||
areaSeriesStyle: {
|
||||
point: { visible: false },
|
||||
},
|
||||
},
|
||||
baseChartTheme,
|
||||
];
|
||||
baseChartTheme,
|
||||
],
|
||||
[baseChartTheme]
|
||||
);
|
||||
}
|
||||
|
|
|
@ -0,0 +1,426 @@
|
|||
/*
|
||||
* 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 { getAggregatedCriticalPathRootNodes } from '@kbn/apm-plugin/common';
|
||||
import { apm, EntityArrayIterable, EntityIterable, timerange } from '@kbn/apm-synthtrace';
|
||||
import expect from '@kbn/expect';
|
||||
import { Assign } from '@kbn/utility-types';
|
||||
import { invert, sortBy, uniq } from 'lodash';
|
||||
import { SupertestReturnType } from '../../common/apm_api_supertest';
|
||||
import { FtrProviderContext } from '../../common/ftr_provider_context';
|
||||
|
||||
export default function ApiTest({ getService }: FtrProviderContext) {
|
||||
const registry = getService('registry');
|
||||
const apmApiClient = getService('apmApiClient');
|
||||
const synthtraceEsClient = getService('synthtraceEsClient');
|
||||
|
||||
const start = new Date('2022-01-01T00:00:00.000Z').getTime();
|
||||
const end = new Date('2022-01-01T00:15:00.000Z').getTime() - 1;
|
||||
|
||||
type Node = ReturnType<typeof getAggregatedCriticalPathRootNodes>['rootNodes'][0];
|
||||
type Metadata = NonNullable<
|
||||
SupertestReturnType<'POST /internal/apm/traces/aggregated_critical_path'>['body']['criticalPath']
|
||||
>['metadata'][string];
|
||||
type HydratedNode = Assign<Node, { metadata?: Metadata; children: HydratedNode[] }>;
|
||||
|
||||
interface FormattedNode {
|
||||
name: string;
|
||||
value: number;
|
||||
children: FormattedNode[];
|
||||
}
|
||||
|
||||
// format tree in somewhat concise format for easier testing
|
||||
function formatTree(nodes: HydratedNode[]): FormattedNode[] {
|
||||
return sortBy(
|
||||
nodes.map((node) => {
|
||||
const name =
|
||||
node.metadata?.['processor.event'] === 'transaction'
|
||||
? node.metadata['transaction.name']
|
||||
: node.metadata?.['span.name'] || 'root';
|
||||
return { name, value: node.countExclusive, children: formatTree(node.children) };
|
||||
}),
|
||||
(node) => node.name
|
||||
);
|
||||
}
|
||||
|
||||
async function fetchAndBuildCriticalPathTree(
|
||||
options: { fn: () => EntityIterable } & ({ serviceName: string; transactionName: string } | {})
|
||||
) {
|
||||
const { fn } = options;
|
||||
|
||||
const generator = fn();
|
||||
|
||||
const events = generator.toArray();
|
||||
|
||||
const traceIds = uniq(events.map((event) => event['trace.id']!));
|
||||
|
||||
await synthtraceEsClient.index(new EntityArrayIterable(events));
|
||||
|
||||
return apmApiClient
|
||||
.readUser({
|
||||
endpoint: 'POST /internal/apm/traces/aggregated_critical_path',
|
||||
params: {
|
||||
body: {
|
||||
start: new Date(start).toISOString(),
|
||||
end: new Date(end).toISOString(),
|
||||
traceIds,
|
||||
serviceName: 'serviceName' in options ? options.serviceName : null,
|
||||
transactionName: 'transactionName' in options ? options.transactionName : null,
|
||||
},
|
||||
},
|
||||
})
|
||||
.then((response) => {
|
||||
const criticalPath = response.body.criticalPath!;
|
||||
|
||||
const nodeIdByOperationId = invert(criticalPath.operationIdByNodeId);
|
||||
|
||||
const { rootNodes, maxDepth } = getAggregatedCriticalPathRootNodes({
|
||||
criticalPath,
|
||||
});
|
||||
|
||||
function hydrateNode(node: Node): HydratedNode {
|
||||
return {
|
||||
...node,
|
||||
metadata: criticalPath.metadata[criticalPath.operationIdByNodeId[node.nodeId]],
|
||||
children: node.children.map(hydrateNode),
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
rootNodes: rootNodes.map(hydrateNode),
|
||||
maxDepth,
|
||||
criticalPath,
|
||||
nodeIdByOperationId,
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
registry.when('Aggregated critical path', { config: 'basic', archives: [] }, () => {
|
||||
it('builds up the correct tree for a single transaction', async () => {
|
||||
const java = apm
|
||||
.service({ name: 'java', environment: 'production', agentName: 'java' })
|
||||
.instance('java');
|
||||
|
||||
const duration = 1000;
|
||||
const rate = 10;
|
||||
|
||||
const { rootNodes } = await fetchAndBuildCriticalPathTree({
|
||||
fn: () =>
|
||||
timerange(start, end)
|
||||
.interval('15m')
|
||||
.rate(rate)
|
||||
.generator((timestamp) => {
|
||||
return java.transaction('GET /api').timestamp(timestamp).duration(duration);
|
||||
}),
|
||||
});
|
||||
|
||||
expect(rootNodes.length).to.be(1);
|
||||
|
||||
expect(rootNodes[0].countInclusive).to.eql(duration * rate * 1000);
|
||||
expect(rootNodes[0].countExclusive).to.eql(duration * rate * 1000);
|
||||
|
||||
expect(rootNodes[0].metadata).to.eql({
|
||||
'processor.event': 'transaction',
|
||||
'transaction.type': 'request',
|
||||
'service.name': 'java',
|
||||
'agent.name': 'java',
|
||||
'transaction.name': 'GET /api',
|
||||
});
|
||||
});
|
||||
|
||||
it('builds up the correct tree for a complicated trace', async () => {
|
||||
const java = apm
|
||||
.service({ name: 'java', environment: 'production', agentName: 'java' })
|
||||
.instance('java');
|
||||
|
||||
const rate = 10;
|
||||
|
||||
const { rootNodes } = await fetchAndBuildCriticalPathTree({
|
||||
fn: () =>
|
||||
timerange(start, end)
|
||||
.interval('15m')
|
||||
.rate(rate)
|
||||
.generator((timestamp) => {
|
||||
return java
|
||||
.transaction('GET /api')
|
||||
.timestamp(timestamp)
|
||||
.duration(1000)
|
||||
.children(
|
||||
java
|
||||
.span('GET /_search', 'db', 'elasticsearch')
|
||||
.timestamp(timestamp)
|
||||
.duration(400),
|
||||
java
|
||||
.span('get index stats', 'custom')
|
||||
.timestamp(timestamp)
|
||||
.duration(500)
|
||||
.children(
|
||||
java
|
||||
.span('GET /*/_stats', 'db', 'elasticsearch')
|
||||
.timestamp(timestamp + 50)
|
||||
.duration(450)
|
||||
)
|
||||
);
|
||||
}),
|
||||
});
|
||||
|
||||
expect(rootNodes.length).to.be(1);
|
||||
|
||||
expect(rootNodes[0].countInclusive).to.eql(1000 * rate * 1000);
|
||||
|
||||
expect(rootNodes[0].children.length).to.eql(1);
|
||||
|
||||
expect(formatTree(rootNodes)).to.eql([
|
||||
{
|
||||
name: 'GET /api',
|
||||
value: 500 * 1000 * rate,
|
||||
children: [
|
||||
{
|
||||
name: 'get index stats',
|
||||
value: 50 * 1000 * rate,
|
||||
children: [{ name: 'GET /*/_stats', value: 450 * 1000 * rate, children: [] }],
|
||||
},
|
||||
],
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
it('slices traces and merges root nodes if service name and transaction name are set', async () => {
|
||||
// this test also fails when hashCode() is used in the scripted metric aggregation,
|
||||
// due to collisions.
|
||||
|
||||
const upstreamA = apm
|
||||
.service({ name: 'upstreamA', environment: 'production', agentName: 'java' })
|
||||
.instance('upstreamA');
|
||||
|
||||
const upstreamB = apm
|
||||
.service({ name: 'upstreamB', environment: 'production', agentName: 'java' })
|
||||
.instance('upstreamB');
|
||||
|
||||
const downstream = apm
|
||||
.service({ name: 'downstream', environment: 'production', agentName: 'java' })
|
||||
.instance('downstream');
|
||||
|
||||
const rate = 10;
|
||||
|
||||
function generateTrace() {
|
||||
return timerange(start, end)
|
||||
.interval('15m')
|
||||
.rate(rate)
|
||||
.generator((timestamp) => {
|
||||
return [
|
||||
upstreamA
|
||||
.transaction('GET /upstreamA')
|
||||
.timestamp(timestamp)
|
||||
.duration(500)
|
||||
.children(
|
||||
upstreamA
|
||||
.span('GET /downstream', 'external', 'http')
|
||||
.timestamp(timestamp)
|
||||
.duration(500)
|
||||
.children(
|
||||
downstream
|
||||
.transaction('downstream')
|
||||
.timestamp(timestamp + 50)
|
||||
.duration(400)
|
||||
.children(
|
||||
downstream
|
||||
.span('from upstreamA', 'custom')
|
||||
.timestamp(timestamp + 100)
|
||||
.duration(300)
|
||||
)
|
||||
)
|
||||
),
|
||||
upstreamB
|
||||
.transaction('GET /upstreamB')
|
||||
.timestamp(timestamp)
|
||||
.duration(500)
|
||||
.children(
|
||||
upstreamB
|
||||
.span('GET /downstream', 'external', 'http')
|
||||
.timestamp(timestamp)
|
||||
.duration(500)
|
||||
.children(
|
||||
downstream
|
||||
.transaction('downstream')
|
||||
.timestamp(timestamp + 50)
|
||||
.duration(400)
|
||||
.children(
|
||||
downstream
|
||||
.span('from upstreamB', 'custom')
|
||||
.timestamp(timestamp + 100)
|
||||
.duration(300)
|
||||
)
|
||||
)
|
||||
),
|
||||
];
|
||||
});
|
||||
}
|
||||
|
||||
const { rootNodes: unfilteredRootNodes } = await fetchAndBuildCriticalPathTree({
|
||||
fn: () => generateTrace(),
|
||||
});
|
||||
|
||||
await synthtraceEsClient.clean();
|
||||
|
||||
const { rootNodes: filteredRootNodes } = await fetchAndBuildCriticalPathTree({
|
||||
fn: () => generateTrace(),
|
||||
serviceName: 'downstream',
|
||||
transactionName: 'downstream',
|
||||
});
|
||||
|
||||
expect(formatTree(unfilteredRootNodes)).eql([
|
||||
{
|
||||
name: 'GET /upstreamA',
|
||||
value: 0,
|
||||
children: [
|
||||
{
|
||||
name: 'GET /downstream',
|
||||
value: 100 * 1000 * rate,
|
||||
children: [
|
||||
{
|
||||
name: 'downstream',
|
||||
value: 100 * 1000 * rate,
|
||||
children: [
|
||||
{
|
||||
name: 'from upstreamA',
|
||||
value: 300 * 1000 * rate,
|
||||
children: [],
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
name: 'GET /upstreamB',
|
||||
value: 0,
|
||||
children: [
|
||||
{
|
||||
name: 'GET /downstream',
|
||||
value: 100 * 1000 * rate,
|
||||
children: [
|
||||
{
|
||||
name: 'downstream',
|
||||
value: 100 * 1000 * rate,
|
||||
children: [
|
||||
{
|
||||
name: 'from upstreamB',
|
||||
value: 300 * 1000 * rate,
|
||||
children: [],
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
]);
|
||||
|
||||
expect(formatTree(filteredRootNodes)).eql([
|
||||
{
|
||||
name: 'downstream',
|
||||
value: 2 * 100 * 1000 * rate,
|
||||
children: [
|
||||
{
|
||||
name: 'from upstreamA',
|
||||
value: 300 * 1000 * rate,
|
||||
children: [],
|
||||
},
|
||||
{
|
||||
name: 'from upstreamB',
|
||||
value: 300 * 1000 * rate,
|
||||
children: [],
|
||||
},
|
||||
],
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
it('calculates the critical path for a specific transaction if its not part of the critical path of the entire trace', async () => {
|
||||
const upstream = apm
|
||||
.service({ name: 'upstream', environment: 'production', agentName: 'java' })
|
||||
.instance('upstream');
|
||||
|
||||
const downstreamA = apm
|
||||
.service({ name: 'downstreamA', environment: 'production', agentName: 'java' })
|
||||
.instance('downstreamB');
|
||||
|
||||
const downstreamB = apm
|
||||
.service({ name: 'downstreamB', environment: 'production', agentName: 'java' })
|
||||
.instance('downstreamB');
|
||||
|
||||
const rate = 10;
|
||||
|
||||
function generateTrace() {
|
||||
return timerange(start, end)
|
||||
.interval('15m')
|
||||
.rate(rate)
|
||||
.generator((timestamp) => {
|
||||
return [
|
||||
upstream
|
||||
.transaction('GET /upstream')
|
||||
.timestamp(timestamp)
|
||||
.duration(500)
|
||||
.children(
|
||||
upstream
|
||||
.span('GET /downstreamA', 'external', 'http')
|
||||
.timestamp(timestamp)
|
||||
.duration(500)
|
||||
.children(
|
||||
downstreamA
|
||||
.transaction('downstreamA')
|
||||
.timestamp(timestamp + 50)
|
||||
.duration(400)
|
||||
),
|
||||
upstream
|
||||
.span('GET /downstreamB', 'external', 'http')
|
||||
.timestamp(timestamp)
|
||||
.duration(400)
|
||||
.children(
|
||||
downstreamB
|
||||
.transaction('downstreamB')
|
||||
.timestamp(timestamp + 50)
|
||||
.duration(400)
|
||||
)
|
||||
),
|
||||
];
|
||||
});
|
||||
}
|
||||
|
||||
const { rootNodes: unfilteredRootNodes } = await fetchAndBuildCriticalPathTree({
|
||||
fn: () => generateTrace(),
|
||||
});
|
||||
|
||||
expect(formatTree(unfilteredRootNodes)[0].children[0].children).to.eql([
|
||||
{
|
||||
name: 'downstreamA',
|
||||
value: 400 * rate * 1000,
|
||||
children: [],
|
||||
},
|
||||
]);
|
||||
|
||||
await synthtraceEsClient.clean();
|
||||
|
||||
const { rootNodes: filteredRootNodes } = await fetchAndBuildCriticalPathTree({
|
||||
fn: () => generateTrace(),
|
||||
serviceName: 'downstreamB',
|
||||
transactionName: 'downstreamB',
|
||||
});
|
||||
|
||||
expect(formatTree(filteredRootNodes)).to.eql([
|
||||
{
|
||||
name: 'downstreamB',
|
||||
value: 400 * rate * 1000,
|
||||
children: [],
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
after(() => synthtraceEsClient.clean());
|
||||
});
|
||||
}
|
|
@ -1,9 +1,3 @@
|
|||
/*
|
||||
* 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.
|
||||
*/
|
||||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue